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.

556 lines
17 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
6 years ago
6 years ago
4 years ago
6 years ago
4 years ago
6 years ago
6 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
6 years ago
4 years ago
4 years ago
4 years ago
6 years ago
4 years ago
4 years ago
6 years ago
4 years ago
6 years ago
4 years ago
4 years ago
6 years ago
6 years ago
4 years ago
6 years ago
4 years ago
6 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
6 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
6 years ago
5 years ago
6 years ago
6 years ago
6 years ago
5 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
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
6 years ago
  1. -- this is a single file with no dependencies which is meant to perform a bunch of mathy stuff
  2. -- related to hexagons, grids of them, and pathfinding on them
  3. --
  4. -- it basically owes its entire existence to this resource: https://www.redblobgames.com/grids/hexagons/
  5. -- it uses some datatypes internal to the amulet game engine: http://www.amulet.xyz/
  6. -- (vec2, mat2)
  7. -- and some utility functions not present in your standard lua, like:
  8. -- table.append
  9. if not math.round then
  10. math.round = function(n) return math.floor(n + 0.5) end
  11. else
  12. error("clobbering 'math.round', oopsie!")
  13. end
  14. -- @TODO
  15. if not table.append then end
  16. if not table.filter then end
  17. -- wherever 'orientation' appears as an argument, use one of these two, or set a default just below
  18. HEX_ORIENTATION = {
  19. -- Forward & Inverse Matrices used for the Flat Orientation
  20. FLAT = {
  21. M = mat2(3.0/2.0, 0.0, 3.0^0.5/2.0, 3.0^0.5 ),
  22. W = mat2(2.0/3.0, 0.0, -1.0/3.0 , 3.0^0.5/3.0),
  23. angle = 0.0
  24. },
  25. -- Forward & Inverse Matrices used for the Pointy Orientation
  26. POINTY = {
  27. M = mat2(3.0^0.5, 3.0^0.5/2.0, 0.0, 3.0/2.0),
  28. W = mat2(3.0^0.5/3.0, -1.0/3.0, 0.0, 2.0/3.0),
  29. angle = 0.5
  30. }
  31. }
  32. -- whenever |orientation| appears as an argument, if it isn't provided, this is used instead.
  33. -- this is useful because most of the time you will only care about one orientation
  34. local HEX_DEFAULT_ORIENTATION = HEX_ORIENTATION.FLAT
  35. -- whenever |size| for a hexagon appears as an argument, if it isn't provided, use this
  36. -- 'size' here is distance from the centerpoint to any vertex in pixel
  37. local HEX_DEFAULT_SIZE = vec2(26)
  38. -- actual width (longest contained horizontal line) of the hexagon
  39. function hex_width(size, orientation)
  40. local orientation = orientation or HEX_DEFAULT_ORIENTATION
  41. if orientation == HEX_ORIENTATION.FLAT then
  42. return size * 2
  43. elseif orientation == HEX_ORIENTATION.POINTY then
  44. return math.sqrt(3) * size
  45. end
  46. end
  47. -- actual height (tallest contained vertical line) of the hexagon
  48. function hex_height(size, orientation)
  49. local orientation = orientation or HEX_DEFAULT_ORIENTATION
  50. if orientation == HEX_ORIENTATION.FLAT then
  51. return math.sqrt(3) * size
  52. elseif orientation == HEX_ORIENTATION.POINTY then
  53. return size * 2
  54. end
  55. end
  56. -- returns actual width and height of a hexagon given it's |size| which is the distance from the centerpoint to any vertex in pixels
  57. function hex_dimensions(size, orientation)
  58. local orientation = orientation or HEX_DEFAULT_ORIENTATION
  59. return vec2(hex_width(size, orientation), hex_height(size, orientation))
  60. end
  61. -- distance between two horizontally adjacent hexagon centerpoints
  62. function hex_horizontal_spacing(size, orientation)
  63. local orientation = orientation or HEX_DEFAULT_ORIENTATION
  64. if orientation == HEX_ORIENTATION.FLAT then
  65. return hex_width(size, orientation) * 3/4
  66. elseif orientation == HEX_ORIENTATION.POINTY then
  67. return hex_height(size, orientation)
  68. end
  69. end
  70. -- distance between two vertically adjacent hexagon centerpoints
  71. function hex_vertical_spacing(size, orientation)
  72. local orientation = orientation or HEX_DEFAULT_ORIENTATION
  73. if orientation == HEX_ORIENTATION.FLAT then
  74. return hex_height(size, orientation)
  75. elseif orientation == HEX_ORIENTATION.POINTY then
  76. return hex_width(size, orientation) * 3/4
  77. end
  78. end
  79. -- returns the distance between adjacent hexagon centers in a grid
  80. function hex_spacing(size, orientation)
  81. local orientation = orientation or HEX_DEFAULT_ORIENTATION
  82. return vec2(hex_horizontal_spacing(size, orientation), hex_vertical_spacing(size, orientation))
  83. end
  84. -- All Non-Diagonal Vector Directions from a Given Hex by Edge
  85. HEX_DIRECTIONS = { vec2( 1 , -1), vec2( 1 , 0), vec2(0 , 1),
  86. vec2(-1 , 1), vec2(-1 , 0), vec2(0 , -1) }
  87. -- Return Hex Vector Direction via Integer Index |direction|
  88. function hex_direction(direction)
  89. return HEX_DIRECTIONS[(direction % 6) % 6 + 1]
  90. end
  91. -- Return Hexagon Adjacent to |hex| in Integer Index |direction|
  92. function hex_neighbour(hex, direction)
  93. return hex + HEX_DIRECTIONS[(direction % 6) % 6 + 1]
  94. end
  95. -- Collect All 6 Neighbours in a Table
  96. function hex_neighbours(hex)
  97. local neighbours = {}
  98. for i = 1, 6 do
  99. table.insert(neighbours, hex_neighbour(hex, i))
  100. end
  101. return neighbours
  102. end
  103. -- Returns a vec2 Which is the Nearest |x, y| to Float Trio |x, y, z|
  104. -- assumes you have a working math.round function (should be guarded at top of this file)
  105. local function hex_round(x, y, z)
  106. local rx = math.round(x)
  107. local ry = math.round(y)
  108. local rz = math.round(z) or math.round(-x - y)
  109. local xdelta = math.abs(rx - x)
  110. local ydelta = math.abs(ry - y)
  111. local zdelta = math.abs(rz - z or math.round(-x - y))
  112. if xdelta > ydelta and xdelta > zdelta then
  113. rx = -ry - rz
  114. elseif ydelta > zdelta then
  115. ry = -rx - rz
  116. else
  117. rz = -rx - ry
  118. end
  119. return vec2(rx, ry)
  120. end
  121. -- Hex to Screen -- Orientation Must be Either POINTY or FLAT
  122. function hex_to_pixel(hex, size, orientation)
  123. local M = orientation and orientation.M or HEX_DEFAULT_ORIENTATION.M
  124. local x = (M[1][1] * hex[1] + M[1][2] * hex[2]) * (size and size[1] or HEX_DEFAULT_SIZE[1])
  125. local y = (M[2][1] * hex[1] + M[2][2] * hex[2]) * (size and size[2] or HEX_DEFAULT_SIZE[2])
  126. return vec2(x, y)
  127. end
  128. -- Screen to Hex -- Orientation Must be Either POINTY or FLAT
  129. function pixel_to_hex(pix, size, orientation)
  130. local W = orientation and orientation.W or HEX_DEFAULT_ORIENTATION.W
  131. local pix = pix / (size or vec2(HEX_DEFAULT_SIZE))
  132. local x = W[1][1] * pix[1] + W[1][2] * pix[2]
  133. local y = W[2][1] * pix[1] + W[2][2] * pix[2]
  134. return hex_round(x, y, -x - y)
  135. end
  136. -- TODO test, learn am.draw
  137. function hex_corner_offset(corner, size, orientation)
  138. local orientation = orientation or HEX_DEFAULT_ORIENTATION
  139. local angle = 2.0 * math.pi * orientation.angle + corner / 6
  140. return vec2(size[1] * math.cos(angle), size[2] * math.sin(angle))
  141. end
  142. -- TODO test this thing
  143. function hex_corners(hex, size, orientation)
  144. local orientation = orientation or HEX_DEFAULT_ORIENTATION
  145. local corners = {}
  146. local center = hex_to_pixel(hex, size, orientation)
  147. for i = 0, 5 do
  148. local offset = hex_corner_offset(i, size, orientation)
  149. table.insert(corners, center + offset)
  150. end
  151. return corners
  152. end
  153. -- @TODO test
  154. function hex_to_oddr(hex)
  155. local z = -hex.x - hex.y
  156. return vec2(hex.x + (z - (z % 2)) / 2)
  157. end
  158. -- @TODO test
  159. function oddr_to_hex(oddr)
  160. return vec2(hex.x - (hex.y - (hex.y % 2)) / 2, -hex.x - hex.y)
  161. end
  162. -- @TODO test
  163. function hex_to_evenr(hex)
  164. local z = -hex.x - hex.y
  165. return vec2(hex.x + (z + (z % 2)) / 2, z)
  166. end
  167. -- @TODO test
  168. function evenr_to_hex(evenr)
  169. return vec2(hex.x - (hex.y + (hex.y % 2)) / 2, -hex.x - hex.y)
  170. end
  171. -- @TODO test
  172. function hex_to_oddq(hex)
  173. return vec2(hex.x, -hex.x - hex.y + (hex.x - (hex.x % 2)) / 2)
  174. end
  175. -- @TODO test
  176. function oddq_to_hex(oddq)
  177. return vec2(hex.x, -hex.x - (hex.y - (hex.x - (hex.y % 2)) / 2))
  178. end
  179. function hex_to_evenq(hex)
  180. return vec2(hex.x, (-hex.x - hex.y) + (hex.x + (hex.x % 2)) / 2)
  181. end
  182. function evenq_to_hex(evenq)
  183. return vec2(evenq.x, -evenq.x - (evenq.y - (evenq.x + (evenq.x % 2)) / 2))
  184. end
  185. --============================================================================
  186. -- MAPS & STORAGE
  187. -- maps that use their indices as the hex coordinates (parallelogram, hexagonal, rectangular, triangular),
  188. -- fail to serialize ideally because they use negative indices, which json doesn't support
  189. -- Returns Ordered Ring-Shaped Map of |radius| from |center|
  190. function hex_ring_map(center, radius)
  191. local map = {}
  192. local walk = center + HEX_DIRECTIONS[6] * radius
  193. for i = 1, 6 do
  194. for j = 1, radius do
  195. table.insert(map, walk)
  196. walk = hex_neighbour(walk, i)
  197. end
  198. end
  199. return setmetatable(map, {__index={center=center, radius=radius}})
  200. end
  201. -- Returns Ordered Spiral Hexagonal Map of |radius| Rings from |center|
  202. function hex_spiral_map(center, radius)
  203. local map = { center }
  204. for i = 1, radius do
  205. table.append(map, hex_ring_map(center, i))
  206. end
  207. return setmetatable(map, {__index={center=center, radius=radius}})
  208. end
  209. function hex_map_get(map, hex, y)
  210. if y then return map[hex] and map[hex][y] end
  211. return map[hex.x] and map[hex.x][hex.y]
  212. end
  213. function hex_map_set(map, hex, y, v)
  214. if v then
  215. if map[hex] then
  216. map[hex][y] = v
  217. else
  218. map[hex] = {}
  219. map[hex][y] = v
  220. end
  221. else
  222. if map[hex.x] then
  223. map[hex.x][hex.y] = y
  224. else
  225. map[hex.x] = {}
  226. map[hex.x][hex.y] = y
  227. end
  228. end
  229. end
  230. -- Returns Unordered Parallelogram-Shaped Map of |width| and |height| with Simplex Noise
  231. function hex_parallelogram_map(width, height, seed)
  232. local seed = seed or math.random(width * height)
  233. local map = {}
  234. for i = 0, width - 1 do
  235. map[i] = {}
  236. for j = 0, height - 1 do
  237. -- Calculate Noise
  238. local idelta = i / width
  239. local jdelta = j / height
  240. local noise = 0
  241. for oct = 1, 6 do
  242. local f = 1/4^oct
  243. local l = 2^oct
  244. local pos = vec2(idelta + seed * width, jdelta + seed * height)
  245. noise = noise + f * math.simplex(pos * l)
  246. end
  247. map[i][j] = noise
  248. end
  249. end
  250. return setmetatable(map, { __index = {
  251. width = width,
  252. height = height,
  253. seed = seed,
  254. neighbours = function(hex)
  255. return table.filter(hex_neighbours(hex), function(_hex)
  256. return hex_map_get(map, _hex)
  257. end)
  258. end
  259. }})
  260. end
  261. -- Returns Unordered Triangular (Equilateral) Map of |size| with Simplex Noise
  262. function hex_triangular_map(size, seed)
  263. local seed = seed or math.random(size * math.cos(size) / 2)
  264. local map = {}
  265. for i = 0, size do
  266. map[i] = {}
  267. for j = size - i, size do
  268. -- Generate Noise
  269. local idelta = i / size
  270. local jdelta = j / size
  271. local noise = 0
  272. for oct = 1, 6 do
  273. local f = 1/3^oct
  274. local l = 2^oct
  275. local pos = vec2(idelta + seed * size, jdelta + seed * size)
  276. noise = noise + f * math.simplex(pos * l)
  277. end
  278. map[i][j] = noise
  279. end
  280. end
  281. return setmetatable(map, { __index = {
  282. size = size,
  283. seed = seed,
  284. neighbours = function(hex)
  285. return table.filter(hex_neighbours(hex), function(_hex)
  286. return hex_map_get(map, _hex)
  287. end)
  288. end
  289. }})
  290. end
  291. -- Returns Unordered Hexagonal Map of |radius| with Simplex Noise
  292. function hex_hexagonal_map(radius, seed)
  293. local seed = seed or math.random(radius * 2 * math.pi)
  294. local map = {}
  295. for i = -radius, radius do
  296. map[i] = {}
  297. local j1 = math.max(-radius, -i - radius)
  298. local j2 = math.min(radius, -i + radius)
  299. for j = j1, j2 do
  300. -- Calculate Noise
  301. local idelta = i / radius
  302. local jdelta = j / radius
  303. local noise = 0
  304. for oct = 1, 6 do
  305. local f = 2/3^oct
  306. local l = 2^oct
  307. local pos = vec2(idelta + seed * radius, jdelta + seed * radius)
  308. noise = noise + f * math.simplex(pos * l)
  309. end
  310. map[i][j] = noise
  311. end
  312. end
  313. return setmetatable(map, { __index = {
  314. radius = radius,
  315. seed = seed,
  316. neighbours = function(hex)
  317. return table.filter(hex_neighbours(hex), function(_hex)
  318. return hex_map_get(map, _hex.x, _hex.y)
  319. end)
  320. end
  321. }})
  322. end
  323. -- Returns Unordered Rectangular Map of |width| and |height| with Simplex Noise
  324. function hex_rectangular_map(width, height, orientation, seed)
  325. local orientation = orientation or HEX_DEFAULT_ORIENTATION
  326. local seed = seed or math.random(width * height)
  327. local map = {}
  328. if orientation == HEX_ORIENTATION.FLAT then
  329. for i = 0, width - 1 do
  330. map[i] = {}
  331. for j = 0, height - 1 do
  332. -- begin to calculate noise
  333. local idelta = i / width
  334. local jdelta = j / height
  335. local noise = 0
  336. for oct = 1, 6 do
  337. local f = 2/3^oct
  338. local l = 2^oct
  339. local pos = vec2(idelta + seed * width, jdelta + seed * height)
  340. noise = noise + f * math.simplex(pos * l)
  341. end
  342. j = j - math.floor(i/2) -- this is what makes it rectangular
  343. map[i][j] = noise
  344. end
  345. end
  346. elseif orientation == HEX_ORIENTATION.POINTY then
  347. for i = 0, height - 1 do
  348. local i_offset = math.floor(i/2)
  349. for j = -i_offset, width - i_offset - 1 do
  350. hex_map_set(map, j, i, 0)
  351. end
  352. end
  353. else
  354. error("bad orientation value")
  355. end
  356. return setmetatable(map, { __index = {
  357. width = width,
  358. height = height,
  359. seed = seed,
  360. neighbours = function(hex)
  361. return table.filter(hex_neighbours(hex), function(_hex)
  362. return hex_map_get(map, _hex)
  363. end)
  364. end
  365. }})
  366. end
  367. --============================================================================
  368. -- PATHFINDING
  369. -- note:
  370. -- i kinda feel like after implementing these and making the game, there are tons of reasons
  371. -- why you might want to specialize pathfinding, like you would any other kind of algorithm
  372. --
  373. -- so, while (in theory) these algorithms work with the maps in this file, your maps and game
  374. -- will have lots of other data which you may want your pathfinding algorithms to care about in some way,
  375. -- that these don't.
  376. --
  377. function hex_breadth_first(map, start, neighbour_f)
  378. local frontier = {}
  379. frontier[1] = start
  380. local distance = {}
  381. hex_map_set(distance, start, 0)
  382. while not (#frontier == 0) do
  383. local current = table.remove(frontier, 1)
  384. for _,neighbour in pairs(neighbour_f(map, current)) do
  385. local d = hex_map_get(distance, neighbour)
  386. if not d then
  387. table.insert(frontier, neighbour)
  388. local current_distance = hex_map_get(distance, current)
  389. hex_map_set(distance, neighbour, current_distance + 1)
  390. end
  391. end
  392. end
  393. return distance
  394. end
  395. function hex_dijkstra(map, start, goal, neighbour_f, cost_f)
  396. local frontier = {}
  397. frontier[1] = { hex = start, priority = 0 }
  398. local came_from = {}
  399. hex_map_set(came_from, start, false)
  400. local cost_so_far = {}
  401. hex_map_set(cost_so_far, start, 0)
  402. while not (#frontier == 0) do
  403. local current = table.remove(frontier, 1)
  404. if goal and current.hex == goal then
  405. break
  406. end
  407. for _,neighbour in pairs(neighbour_f(map, current.hex)) do
  408. local new_cost = hex_map_get(cost_so_far, current.hex) + cost_f(map, current.hex, neighbour)
  409. local neighbour_cost = hex_map_get(cost_so_far, neighbour)
  410. if not neighbour_cost or (new_cost < neighbour_cost) then
  411. hex_map_set(cost_so_far, neighbour, new_cost)
  412. local priority = new_cost + math.distance(start, neighbour)
  413. table.insert(frontier, { hex = neighbour, priority = priority })
  414. hex_map_set(came_from, neighbour, current)
  415. end
  416. end
  417. end
  418. return came_from
  419. end
  420. -- A* pathfinding
  421. --
  422. -- |heuristic| has the form:
  423. -- function(source, target) -- source and target are vec2's
  424. -- return some numeric value
  425. --
  426. -- |cost_f| has the form:
  427. -- function (from, to) -- from and to are vec2's
  428. -- return some numeric value
  429. --
  430. function hex_Astar(map, start, goal, neighbour_f, cost_f, heuristic)
  431. local path = {}
  432. hex_map_set(path, start, false)
  433. local frontier = {}
  434. frontier[1] = { hex = start, priority = 0 }
  435. local path_so_far = {}
  436. hex_map_set(path_so_far, start, 0)
  437. local made_it = false
  438. while not (#frontier == 0) do
  439. local current = table.remove(frontier, 1)
  440. if current.hex == goal then
  441. made_it = true
  442. break
  443. end
  444. for _,next_ in pairs(neighbour_f(map, current.hex)) do
  445. local new_cost = hex_map_get(path_so_far, current.hex) + cost_f(map, current.hex, next_)
  446. local next_cost = hex_map_get(path_so_far, next_)
  447. if not next_cost or new_cost < next_cost then
  448. hex_map_set(path_so_far, next_, new_cost)
  449. local priority = new_cost + heuristic(goal, next_)
  450. table.insert(frontier, { hex = next_, priority = priority })
  451. hex_map_set(path, next_, current)
  452. end
  453. end
  454. end
  455. return path, made_it
  456. end