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.

398 lines
14 KiB

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
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
  1. state = {}
  2. function game_end()
  3. delete_all_entities()
  4. game_init()
  5. end
  6. function update_score(diff) state.score = state.score + diff end
  7. function update_money(diff) state.money = state.money + diff end
  8. -- top right display types
  9. local TRDTS = {
  10. NOTHING = 0,
  11. CENTERED_EVENQ = 1,
  12. EVENQ = 2,
  13. HEX = 3,
  14. PLATFORM = 4,
  15. PERF = 5,
  16. SEED = 6,
  17. TILE = 7,
  18. }
  19. local function get_initial_game_state(seed)
  20. local STARTING_MONEY = 10000
  21. local map, world = random_map(seed)
  22. return {
  23. map = map, -- map of hex coords map[x][y] to some stuff at that location
  24. world = world, -- the root scene graph node for the game 'world'
  25. perf = {}, -- result of call to am.perf_stats, called every frame
  26. time = 0, -- time since game started in seconds
  27. score = 0, -- current game score
  28. money = STARTING_MONEY, -- current money
  29. selected_tower_type = false,
  30. selected_toolbelt_button = 9,
  31. selected_top_right_display_type = TRDTS.SEED,
  32. }
  33. end
  34. local function get_top_right_display_text(hex, evenq, centered_evenq, display_type)
  35. local str = ""
  36. if display_type == TRDTS.CENTERED_EVENQ then
  37. str = centered_evenq.x .. "," .. centered_evenq.y .. " (cevenq)"
  38. elseif display_type == TRDTS.EVENQ then
  39. str = evenq.x .. "," .. evenq.y .. " (evenq)"
  40. elseif display_type == TRDTS.HEX then
  41. str = hex.x .. "," .. hex.y .. " (hex)"
  42. elseif display_type == TRDTS.PLATFORM then
  43. str = string.format("%s %s lang %s", am.platform, am.version, am.language())
  44. elseif display_type == TRDTS.PERF then
  45. str = table.tostring(state.perf)
  46. elseif display_type == TRDTS.SEED then
  47. str = "SEED: " .. state.map.seed
  48. elseif display_type == TRDTS.TILE then
  49. str = table.tostring(state.map.get(hex.x, hex.y))
  50. end
  51. return str
  52. end
  53. local function handle_left_click(hex, tile, tower_type, toolbelt_button)
  54. end
  55. -- initialized later, as part of the init of the toolbelt
  56. function select_tower_type(tower_type) end
  57. function select_toolbelt_button(i)
  58. state.selected_toolbelt_button = i
  59. if i <= 10 then
  60. select_tower_type(i)
  61. else
  62. select_tower_type(nil)
  63. end
  64. end
  65. function do_day_night_cycle()
  66. local tstep = (math.sin(state.time / 100) + 1) / state.perf.avg_fps
  67. state.world"negative_mask".color = vec4(tstep){a=1}
  68. end
  69. local function game_pause()
  70. WIN.scene"game".paused = true
  71. WIN.scene"game":append(am.group{
  72. am.rect(WIN.left, WIN.bottom, WIN.right, WIN.top, COLORS.TRANSPARENT),
  73. am.scale(3) ^ am.text("Paused.\nEscape to Resume\nf4 to start a new game", COLORS.BLACK)
  74. }
  75. :tag"pause_menu")
  76. WIN.scene:action(function()
  77. if WIN:key_pressed"escape" then
  78. WIN.scene:remove"pause_menu"
  79. WIN.scene"game".paused = false
  80. return true
  81. elseif WIN:key_pressed"f4" then
  82. game_end()
  83. return true
  84. end
  85. end)
  86. end
  87. local function game_action(scene)
  88. if state.score < 0 then game_end() return true end
  89. state.perf = am.perf_stats()
  90. state.time = state.time + am.delta_time
  91. state.score = state.score + am.delta_time
  92. local mouse = WIN:mouse_position()
  93. local hex = pixel_to_hex(mouse - WORLDSPACE_COORDINATE_OFFSET)
  94. local rounded_mouse = hex_to_pixel(hex) + WORLDSPACE_COORDINATE_OFFSET
  95. local evenq = hex_to_evenq(hex)
  96. local centered_evenq = evenq{ y = -evenq.y } - vec2(math.floor(HEX_GRID_WIDTH/2)
  97. , math.floor(HEX_GRID_HEIGHT/2))
  98. local tile = state.map.get(hex.x, hex.y)
  99. local interactable = evenq_is_in_interactable_region(evenq{ y = -evenq.y })
  100. local buildable = tower_type_is_buildable_on(hex, tile, state.selected_tower_type)
  101. local firable = false
  102. if WIN:mouse_pressed"left" then
  103. if interactable then
  104. if buildable then
  105. local broken, flow_field = making_hex_unwalkable_breaks_flow_field(hex, tile)
  106. local cost = get_tower_cost(state.selected_tower_type)
  107. if broken then
  108. local node = WIN.scene("cursor"):child(2)
  109. node.color = COLORS.CLARET
  110. node:action(am.tween(0.1, { color = COLORS.TRANSPARENT }))
  111. play_sfx(SOUNDS.BIRD2)
  112. elseif cost > state.money then
  113. local node = WIN.scene("cursor"):child(2)
  114. node.color = COLORS.CLARET
  115. node:action(am.tween(0.1, { color = COLORS.TRANSPARENT }))
  116. play_sfx(SOUNDS.BIRD2)
  117. else
  118. update_money(-cost)
  119. build_tower(hex, state.selected_tower_type, flow_field)
  120. if flow_field then
  121. apply_flow_field(state.map, flow_field, state.world)
  122. end
  123. end
  124. end
  125. end
  126. end
  127. if WIN:key_pressed"escape" then
  128. game_pause()
  129. elseif WIN:key_pressed"f1" then
  130. state.top_right_display_type = (state.top_right_display_type + 1) % #table.keys(TRDTS)
  131. elseif WIN:key_pressed"f2" then
  132. state.world"flow_field".hidden = not state.world"flow_field".hidden
  133. elseif WIN:key_pressed"tab" then
  134. if WIN:key_down"lshift" then
  135. select_toolbelt_button((state.selected_toolbelt_button + 12 - 2) % 12 + 1)
  136. else
  137. select_toolbelt_button((state.selected_toolbelt_button) % 12 + 1)
  138. end
  139. elseif WIN:key_pressed"1" then select_toolbelt_button(1)
  140. elseif WIN:key_pressed"2" then select_toolbelt_button(2)
  141. elseif WIN:key_pressed"3" then select_toolbelt_button(3)
  142. elseif WIN:key_pressed"4" then select_toolbelt_button(4)
  143. elseif WIN:key_pressed"q" then select_toolbelt_button(5)
  144. elseif WIN:key_pressed"w" then select_toolbelt_button(6)
  145. elseif WIN:key_pressed"e" then select_toolbelt_button(7)
  146. elseif WIN:key_pressed"r" then select_toolbelt_button(8)
  147. elseif WIN:key_pressed"a" then select_toolbelt_button(9)
  148. elseif WIN:key_pressed"s" then select_toolbelt_button(10)
  149. elseif WIN:key_pressed"d" then select_toolbelt_button(11)
  150. elseif WIN:key_pressed"f" then select_toolbelt_button(12)
  151. end
  152. do_entity_updates()
  153. do_mob_spawning()
  154. do_gui_updates()
  155. do_day_night_cycle()
  156. WIN.scene("top_right_display").text = get_top_right_display_text(hex, evenq, centered_evenq, state.selected_top_right_display_type)
  157. WIN.scene("score").text = string.format("SCORE: %.2f", state.score)
  158. WIN.scene("money").text = string.format("MONEY: %d", state.money)
  159. if interactable then
  160. WIN.scene("cursor").hidden = false
  161. if buildable then
  162. WIN.scene("cursor_translate").position2d = rounded_mouse
  163. else
  164. WIN.scene("cursor").hidden = true
  165. end
  166. else
  167. WIN.scene("cursor").hidden = true
  168. end
  169. end
  170. local function make_game_toolbelt()
  171. local function toolbelt_button(size, half_size, tower_texture, padding, i, offset, key_name)
  172. local x1 = (size + padding) * i + offset.x
  173. local y1 = offset.y
  174. local x2 = (size + padding) * i + offset.x + size
  175. local y2 = offset.y + size
  176. register_button_widget("toolbelt_tower_button" .. i
  177. , am.rect(x1, y1, x2, y2)
  178. , function() select_tower_type(i) end)
  179. return am.translate(vec2(size + padding, 0) * i + offset)
  180. ^ am.group{
  181. am.translate(0, half_size)
  182. ^ pack_texture_into_sprite(TEXTURES.BUTTON1, size, size),
  183. am.translate(0, half_size)
  184. ^ pack_texture_into_sprite(tower_texture, size, size),
  185. am.translate(vec2(half_size))
  186. ^ am.group{
  187. pack_texture_into_sprite(TEXTURES.BUTTON1, half_size, half_size),
  188. am.scale(2)
  189. ^ am.text(key_name, COLORS.BLACK)
  190. }
  191. }
  192. end
  193. local toolbelt_height = hex_height(HEX_SIZE) * 2
  194. local function get_tower_tooltip_text_node(tower_type)
  195. local name = get_tower_name(tower_type)
  196. local placement_rules = get_tower_placement_rules_text(tower_type)
  197. local short_desc = get_tower_short_description(tower_type)
  198. local cost = get_tower_cost(tower_type)
  199. if not (name or placement_rules or short_desc or cost) then
  200. return am.group():tag"tower_tooltip_text"
  201. end
  202. local color = COLORS.PALE_SILVER
  203. return (am.translate(WIN.left + 10, WIN.bottom + toolbelt_height + 20)
  204. ^ am.group{
  205. am.translate(0, 60)
  206. ^ am.text(name, color, "left"):tag"tower_name",
  207. am.translate(0, 40)
  208. ^ am.text(placement_rules, color, "left"):tag"tower_placement_rules",
  209. am.translate(0, 20)
  210. ^ am.text(short_desc, color, "left"):tag"tower_short_description",
  211. am.translate(0, 0)
  212. ^ am.text(string.format("cost: %d", cost), color, "left"):tag"tower_cost"
  213. }
  214. )
  215. :tag"tower_tooltip_text"
  216. end
  217. local toolbelt = am.group{
  218. am.group():tag"tower_tooltip_text",
  219. am.rect(WIN.left, WIN.bottom, WIN.right, WIN.bottom + toolbelt_height, COLORS.TRANSPARENT)
  220. }:tag"toolbelt"
  221. local padding = 15
  222. local size = toolbelt_height - padding
  223. local half_size = size/2
  224. local offset = vec2(WIN.left + padding*3, WIN.bottom + padding/3)
  225. local tab_button = am.translate(vec2(0, half_size) + offset) ^ am.group{
  226. pack_texture_into_sprite(TEXTURES.WIDER_BUTTON1, 54, 32),
  227. pack_texture_into_sprite(TEXTURES.TAB_ICON, 25, 25)
  228. }
  229. toolbelt:append(tab_button)
  230. local tower_select_square = (
  231. am.translate(vec2(size + padding, half_size) + offset)
  232. ^ am.rect(-size/2-3, -size/2-3, size/2+3, size/2+3, COLORS.SUNRAY)
  233. ):tag"tower_select_square"
  234. tower_select_square.hidden = true
  235. toolbelt:append(tower_select_square)
  236. local keys = { '1', '2', '3', '4', 'q', 'w', 'e', 'r', 'a', 's', 'd', 'f' }
  237. -- order of this array is the order of towers on the toolbelt.
  238. local tower_type_values = {
  239. TOWER_TYPE.WALL,
  240. TOWER_TYPE.HOWITZER,
  241. TOWER_TYPE.REDEYE,
  242. TOWER_TYPE.MOAT,
  243. TOWER_TYPE.RADAR,
  244. TOWER_TYPE.LIGHTHOUSE
  245. }
  246. for i = 1, #keys do
  247. local icon_texture = get_tower_icon_texture(tower_type_values[i])
  248. toolbelt:append(
  249. toolbelt_button(
  250. size,
  251. half_size,
  252. icon_texture,
  253. padding,
  254. i,
  255. offset,
  256. keys[i]
  257. )
  258. )
  259. end
  260. select_tower_type = function(tower_type)
  261. state.selected_tower_type = tower_type
  262. if get_tower_spec(tower_type) then
  263. WIN.scene:replace("tower_tooltip_text", get_tower_tooltip_text_node(tower_type))
  264. local new_position = vec2((size + padding) * tower_type, size/2) + offset
  265. if toolbelt("tower_select_square").hidden then
  266. toolbelt("tower_select_square").position2d = new_position
  267. toolbelt("tower_select_square").hidden = false
  268. else
  269. toolbelt("tower_select_square"):action(am.tween(0.1, { position2d = new_position }))
  270. end
  271. WIN.scene:replace("cursor", get_tower_cursor(tower_type):tag"cursor")
  272. WIN.scene:action(am.play(am.sfxr_synth(SOUNDS.SELECT1), false, 1, SFX_VOLUME))
  273. else
  274. toolbelt("tower_select_square").hidden = true
  275. WIN.scene:replace("cursor", make_hex_cursor(0, COLORS.TRANSPARENT))
  276. end
  277. end
  278. return toolbelt
  279. end
  280. -- |color_f| can be a function that takes a hex and returns a color, or just a color
  281. -- |action_f| should be an action that operates on the group node or nil
  282. function make_hex_cursor(radius, color_f, action_f)
  283. local color = type(color_f) == "userdata" and color_f or nil
  284. local map = spiral_map(vec2(0), radius)
  285. local group = am.group()
  286. for _,h in pairs(map) do
  287. local hexagon = am.circle(hex_to_pixel(h), HEX_SIZE, color or color_f(h), 6)
  288. group:append(hexagon)
  289. end
  290. if action_f then
  291. group:action(action_f)
  292. end
  293. return group:tag"cursor"
  294. end
  295. function game_scene()
  296. local score = am.translate(WIN.left + 10, WIN.top - 20)
  297. ^ am.text("", "left"):tag"score"
  298. local money = am.translate(WIN.left + 10, WIN.top - 40)
  299. ^ am.text("", "left"):tag"money"
  300. local top_right_display = am.translate(WIN.right - 10, WIN.top - 20)
  301. ^ am.text("", "right", "top"):tag"top_right_display"
  302. local curtain = am.rect(WIN.left, WIN.bottom, WIN.right, WIN.top, COLORS.TRUE_BLACK)
  303. curtain:action(coroutine.create(function()
  304. am.wait(am.tween(curtain, 3, { color = vec4(0) }, am.ease.out(am.ease.hyperbola)))
  305. WIN.scene:remove(curtain)
  306. return true
  307. end))
  308. local scene = am.group{
  309. state.world,
  310. am.translate(OFF_SCREEN):tag"cursor_translate" ^ make_hex_cursor(0, COLORS.TRANSPARENT),
  311. score,
  312. money,
  313. top_right_display,
  314. make_game_toolbelt(),
  315. curtain,
  316. }:tag"game"
  317. scene:action(game_action)
  318. return scene
  319. end
  320. function game_init()
  321. state = get_initial_game_state()
  322. build_tower(HEX_GRID_CENTER, TOWER_TYPE.RADAR)
  323. WIN.scene:remove("game")
  324. WIN.scene:append(game_scene())
  325. end