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.

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