Browse Source

refactor hexyz

master
Nicholas Hayashi 4 years ago
parent
commit
471aab4846
  1. 62
      main.lua
  2. BIN
      res/shaded_hex1.png
  3. 4
      src/extra.lua
  4. 24
      src/game.lua
  5. 66
      src/grid.lua
  6. 123
      src/gui.lua
  7. 157
      src/hexyz.lua
  8. 14
      src/mob.lua
  9. 6
      src/projectile.lua
  10. 19
      src/tower.lua
  11. 1
      texture.lua

62
main.lua

@ -26,7 +26,7 @@ do
} }
end end
-- assets and/or trivial code
-- asset interfaces and/or trivial code
require "color" require "color"
require "sound" require "sound"
require "texture" require "texture"
@ -42,10 +42,64 @@ require "src/mob"
require "src/projectile" require "src/projectile"
require "src/tower" require "src/tower"
function main_action() end
function main_scene() end
function main_action(self)
self"hex_backdrop""rotate".angle = math.wrapf(self"hex_backdrop""rotate".angle - 0.002 * am.delta_time, math.pi*2)
end
function make_main_scene_toolbelt()
local options = {
{
label = "new game",
action = function(self) end
},
{
label = "load game",
action = function(self) game_init(am.load_state("save", "json")) end
},
{
label = "map editor",
action = function(self) log("map editor not implemented") end
},
{
label = "settings",
action = function(self) end
},
}
--local map = hex_rectangular_map(10, 20, HEX_ORIENTATION.POINTY)
return group
end
function main_scene()
local group = am.group()
local map = hex_hexagonal_map(30)
local hex_backdrop = (am.rotate(0) ^ am.group()):tag"hex_backdrop"
for i,_ in pairs(map) do
for j,n in pairs(map[i]) do
local color = map_elevation_color(n)
color = color{a=color.a - 0.1}
local node = am.translate(hex_to_pixel(vec2(i, j), vec2(HEX_SIZE)))
^ am.circle(vec2(0), HEX_SIZE, vec4(0), 6)
node"circle":action(am.tween(1, { color = color }))
hex_backdrop:append(node)
end
end
group:append(hex_backdrop)
group:append(am.translate(0, 200) ^ am.sprite("res/logo.png"))
group:append(make_main_scene_toolbelt())
group:action(main_action)
return group
end
win.scene = am.group() win.scene = am.group()
game_init(nil)
game_init()
noglobals() noglobals()

BIN
res/shaded_hex1.png

After

Width: 268  |  Height: 308  |  Size: 55 KiB

4
src/extra.lua

@ -9,10 +9,6 @@ function fprofile(f, ...)
return result return result
end end
function booltostring(bool)
return bool and "true" or "false"
end
function math.wrapf(float, range) function math.wrapf(float, range)
return float - range * math.floor(float / range) return float - range * math.floor(float / range)
end end

24
src/game.lua

@ -82,7 +82,7 @@ local function get_top_right_display_text(hex, evenq, centered_evenq, display_ty
str = "SEED: " .. state.map.seed str = "SEED: " .. state.map.seed
elseif display_type == TRDTS.TILE then elseif display_type == TRDTS.TILE then
str = table.tostring(map_get(state.map, hex))
str = table.tostring(hex_map_get(state.map, hex))
end end
return str return str
end end
@ -247,7 +247,7 @@ local function game_action(scene)
local evenq = hex_to_evenq(hex) local evenq = hex_to_evenq(hex)
local centered_evenq = evenq{ y = -evenq.y } - vec2(math.floor(HEX_GRID_WIDTH/2) local centered_evenq = evenq{ y = -evenq.y } - vec2(math.floor(HEX_GRID_WIDTH/2)
, math.floor(HEX_GRID_HEIGHT/2)) , math.floor(HEX_GRID_HEIGHT/2))
local tile = map_get(state.map, hex)
local tile = hex_map_get(state.map, hex)
local interactable = evenq_is_in_interactable_region(evenq{ y = -evenq.y }) local interactable = evenq_is_in_interactable_region(evenq{ y = -evenq.y })
local buildable = tower_type_is_buildable_on(hex, tile, state.selected_tower_type) local buildable = tower_type_is_buildable_on(hex, tile, state.selected_tower_type)
@ -416,7 +416,8 @@ local function make_game_toolbelt()
local toolbelt = am.group{ local toolbelt = am.group{
am.group():tag"tower_tooltip_text", am.group():tag"tower_tooltip_text",
am.rect(win.left, win.bottom, win.right, win.bottom + toolbelt_height, COLORS.TRANSPARENT) am.rect(win.left, win.bottom, win.right, win.bottom + toolbelt_height, COLORS.TRANSPARENT)
}:tag"toolbelt"
}
:tag"toolbelt"
local padding = 15 local padding = 15
local size = toolbelt_height - padding local size = toolbelt_height - padding
@ -430,7 +431,9 @@ local function make_game_toolbelt()
local tower_select_square = ( local tower_select_square = (
am.translate(vec2(size + padding, half_size) + offset) am.translate(vec2(size + padding, half_size) + offset)
^ am.rect(-size/2-3, -size/2-3, size/2+3, size/2+3, COLORS.SUNRAY) ^ am.rect(-size/2-3, -size/2-3, size/2+3, size/2+3, COLORS.SUNRAY)
):tag"tower_select_square"
)
:tag"tower_select_square"
tower_select_square.hidden = true tower_select_square.hidden = true
toolbelt:append(tower_select_square) toolbelt:append(tower_select_square)
@ -503,7 +506,7 @@ end
-- optionally, |action_f| is a function that operates on the group node every frame -- optionally, |action_f| is a function that operates on the group node every frame
function make_hex_cursor(radius, color_f, action_f) function make_hex_cursor(radius, color_f, action_f)
local color = type(color_f) == "userdata" and color_f or nil local color = type(color_f) == "userdata" and color_f or nil
local map = spiral_map(vec2(0), radius)
local map = hex_spiral_map(vec2(0), radius)
local group = am.group() local group = am.group()
for _,h in pairs(map) do for _,h in pairs(map) do
@ -557,21 +560,14 @@ end
function game_init(saved_state) function game_init(saved_state)
if saved_state then if saved_state then
state = game_deserialize(saved_state) state = game_deserialize(saved_state)
-- scene nodes aren't (can't be?) serialized, so we re-generate them if we're loading from a save
else else
state = get_initial_game_state() state = get_initial_game_state()
end
--[[
local home_tower = build_tower(HEX_GRID_CENTER, TOWER_TYPE.RADAR) local home_tower = build_tower(HEX_GRID_CENTER, TOWER_TYPE.RADAR)
for _,h in pairs(home_tower.hexes) do for _,h in pairs(home_tower.hexes) do
-- @HACK to make the center tile(s) passable even though there's a tower on it -- @HACK to make the center tile(s) passable even though there's a tower on it
map_get(state.map, h).elevation = 0
hex_map_get(state.map, h).elevation = 0
end
end end
]]
win.scene:remove("game") win.scene:remove("game")
win.scene:append(game_scene()) win.scene:append(game_scene())

66
src/grid.lua

@ -3,8 +3,8 @@
-- distance from hex centerpoint to any vertex -- distance from hex centerpoint to any vertex
HEX_SIZE = 26 HEX_SIZE = 26
HEX_PIXEL_WIDTH = hex_width(HEX_SIZE, ORIENTATION.FLAT)
HEX_PIXEL_HEIGHT = hex_height(HEX_SIZE, ORIENTATION.FLAT)
HEX_PIXEL_WIDTH = hex_width(HEX_SIZE, HEX_ORIENTATION.FLAT)
HEX_PIXEL_HEIGHT = hex_height(HEX_SIZE, HEX_ORIENTATION.FLAT)
HEX_PIXEL_DIMENSIONS = vec2(HEX_PIXEL_WIDTH, HEX_PIXEL_HEIGHT) HEX_PIXEL_DIMENSIONS = vec2(HEX_PIXEL_WIDTH, HEX_PIXEL_HEIGHT)
do do
@ -80,7 +80,7 @@ function grid_heuristic(source, target)
end end
function grid_cost(map, from, to) function grid_cost(map, from, to)
local t1, t2 = map_get(map, from), map_get(map, to)
local t1, t2 = hex_map_get(map, from), hex_map_get(map, to)
-- i have no fucking clue why, but adding +0.2 to the end of this fixes a bug where sometimes two (or more) -- i have no fucking clue why, but adding +0.2 to the end of this fixes a bug where sometimes two (or more)
-- equivalent paths are found and mobs backpedal trying to decide between them -- equivalent paths are found and mobs backpedal trying to decide between them
@ -97,13 +97,13 @@ end
function grid_neighbours(map, hex) function grid_neighbours(map, hex)
return table.filter(hex_neighbours(hex), function(_hex) return table.filter(hex_neighbours(hex), function(_hex)
local tile = map_get(map, _hex)
local tile = hex_map_get(map, _hex)
return tile and tile_is_medium_elevation(tile) return tile and tile_is_medium_elevation(tile)
end) end)
end end
function generate_flow_field(map, start) function generate_flow_field(map, start)
return dijkstra(map, start, nil, grid_cost, grid_neighbours)
return hex_dijkstra(map, start, nil, grid_cost, grid_neighbours)
end end
function apply_flow_field(map, flow_field, world) function apply_flow_field(map, flow_field, world)
@ -115,7 +115,7 @@ function apply_flow_field(map, flow_field, world)
local overlay_group = am.group():tag"flow_field" local overlay_group = am.group():tag"flow_field"
for i,_ in pairs(map) do for i,_ in pairs(map) do
for j,f in pairs(map[i]) do for j,f in pairs(map[i]) do
local flow = map_get(flow_field, i, j)
local flow = hex_map_get(flow_field, i, j)
if flow then if flow then
map[i][j].priority = flow.priority map[i][j].priority = flow.priority
@ -134,38 +134,12 @@ function apply_flow_field(map, flow_field, world)
end end
end end
-- some convenience functions for setting and retrieving values from a 2d sparse array
-- where the first index might return a nil value, causing the second second to crash the game
-- and where it's often the case that the indexer is a vec2
function map_get(map, hex, y)
if y then return map[hex] and map[hex][y] end
return map[hex.x] and map[hex.x][hex.y]
end
function map_set(map, hex, y, v)
if v then
if map[hex] then
map[hex][y] = v
else
map[hex] = {}
map[hex][y] = v
end
else
if map[hex.x] then
map[hex.x][hex.y] = y
else
map[hex.x] = {}
map[hex.x][hex.y] = y
end
end
end
function building_tower_breaks_flow_field(tower_type, hex) function building_tower_breaks_flow_field(tower_type, hex)
local original_elevations = {} local original_elevations = {}
local all_impassable = true local all_impassable = true
local hexes = spiral_map(hex, get_tower_size(tower_type))
local hexes = hex_spiral_map(hex, get_tower_size(tower_type))
for _,h in pairs(hexes) do for _,h in pairs(hexes) do
local tile = map_get(state.map, h)
local tile = hex_map_get(state.map, h)
if all_impassable and mob_can_pass_through(nil, h) then if all_impassable and mob_can_pass_through(nil, h) then
all_impassable = false all_impassable = false
@ -182,23 +156,22 @@ function building_tower_breaks_flow_field(tower_type, hex)
-- (besides return all the tile's elevations back to their original state) -- (besides return all the tile's elevations back to their original state)
if all_impassable then if all_impassable then
for i,h in pairs(hexes) do for i,h in pairs(hexes) do
map_get(state.map, h).elevation = original_elevations[i]
hex_map_get(state.map, h).elevation = original_elevations[i]
end end
return false return false
end end
local flow_field = generate_flow_field(state.map, HEX_GRID_CENTER) local flow_field = generate_flow_field(state.map, HEX_GRID_CENTER)
local result = not map_get(flow_field, 0, 0)
local result = not hex_map_get(flow_field, 0, 0)
for i,h in pairs(hexes) do for i,h in pairs(hexes) do
map_get(state.map, h).elevation = original_elevations[i]
hex_map_get(state.map, h).elevation = original_elevations[i]
end end
return result, flow_field return result, flow_field
end end
function make_hex_grid_scene(map)
local function color_at(elevation)
function map_elevation_color(elevation)
if elevation < -0.5 then -- lowest elevation if elevation < -0.5 then -- lowest elevation
return COLORS.WATER{ a = (elevation + 1.4) / 2 + 0.2 } return COLORS.WATER{ a = (elevation + 1.4) / 2 + 0.2 }
@ -217,6 +190,8 @@ function make_hex_grid_scene(map)
return vec4(0.1) return vec4(0.1)
end end
end end
function make_hex_grid_scene(map)
-- the world's appearance relies largely on a backdrop which can be scaled in -- the world's appearance relies largely on a backdrop which can be scaled in
-- tone to give the appearance of light or darkness -- tone to give the appearance of light or darkness
-- @NOTE replace this with a shader program -- @NOTE replace this with a shader program
@ -239,12 +214,12 @@ function make_hex_grid_scene(map)
local mask = vec4(0, 0, 0, math.max(((evenq.x - HEX_GRID_WIDTH/2) / HEX_GRID_WIDTH) ^ 2 local mask = vec4(0, 0, 0, math.max(((evenq.x - HEX_GRID_WIDTH/2) / HEX_GRID_WIDTH) ^ 2
, ((-evenq.y - HEX_GRID_HEIGHT/2) / HEX_GRID_HEIGHT) ^ 2)) , ((-evenq.y - HEX_GRID_HEIGHT/2) / HEX_GRID_HEIGHT) ^ 2))
local color = color_at(tile.elevation) - mask
local color = map_elevation_color(tile.elevation) - mask
local node = am.translate(hex_to_pixel(vec2(i, j), vec2(HEX_SIZE))) local node = am.translate(hex_to_pixel(vec2(i, j), vec2(HEX_SIZE)))
^ am.circle(vec2(0), HEX_SIZE, color, 6) ^ am.circle(vec2(0), HEX_SIZE, color, 6)
map_set(map, i, j, {
hex_map_set(map, i, j, {
elevation = tile.elevation, elevation = tile.elevation,
node = node node = node
}) })
@ -259,7 +234,12 @@ function make_hex_grid_scene(map)
end end
function random_map(seed) function random_map(seed)
local map = rectangular_map(HEX_GRID_DIMENSIONS.x, HEX_GRID_DIMENSIONS.y, seed)
local map = hex_rectangular_map(
HEX_GRID_DIMENSIONS.x,
HEX_GRID_DIMENSIONS.y,
HEX_ORIENTATION.FLAT,
seed
)
math.randomseed(map.seed) math.randomseed(map.seed)
-- there are some things about the generated map we'd like to change... -- there are some things about the generated map we'd like to change...
@ -286,7 +266,7 @@ function random_map(seed)
noise = noise * d^0.125 -- arbitrary, seems to work good noise = noise * d^0.125 -- arbitrary, seems to work good
end end
map_set(map, i, j, {
hex_map_set(map, i, j, {
elevation = noise, elevation = noise,
}) })
end end

123
src/gui.lua

@ -4,19 +4,25 @@ function gui_numberfield(dimensions, opts)
end end
function gui_textfield(position, dimensions, max, disallowed_keys)
function gui_textfield(position, dimensions, max, disallowed_chars)
local width, height = dimensions.x, dimensions.y local width, height = dimensions.x, dimensions.y
local disallowed_keys = disallowed_keys or {}
local max = max or 99
local padding = 2
local outer_rect = am.rect(-width/2, -height/2, width/2, height/2, COLORS.VERY_DARK_GRAY)
local inner_rect = am.rect(-width/2 + padding
, -height/2 + padding
, width/2 - padding
, height/2 - padding
, COLORS.PALE_SILVER)
local disallowed_chars = disallowed_chars or {}
local max = max or 10
local outer_rect = am.rect(
-width/2,
-height/2,
width/2,
height/2,
COLORS.VERY_DARK_GRAY
)
local inner_rect = am.rect(
-width/2 + 1,
-height/2 + 1,
width/2 - 2,
height/2 - 2,
COLORS.PALE_SILVER
)
local group = am.group{ local group = am.group{
outer_rect, outer_rect,
@ -29,29 +35,90 @@ function gui_textfield(position, dimensions, max, disallowed_keys)
local keys = win:keys_pressed() local keys = win:keys_pressed()
if #keys == 0 then return end if #keys == 0 then return end
-- @HACK all characters and digits are represented by a single string in amulet
-- so we don't have to iterate over everything
-- pattern matching doesn't work because control characters are also just normal strings
local chars = {}
local shift = win:key_down("lshift") or win:key_down("rshift")
for i,k in pairs(keys) do for i,k in pairs(keys) do
if not disallowed_keys[k] then
if k:len() == 1 then
if string.match(k, "%d") then
self"text".text = self"text".text .. k
elseif win:key_down("lshift") or win:key_down("rshift") then
self"text".text = self"text".text .. k:upper()
if k:len() == 1 then -- @HACK alphabetical or digit characters
if string.match(k, "%a") then
if shift then
table.insert(chars, k:upper())
else else
self"text".text = self"text".text .. k
table.insert(chars, k)
end end
elseif k == "space" then
self"text".text = self"text".text .. " "
elseif string.match(k, "%d") then
if shift then
if k == "1" then table.insert(chars, "!")
elseif k == "2" then table.insert(chars, "@")
elseif k == "3" then table.insert(chars, "#")
elseif k == "4" then table.insert(chars, "$")
elseif k == "5" then table.insert(chars, "%")
elseif k == "6" then table.insert(chars, "^")
elseif k == "7" then table.insert(chars, "&")
elseif k == "8" then table.insert(chars, "*")
elseif k == "9" then table.insert(chars, "(")
elseif k == "0" then table.insert(chars, ")")
end
else
table.insert(chars, k)
end
end
-- begin non-alphabetical/digit
elseif k == "minus" then
if shift then table.insert(chars, "_")
else table.insert(chars, "-") end
elseif k == "equals" then
if shift then table.insert(chars, "=")
else table.insert(chars, "+") end
elseif k == "leftbracket" then
if shift then table.insert(chars, "{")
else table.insert(chars, "[") end
elseif k == "rightbracket" then
if shift then table.insert(chars, "}")
else table.insert(chars, "]") end
elseif k == "backslash" then
if shift then table.insert(chars, "|")
else table.insert(chars, "\\") end
elseif k == "semicolon" then
if shift then table.insert(chars, ":")
else table.insert(chars, ";") end
elseif k == "quote" then
if shift then table.insert(chars, "\"")
else table.insert(chars, "'") end
elseif k == "backquote" then
if shift then table.insert(chars, "~")
else table.insert(chars, "`") end
elseif k == "comma" then
if shift then table.insert(chars, "<")
else table.insert(chars, ",") end
elseif k == "period" then
if shift then table.insert(chars, ">")
else table.insert(chars, ".") end
elseif k == "slash" then
if shift then table.insert(chars, "?")
else table.insert(chars, "/") end
-- control characters
elseif k == "backspace" then elseif k == "backspace" then
-- @NOTE this doesn't preserve the order of chars in the array so if
-- someone presses a the key "a" then the backspace key in the same frame, in that order
-- the backspace occurs first
self"text".text = self"text".text:sub(1, self"text".text:len() - 1) self"text".text = self"text".text:sub(1, self"text".text:len() - 1)
elseif k == "enter" then
elseif k == "tab" then
-- @TODO
elseif k == "space" then
table.insert(chars, " ")
elseif k == "capslock" then
-- @OTOD
end
end
for _,c in pairs(chars) do
if not disallowed_chars[c] then
if self"text".text:len() <= max then
self"text".text = self"text".text .. c
end end
end end
end end

157
src/hexyz.lua

@ -1,14 +1,26 @@
-- this is a single file with no dependencies which is meant to perform a bunch of mathy stuff
-- related to hexagons, grids of them, and pathfinding on them
--
-- it basically owes its entire existence to this resource: https://www.redblobgames.com/grids/hexagons/
-- it uses some datatypes internal to the amulet game engine: http://www.amulet.xyz/
-- (vec2, mat2)
-- and some utility functions not present in your standard lua, like:
-- table.append
if not math.round then if not math.round then
math.round = function(n) return math.floor(n + 0.5) end math.round = function(n) return math.floor(n + 0.5) end
else else
log("clobbering a math.round function.")
error("clobbering a math.round function, oopsie!")
end
if not table.append then
end end
-- wherever 'orientation' appears as an argument, use one of these two, or set a default just below -- wherever 'orientation' appears as an argument, use one of these two, or set a default just below
ORIENTATION = {
HEX_ORIENTATION = {
-- Forward & Inverse Matrices used for the Flat Orientation -- Forward & Inverse Matrices used for the Flat Orientation
FLAT = { FLAT = {
M = mat2(3.0/2.0, 0.0, 3.0^0.5/2.0, 3.0^0.5 ), M = mat2(3.0/2.0, 0.0, 3.0^0.5/2.0, 3.0^0.5 ),
@ -23,70 +35,71 @@ ORIENTATION = {
} }
} }
-- whenver |orientation| appears as an argument, if it isn't provided, this is used instead.
local DEFAULT_ORIENTATION = ORIENTATION.FLAT
-- whenever |orientation| appears as an argument, if it isn't provided, this is used instead.
-- this is useful because most of the time you will only care about one orientation
local HEX_DEFAULT_ORIENTATION = HEX_ORIENTATION.FLAT
-- whenever |size| for a hexagon appears as an argument, if it isn't provided, use this -- whenever |size| for a hexagon appears as an argument, if it isn't provided, use this
-- 'size' here is distance from the centerpoint to any vertex in pixel -- 'size' here is distance from the centerpoint to any vertex in pixel
local DEFAULT_HEX_SIZE = vec2(20)
local HEX_DEFAULT_SIZE = vec2(20)
-- actual width (longest contained horizontal line) of the hexagon -- actual width (longest contained horizontal line) of the hexagon
function hex_width(size, orientation) function hex_width(size, orientation)
local orientation = orientation or DEFAULT_ORIENTATION
local orientation = orientation or HEX_DEFAULT_ORIENTATION
if orientation == ORIENTATION.FLAT then
if orientation == HEX_ORIENTATION.FLAT then
return size * 2 return size * 2
elseif orientation == ORIENTATION.POINTY then
elseif orientation == HEX_ORIENTATION.POINTY then
return math.sqrt(3) * size return math.sqrt(3) * size
end end
end end
-- actual height (tallest contained vertical line) of the hexagon -- actual height (tallest contained vertical line) of the hexagon
function hex_height(size, orientation) function hex_height(size, orientation)
local orientation = orientation or DEFAULT_ORIENTATION
local orientation = orientation or HEX_DEFAULT_ORIENTATION
if orientation == ORIENTATION.FLAT then
if orientation == HEX_ORIENTATION.FLAT then
return math.sqrt(3) * size return math.sqrt(3) * size
elseif orientation == ORIENTATION.POINTY then
elseif orientation == HEX_ORIENTATION.POINTY then
return size * 2 return size * 2
end end
end end
-- returns actual width and height of a hexagon given it's |size| which is the distance from the centerpoint to any vertex in pixels -- returns actual width and height of a hexagon given it's |size| which is the distance from the centerpoint to any vertex in pixels
function hex_dimensions(size, orientation) function hex_dimensions(size, orientation)
local orientation = orientation or DEFAULT_ORIENTATION
local orientation = orientation or HEX_DEFAULT_ORIENTATION
return vec2(hex_width(size, orientation), hex_height(size, orientation)) return vec2(hex_width(size, orientation), hex_height(size, orientation))
end end
-- distance between two horizontally adjacent hexagon centerpoints -- distance between two horizontally adjacent hexagon centerpoints
function hex_horizontal_spacing(size, orientation) function hex_horizontal_spacing(size, orientation)
local orientation = orientation or DEFAULT_ORIENTATION
local orientation = orientation or HEX_DEFAULT_ORIENTATION
if orientation == ORIENTATION.FLAT then
if orientation == HEX_ORIENTATION.FLAT then
return hex_width(size, orientation) * 3/4 return hex_width(size, orientation) * 3/4
elseif orientation == ORIENTATION.POINTY then
elseif orientation == HEX_ORIENTATION.POINTY then
return hex_height(size, orientation) return hex_height(size, orientation)
end end
end end
-- distance between two vertically adjacent hexagon centerpoints -- distance between two vertically adjacent hexagon centerpoints
function hex_vertical_spacing(size, orientation) function hex_vertical_spacing(size, orientation)
local orientation = orientation or DEFAULT_ORIENTATION
local orientation = orientation or HEX_DEFAULT_ORIENTATION
if orientation == ORIENTATION.FLAT then
if orientation == HEX_ORIENTATION.FLAT then
return hex_height(size, orientation) return hex_height(size, orientation)
elseif orientation == ORIENTATION.POINTY then
elseif orientation == HEX_ORIENTATION.POINTY then
return hex_width(size, orientation) * 3/4 return hex_width(size, orientation) * 3/4
end end
end end
-- returns the distance between adjacent hexagon centers in a grid -- returns the distance between adjacent hexagon centers in a grid
function hex_spacing(size, orientation) function hex_spacing(size, orientation)
local orientation = orientation or DEFAULT_ORIENTATION
local orientation = orientation or HEX_DEFAULT_ORIENTATION
return vec2(hex_horizontal_spacing(size, orientation), hex_vertical_spacing(size, orientation)) return vec2(hex_horizontal_spacing(size, orientation), hex_vertical_spacing(size, orientation))
end end
@ -137,19 +150,19 @@ end
-- Hex to Screen -- Orientation Must be Either POINTY or FLAT -- Hex to Screen -- Orientation Must be Either POINTY or FLAT
function hex_to_pixel(hex, size, orientation) function hex_to_pixel(hex, size, orientation)
local M = orientation and orientation.M or DEFAULT_ORIENTATION.M
local M = orientation and orientation.M or HEX_DEFAULT_ORIENTATION.M
local x = (M[1][1] * hex[1] + M[1][2] * hex[2]) * (size and size[1] or DEFAULT_HEX_SIZE[1])
local y = (M[2][1] * hex[1] + M[2][2] * hex[2]) * (size and size[2] or DEFAULT_HEX_SIZE[2])
local x = (M[1][1] * hex[1] + M[1][2] * hex[2]) * (size and size[1] or HEX_DEFAULT_SIZE[1])
local y = (M[2][1] * hex[1] + M[2][2] * hex[2]) * (size and size[2] or HEX_DEFAULT_SIZE[2])
return vec2(x, y) return vec2(x, y)
end end
-- Screen to Hex -- Orientation Must be Either POINTY or FLAT -- Screen to Hex -- Orientation Must be Either POINTY or FLAT
function pixel_to_hex(pix, size, orientation) function pixel_to_hex(pix, size, orientation)
local W = orientation and orientation.W or DEFAULT_ORIENTATION.W
local W = orientation and orientation.W or HEX_DEFAULT_ORIENTATION.W
local pix = pix / (size or vec2(DEFAULT_HEX_SIZE))
local pix = pix / (size or vec2(HEX_DEFAULT_SIZE))
local x = W[1][1] * pix[1] + W[1][2] * pix[2] local x = W[1][1] * pix[1] + W[1][2] * pix[2]
local y = W[2][1] * pix[1] + W[2][2] * pix[2] local y = W[2][1] * pix[1] + W[2][2] * pix[2]
@ -159,14 +172,14 @@ end
-- TODO test, learn am.draw -- TODO test, learn am.draw
function hex_corner_offset(corner, size, orientation) function hex_corner_offset(corner, size, orientation)
local orientation = orientation or DEFAULT_ORIENTATION
local orientation = orientation or HEX_DEFAULT_ORIENTATION
local angle = 2.0 * math.pi * orientation.angle + corner / 6 local angle = 2.0 * math.pi * orientation.angle + corner / 6
return vec2(size[1] * math.cos(angle), size[2] * math.sin(angle)) return vec2(size[1] * math.cos(angle), size[2] * math.sin(angle))
end end
-- TODO test this thing -- TODO test this thing
function hex_corners(hex, size, orientation) function hex_corners(hex, size, orientation)
local orientation = orientation or DEFAULT_ORIENTATION
local orientation = orientation or HEX_DEFAULT_ORIENTATION
local corners = {} local corners = {}
local center = hex_to_pixel(hex, size, orientation) local center = hex_to_pixel(hex, size, orientation)
for i = 0, 5 do for i = 0, 5 do
@ -220,7 +233,7 @@ end
-- MAPS & STORAGE -- MAPS & STORAGE
-- Returns Ordered Ring-Shaped Map of |radius| from |center| -- Returns Ordered Ring-Shaped Map of |radius| from |center|
function ring_map(center, radius)
function hex_ring_map(center, radius)
local map = {} local map = {}
local walk = center + HEX_DIRECTIONS[6] * radius local walk = center + HEX_DIRECTIONS[6] * radius
@ -235,21 +248,21 @@ function ring_map(center, radius)
end end
-- Returns Ordered Spiral Hexagonal Map of |radius| Rings from |center| -- Returns Ordered Spiral Hexagonal Map of |radius| Rings from |center|
function spiral_map(center, radius)
function hex_spiral_map(center, radius)
local map = { center } local map = { center }
for i = 1, radius do for i = 1, radius do
table.append(map, ring_map(center, i))
table.append(map, hex_ring_map(center, i))
end end
return setmetatable(map, {__index={center=center, radius=radius}}) return setmetatable(map, {__index={center=center, radius=radius}})
end end
local function map_get(map, hex, y)
function hex_map_get(map, hex, y)
if y then return map[hex] and map[hex][y] end if y then return map[hex] and map[hex][y] end
return map[hex.x] and map[hex.x][hex.y] return map[hex.x] and map[hex.x][hex.y]
end end
local function map_set(map, hex, y, v)
function hex_map_set(map, hex, y, v)
if v then if v then
if map[hex] then if map[hex] then
map[hex][y] = v map[hex][y] = v
@ -268,7 +281,7 @@ local function map_set(map, hex, y, v)
end end
-- Returns Unordered Parallelogram-Shaped Map of |width| and |height| with Simplex Noise -- Returns Unordered Parallelogram-Shaped Map of |width| and |height| with Simplex Noise
function parallelogram_map(width, height, seed)
function hex_parallelogram_map(width, height, seed)
local seed = seed or math.random(width * height) local seed = seed or math.random(width * height)
local map = {} local map = {}
@ -296,14 +309,14 @@ function parallelogram_map(width, height, seed)
seed = seed, seed = seed,
neighbours = function(hex) neighbours = function(hex)
return table.filter(hex_neighbours(hex), function(_hex) return table.filter(hex_neighbours(hex), function(_hex)
return map_get(map, _hex)
return hex_map_get(map, _hex)
end) end)
end end
}}) }})
end end
-- Returns Unordered Triangular (Equilateral) Map of |size| with Simplex Noise -- Returns Unordered Triangular (Equilateral) Map of |size| with Simplex Noise
function triangular_map(size, seed)
function hex_triangular_map(size, seed)
local seed = seed or math.random(size * math.cos(size) / 2) local seed = seed or math.random(size * math.cos(size) / 2)
local map = {} local map = {}
@ -330,14 +343,14 @@ function triangular_map(size, seed)
seed = seed, seed = seed,
neighbours = function(hex) neighbours = function(hex)
return table.filter(hex_neighbours(hex), function(_hex) return table.filter(hex_neighbours(hex), function(_hex)
return map_get(map, _hex)
return hex_map_get(map, _hex)
end) end)
end end
}}) }})
end end
-- Returns Unordered Hexagonal Map of |radius| with Simplex Noise -- Returns Unordered Hexagonal Map of |radius| with Simplex Noise
function hexagonal_map(radius, seed)
function hex_hexagonal_map(radius, seed)
local seed = seed or math.random(radius * 2 * math.pi) local seed = seed or math.random(radius * 2 * math.pi)
local map = {} local map = {}
@ -369,7 +382,7 @@ function hexagonal_map(radius, seed)
seed = seed, seed = seed,
neighbours = function(hex) neighbours = function(hex)
return table.filter(hex_neighbours(hex), function(_hex) return table.filter(hex_neighbours(hex), function(_hex)
return map_get(map, _hex.x, _hex.y)
return hex_map_get(map, _hex.x, _hex.y)
end) end)
end end
}}) }})
@ -377,15 +390,17 @@ end
-- Returns Unordered Rectangular Map of |width| and |height| with Simplex Noise -- Returns Unordered Rectangular Map of |width| and |height| with Simplex Noise
-- @TODO - this doesn't work for pointy orientations -- @TODO - this doesn't work for pointy orientations
function rectangular_map(width, height, seed)
function hex_rectangular_map(width, height, orientation, seed)
local orientation = orientation or HEX_DEFAULT_ORIENTATION
local seed = seed or math.random(width * height) local seed = seed or math.random(width * height)
local map = {} local map = {}
if orientation == HEX_ORIENTATION.FLAT then
for i = 0, width - 1 do for i = 0, width - 1 do
map[i] = {} map[i] = {}
for j = 0, height - 1 do for j = 0, height - 1 do
-- Begin to Calculate Noise
-- begin to calculate noise
local idelta = i / width local idelta = i / width
local jdelta = j / height local jdelta = j / height
local noise = 0 local noise = 0
@ -401,13 +416,17 @@ function rectangular_map(width, height, seed)
map[i][j] = noise map[i][j] = noise
end end
end end
elseif orientation == HEX_ORIENTATION.POINTY then
error("don't use this, it's broken")
end
return setmetatable(map, { __index = { return setmetatable(map, { __index = {
width = width, width = width,
height = height, height = height,
seed = seed, seed = seed,
neighbours = function(hex) neighbours = function(hex)
return table.filter(hex_neighbours(hex), function(_hex) return table.filter(hex_neighbours(hex), function(_hex)
return map_get(map, _hex)
return hex_map_get(map, _hex)
end) end)
end end
}}) }})
@ -415,25 +434,30 @@ end
--============================================================================ --============================================================================
-- PATHFINDING -- PATHFINDING
function breadth_first(map, start)
-- note:
-- i kinda feel like after implementing these and making the game, there are tons of reasons
-- why you might want to specialize pathfinding, like you would any other kind of algorithm
--
-- so, while (in theory) these algorithms work with the maps in this file, your maps and game
-- will have lots of other data which you may want your pathfinding algorithms to care about in some way,
-- that these don't.
--
function hex_breadth_first(map, start)
local frontier = {} local frontier = {}
frontier[1] = start frontier[1] = start
local distance = {} local distance = {}
distance[start.x] = {}
distance[start.x][start.y] = 0
hex_map_set(distance, start, 0)
while not (#frontier == 0) do while not (#frontier == 0) do
local current = table.remove(frontier, 1) local current = table.remove(frontier, 1)
for _,neighbour in pairs(map.neighbours(current)) do for _,neighbour in pairs(map.neighbours(current)) do
local d = map_get(distance, neighbour.x, neighbour.y)
local d = hex_map_get(distance, neighbour.x, neighbour.y)
if not d then if not d then
table.insert(frontier, neighbour) table.insert(frontier, neighbour)
local current_distance = map_get(distance, current.x, current.y)
map_set(distance, neighbour.x, neighbour.y, current_distance + 1)
local current_distance = hex_map_get(distance, current.x, current.y)
hex_map_set(distance, neighbour.x, neighbour.y, current_distance + 1)
end end
end end
end end
@ -441,17 +465,15 @@ function breadth_first(map, start)
return distance return distance
end end
function dijkstra(map, start, goal, cost_f, neighbour_f)
function hex_dijkstra(map, start, goal, cost_f, neighbour_f)
local frontier = {} local frontier = {}
frontier[1] = { hex = start, priority = 0 } frontier[1] = { hex = start, priority = 0 }
local came_from = {} local came_from = {}
came_from[start.x] = {}
came_from[start.x][start.y] = false
hex_map_set(came_from, start, false)
local cost_so_far = {} local cost_so_far = {}
cost_so_far[start.x] = {}
cost_so_far[start.x][start.y] = 0
hex_map_set(cost_so_far, start, 0)
while not (#frontier == 0) do while not (#frontier == 0) do
local current = table.remove(frontier, 1) local current = table.remove(frontier, 1)
@ -461,14 +483,14 @@ function dijkstra(map, start, goal, cost_f, neighbour_f)
end end
for _,neighbour in pairs(neighbour_f(map, current.hex)) do for _,neighbour in pairs(neighbour_f(map, current.hex)) do
local new_cost = map_get(cost_so_far, current.hex) + cost_f(map, current.hex, neighbour)
local neighbour_cost = map_get(cost_so_far, neighbour)
local new_cost = hex_map_get(cost_so_far, current.hex) + cost_f(map, current.hex, neighbour)
local neighbour_cost = hex_map_get(cost_so_far, neighbour)
if not neighbour_cost or (new_cost < neighbour_cost) then if not neighbour_cost or (new_cost < neighbour_cost) then
map_set(cost_so_far, neighbour, new_cost)
hex_map_set(cost_so_far, neighbour, new_cost)
local priority = new_cost + math.distance(start, neighbour) local priority = new_cost + math.distance(start, neighbour)
table.insert(frontier, { hex = neighbour, priority = priority }) table.insert(frontier, { hex = neighbour, priority = priority })
map_set(came_from, neighbour, current)
hex_map_set(came_from, neighbour, current)
end end
end end
end end
@ -476,7 +498,7 @@ function dijkstra(map, start, goal, cost_f, neighbour_f)
return came_from return came_from
end end
-- generic A* pathfinding
-- A* pathfinding
-- --
-- |heuristic| has the form: -- |heuristic| has the form:
-- function(source, target) -- source and target are vec2's -- function(source, target) -- source and target are vec2's
@ -486,20 +508,15 @@ end
-- function (from, to) -- from and to are vec2's -- function (from, to) -- from and to are vec2's
-- return some numeric value -- return some numeric value
-- --
-- returns a map that has map[hex.x][hex.y] = { hex = vec2, priority = number },
-- where the hex is the spot it thinks you should go to from the indexed hex, and priority is the cost of that decision,
-- as well as 'made_it' a bool that tells you if we were successful in reaching |goal|
function Astar(map, start, goal, heuristic, cost_f)
function hex_Astar(map, start, goal, heuristic, cost_f)
local path = {} local path = {}
path[start.x] = {}
path[start.x][start.y] = false
hex_map_set(path, start, false)
local frontier = {} local frontier = {}
frontier[1] = { hex = start, priority = 0 } frontier[1] = { hex = start, priority = 0 }
local path_so_far = {} local path_so_far = {}
path_so_far[start.x] = {}
path_so_far[start.x][start.y] = 0
hex_map_set(path_so_far, start, 0)
local made_it = false local made_it = false
while not (#frontier == 0) do while not (#frontier == 0) do
@ -511,14 +528,14 @@ function Astar(map, start, goal, heuristic, cost_f)
end end
for _,next_ in pairs(map.neighbours(current.hex)) do for _,next_ in pairs(map.neighbours(current.hex)) do
local new_cost = map_get(path_so_far, current.hex.x, current.hex.y) + cost_f(map, current.hex, next_)
local next_cost = map_get(path_so_far, next_.x, next_.y)
local new_cost = hex_map_get(path_so_far, current.hex.x, current.hex.y) + cost_f(map, current.hex, next_)
local next_cost = hex_map_get(path_so_far, next_.x, next_.y)
if not next_cost or new_cost < next_cost then if not next_cost or new_cost < next_cost then
map_set(path_so_far, next_.x, next_.y, new_cost)
hex_map_set(path_so_far, next_.x, next_.y, new_cost)
local priority = new_cost + heuristic(goal, next_) local priority = new_cost + heuristic(goal, next_)
table.insert(frontier, { hex = next_, priority = priority }) table.insert(frontier, { hex = next_, priority = priority })
map_set(path, next_.x, next_.y, current)
hex_map_set(path, next_.x, next_.y, current)
end end
end end
end end

14
src/mob.lua

@ -7,7 +7,7 @@ MOB_TYPE = {
SPOODER = 2 SPOODER = 2
} }
MAX_MOB_SIZE = hex_height(HEX_SIZE, ORIENTATION.FLAT) / 2
MAX_MOB_SIZE = hex_height(HEX_SIZE, HEX_ORIENTATION.FLAT) / 2
MOB_SIZE = MAX_MOB_SIZE MOB_SIZE = MAX_MOB_SIZE
MOB_SPECS = { MOB_SPECS = {
@ -63,7 +63,7 @@ end
-- check if a the tile at |hex| is passable by |mob| -- check if a the tile at |hex| is passable by |mob|
function mob_can_pass_through(mob, hex) function mob_can_pass_through(mob, hex)
local tile = map_get(state.map, hex)
local tile = hex_map_get(state.map, hex)
return tile_is_medium_elevation(tile) return tile_is_medium_elevation(tile)
end end
@ -158,7 +158,7 @@ local function resolve_frame_target_for_mob(mob, mob_index)
local frame_target, tile = false, false local frame_target, tile = false, false
if mob.path then if mob.path then
-- we (should) have an explicitly stored target -- we (should) have an explicitly stored target
local path_entry = map_get(mob.path, mob.hex)
local path_entry = hex_map_get(mob.path, mob.hex)
if not path_entry then if not path_entry then
-- we should be just about to reach the target, delete the path. -- we should be just about to reach the target, delete the path.
@ -180,12 +180,12 @@ local function resolve_frame_target_for_mob(mob, mob_index)
if #neighbours > 0 then if #neighbours > 0 then
local first_neighbour = neighbours[1] local first_neighbour = neighbours[1]
tile = map_get(state.map, first_neighbour)
tile = hex_map_get(state.map, first_neighbour)
local lowest_cost_hex = first_neighbour local lowest_cost_hex = first_neighbour
local lowest_cost = tile.priority or 0 local lowest_cost = tile.priority or 0
for _,n in pairs(neighbours) do for _,n in pairs(neighbours) do
tile = map_get(state.map, n)
tile = hex_map_get(state.map, n)
if not tile.priority then if not tile.priority then
-- if there's no stored priority, that should mean it's the center tile -- if there's no stored priority, that should mean it's the center tile
@ -247,8 +247,8 @@ local function update_mob_beeper(mob, mob_index)
-- or between when we last calculated this target and now -- or between when we last calculated this target and now
-- check for that now -- check for that now
if mob_can_pass_through(mob, mob.frame_target) then if mob_can_pass_through(mob, mob.frame_target) then
local from = map_get(state.map, mob.hex)
local to = map_get(state.map, mob.frame_target)
local from = hex_map_get(state.map, mob.hex)
local to = hex_map_get(state.map, mob.frame_target)
local rate = (4 * mob.speed - math.abs(to.elevation - from.elevation)) * am.delta_time local rate = (4 * mob.speed - math.abs(to.elevation - from.elevation)) * am.delta_time
mob.position = mob.position + math.normalize(hex_to_pixel(mob.frame_target, vec2(HEX_SIZE)) - mob.position) * rate mob.position = mob.position + math.normalize(hex_to_pixel(mob.frame_target, vec2(HEX_SIZE)) - mob.position) * rate

6
src/projectile.lua

@ -85,7 +85,7 @@ local function update_projectile_shell(projectile, projectile_index)
-- right now, it's just the hex we're on and all of its neighbours. -- right now, it's just the hex we're on and all of its neighbours.
-- this is done to avoid having to check every mob on screen, though maybe it's not necessary. -- this is done to avoid having to check every mob on screen, though maybe it's not necessary.
local do_explode = false local do_explode = false
local search_hexes = spiral_map(projectile.hex, 1)
local search_hexes = hex_spiral_map(projectile.hex, 1)
local mobs = {} local mobs = {}
for _,hex in pairs(search_hexes) do for _,hex in pairs(search_hexes) do
for index,mob in pairs(mobs_on_hex(hex)) do for index,mob in pairs(mobs_on_hex(hex)) do
@ -103,7 +103,7 @@ local function update_projectile_shell(projectile, projectile_index)
end end
end end
local tile = map_get(state.map, projectile.hex)
local tile = hex_map_get(state.map, projectile.hex)
if tile and tile.elevation >= projectile.props.z then if tile and tile.elevation >= projectile.props.z then
--do_explode = true --do_explode = true
@ -143,7 +143,7 @@ local function update_projectile_laser(projectile, projectile_index)
-- get a list of hexes that could have something we could hit on them -- get a list of hexes that could have something we could hit on them
-- right now, it's just the hex we're on and all of its neighbours. -- right now, it's just the hex we're on and all of its neighbours.
-- this is done to avoid having to check every mob on screen, though maybe it's not necessary. -- this is done to avoid having to check every mob on screen, though maybe it's not necessary.
local search_hexes = spiral_map(projectile.hex, 1)
local search_hexes = hex_spiral_map(projectile.hex, 1)
local hit_mob_count = 0 local hit_mob_count = 0
local hit_mobs = {} local hit_mobs = {}
for _,hex in pairs(search_hexes) do for _,hex in pairs(search_hexes) do

19
src/tower.lua

@ -131,9 +131,9 @@ function make_tower_node(tower_type)
elseif tower_type == TOWER_TYPE.HOWITZER then elseif tower_type == TOWER_TYPE.HOWITZER then
return am.group{ return am.group{
am.circle(vec2(0), HEX_SIZE, COLORS.VERY_DARK_GRAY, 6),
am.circle(vec2(0), HEX_SIZE, COLORS.VERY_DARK_GRAY{a=0.8}, 6),
am.rotate(state.time or 0) ^ am.group{ am.rotate(state.time or 0) ^ am.group{
pack_texture_into_sprite(TEXTURES.CANNON1, HEX_PIXEL_HEIGHT, HEX_PIXEL_WIDTH*2) -- CHONK
pack_texture_into_sprite(TEXTURES.CANNON1, HEX_PIXEL_HEIGHT*2, HEX_PIXEL_WIDTH*3) -- CHONK
} }
} }
elseif tower_type == TOWER_TYPE.LIGHTHOUSE then elseif tower_type == TOWER_TYPE.LIGHTHOUSE then
@ -276,11 +276,11 @@ function tower_type_is_buildable_on(hex, tile, tower_type)
local has_mountain = false local has_mountain = false
local has_ground = false local has_ground = false
for _,h in pairs(spiral_map(hex, get_tower_size(tower_type))) do
for _,h in pairs(hex_spiral_map(hex, get_tower_size(tower_type))) do
table.merge(blocking_towers, towers_on_hex(h)) table.merge(blocking_towers, towers_on_hex(h))
table.merge(blocking_mobs, mobs_on_hex(h)) table.merge(blocking_mobs, mobs_on_hex(h))
local tile = map_get(state.map, h)
local tile = hex_map_get(state.map, h)
-- this should always be true, unless it is possible to place a tower -- this should always be true, unless it is possible to place a tower
-- where part of the tower overflows the edge of the map -- where part of the tower overflows the edge of the map
if tile then if tile then
@ -332,7 +332,7 @@ function tower_type_is_buildable_on(hex, tile, tower_type)
elseif tower_type == TOWER_TYPE.LIGHTHOUSE then elseif tower_type == TOWER_TYPE.LIGHTHOUSE then
local has_water_neighbour = false local has_water_neighbour = false
for _,h in pairs(hex_neighbours(hex)) do for _,h in pairs(hex_neighbours(hex)) do
local tile = map_get(state.map, h)
local tile = hex_map_get(state.map, h)
if tile and tile.elevation < -0.5 then if tile and tile.elevation < -0.5 then
has_water_neighbour = true has_water_neighbour = true
@ -442,7 +442,7 @@ function update_tower_lighthouse(tower, tower_index)
-- is within some angle range...? if the mob is heading directly away from the tower, then -- is within some angle range...? if the mob is heading directly away from the tower, then
-- the lighthouse shouldn't do much -- the lighthouse shouldn't do much
local path, made_it = Astar(state.map, tower.hex, m.hex, grid_heuristic, grid_cost)
local path, made_it = hex_Astar(state.map, tower.hex, m.hex, grid_heuristic, grid_cost)
if made_it then if made_it then
m.path = path m.path = path
@ -485,17 +485,20 @@ function make_and_register_tower(hex, tower_type)
if tower.size == 0 then if tower.size == 0 then
tower.hexes = { tower.hex } tower.hexes = { tower.hex }
else else
tower.hexes = spiral_map(tower.hex, tower.size)
tower.hexes = hex_spiral_map(tower.hex, tower.size)
end end
tower.height = spec.height tower.height = spec.height
for _,h in pairs(tower.hexes) do for _,h in pairs(tower.hexes) do
local tile = map_get(state.map, h.x, h.y)
local tile = hex_map_get(state.map, h.x, h.y)
tile.elevation = tile.elevation + tower.height tile.elevation = tile.elevation + tower.height
end end
if tower.type == TOWER_TYPE.HOWITZER then if tower.type == TOWER_TYPE.HOWITZER then
tower.props.z = tower.height tower.props.z = tower.height
elseif tower.type == TOWER_TYPE.LIGHTHOUSE then
tower.perimeter = hex_ring_map(tower.hex, tower.range)
end end
register_entity(state.towers, tower) register_entity(state.towers, tower)

1
texture.lua

@ -13,6 +13,7 @@ end
TEXTURES = { TEXTURES = {
LOGO = load_texture("res/logo.png"), LOGO = load_texture("res/logo.png"),
GEM1 = load_texture("res/gem1.png"), GEM1 = load_texture("res/gem1.png"),
SHADED_HEX = load_texture("res/shaded_hex1.png"),
-- gui stuff -- gui stuff
BUTTON1 = load_texture("res/button1.png"), BUTTON1 = load_texture("res/button1.png"),

Loading…
Cancel
Save