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.

345 lines
17 KiB

  1. --[[
  2. the following is a list of tower specifications, which are declarations of a variety of properties describing what a tower is, and how it functions
  3. this a lua file. a quick run-down of what writing code in lua looks like: https://www.amulet.xyz/doc/#lua-primer
  4. each tower spec is a lua table. lua tables are the thing that you use to represent bundles of data (both arrays and hashtables are represented by tables)
  5. the format of the bundles in our case are described below.
  6. some propreties are optional. required properties are marked with an asterisk (*), and are generally included at the top of the list.
  7. # TOWER SPEC TABLE
  8. | --------------------------| -------- | -------------------------------------------------------------- |
  9. | property name, required* | datatype | general description / details |
  10. | --------------------------| -------- | -------------------------------------------------------------- |
  11. | name* | string | exact one-line display name text of the tower |
  12. | placement_rules_text* | string | one-line description of the placement rules for this tower |
  13. | short_description* | string | one-line description of the nature of this tower |
  14. | texture* | userdata | @TODO |
  15. | icon_texture* | userdata | @TODO |
  16. | cost* | number | the starting cost of placing this tower |
  17. | | | |
  18. | weapons* | table | an array of weapons. |
  19. | | | order matters - two weapons share a 'choke' value, and both |
  20. | | | could acquire a target in a frame, the first one is choosen. |
  21. | | | |
  22. | placement_f | function |
  23. | | | |
  24. | | | |
  25. | | | |
  26. | | | |
  27. | | | |
  28. | visual_range | number | when the tower has multiple weapons, what range represents the |
  29. | | | overall range of the tower. default is calculated on load as |
  30. | | | the largest range among the weapons the tower has. |
  31. | min_visual_range | number | same as above but the largest minimum range among weapons |
  32. | | | |
  33. | update_f | function | default value is complicated @TODO |
  34. | grow_f | function | default value is false/nil. @TODO |
  35. | size | number | default value of 1, which means the tower occupies one hex. |
  36. | height | number | default value of 1. height is relevant for mob pathing and |
  37. | | | projectile collision |
  38. | | | |
  39. | --------------------------| -------- | -------------------------------------------------------------- |
  40. # WEAPON TABLE
  41. | --------------------------| -------- | -------------------------------------------------------------- |
  42. | property name, required* | datatype | general description / details |
  43. | --------------------------| -------- | -------------------------------------------------------------- |
  44. | type | number | sometimes, instead of specifying everything for a weapon, it's |
  45. | | | convenient to refer to a base type. if this is provided all of |
  46. | | | the weapon's other fields will be initialized to preset values |
  47. | | | and any other values you provide with the weapon spec will |
  48. | | | overwrite those preset values. |
  49. | | | if you provide a value here, all other properties become |
  50. | | | optional. |
  51. | | | values you can provide, and what they mean: |
  52. | | | @TODO |
  53. | | | |
  54. | fire_rate* | number | 'shots' per second, if the weapon has a valid target |
  55. | range* | number | max distance (in hexes) at which this weapon acquires targets |
  56. | | | |
  57. | min-range | number | default of 0. min distance (in hexes) at which this weapon acquires targets |
  58. | target_acquisition_f | function | default value is complicated @TODO |
  59. | choke | number | default of false/nil. @TODO |
  60. | | | |
  61. | --------------------------| -------- | -------------------------------------------------------------- |
  62. ]]
  63. return {
  64. {
  65. name = "Wall",
  66. placement_rules_text = "Place on Ground",
  67. short_description = "Restricts movement, similar to a mountain.",
  68. texture = TEXTURES.TOWER_WALL,
  69. icon_texture = TEXTURES.TOWER_WALL_ICON,
  70. cost = 10,
  71. range = 0,
  72. fire_rate = 2,
  73. update = false,
  74. },
  75. {
  76. name = "Gattler",
  77. placement_rules_text = "Place on Ground",
  78. short_description = "Short-range, fast-fire rate single-target tower.",
  79. texture = TEXTURES.TOWER_GATTLER,
  80. icon_texture = TEXTURES.TOWER_GATTLER_ICON,
  81. cost = 20,
  82. weapons = {
  83. {
  84. range = 4,
  85. fire_rate = 0.5,
  86. projectile_type = 3,
  87. }
  88. },
  89. update = function(tower, tower_index)
  90. if not tower.target_index then
  91. -- we should try and acquire a target
  92. -- passive animation
  93. tower.node("rotate").angle = math.wrapf(tower.node("rotate").angle + 0.1 * am.delta_time, math.pi*2)
  94. else
  95. -- should have a target, so we should try and shoot it
  96. if not game_state.mobs[tower.target_index] then
  97. -- the target we have was invalidated
  98. tower.target_index = false
  99. else
  100. -- the target we have is valid
  101. local mob = game_state.mobs[tower.target_index]
  102. local vector = math.normalize(mob.position - tower.position)
  103. if (game_state.time - tower.last_shot_time) > tower.fire_rate then
  104. local projectile = make_and_register_projectile(
  105. tower.hex,
  106. PROJECTILE_TYPE.BULLET,
  107. vector
  108. )
  109. tower.last_shot_time = game_state.time
  110. play_sfx(SOUNDS.HIT1)
  111. end
  112. -- point the cannon at the dude
  113. local theta = math.rad(90) - math.atan((tower.position.y - mob.position.y)/(tower.position.x - mob.position.x))
  114. local diff = tower.node("rotate").angle - theta
  115. tower.node("rotate").angle = -theta + math.pi/2
  116. end
  117. end
  118. end
  119. },
  120. {
  121. name = "Howitzer",
  122. placement_rules_text = "Place on Ground, with a 1 space gap between other towers and mountains - walls/moats don't count.",
  123. short_description = "Medium-range, medium fire-rate area of effect artillery tower.",
  124. texture = TEXTURES.TOWER_HOWITZER,
  125. icon_texture = TEXTURES.TOWER_HOWITZER_ICON,
  126. cost = 50,
  127. weapons = {
  128. {
  129. range = 6,
  130. fire_rate = 4,
  131. projectile_type = 1,
  132. }
  133. },
  134. placement_f = function(blocked, has_water, has_mountain, has_ground, hex)
  135. local has_mountain_neighbour = false
  136. local has_non_wall_non_moat_tower_neighbour = false
  137. for _,h in pairs(hex_neighbours(hex)) do
  138. local towers = towers_on_hex(h)
  139. local wall_on_hex = false
  140. has_non_wall_non_moat_tower_neighbour = table.find(towers, function(tower)
  141. if tower.type == TOWER_TYPE.WALL then
  142. wall_on_hex = true
  143. return false
  144. elseif tower.type == TOWER_TYPE.MOAT then
  145. return false
  146. end
  147. return true
  148. end)
  149. if has_non_wall_non_moat_tower_neighbour then
  150. break
  151. end
  152. local tile = hex_map_get(game_state.map, h)
  153. if not wall_on_hex and tile and tile.elevation >= 0.5 then
  154. has_mountain_neighbour = true
  155. break
  156. end
  157. end
  158. return not (blocked or has_water or has_mountain or has_mountain_neighbour or has_non_wall_non_moat_tower_neighbour)
  159. end,
  160. update = function(tower, tower_index)
  161. if not tower.target_index then
  162. -- we don't have a target
  163. for index,mob in pairs(game_state.mobs) do
  164. if mob then
  165. local d = math.distance(mob.hex, tower.hex)
  166. if d <= tower.range then
  167. tower.target_index = index
  168. break
  169. end
  170. end
  171. end
  172. -- passive animation
  173. tower.node("rotate").angle = math.wrapf(tower.node("rotate").angle + 0.1 * am.delta_time, math.pi*2)
  174. else
  175. -- we should have a target
  176. -- @NOTE don't compare to false, empty indexes appear on game reload
  177. if not game_state.mobs[tower.target_index] then
  178. -- the target we have was invalidated
  179. tower.target_index = false
  180. else
  181. -- the target we have is valid
  182. local mob = game_state.mobs[tower.target_index]
  183. local vector = math.normalize(mob.position - tower.position)
  184. if (game_state.time - tower.last_shot_time) > tower.fire_rate then
  185. local projectile = make_and_register_projectile(
  186. tower.hex,
  187. PROJECTILE_TYPE.SHELL,
  188. vector
  189. )
  190. -- @HACK, the projectile will explode if it encounters something taller than it,
  191. -- but the tower it spawns on quickly becomes taller than it, so we just pad it
  192. -- if it's not enough the shell explodes before it leaves its spawning hex
  193. projectile.props.z = tower.props.z + 0.1
  194. tower.last_shot_time = game_state.time
  195. play_sfx(SOUNDS.EXPLOSION2)
  196. end
  197. -- point the cannon at the dude
  198. local theta = math.rad(90) - math.atan((tower.position.y - mob.position.y)/(tower.position.x - mob.position.x))
  199. local diff = tower.node("rotate").angle - theta
  200. tower.node("rotate").angle = -theta + math.pi/2
  201. end
  202. end
  203. end
  204. },
  205. {
  206. name = "Redeye",
  207. placement_rules_text = "Place on Mountains.",
  208. short_description = "Long-range, penetrating high-velocity laser tower.",
  209. texture = TEXTURES.TOWER_REDEYE,
  210. icon_texture = TEXTURES.TOWER_REDEYE_ICON,
  211. cost = 75,
  212. weapons = {
  213. {
  214. range = 9,
  215. fire_rate = 3,
  216. projectile_type = 2,
  217. }
  218. },
  219. placement_f = function(blocked, has_water, has_mountain, has_ground, hex)
  220. return not blocked and has_mountain
  221. end,
  222. update = function(tower, tower_index)
  223. if not tower.target_index then
  224. for index,mob in pairs(game_state.mobs) do
  225. if mob then
  226. local d = math.distance(mob.hex, tower.hex)
  227. if d <= tower.range then
  228. tower.target_index = index
  229. break
  230. end
  231. end
  232. end
  233. else
  234. if not game_state.mobs[tower.target_index] then
  235. tower.target_index = false
  236. elseif (game_state.time - tower.last_shot_time) > tower.fire_rate then
  237. local mob = game_state.mobs[tower.target_index]
  238. make_and_register_projectile(
  239. tower.hex,
  240. PROJECTILE_TYPE.LASER,
  241. math.normalize(mob.position - tower.position)
  242. )
  243. tower.last_shot_time = game_state.time
  244. vplay_sfx(SOUNDS.LASER2)
  245. end
  246. end
  247. end
  248. },
  249. {
  250. name = "Moat",
  251. placement_rules_text = "Place on Ground",
  252. short_description = "Restricts movement, similar to water.",
  253. texture = TEXTURES.TOWER_MOAT,
  254. icon_texture = TEXTURES.TOWER_MOAT_ICON,
  255. cost = 10,
  256. range = 0,
  257. fire_rate = 2,
  258. height = -1,
  259. update = false
  260. },
  261. {
  262. name = "Radar",
  263. placement_rules_text = "n/a",
  264. short_description = "Doesn't do anything right now :(",
  265. texture = TEXTURES.TOWER_RADAR,
  266. icon_texture = TEXTURES.TOWER_RADAR_ICON,
  267. cost = 100,
  268. range = 0,
  269. fire_rate = 1,
  270. update = false
  271. },
  272. {
  273. name = "Lighthouse",
  274. placement_rules_text = "Place on Ground, adjacent to Water or Moats",
  275. short_description = "Attracts nearby mobs; temporarily redirects their path",
  276. texture = TEXTURES.TOWER_LIGHTHOUSE,
  277. icon_texture = TEXTURES.TOWER_LIGHTHOUSE_ICON,
  278. cost = 150,
  279. range = 7,
  280. fire_rate = 1,
  281. placement_f = function(blocked, has_water, has_mountain, has_ground, hex)
  282. local has_water_neighbour = false
  283. for _,h in pairs(hex_neighbours(hex)) do
  284. local tile = hex_map_get(game_state.map, h)
  285. if tile and tile.elevation < -0.5 then
  286. has_water_neighbour = true
  287. break
  288. end
  289. end
  290. return not blocked
  291. and not has_mountain
  292. and not has_water
  293. and has_water_neighbour
  294. end,
  295. update = function(tower, tower_index)
  296. -- check if there's a mob on a hex in our perimeter
  297. for _,h in pairs(tower.perimeter) do
  298. local mobs = mobs_on_hex(h)
  299. for _,m in pairs(mobs) do
  300. if not m.path and not m.seen_lighthouse then
  301. -- @TODO only attract the mob if its frame target (direction vector)
  302. -- is within some angle range...? if the mob is heading directly away from the tower, then
  303. -- the lighthouse shouldn't do much
  304. local path, made_it = hex_Astar(game_state.map, tower.hex, m.hex, grid_neighbours, grid_cost, grid_heuristic)
  305. if made_it then
  306. m.path = path
  307. m.seen_lighthouse = true -- right now mobs don't care about lighthouses if they've already seen one.
  308. end
  309. end
  310. end
  311. end
  312. end
  313. },
  314. }