hexyz is tower defense game, and a lua library for dealing with hexagonal grids
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

417 lines
13 KiB

4 years ago
3 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
3 years ago
4 years ago
4 years ago
3 years ago
4 years ago
3 years ago
3 years ago
3 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
3 years ago
4 years ago
3 years ago
3 years ago
3 years ago
4 years ago
4 years ago
3 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
3 years ago
3 years ago
3 years ago
4 years ago
4 years ago
4 years ago
3 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
3 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
3 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
3 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
3 years ago
3 years ago
4 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
4 years ago
4 years ago
4 years ago
  1. MOB_TYPE = {
  2. BEEPER = 1,
  3. SPOODER = 2,
  4. VELKOOZ = 3,
  5. }
  6. MAX_MOB_SIZE = hex_height(HEX_SIZE, HEX_ORIENTATION.FLAT) / 2
  7. MOB_SIZE = MAX_MOB_SIZE
  8. local MOB_SPECS = {
  9. [MOB_TYPE.BEEPER] = {
  10. health = 30,
  11. speed = 6,
  12. bounty = 5,
  13. hurtbox_radius = MOB_SIZE/2
  14. },
  15. [MOB_TYPE.SPOODER] = {
  16. health = 15,
  17. speed = 10,
  18. bounty = 5,
  19. hurtbox_radius = MOB_SIZE/2
  20. },
  21. [MOB_TYPE.VELKOOZ] = {
  22. health = 10,
  23. speed = 20,
  24. bounty = 40,
  25. hurtbox_radius = MOB_SIZE
  26. },
  27. }
  28. function get_mob_health(mob_type)
  29. return MOB_SPECS[mob_type].health
  30. end
  31. function get_mob_spec(mob_type)
  32. return MOB_SPECS[mob_type]
  33. end
  34. local function grow_mob_health(mob_type, spec_health, time)
  35. return spec_health + math.pow(game_state.current_wave - 1, 2)
  36. end
  37. local function grow_mob_speed(mob_type, spec_speed, time)
  38. return spec_speed + math.log(game_state.current_wave + 1)
  39. end
  40. local function grow_mob_bounty(mob_type, spec_bounty, time)
  41. return spec_bounty + (game_state.current_wave - 1) * 2
  42. end
  43. function mobs_on_hex(hex)
  44. local t = {}
  45. for mob_index,mob in pairs(game_state.mobs) do
  46. if mob and mob.hex == hex then
  47. table.insert(t, mob_index, mob)
  48. end
  49. end
  50. return t
  51. end
  52. function mob_on_hex(hex)
  53. -- table.find returns i,v in the table
  54. return table.find(game_state.mobs, function(mob)
  55. return mob and mob.hex == hex
  56. end)
  57. end
  58. -- check if a the tile at |hex| is passable by |mob|
  59. function mob_can_pass_through(mob, hex)
  60. local tile = hex_map_get(game_state.map, hex)
  61. return tile_is_medium_elevation(tile)
  62. end
  63. function mob_die(mob, mob_index)
  64. vplay_sfx(SOUNDS.EXPLOSION1)
  65. delete_entity(game_state.mobs, mob_index)
  66. end
  67. function mob_reach_center(mob, mob_index)
  68. update_score(-(mob.health + mob.bounty))
  69. delete_entity(game_state.mobs, mob_index)
  70. end
  71. local HEALTHBAR_WIDTH = HEX_PIXEL_WIDTH/2
  72. local HEALTHBAR_HEIGHT = HEALTHBAR_WIDTH/4
  73. function do_hit_mob(mob, damage, mob_index)
  74. mob.health = mob.health - damage
  75. if mob.health <= 0 then
  76. update_score(mob.bounty)
  77. update_money(mob.bounty)
  78. mob_die(mob, mob_index)
  79. else
  80. mob.healthbar:action(coroutine.create(function(self)
  81. local x2 = -HEALTHBAR_WIDTH/2 + mob.health/grow_mob_health(mob.type, get_mob_health(mob.type), game_state.time) * HEALTHBAR_WIDTH/2
  82. self:child(2).x2 = x2
  83. self.hidden = false
  84. am.wait(am.delay(0.8))
  85. self.hidden = true
  86. end))
  87. end
  88. end
  89. function make_mob_node(mob_type, mob)
  90. local healthbar = am.group{
  91. am.rect(-HEALTHBAR_WIDTH/2, -HEALTHBAR_HEIGHT/2, HEALTHBAR_WIDTH/2, HEALTHBAR_HEIGHT/2, COLORS.VERY_DARK_GRAY),
  92. am.rect(-HEALTHBAR_WIDTH/2, -HEALTHBAR_HEIGHT/2, HEALTHBAR_WIDTH/2, HEALTHBAR_HEIGHT/2, COLORS.GREEN_YELLOW)
  93. }
  94. healthbar.hidden = true
  95. if mob_type == MOB_TYPE.BEEPER then
  96. return am.group{
  97. am.rotate(mob.TOB)
  98. ^ pack_texture_into_sprite(TEXTURES.MOB_BEEPER, MOB_SIZE, MOB_SIZE),
  99. am.translate(0, -10)
  100. ^ healthbar
  101. }
  102. elseif mob_type == MOB_TYPE.SPOODER then
  103. return am.group{
  104. am.rotate(0)
  105. ^ pack_texture_into_sprite(TEXTURES.MOB_SPOODER, MOB_SIZE, MOB_SIZE),
  106. am.translate(0, -10)
  107. ^ healthbar
  108. }
  109. elseif mob_type == MOB_TYPE.VELKOOZ then
  110. return am.group{
  111. am.rotate(0)
  112. ^ pack_texture_into_sprite(TEXTURES.MOB_VELKOOZ, MOB_SIZE*4, MOB_SIZE*4):tag"velk_sprite",
  113. am.translate(0, -10)
  114. ^ healthbar
  115. }
  116. end
  117. end
  118. local function get_spawn_hex()
  119. -- ensure we spawn on an random tile along the map's edges
  120. local roll = math.floor(math.random() * (HEX_GRID_WIDTH * 2 + HEX_GRID_HEIGHT * 2) - 1)
  121. local x, y
  122. if roll < HEX_GRID_HEIGHT then
  123. x, y = 0, roll
  124. elseif roll < (HEX_GRID_WIDTH + HEX_GRID_HEIGHT) then
  125. x, y = roll - HEX_GRID_HEIGHT, HEX_GRID_HEIGHT - 1
  126. elseif roll < (HEX_GRID_HEIGHT * 2 + HEX_GRID_WIDTH) then
  127. x, y = HEX_GRID_WIDTH - 1, roll - HEX_GRID_WIDTH - HEX_GRID_HEIGHT
  128. else
  129. x, y = roll - (HEX_GRID_HEIGHT * 2) - HEX_GRID_WIDTH, 0
  130. end
  131. -- @NOTE negate 'y' because hexyz algorithms assume south is positive, in amulet north is positive
  132. local hex = evenq_to_hex(vec2(x, -y))
  133. return hex
  134. end
  135. local function resolve_frame_target_for_mob(mob, mob_index)
  136. local last_frame_hex = mob.hex
  137. mob.hex = pixel_to_hex(mob.position, vec2(HEX_SIZE))
  138. if mob.hex == HEX_GRID_CENTER then
  139. mob_reach_center(mob, mob_index)
  140. return true
  141. end
  142. -- figure out movement
  143. if last_frame_hex ~= mob.hex or not mob.frame_target then
  144. local frame_target, tile = false, false
  145. if mob.path then
  146. -- we (should) have an explicitly stored target
  147. local path_entry = hex_map_get(mob.path, mob.hex)
  148. if not path_entry then
  149. -- we should be just about to reach the target, delete the path.
  150. mob.path = false
  151. mob.frame_target = false
  152. return
  153. end
  154. mob.frame_target = path_entry.hex
  155. -- check if our target is valid, and if it's not we aren't going to move this frame.
  156. if last_frame_hex ~= mob.hex and not mob_can_pass_through(mob, mob.frame_target) then
  157. mob.path = false
  158. mob.frame_target = false
  159. end
  160. else
  161. -- use the map's flow field - gotta find the the best neighbour
  162. local neighbours = grid_neighbours(game_state.map, mob.hex)
  163. if #neighbours > 0 then
  164. local first_neighbour = neighbours[1]
  165. tile = hex_map_get(game_state.map, first_neighbour)
  166. local lowest_cost_hex = first_neighbour
  167. local lowest_cost = tile.priority or 0
  168. for _,n in pairs(neighbours) do
  169. tile = hex_map_get(game_state.map, n)
  170. if not tile.priority then
  171. -- if there's no stored priority, that should mean it's the center tile
  172. -- in which case, it should be the best target
  173. lowest_cost_hex = n
  174. break
  175. end
  176. local current_cost = tile.priority
  177. if current_cost and current_cost < lowest_cost then
  178. lowest_cost_hex = n
  179. lowest_cost = current_cost
  180. end
  181. end
  182. mob.frame_target = lowest_cost_hex
  183. else
  184. --log('no neighbours')
  185. end
  186. end
  187. end
  188. end
  189. local function update_mob_velkooz(mob, mob_index)
  190. resolve_frame_target_for_mob(mob, mob_index)
  191. if mob.frame_target then
  192. if mob_can_pass_through(mob, mob.frame_target) then
  193. local from = hex_map_get(game_state.map, mob.hex)
  194. local to = hex_map_get(game_state.map, mob.frame_target)
  195. local rate = mob.speed * am.delta_time
  196. mob.position = mob.position + math.normalize(hex_to_pixel(mob.frame_target, vec2(HEX_SIZE)) - mob.position) * rate
  197. mob.node.position2d = mob.position
  198. else
  199. mob.frame_target = false
  200. end
  201. else
  202. --log('no targetssdsdsdsdsd')
  203. end
  204. local theta = math.rad(90) - math.atan((HEX_GRID_CENTER.y - mob.position.y)/(HEX_GRID_CENTER.x - mob.position.x))
  205. local diff = mob.node("rotate").angle - theta
  206. mob.node("rotate").angle = -theta + math.pi/2
  207. local roll = math.floor((game_state.time - mob.TOB) * 10) % 4
  208. if roll == 0 then
  209. mob.node"velk_sprite".source = "res/mob_velkooz0.png"
  210. elseif roll == 1 then
  211. mob.node"velk_sprite".source = "res/mob_velkooz1.png"
  212. elseif roll == 2 then
  213. mob.node"velk_sprite".source = "res/mob_velkooz2.png"
  214. elseif roll == 3 then
  215. mob.node"velk_sprite".source = "res/mob_velkooz3.png"
  216. end
  217. end
  218. local function update_mob_spooder(mob, mob_index)
  219. resolve_frame_target_for_mob(mob, mob_index)
  220. if mob.frame_target then
  221. -- do movement
  222. -- it's possible that the target we have was invalidated by a tower placed this frame,
  223. -- or between when we last calculated this target and now
  224. -- check for that now
  225. if mob_can_pass_through(mob, mob.frame_target) then
  226. local from = hex_map_get(game_state.map, mob.hex)
  227. local to = hex_map_get(game_state.map, mob.frame_target)
  228. if not from or not to then
  229. -- @TODO this happens rarely, why? when?
  230. return
  231. end
  232. local spider_speed = ((math.simplex(mob.hex) + 1.5) * 1.5) ^ 2
  233. local rate = (spider_speed * mob.speed - math.abs(to.elevation - from.elevation)) * am.delta_time
  234. mob.position = mob.position + math.normalize(hex_to_pixel(mob.frame_target, vec2(HEX_SIZE)) - mob.position) * rate
  235. mob.node.position2d = mob.position
  236. else
  237. mob.frame_target = false
  238. end
  239. else
  240. --log('no target')
  241. end
  242. -- passive animation
  243. if math.random() < 0.1 then
  244. mob.node"rotate":action(am.tween(0.3, { angle = math.random() * math.rad(0, -180)}))
  245. end
  246. end
  247. local function update_mob_beeper(mob, mob_index)
  248. resolve_frame_target_for_mob(mob, mob_index)
  249. if mob.frame_target then
  250. -- do movement
  251. -- it's totally possible that the target we have was invalidated by a tower placed this frame,
  252. -- or between when we last calculated this target and now
  253. -- check for that now
  254. if mob_can_pass_through(mob, mob.frame_target) then
  255. local from = hex_map_get(game_state.map, mob.hex)
  256. local to = hex_map_get(game_state.map, mob.frame_target)
  257. if not from or not to then
  258. -- @TODO this happens rarely, why? when?
  259. return
  260. end
  261. local rate = (4 * mob.speed - math.abs(to.elevation - from.elevation)) * am.delta_time
  262. mob.position = mob.position + math.normalize(hex_to_pixel(mob.frame_target, vec2(HEX_SIZE)) - mob.position) * rate
  263. mob.node.position2d = mob.position
  264. else
  265. mob.frame_target = false
  266. end
  267. else
  268. --log('no target')
  269. end
  270. -- passive animation
  271. if math.random() < 0.01 then
  272. mob.node"rotate":action(am.tween(0.3, { angle = mob.node"rotate".angle + math.pi*3 }))
  273. else
  274. mob.node"rotate".angle = math.wrapf(mob.node"rotate".angle + am.delta_time, math.pi*2)
  275. end
  276. end
  277. local function get_mob_update_function(mob_type)
  278. if mob_type == MOB_TYPE.BEEPER then
  279. return update_mob_beeper
  280. elseif mob_type == MOB_TYPE.SPOODER then
  281. return update_mob_spooder
  282. elseif mob_type == MOB_TYPE.VELKOOZ then
  283. return update_mob_velkooz
  284. end
  285. end
  286. local function make_and_register_mob(mob_type)
  287. local mob = make_basic_entity(
  288. get_spawn_hex(),
  289. get_mob_update_function(mob_type)
  290. )
  291. mob.type = mob_type
  292. mob.node = am.translate(mob.position) ^ make_mob_node(mob_type, mob)
  293. local spec = get_mob_spec(mob_type)
  294. mob.health = grow_mob_health(mob_type, spec.health, game_state.time)
  295. mob.speed = grow_mob_speed(mob_type, spec.speed, game_state.time)
  296. mob.bounty = grow_mob_bounty(mob_type, spec.bounty, game_state.time)
  297. mob.hurtbox_radius = spec.hurtbox_radius
  298. mob.healthbar = mob.node:child(1):child(2):child(1) -- lmao
  299. if mob.type == MOB_TYPE.VELKOOZ then
  300. mob.game_states = {
  301. pack_texture_into_sprite(TEXTURES.VELKOOZ, MOB_SIZE, MOB_SIZE):tag"velk_sprite",
  302. pack_texture_into_sprite(TEXTURES.VELKOOZ1, MOB_SIZE, MOB_SIZE):tag"velk_sprite",
  303. pack_texture_into_sprite(TEXTURES.VELKOOZ2, MOB_SIZE, MOB_SIZE):tag"velk_sprite",
  304. pack_texture_into_sprite(TEXTURES.VELKOOZ3, MOB_SIZE, MOB_SIZE):tag"velk_sprite",
  305. }
  306. end
  307. register_entity(game_state.mobs, mob)
  308. return mob
  309. end
  310. function mob_serialize(mob)
  311. local serialized = entity_basic_devectored_copy(mob)
  312. return am.to_json(serialized)
  313. end
  314. function mob_deserialize(json_string)
  315. local mob = entity_basic_json_parse(json_string)
  316. mob.update = get_mob_update_function(mob.type)
  317. mob.node = am.translate(mob.position) ^ make_mob_node(mob.type, mob)
  318. mob.healthbar = mob.node:child(1):child(2):child(1) -- lmaoooo
  319. return mob
  320. end
  321. local function can_spawn_mob()
  322. local MAX_SPAWN_RATE = 0.1
  323. if not game_state.spawning or (game_state.time - game_state.last_mob_spawn_time) < MAX_SPAWN_RATE then
  324. return false
  325. end
  326. if math.random() <= game_state.spawn_chance then
  327. game_state.last_mob_spawn_time = game_state.time
  328. return true
  329. else
  330. return false
  331. end
  332. end
  333. function do_mob_spawning()
  334. if can_spawn_mob() then
  335. if game_state.current_wave % 2 == 0 then
  336. make_and_register_mob(MOB_TYPE.SPOODER)
  337. else
  338. make_and_register_mob(MOB_TYPE.BEEPER)
  339. end
  340. end
  341. end
  342. function do_mob_updates()
  343. for mob_index,mob in pairs(game_state.mobs) do
  344. if mob and mob.update then
  345. mob.update(mob, mob_index)
  346. end
  347. end
  348. end