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.

371 lines
12 KiB

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
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
3 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
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
3 years ago
  1. do
  2. -- add padding, because we terraform the very outer edge and it looks ugly, so hide it
  3. -- should be an even number to preserve a 'true' center
  4. local padding = 4
  5. -- the size of the grid should basically always be constant (i think),
  6. -- but different aspect ratios complicate this in an annoying way
  7. -- grid width should be ~== height * 3 / 2
  8. HEX_GRID_WIDTH = 33 + padding
  9. HEX_GRID_HEIGHT = 23 + padding
  10. HEX_GRID_DIMENSIONS = vec2(HEX_GRID_WIDTH, HEX_GRID_HEIGHT)
  11. HEX_GRID_CENTER = evenq_to_hex(vec2(math.floor(HEX_GRID_WIDTH/2)
  12. , -math.floor(HEX_GRID_HEIGHT/2)))
  13. -- pixel distance from hex centerpoint to any vertex
  14. -- given a grid width gx, and window width wx, what's the smallest size a hex can be to fill the whole screen?
  15. -- wx / (gx * 3 / 2)
  16. HEX_SIZE = win.width / ((HEX_GRID_WIDTH - padding) * 3 / 2)
  17. hex_set_default_size(vec2(HEX_SIZE))
  18. HEX_PIXEL_WIDTH = hex_width(HEX_SIZE, HEX_ORIENTATION.FLAT)
  19. HEX_PIXEL_HEIGHT = hex_height(HEX_SIZE, HEX_ORIENTATION.FLAT)
  20. HEX_PIXEL_DIMENSIONS = vec2(HEX_PIXEL_WIDTH, HEX_PIXEL_HEIGHT)
  21. local hhs = hex_horizontal_spacing(HEX_SIZE)
  22. local hvs = hex_vertical_spacing(HEX_SIZE)
  23. -- number of 'spacings' on the grid == number of cells - 1
  24. HEX_GRID_PIXEL_WIDTH = (HEX_GRID_WIDTH - 1) * hhs
  25. HEX_GRID_PIXEL_HEIGHT = (HEX_GRID_HEIGHT - 1) * hvs
  26. HEX_GRID_PIXEL_DIMENSIONS = vec2(HEX_GRID_PIXEL_WIDTH
  27. , HEX_GRID_PIXEL_HEIGHT)
  28. end
  29. -- amulet puts 0,0 in the middle of the screen
  30. -- transform coordinates by this to pretend 0,0 is elsewhere
  31. -- note this is isn't necessary when adding stuff to the worldspace in general,
  32. -- because the worldspace parent node should be translated by this constant
  33. WORLDSPACE_COORDINATE_OFFSET = -HEX_GRID_PIXEL_DIMENSIONS/2
  34. -- the outer edges of the map are not interactable
  35. -- the interactable region is defined with this function and constant
  36. HEX_GRID_INTERACTABLE_REGION_MARGIN = vec2(3, 4)
  37. function evenq_is_in_interactable_region(evenq)
  38. return point_in_rect(evenq, {
  39. x1 = HEX_GRID_INTERACTABLE_REGION_MARGIN.x,
  40. x2 = HEX_GRID_WIDTH - HEX_GRID_INTERACTABLE_REGION_MARGIN.x,
  41. y1 = HEX_GRID_INTERACTABLE_REGION_MARGIN.y,
  42. y2 = HEX_GRID_HEIGHT - HEX_GRID_INTERACTABLE_REGION_MARGIN.y
  43. })
  44. end
  45. function map_elevation_to_tile_type(elevation)
  46. if elevation < -0.5 then -- lowest elevation
  47. return "Water"
  48. elseif elevation < 0 then -- med-low elevation
  49. return "Ground - Grass"
  50. elseif elevation < 0.5 then -- med-high elevation
  51. return "Ground - Dirt"
  52. elseif elevation <= 1 then -- high elevation
  53. return "Mountain"
  54. else
  55. -- not a normal elevation? not sure when this happens
  56. return "n/a"
  57. end
  58. end
  59. function is_water_elevation(elevation) return elevation < -0.5 end
  60. function is_mountain_elevation(elevation) return elevation >= 0.5 end
  61. function tile_is_medium_elevation(tile)
  62. return tile.elevation >= -0.5 and tile.elevation < 0.5
  63. end
  64. function grid_heuristic(source, target)
  65. return math.distance(source, target)
  66. end
  67. HEX_GRID_MINIMUM_ELEVATION = -1
  68. HEX_GRID_MAXIMUM_ELEVATION = 1
  69. function grid_cost(map, from, to)
  70. local t1, t2 = hex_map_get(map, from), hex_map_get(map, to)
  71. return 2 + math.abs(t1.elevation)^0.5 - math.abs(t2.elevation)^0.5
  72. end
  73. function grid_neighbours(map, hex)
  74. return table.filter(hex_neighbours(hex), function(_hex)
  75. local tile = hex_map_get(map, _hex)
  76. return tile and tile_is_medium_elevation(tile)
  77. end)
  78. end
  79. function generate_flow_field(map, start)
  80. return hex_dijkstra(map, start, nil, grid_neighbours, grid_cost)
  81. end
  82. function apply_flow_field(map, flow_field, world)
  83. local flow_field_hidden = world and world"flow_field" and world"flow_field".hidden or true
  84. if world and world"flow_field" then
  85. world:remove"flow_field"
  86. end
  87. local overlay_group = am.group():tag"flow_field"
  88. for i,_ in pairs(map) do
  89. for j,f in pairs(map[i]) do
  90. local flow = hex_map_get(flow_field, i, j)
  91. if flow then
  92. map[i][j].priority = flow.priority
  93. overlay_group:append(am.translate(hex_to_pixel(vec2(i, j), vec2(HEX_SIZE)))
  94. ^ am.text(string.format("%.1f", flow.priority * 10)))
  95. else
  96. map[i][j].priority = nil
  97. end
  98. end
  99. end
  100. if world then
  101. overlay_group.hidden = flow_field_hidden
  102. world:append(overlay_group)
  103. end
  104. end
  105. function building_tower_breaks_flow_field(tower_type, hex)
  106. local original_elevations = {}
  107. local all_impassable = true
  108. local hexes = hex_spiral_map(hex, get_tower_size(tower_type) - 1)
  109. for _,h in pairs(hexes) do
  110. local tile = hex_map_get(game_state.map, h)
  111. if all_impassable and mob_can_pass_through(nil, h) then
  112. all_impassable = false
  113. end
  114. table.insert(original_elevations, tile.elevation)
  115. -- making the tile's elevation very large *should* make it unwalkable
  116. tile.elevation = math.huge
  117. end
  118. -- if no mobs can pass over any of the tiles we're building on
  119. -- there is no need to regenerate the flow field, or do anything more
  120. -- (besides return all the tile's elevations back to their original game_state)
  121. if all_impassable then
  122. for i,h in pairs(hexes) do
  123. hex_map_get(game_state.map, h).elevation = original_elevations[i]
  124. end
  125. return false
  126. end
  127. local flow_field = generate_flow_field(game_state.map, HEX_GRID_CENTER)
  128. local result = not hex_map_get(flow_field, 0, 0)
  129. for i,h in pairs(hexes) do
  130. hex_map_get(game_state.map, h).elevation = original_elevations[i]
  131. end
  132. return result, flow_field
  133. end
  134. function map_elevation_to_color(elevation)
  135. if elevation < -0.5 then -- lowest elevation
  136. return COLORS.WATER{ a = (elevation + 1.4) / 2 + 0.2 }
  137. elseif elevation < 0 then -- med-low elevation
  138. return math.lerp(COLORS.DIRT, COLORS.GRASS, elevation + 0.5){ a = (elevation + 1.8) / 2 + 0.3 }
  139. elseif elevation < 0.5 then -- med-high elevation
  140. return math.lerp(COLORS.DIRT, COLORS.GRASS, elevation + 0.5){ a = (elevation + 1.6) / 2 + 0.3 }
  141. elseif elevation <= 1 then -- high elevation
  142. return COLORS.MOUNTAIN{ ra = elevation }
  143. else
  144. -- this only happens when loading a save, and the tile has an elevation that's
  145. -- higher that anything here. it isn't really of any consequence though
  146. return vec4(0)
  147. end
  148. end
  149. function make_hex_node(hex, tile, color)
  150. if not color then
  151. local evenq = hex_to_evenq(vec2(hex.x, hex.y))
  152. -- light shading on edge cells
  153. local mask = vec4(0, 0, 0, math.max(((evenq.x - HEX_GRID_WIDTH/2) / HEX_GRID_WIDTH) ^ 2
  154. , ((-evenq.y - HEX_GRID_HEIGHT/2) / HEX_GRID_HEIGHT) ^ 2))
  155. color = map_elevation_to_color(tile.elevation) - mask
  156. end
  157. return am.translate(hex_to_pixel(vec2(hex.x, hex.y), vec2(HEX_SIZE)))
  158. ^ am.circle(vec2(0), HEX_SIZE, color, 6)
  159. end
  160. function make_hex_grid_scene(map, do_generate_flow_field)
  161. local world = am.group():tag"world"
  162. local texture = TEXTURES.WHITE
  163. local quads = am.quads(map.size * 2, {"vert", "vec2", "uv", "vec2", "color", "vec4"})
  164. quads.usage = "static" -- see am.buffer documentation, hint to gpu
  165. local prog = am.program([[
  166. precision highp float;
  167. attribute vec2 uv;
  168. attribute vec2 vert;
  169. attribute vec4 color;
  170. uniform mat4 MV;
  171. uniform mat4 P;
  172. varying vec2 v_uv;
  173. varying vec4 v_color;
  174. void main() {
  175. v_uv = uv;
  176. v_color = color;
  177. gl_Position = P * MV * vec4(vert, 0.0, 1.0);
  178. }
  179. ]], [[
  180. precision mediump float;
  181. uniform sampler2D texture;
  182. varying vec2 v_uv;
  183. varying vec4 v_color;
  184. void main() {
  185. gl_FragColor = texture2D(texture, v_uv) * v_color;
  186. }
  187. ]])
  188. local s60 = math.sin(math.rad(60))
  189. local c60 = math.cos(math.rad(60))
  190. for i,_ in pairs(map) do
  191. for j,tile in pairs(map[i]) do
  192. local v = vec2(i, j)
  193. local p = hex_to_pixel(v)
  194. local d = math.distance(p, vec2(0)) -- distance to center
  195. -- light shading on edge cells, scaled by distance to center
  196. local mask = vec4(0, 0, 0, 1/d)
  197. local color = map_elevation_to_color(tile.elevation) - mask
  198. local radius = HEX_SIZE
  199. quads:add_quad{
  200. vert = {
  201. p.x - c60 * radius, p.y + s60 * radius,
  202. p.x - radius, p.y,
  203. p.x + radius, p.y,
  204. p.x + c60 * radius, p.y + s60 * radius
  205. },
  206. uv = am.vec2_array{
  207. vec2(0, 0),
  208. vec2(1, 0),
  209. vec2(1, 1),
  210. vec2(0, 1)
  211. },
  212. color = color,
  213. }
  214. quads:add_quad{
  215. vert = {
  216. p.x - radius, p.y,
  217. p.x - c60 * radius, p.y - s60 * radius,
  218. p.x + c60 * radius, p.y - s60 * radius,
  219. p.x + radius, p.y
  220. },
  221. uv = am.vec2_array{
  222. vec2(0, 0),
  223. vec2(1, 0),
  224. vec2(1, 1),
  225. vec2(0, 1)
  226. },
  227. color = color,
  228. }
  229. end
  230. end
  231. world:append(am.blend("alpha") ^ am.use_program(prog) ^ am.bind{ texture = texture } ^ quads)
  232. -- add the magenta diamond that represents 'home'
  233. world:append(
  234. am.translate(hex_to_pixel(HEX_GRID_CENTER, vec2(HEX_SIZE)))
  235. ^ pack_texture_into_sprite(TEXTURES.GEM1, HEX_SIZE, HEX_SIZE*1.1)
  236. )
  237. if do_generate_flow_field then
  238. apply_flow_field(map, generate_flow_field(map, HEX_GRID_CENTER), world)
  239. end
  240. return am.translate(WORLDSPACE_COORDINATE_OFFSET) ^ world
  241. end
  242. function random_map(seed)
  243. local map = hex_rectangular_map(
  244. HEX_GRID_DIMENSIONS.x,
  245. HEX_GRID_DIMENSIONS.y,
  246. HEX_ORIENTATION.FLAT,
  247. seed,
  248. true
  249. )
  250. math.randomseed(map.seed)
  251. -- there are some things about the generated map we'd like to change...
  252. for i,_ in pairs(map) do
  253. for j,noise in pairs(map[i]) do
  254. local evenq = hex_to_evenq(vec2(i, j))
  255. if evenq.x == 0 or evenq.x == (HEX_GRID_WIDTH - 1)
  256. or -evenq.y == 0 or -evenq.y == (HEX_GRID_HEIGHT - 1) then
  257. -- if we're on an edge -- terraform edges to be passable
  258. noise = 0
  259. elseif j == HEX_GRID_CENTER.y and i == HEX_GRID_CENTER.x then
  260. -- also terraform the center of the grid to be passable
  261. -- very infrequently, but still sometimes it is not medium elevation
  262. noise = 0
  263. else
  264. -- scale noise to be closer to 0 the closer we are to the center
  265. -- @NOTE i don't know if this 100% of the time makes the center tile passable, but it seems to 99.9+% of the time
  266. -- @NOTE it doesn't. seed: 1835, 2227?
  267. local nx, ny = evenq.x/HEX_GRID_WIDTH - 0.5, -evenq.y/HEX_GRID_HEIGHT - 0.5
  268. local d = (nx^2 + ny^2)^0.5 / 0.5^0.5
  269. noise = noise * d^0.125 -- arbitrary, seems to work good
  270. end
  271. hex_map_set(map, i, j, {
  272. elevation = noise
  273. })
  274. end
  275. end
  276. return map
  277. end
  278. function default_map_editor_map(seed)
  279. if seed then
  280. return random_map(seed)
  281. end
  282. -- if there's no seed then we want a map w/ all noise values = 0
  283. local map = hex_rectangular_map(
  284. HEX_GRID_DIMENSIONS.x,
  285. HEX_GRID_DIMENSIONS.y,
  286. HEX_ORIENTATION.FLAT,
  287. seed,
  288. false
  289. )
  290. for i,_ in pairs(map) do
  291. for j,noise in pairs(map[i]) do
  292. hex_map_set(map, i, j, {
  293. elevation = noise
  294. })
  295. end
  296. end
  297. return map
  298. end