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.

351 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. id = "WALL",
  66. name = "Wall",
  67. placement_rules_text = "Place on Ground",
  68. short_description = "Restricts movement, similar to a mountain.",
  69. texture = TEXTURES.TOWER_WALL,
  70. icon_texture = TEXTURES.TOWER_WALL_ICON,
  71. cost = 10,
  72. range = 0,
  73. fire_rate = 2,
  74. update = false,
  75. },
  76. {
  77. id = "GATTLER",
  78. name = "Gattler",
  79. placement_rules_text = "Place on Ground",
  80. short_description = "Short-range, fast-fire rate single-target tower.",
  81. texture = TEXTURES.TOWER_GATTLER,
  82. icon_texture = TEXTURES.TOWER_GATTLER_ICON,
  83. cost = 20,
  84. weapons = {
  85. {
  86. range = 4,
  87. fire_rate = 0.5,
  88. projectile_type = 3,
  89. }
  90. },
  91. update = function(tower, tower_index)
  92. if not tower.target_index then
  93. -- we should try and acquire a target
  94. -- passive animation
  95. tower.node("rotate").angle = math.wrapf(tower.node("rotate").angle + 0.1 * am.delta_time, math.pi*2)
  96. else
  97. -- should have a target, so we should try and shoot it
  98. if not game_state.mobs[tower.target_index] then
  99. -- the target we have was invalidated
  100. tower.target_index = false
  101. else
  102. -- the target we have is valid
  103. local mob = game_state.mobs[tower.target_index]
  104. local vector = math.normalize(mob.position - tower.position)
  105. if (game_state.time - tower.last_shot_time) > tower.fire_rate then
  106. local projectile = make_and_register_projectile(
  107. tower.hex,
  108. PROJECTILE_TYPE.BULLET,
  109. vector
  110. )
  111. tower.last_shot_time = game_state.time
  112. play_sfx(SOUNDS.HIT1)
  113. end
  114. -- point the cannon at the dude
  115. local theta = math.rad(90) - math.atan((tower.position.y - mob.position.y)/(tower.position.x - mob.position.x))
  116. local diff = tower.node("rotate").angle - theta
  117. tower.node("rotate").angle = -theta + math.pi/2
  118. end
  119. end
  120. end
  121. },
  122. {
  123. id = "HOWITZER",
  124. name = "Howitzer",
  125. placement_rules_text = "Place on Ground, with a 1 space gap between other towers and mountains - walls/moats don't count.",
  126. short_description = "Medium-range, medium fire-rate area of effect artillery tower.",
  127. texture = TEXTURES.TOWER_HOWITZER,
  128. icon_texture = TEXTURES.TOWER_HOWITZER_ICON,
  129. cost = 50,
  130. weapons = {
  131. {
  132. range = 6,
  133. fire_rate = 4,
  134. projectile_type = 1,
  135. }
  136. },
  137. placement_f = function(blocked, has_water, has_mountain, has_ground, hex)
  138. local has_mountain_neighbour = false
  139. local has_non_wall_non_moat_tower_neighbour = false
  140. for _,h in pairs(hex_neighbours(hex)) do
  141. local towers = towers_on_hex(h)
  142. local wall_on_hex = false
  143. has_non_wall_non_moat_tower_neighbour = table.find(towers, function(tower)
  144. if tower.type == TOWER_TYPE.WALL then
  145. wall_on_hex = true
  146. return false
  147. elseif tower.type == TOWER_TYPE.MOAT then
  148. return false
  149. end
  150. return true
  151. end)
  152. if has_non_wall_non_moat_tower_neighbour then
  153. break
  154. end
  155. local tile = hex_map_get(game_state.map, h)
  156. if not wall_on_hex and tile and tile.elevation >= 0.5 then
  157. has_mountain_neighbour = true
  158. break
  159. end
  160. end
  161. return not (blocked or has_water or has_mountain or has_mountain_neighbour or has_non_wall_non_moat_tower_neighbour)
  162. end,
  163. update = function(tower, tower_index)
  164. if not tower.target_index then
  165. -- we don't have a target
  166. for index,mob in pairs(game_state.mobs) do
  167. if mob then
  168. local d = math.distance(mob.hex, tower.hex)
  169. if d <= tower.range then
  170. tower.target_index = index
  171. break
  172. end
  173. end
  174. end
  175. -- passive animation
  176. tower.node("rotate").angle = math.wrapf(tower.node("rotate").angle + 0.1 * am.delta_time, math.pi*2)
  177. else
  178. -- we should have a target
  179. -- @NOTE don't compare to false, empty indexes appear on game reload
  180. if not game_state.mobs[tower.target_index] then
  181. -- the target we have was invalidated
  182. tower.target_index = false
  183. else
  184. -- the target we have is valid
  185. local mob = game_state.mobs[tower.target_index]
  186. local vector = math.normalize(mob.position - tower.position)
  187. if (game_state.time - tower.last_shot_time) > tower.fire_rate then
  188. local projectile = make_and_register_projectile(
  189. tower.hex,
  190. PROJECTILE_TYPE.SHELL,
  191. vector
  192. )
  193. -- @HACK, the projectile will explode if it encounters something taller than it,
  194. -- but the tower it spawns on quickly becomes taller than it, so we just pad it
  195. -- if it's not enough the shell explodes before it leaves its spawning hex
  196. projectile.props.z = tower.props.z + 0.1
  197. tower.last_shot_time = game_state.time
  198. play_sfx(SOUNDS.EXPLOSION2)
  199. end
  200. -- point the cannon at the dude
  201. local theta = math.rad(90) - math.atan((tower.position.y - mob.position.y)/(tower.position.x - mob.position.x))
  202. local diff = tower.node("rotate").angle - theta
  203. tower.node("rotate").angle = -theta + math.pi/2
  204. end
  205. end
  206. end
  207. },
  208. {
  209. id = "REDEYE",
  210. name = "Redeye",
  211. placement_rules_text = "Place on Mountains.",
  212. short_description = "Long-range, penetrating high-velocity laser tower.",
  213. texture = TEXTURES.TOWER_REDEYE,
  214. icon_texture = TEXTURES.TOWER_REDEYE_ICON,
  215. cost = 75,
  216. weapons = {
  217. {
  218. range = 9,
  219. fire_rate = 3,
  220. projectile_type = 2,
  221. }
  222. },
  223. placement_f = function(blocked, has_water, has_mountain, has_ground, hex)
  224. return not blocked and has_mountain
  225. end,
  226. update = function(tower, tower_index)
  227. if not tower.target_index then
  228. for index,mob in pairs(game_state.mobs) do
  229. if mob then
  230. local d = math.distance(mob.hex, tower.hex)
  231. if d <= tower.range then
  232. tower.target_index = index
  233. break
  234. end
  235. end
  236. end
  237. else
  238. if not game_state.mobs[tower.target_index] then
  239. tower.target_index = false
  240. elseif (game_state.time - tower.last_shot_time) > tower.fire_rate then
  241. local mob = game_state.mobs[tower.target_index]
  242. make_and_register_projectile(
  243. tower.hex,
  244. PROJECTILE_TYPE.LASER,
  245. math.normalize(mob.position - tower.position)
  246. )
  247. tower.last_shot_time = game_state.time
  248. vplay_sfx(SOUNDS.LASER2)
  249. end
  250. end
  251. end
  252. },
  253. {
  254. id = "MOAT",
  255. name = "Moat",
  256. placement_rules_text = "Place on Ground",
  257. short_description = "Restricts movement, similar to water.",
  258. texture = TEXTURES.TOWER_MOAT,
  259. icon_texture = TEXTURES.TOWER_MOAT_ICON,
  260. cost = 10,
  261. range = 0,
  262. fire_rate = 2,
  263. height = -1,
  264. update = false
  265. },
  266. {
  267. id = "RADAR",
  268. name = "Radar",
  269. placement_rules_text = "n/a",
  270. short_description = "Doesn't do anything right now :(",
  271. texture = TEXTURES.TOWER_RADAR,
  272. icon_texture = TEXTURES.TOWER_RADAR_ICON,
  273. cost = 100,
  274. range = 0,
  275. fire_rate = 1,
  276. update = false
  277. },
  278. {
  279. id = "LIGHTHOUSE",
  280. name = "Lighthouse",
  281. placement_rules_text = "Place on Ground, adjacent to Water or Moats",
  282. short_description = "Attracts nearby mobs; temporarily redirects their path",
  283. texture = TEXTURES.TOWER_LIGHTHOUSE,
  284. icon_texture = TEXTURES.TOWER_LIGHTHOUSE_ICON,
  285. cost = 150,
  286. range = 7,
  287. fire_rate = 1,
  288. placement_f = function(blocked, has_water, has_mountain, has_ground, hex)
  289. local has_water_neighbour = false
  290. for _,h in pairs(hex_neighbours(hex)) do
  291. local tile = hex_map_get(game_state.map, h)
  292. if tile and tile.elevation < -0.5 then
  293. has_water_neighbour = true
  294. break
  295. end
  296. end
  297. return not blocked
  298. and not has_mountain
  299. and not has_water
  300. and has_water_neighbour
  301. end,
  302. update = function(tower, tower_index)
  303. -- check if there's a mob on a hex in our perimeter
  304. for _,h in pairs(tower.perimeter) do
  305. local mobs = mobs_on_hex(h)
  306. for _,m in pairs(mobs) do
  307. if not m.path and not m.seen_lighthouse then
  308. -- @TODO only attract the mob if its frame target (direction vector)
  309. -- is within some angle range...? if the mob is heading directly away from the tower, then
  310. -- the lighthouse shouldn't do much
  311. local path, made_it = hex_Astar(game_state.map, tower.hex, m.hex, grid_neighbours, grid_cost, grid_heuristic)
  312. if made_it then
  313. m.path = path
  314. m.seen_lighthouse = true -- right now mobs don't care about lighthouses if they've already seen one.
  315. end
  316. end
  317. end
  318. end
  319. end
  320. },
  321. }