Browse Source

basic serialization

master
Nicholas Hayashi 4 years ago
parent
commit
ea0d10c93e
  1. 16
      main.lua
  2. 30
      src/entity.lua
  3. 115
      src/game.lua
  4. 143
      src/grid.lua
  5. 92
      src/hexyz.lua
  6. 52
      src/mob.lua
  7. 40
      src/projectile.lua
  8. 63
      src/tower.lua

16
main.lua

@ -16,13 +16,13 @@ math.random()
do do
win = am.window{ win = am.window{
width = settings.window_width,
height = settings.window_height,
title = "hexyz",
mode = settings.fullscreen and "fullscreen" or "windowed",
highdpi = true,
letterbox = true,
resizable = true, -- user should probably set their resolution instead of resizing the window, but hey.
width = settings.window_width,
height = settings.window_height,
title = "hexyz",
mode = settings.fullscreen and "fullscreen" or "windowed",
highdpi = true,
letterbox = true,
resizable = true, -- user should probably set their resolution instead of resizing the window, but hey.
} }
end end
@ -46,6 +46,6 @@ function main_action() end
function main_scene() end function main_scene() end
win.scene = am.group() win.scene = am.group()
game_init()
game_init(nil)
noglobals() noglobals()

30
src/entity.lua

@ -7,7 +7,7 @@ entity structure:
hex - vec2 - current occupied hex, if any hex - vec2 - current occupied hex, if any
position - vec2 - current pixel position of it's translate (forced parent) node position - vec2 - current pixel position of it's translate (forced parent) node
update - function - runs every frame with itself and its index in some array as an argument update - function - runs every frame with itself and its index in some array as an argument
node - node - scene graph node
node - node - scene graph node - should be initialized by caller after, though all entities have a node
type - enum - sub type - unset if 'basic' entity type - enum - sub type - unset if 'basic' entity
props - table - table of properties specific to this entity subtype props - table - table of properties specific to this entity subtype
@ -15,7 +15,7 @@ entity structure:
... - any - a bunch of other shit depending on what entity type it is ... - any - a bunch of other shit depending on what entity type it is
} }
--]] --]]
function make_basic_entity(hex, node, update, position)
function make_basic_entity(hex, update, position)
local entity = {} local entity = {}
entity.TOB = state.time entity.TOB = state.time
@ -33,8 +33,8 @@ function make_basic_entity(hex, node, update, position)
end end
entity.update = update entity.update = update
entity.node = am.translate(entity.position) ^ node
entity.type = false
entity.node = false -- set by caller
entity.type = false -- set by caller
entity.props = {} entity.props = {}
return entity return entity
@ -45,9 +45,11 @@ function register_entity(t, entity)
state.world:append(entity.node) state.world:append(entity.node)
end end
-- |t| is the source table, probably MOBS, TOWERS, or PROJECTILES
-- |t| is the source table, probably state.mobs, state.towers, or state.projectiles
function delete_entity(t, index) function delete_entity(t, index)
if not t then log("splat!") end
if not t then
error("splat!")
end
state.world:remove(t[index].node) state.world:remove(t[index].node)
t[index] = false -- leave empty indexes so other entities can learn that this entity was deleted t[index] = false -- leave empty indexes so other entities can learn that this entity was deleted
@ -65,3 +67,19 @@ function do_entity_updates()
do_projectile_updates() do_projectile_updates()
end end
function entity_basic_devectored_copy(entity)
local copy = table.shallow_copy(entity)
copy.position = { copy.position.x, copy.position.y }
copy.hex = { copy.hex.x, copy.hex.y }
return copy
end
function entity_basic_json_parse(json_string)
local entity = am.parse_json(json_string)
entity.position = vec2(entity.position[0], entity.position[1])
entity.hex = vec2(entity.hex[0], entity.hex[1])
return entity
end

115
src/game.lua

@ -2,9 +2,9 @@
state = {} state = {}
function game_end()
function game_end(saved_state)
delete_all_entities() delete_all_entities()
game_init()
game_init(saved_state)
end end
function update_score(diff) state.score = state.score + diff end function update_score(diff) state.score = state.score + diff end
@ -37,6 +37,10 @@ local function get_initial_game_state(seed)
score = 0, -- current game score score = 0, -- current game score
money = STARTING_MONEY, -- current money money = STARTING_MONEY, -- current money
towers = {},
mobs = {},
projectiles = {},
current_wave = 1, current_wave = 1,
time_until_next_wave = 0, time_until_next_wave = 0,
time_until_next_break = 0, time_until_next_break = 0,
@ -78,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(state.map.get(hex.x, hex.y))
str = table.tostring(map_get(state.map, hex))
end end
return str return str
end end
@ -119,7 +123,7 @@ local function game_pause()
} }
:tag"pause_menu") :tag"pause_menu")
win.scene:action(function()
win.scene:action(function(self)
if win:key_pressed("escape") then if win:key_pressed("escape") then
win.scene:remove("pause_menu") win.scene:remove("pause_menu")
win.scene("game").paused = false win.scene("game").paused = false
@ -132,6 +136,84 @@ local function game_pause()
end) end)
end end
local function game_deserialize(json_string)
-- @TODO decode from some compressed format or whatever
local new_state = am.parse_json(json_string)
new_state.map, new_state.world = random_map(new_state.seed)
new_state.seed = nil
new_state.towers = {}
for i,t in pairs(new_state.towers) do
if t then
new_state.towers[i] = tower_deserialize(t)
-- @STATEFUL, shouldn't be done here
new_state.world:append(new_state.towers[i].node)
end
end
new_state.mobs = {}
for i,m in pairs(new_state.mobs) do
if m then
new_state.mobs[i] = mob_deserialize(m)
-- @STATEFUL, shouldn't be done here
new_state.world:append(new_state.mobs[i].node)
end
end
new_state.projectiles = {}
for i,p in pairs(new_state.projectiles) do
if p then
new_state.projectiles[i] = projectile_deserialize(p)
-- @STATEFUL, shouldn't be done here
new_state.world:append(new_state.projectiles[i].node)
end
end
return new_state
end
local function game_serialize()
local serialized = table.shallow_copy(state)
serialized.seed = state.map.seed
serialized.map = nil -- we re-generate the entire map from the seed on de-serialize
-- in order to serialize the game state, we have to convert all relevant userdata into
-- something else. this practically only means vectors need to become arrays of floats.
-- this is dumb and if i forsaw this i would have probably used float arrays the whole time.
serialized.towers = {}
for i,t in pairs(state.towers) do
if t then
serialized.towers[i] = tower_serialize(t)
end
end
serialized.mobs = {}
for i,m in pairs(state.mobs) do
if m then
serialized.mobs[i] = mob_serialize(m)
end
end
serialized.projectiles = {}
for i,p in pairs(state.projectiles) do
if p then
serialized.projectiles[i] = projectile_serialize(p)
end
end
-- @TODO b64 encode or otherwise scramble/compress
return am.to_json(serialized)
end
local function game_save()
am.save_state("save", game_serialize(), "json")
log("succesfully saved!")
end
local function game_action(scene) local function game_action(scene)
if state.score < 0 then game_end() return true end if state.score < 0 then game_end() return true end
@ -168,7 +250,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 = state.map.get(hex.x, hex.y)
local tile = 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)
@ -223,6 +305,13 @@ local function game_action(scene)
elseif win:key_pressed"f2" then elseif win:key_pressed"f2" then
state.world"flow_field".hidden = not state.world"flow_field".hidden state.world"flow_field".hidden = not state.world"flow_field".hidden
elseif win:key_pressed"f3" then
game_save()
elseif win:key_pressed"f4" then
game_end(am.load_state("save", "json"))
return true
elseif win:key_pressed"tab" then elseif win:key_pressed"tab" then
if win:key_down"lshift" then if win:key_down"lshift" then
select_toolbelt_button((state.selected_toolbelt_button + table.count(TOWER_TYPE) - 2) % table.count(TOWER_TYPE) + 1) select_toolbelt_button((state.selected_toolbelt_button + table.count(TOWER_TYPE) - 2) % table.count(TOWER_TYPE) + 1)
@ -468,14 +557,24 @@ function game_scene()
return scene return scene
end end
function game_init()
state = get_initial_game_state()
function game_init(saved_state)
if saved_state then
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
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
state.map.get(h.x, h.y).elevation = 0
map_get(state.map, h).elevation = 0
end end
]]
win.scene:remove("game") win.scene:remove("game")
win.scene:append(game_scene()) win.scene:append(game_scene())

143
src/grid.lua

@ -75,27 +75,12 @@ function tile_is_medium_elevation(tile)
return tile.elevation >= -0.5 and tile.elevation < 0.5 return tile.elevation >= -0.5 and tile.elevation < 0.5
end end
function color_at(elevation)
if elevation < -0.5 then -- lowest elevation
return COLORS.WATER{ a = (elevation + 1.4) / 2 + 0.2 }
elseif elevation < 0 then -- med-low elevation
return math.lerp(COLORS.DIRT, COLORS.GRASS, elevation + 0.5){ a = (elevation + 1.8) / 2 + 0.3 }
elseif elevation < 0.5 then -- med-high elevation
return math.lerp(COLORS.DIRT, COLORS.GRASS, elevation + 0.5){ a = (elevation + 1.6) / 2 + 0.3 }
elseif elevation < 1 then -- high elevation
return COLORS.MOUNTAIN{ ra = elevation }
end
end
function grid_heuristic(source, target) function grid_heuristic(source, target)
return math.distance(source, target) return math.distance(source, target)
end end
function grid_cost(map, from, to) function grid_cost(map, from, to)
local t1, t2 = map.get(from.x, from.y), map.get(to.x, to.y)
local t1, t2 = map_get(map, from), 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
@ -107,11 +92,18 @@ function grid_cost(map, from, to)
local epsilon = elevation_epsilon local epsilon = elevation_epsilon
local cost = elevation_cost local cost = elevation_cost
return epsilon - cost
return 1
end
function grid_neighbours(map, hex)
return table.filter(hex_neighbours(hex), function(_hex)
local tile = map_get(map, _hex)
return tile and tile_is_medium_elevation(tile)
end)
end end
function generate_flow_field(map, start) function generate_flow_field(map, start)
return dijkstra(map, start, nil, grid_cost)
return 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)
@ -123,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 = hex_map_get(flow_field, i, j)
local flow = map_get(flow_field, i, j)
if flow then if flow then
map[i][j].priority = flow.priority map[i][j].priority = flow.priority
@ -142,12 +134,38 @@ 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 = spiral_map(hex, get_tower_size(tower_type))
for _,h in pairs(hexes) do for _,h in pairs(hexes) do
local tile = state.map.get(h.x, h.y)
local tile = 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
@ -164,25 +182,41 @@ 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
state.map.get(h.x, h.y).elevation = original_elevations[i]
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 hex_map_get(flow_field, 0, 0)
local result = not map_get(flow_field, 0, 0)
for i,h in pairs(hexes) do for i,h in pairs(hexes) do
state.map.get(h.x, h.y).elevation = original_elevations[i]
map_get(state.map, h).elevation = original_elevations[i]
end end
return result, flow_field return result, flow_field
end end
function random_map(seed)
local map = rectangular_map(HEX_GRID_DIMENSIONS.x, HEX_GRID_DIMENSIONS.y, seed)
math.randomseed(map.seed)
function make_hex_grid_scene(map)
local function color_at(elevation)
if elevation < -0.5 then -- lowest elevation
return COLORS.WATER{ a = (elevation + 1.4) / 2 + 0.2 }
elseif elevation < 0 then -- med-low elevation
return math.lerp(COLORS.DIRT, COLORS.GRASS, elevation + 0.5){ a = (elevation + 1.8) / 2 + 0.3 }
elseif elevation < 0.5 then -- med-high elevation
return math.lerp(COLORS.DIRT, COLORS.GRASS, elevation + 0.5){ a = (elevation + 1.6) / 2 + 0.3 }
elseif elevation < 1 then -- high elevation
return COLORS.MOUNTAIN{ ra = elevation }
else
-- @TODO probably fix... this only happens when loading a save, and the tile has an elevation that's
-- higher that anything here
return vec4(0.1)
end
end
-- 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
@ -197,6 +231,38 @@ function random_map(seed)
:tag"negative_mask" :tag"negative_mask"
local world = am.group(neg_mask):tag"world" local world = am.group(neg_mask):tag"world"
for i,_ in pairs(map) do
for j,tile in pairs(map[i]) do
local evenq = hex_to_evenq(vec2(i, j))
-- light shading on edge cells
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))
local color = color_at(tile.elevation) - mask
local node = am.translate(hex_to_pixel(vec2(i, j), vec2(HEX_SIZE)))
^ am.circle(vec2(0), HEX_SIZE, color, 6)
map_set(map, i, j, {
elevation = tile.elevation,
node = node
})
world:append(node)
end
end
apply_flow_field(map, generate_flow_field(map, HEX_GRID_CENTER), world)
return am.translate(WORLDSPACE_COORDINATE_OFFSET) ^ world
end
function random_map(seed)
local map = rectangular_map(HEX_GRID_DIMENSIONS.x, HEX_GRID_DIMENSIONS.y, seed)
math.randomseed(map.seed)
-- there are some things about the generated map we'd like to change...
for i,_ in pairs(map) do for i,_ in pairs(map) do
for j,noise in pairs(map[i]) do for j,noise in pairs(map[i]) do
local evenq = hex_to_evenq(vec2(i, j)) local evenq = hex_to_evenq(vec2(i, j))
@ -220,33 +286,12 @@ 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
-- light shading on edge cells
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))
local color = color_at(noise) - mask
local node = am.translate(hex_to_pixel(vec2(i, j), vec2(HEX_SIZE)))
^ am.circle(vec2(0), HEX_SIZE, color, 6)
map.set(i, j, {
map_set(map, i, j, {
elevation = noise, elevation = noise,
node = node
}) })
world:append(node)
end end
end end
getmetatable(map).__index.neighbours = function(hex)
return table.filter(hex_neighbours(hex), function(_hex)
--local interactable = evenq_is_in_interactable_region(hex_to_evenq(_hex))
local tile = map.get(_hex.x, _hex.y)
return tile and tile_is_medium_elevation(tile)
end)
end
apply_flow_field(map, generate_flow_field(map, HEX_GRID_CENTER), world)
return map, am.translate(WORLDSPACE_COORDINATE_OFFSET) ^ world
return map, make_hex_grid_scene(map)
end end

92
src/hexyz.lua

@ -244,47 +244,27 @@ function spiral_map(center, radius)
return setmetatable(map, {__index={center=center, radius=radius}}) return setmetatable(map, {__index={center=center, radius=radius}})
end end
local function map_get(t, x, y)
return t[x] and t[x][y]
end
function hex_map_get(t, x, y)
return map_get(t, x, y)
end
local function map_set(t, x, y, v)
if t[x] then
t[x][y] = v
else
t[x] = {}
t[x][y] = v
end
return t
end
function hex_map_set(t, x, y, v)
return map_set(t, x, y, v)
end
local function map_traverse(t, callback)
for i,_ in pairs(t) do
for _,entry in pairs(t[i]) do
callback(entry)
local 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
local 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 end
end
end
-- @NOTE probably shouldn't use this...
local function map_partial_set(t, x, y, k, v)
local entry = map_get(t, x, y)
if not entry then
map_set(t, x, y, { k = v })
else else
entry.k = v
if map[hex.x] then
map[hex.x][hex.y] = y
else
map[hex.x] = {}
map[hex.x][hex.y] = y
end
end end
return t
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
@ -314,13 +294,9 @@ function parallelogram_map(width, height, seed)
width = width, width = width,
height = height, height = height,
seed = seed, seed = seed,
get = function(x, y) return map_get(map, x, y) end,
set = function(x, y, v) return map_set(map, x, y, v) end,
partial = function(x, y, k, v) return map_partial_set(map, x, y, k, v) end,
traverse = function(callback) return map_traverse(map, callback) end,
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(_hex.x, _hex.y)
return map_get(map, _hex)
end) end)
end end
}}) }})
@ -352,13 +328,9 @@ function triangular_map(size, seed)
return setmetatable(map, { __index = { return setmetatable(map, { __index = {
size = size, size = size,
seed = seed, seed = seed,
get = function(x, y) return map_get(map, x, y) end,
set = function(x, y, v) return map_set(map, x, y, v) end,
partial = function(x, y, k, v) return map_partial_set(map, x, y, k, v) end,
traverse = function(callback) return map_traverse(map, callback) end,
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(_hex.x, _hex.y)
return map_get(map, _hex)
end) end)
end end
}}) }})
@ -395,13 +367,9 @@ function hexagonal_map(radius, seed)
return setmetatable(map, { __index = { return setmetatable(map, { __index = {
radius = radius, radius = radius,
seed = seed, seed = seed,
get = function(x, y) return map_get(map, x, y) end,
set = function(x, y, v) return map_set(map, x, y, v) end,
partial = function(x, y, k, v) return map_partial_set(map, x, y, k, v) end,
traverse = function(callback) return map_traverse(map, callback) end,
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(_hex.x, _hex.y)
return map_get(map, _hex.x, _hex.y)
end) end)
end end
}}) }})
@ -437,13 +405,9 @@ function rectangular_map(width, height, seed)
width = width, width = width,
height = height, height = height,
seed = seed, seed = seed,
get = function(x, y) return map_get(map, x, y) end,
set = function(x, y, v) return map_set(map, x, y, v) end,
partial = function(x, y, k, v) return map_partial_set(map, x, y, k, v) end,
traverse = function(callback) return map_traverse(map, callback) end,
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(_hex.x, _hex.y)
return map_get(map, _hex)
end) end)
end end
}}) }})
@ -477,7 +441,7 @@ function breadth_first(map, start)
return distance return distance
end end
function dijkstra(map, start, goal, cost_f)
function dijkstra(map, start, goal, cost_f, neighbour_f)
local frontier = {} local frontier = {}
frontier[1] = { hex = start, priority = 0 } frontier[1] = { hex = start, priority = 0 }
@ -496,15 +460,15 @@ function dijkstra(map, start, goal, cost_f)
break break
end end
for _,neighbour in pairs(map.neighbours(current.hex)) do
local new_cost = map_get(cost_so_far, current.hex.x, current.hex.y) + cost_f(map, current.hex, neighbour)
local neighbour_cost = map_get(cost_so_far, neighbour.x, neighbour.y)
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)
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.x, neighbour.y, new_cost)
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.x, neighbour.y, current)
map_set(came_from, neighbour, current)
end end
end end
end end

52
src/mob.lua

@ -1,6 +1,6 @@
MOBS = {}
state.mobs = {}
MOB_TYPE = { MOB_TYPE = {
BEEPER = 1, BEEPER = 1,
@ -46,7 +46,7 @@ end
function mobs_on_hex(hex) function mobs_on_hex(hex)
local t = {} local t = {}
for mob_index,mob in pairs(MOBS) do
for mob_index,mob in pairs(state.mobs) do
if mob and mob.hex == hex then if mob and mob.hex == hex then
table.insert(t, mob_index, mob) table.insert(t, mob_index, mob)
end end
@ -56,20 +56,20 @@ end
function mob_on_hex(hex) function mob_on_hex(hex)
-- table.find returns i,v in the table -- table.find returns i,v in the table
return table.find(MOBS, function(mob)
return table.find(state.mobs, function(mob)
return mob and mob.hex == hex return mob and mob.hex == hex
end) end)
end 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 = state.map.get(hex.x, hex.y)
local tile = map_get(state.map, hex)
return tile_is_medium_elevation(tile) return tile_is_medium_elevation(tile)
end end
function mob_die(mob, mob_index) function mob_die(mob, mob_index)
vplay_sfx(SOUNDS.EXPLOSION1) vplay_sfx(SOUNDS.EXPLOSION1)
delete_entity(MOBS, mob_index)
delete_entity(state.mobs, mob_index)
end end
local HEALTHBAR_WIDTH = HEX_PIXEL_WIDTH/2 local HEALTHBAR_WIDTH = HEX_PIXEL_WIDTH/2
@ -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 = mob.path[mob.hex.x] and mob.path[mob.hex.x][mob.hex.y]
local path_entry = 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.
@ -176,16 +176,16 @@ local function resolve_frame_target_for_mob(mob, mob_index)
end end
else else
-- use the map's flow field - gotta find the the best neighbour -- use the map's flow field - gotta find the the best neighbour
local neighbours = state.map.neighbours(mob.hex)
local neighbours = grid_neighbours(state.map, mob.hex)
if #neighbours > 0 then if #neighbours > 0 then
local first_neighbour = neighbours[1] local first_neighbour = neighbours[1]
tile = state.map.get(first_neighbour.x, first_neighbour.y)
tile = 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 = state.map.get(n.x, n.y)
tile = 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 = state.map.get(mob.hex.x, mob.hex.y)
local to = state.map.get(mob.frame_target.x, mob.frame_target.y)
local from = map_get(state.map, mob.hex)
local to = 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
@ -257,7 +257,7 @@ local function update_mob_beeper(mob, mob_index)
mob.frame_target = false mob.frame_target = false
end end
else else
log('no target')
--log('no target')
end end
-- passive animation -- passive animation
@ -280,11 +280,11 @@ end
local function make_and_register_mob(mob_type) local function make_and_register_mob(mob_type)
local mob = make_basic_entity( local mob = make_basic_entity(
get_spawn_hex(), get_spawn_hex(),
make_mob_node(mob_type),
get_mob_update_function(mob_type) get_mob_update_function(mob_type)
) )
mob.type = mob_type mob.type = mob_type
mob.node = am.translate(mob.position) ^ make_mob_node(mob_type, mob)
local spec = get_mob_spec(mob_type) local spec = get_mob_spec(mob_type)
mob.health = grow_mob_health(mob_type, spec.health, state.time) mob.health = grow_mob_health(mob_type, spec.health, state.time)
@ -293,26 +293,42 @@ local function make_and_register_mob(mob_type)
mob.hurtbox_radius = spec.hurtbox_radius mob.hurtbox_radius = spec.hurtbox_radius
mob.healthbar = mob.node:child(1):child(2):child(1) -- lmao mob.healthbar = mob.node:child(1):child(2):child(1) -- lmao
register_entity(MOBS, mob)
register_entity(state.mobs, mob)
return mob
end
function mob_serialize(mob)
local serialized = entity_basic_devectored_copy(mob)
return am.to_json(serialized)
end
function mob_deserialize(json_string)
local mob = entity_basic_json_parse(json_string)
mob.update = get_mob_update_function(mob.type)
mob.node = am.translate(mob.position) ^ make_mob_node(mob.type, mob)
mob.healthbar = mob.node:child(1):child(2):child(1) -- lmaoooo
return mob return mob
end end
function do_mob_spawning(spawn_chance) function do_mob_spawning(spawn_chance)
--if win:key_pressed"space" then --if win:key_pressed"space" then
if state.spawning and math.random(spawn_chance) == 1 then if state.spawning and math.random(spawn_chance) == 1 then
--if #MOBS < 1 then
--if #state.mobs < 1 then
make_and_register_mob(MOB_TYPE.BEEPER) make_and_register_mob(MOB_TYPE.BEEPER)
end end
end end
function delete_all_mobs() function delete_all_mobs()
for mob_index,mob in pairs(MOBS) do
if mob then delete_entity(MOBS, mob_index) end
for mob_index,mob in pairs(state.mobs) do
if mob then delete_entity(state.mobs, mob_index) end
end end
end end
function do_mob_updates() function do_mob_updates()
for mob_index,mob in pairs(MOBS) do
for mob_index,mob in pairs(state.mobs) do
if mob and mob.update then if mob and mob.update then
mob.update(mob, mob_index) mob.update(mob, mob_index)
end end

40
src/projectile.lua

@ -1,6 +1,6 @@
PROJECTILES = {}
state.projectiles = {}
PROJECTILE_TYPE = { PROJECTILE_TYPE = {
SHELL = 1, SHELL = 1,
@ -72,7 +72,7 @@ local function update_projectile_shell(projectile, projectile_index)
x2 = win.right, x2 = win.right,
y2 = win.top y2 = win.top
}) then }) then
delete_entity(PROJECTILES, projectile_index)
delete_entity(state.projectiles, projectile_index)
return true return true
end end
@ -103,7 +103,7 @@ local function update_projectile_shell(projectile, projectile_index)
end end
end end
local tile = state.map.get(projectile.hex.x, projectile.hex.y)
local tile = 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
@ -117,7 +117,7 @@ local function update_projectile_shell(projectile, projectile_index)
do_hit_mob(mob, damage, index) do_hit_mob(mob, damage, index)
end end
win.scene:append(make_shell_explosion_node(projectile.position)) win.scene:append(make_shell_explosion_node(projectile.position))
delete_entity(PROJECTILES, projectile_index)
delete_entity(state.projectiles, projectile_index)
return true return true
end end
end end
@ -132,7 +132,7 @@ local function update_projectile_laser(projectile, projectile_index)
x2 = win.right, x2 = win.right,
y2 = win.top y2 = win.top
}) then }) then
delete_entity(PROJECTILES, projectile_index)
delete_entity(state.projectiles, projectile_index)
return true return true
end end
@ -177,7 +177,7 @@ local function update_projectile_laser(projectile, projectile_index)
-- hit the mob, delete ourselves, affect the world -- hit the mob, delete ourselves, affect the world
do_hit_mob(closest_mob, projectile.damage, closest_mob_index) do_hit_mob(closest_mob, projectile.damage, closest_mob_index)
delete_entity(PROJECTILES, projectile_index)
delete_entity(state.projectiles, projectile_index)
vplay_sfx(SOUNDS.HIT1, 0.5) vplay_sfx(SOUNDS.HIT1, 0.5)
end end
@ -202,11 +202,11 @@ end
function make_and_register_projectile(hex, projectile_type, vector) function make_and_register_projectile(hex, projectile_type, vector)
local projectile = make_basic_entity( local projectile = make_basic_entity(
hex, hex,
make_projectile_node(projectile_type, vector),
get_projectile_update_function(projectile_type) get_projectile_update_function(projectile_type)
) )
projectile.type = projectile_type projectile.type = projectile_type
projectile.node = am.translate(projectile.position) ^ make_projectile_node(projectile_type, vector)
projectile.vector = vector projectile.vector = vector
local spec = get_projectile_spec(projectile_type) local spec = get_projectile_spec(projectile_type)
@ -214,18 +214,36 @@ function make_and_register_projectile(hex, projectile_type, vector)
projectile.damage = spec.damage projectile.damage = spec.damage
projectile.hitbox_radius = spec.hitbox_radius projectile.hitbox_radius = spec.hitbox_radius
register_entity(PROJECTILES, projectile)
register_entity(state.projectiles, projectile)
return projectile
end
function projectile_serialize(projectile)
local serialized = entity_basic_devectored_copy(projectile)
serialized.vector = { serialized.vector.x, serialized.vector.y }
return am.to_json(serialized)
end
function projectile_deserialize(json_string)
local projectile = entity_basic_json_parse(json_string)
projectile.vector = vec2(projectile.vector[0], projectile.vector[1])
projectile.update = get_projectile_update_function(projectile.type)
projectile.node = am.translate(projectile.position)
^ make_projectile_node(projectile.type, projectile.vector)
return projectile return projectile
end end
function delete_all_projectiles() function delete_all_projectiles()
for projectile_index,projectile in pairs(PROJECTILES) do
if projectile then delete_entity(PROJECTILES, projectile_index) end
for projectile_index,projectile in pairs(state.projectiles) do
if projectile then delete_entity(state.projectiles, projectile_index) end
end end
end end
function do_projectile_updates() function do_projectile_updates()
for projectile_index,projectile in pairs(PROJECTILES) do
for projectile_index,projectile in pairs(state.projectiles) do
if projectile and projectile.update then if projectile and projectile.update then
projectile.update(projectile, projectile_index) projectile.update(projectile, projectile_index)
end end

63
src/tower.lua

@ -1,6 +1,6 @@
TOWERS = {}
state.towers = {}
TOWER_TYPE = { TOWER_TYPE = {
WALL = 1, WALL = 1,
@ -125,8 +125,7 @@ local function make_tower_sprite(tower_type)
return pack_texture_into_sprite(get_tower_texture(tower_type), HEX_PIXEL_WIDTH, HEX_PIXEL_HEIGHT) return pack_texture_into_sprite(get_tower_texture(tower_type), HEX_PIXEL_WIDTH, HEX_PIXEL_HEIGHT)
end end
local HEX_FLOWER_DIMENSIONS = vec2(115, 125)
local function make_tower_node(tower_type)
function make_tower_node(tower_type)
if tower_type == TOWER_TYPE.REDEYE then if tower_type == TOWER_TYPE.REDEYE then
return make_tower_sprite(tower_type) return make_tower_sprite(tower_type)
@ -218,9 +217,33 @@ local function get_tower_update_function(tower_type)
end end
end end
function tower_serialize(tower)
local serialized = entity_basic_devectored_copy(tower)
for i,h in pairs(serialized.hexes) do
serialized.hexes[i] = { h.x, h.y }
end
return am.to_json(serialized)
end
function tower_deserialize(json_string)
local tower = entity_basic_json_parse(json_string)
tower.hexes = {}
for i,h in pairs(tower.hexes) do
tower.hexes[i] = vec2(tower.hexes[i][0], tower.hexes[i][1])
end
tower.update = get_tower_update_function(tower.type)
tower.node = am.translate(tower.position) ^ make_tower_node(tower_type)
return tower
end
function towers_on_hex(hex) function towers_on_hex(hex)
local t = {} local t = {}
for tower_index,tower in pairs(TOWERS) do
for tower_index,tower in pairs(state.towers) do
if tower then if tower then
for _,h in pairs(tower.hexes) do for _,h in pairs(tower.hexes) do
if h == hex then if h == hex then
@ -234,7 +257,7 @@ function towers_on_hex(hex)
end end
function tower_on_hex(hex) function tower_on_hex(hex)
return table.find(TOWERS, function(tower)
return table.find(state.towers, function(tower)
for _,h in pairs(tower.hexes) do for _,h in pairs(tower.hexes) do
if h == hex then return true end if h == hex then return true end
end end
@ -257,7 +280,7 @@ function tower_type_is_buildable_on(hex, tile, tower_type)
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 = state.map.get(h.x, h.y)
local tile = 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
@ -309,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 = state.map.get(h.x, h.y)
local tile = 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
@ -331,7 +354,7 @@ end
function update_tower_redeye(tower, tower_index) function update_tower_redeye(tower, tower_index)
if not tower.target_index then if not tower.target_index then
for index,mob in pairs(MOBS) do
for index,mob in pairs(state.mobs) do
if mob then if mob then
local d = math.distance(mob.hex, tower.hex) local d = math.distance(mob.hex, tower.hex)
if d <= tower.range then if d <= tower.range then
@ -341,11 +364,11 @@ function update_tower_redeye(tower, tower_index)
end end
end end
else else
if MOBS[tower.target_index] == false then
if state.mobs[tower.target_index] == false then
tower.target_index = false tower.target_index = false
elseif (state.time - tower.last_shot_time) > tower.fire_rate then elseif (state.time - tower.last_shot_time) > tower.fire_rate then
local mob = MOBS[tower.target_index]
local mob = state.mobs[tower.target_index]
make_and_register_projectile( make_and_register_projectile(
tower.hex, tower.hex,
@ -362,7 +385,7 @@ end
function update_tower_howitzer(tower, tower_index) function update_tower_howitzer(tower, tower_index)
if not tower.target_index then if not tower.target_index then
-- we don't have a target -- we don't have a target
for index,mob in pairs(MOBS) do
for index,mob in pairs(state.mobs) do
if mob then if mob then
local d = math.distance(mob.hex, tower.hex) local d = math.distance(mob.hex, tower.hex)
if d <= tower.range then if d <= tower.range then
@ -374,13 +397,13 @@ function update_tower_howitzer(tower, tower_index)
tower.node("rotate").angle = math.wrapf(tower.node("rotate").angle + 0.1 * am.delta_time, math.pi*2) tower.node("rotate").angle = math.wrapf(tower.node("rotate").angle + 0.1 * am.delta_time, math.pi*2)
else else
-- we should have a target -- we should have a target
if MOBS[tower.target_index] == false then
if state.mobs[tower.target_index] == false then
-- the target we have was invalidated -- the target we have was invalidated
tower.target_index = false tower.target_index = false
else else
-- the target we have is valid -- the target we have is valid
local mob = MOBS[tower.target_index]
local mob = state.mobs[tower.target_index]
local vector = math.normalize(mob.position - tower.position) local vector = math.normalize(mob.position - tower.position)
if (state.time - tower.last_shot_time) > tower.fire_rate then if (state.time - tower.last_shot_time) > tower.fire_rate then
@ -446,17 +469,17 @@ end
function make_and_register_tower(hex, tower_type) function make_and_register_tower(hex, tower_type)
local tower = make_basic_entity( local tower = make_basic_entity(
hex, hex,
make_tower_node(tower_type),
get_tower_update_function(tower_type) get_tower_update_function(tower_type)
) )
tower.type = tower_type tower.type = tower_type
tower.node = am.translate(tower.position) ^ make_tower_node(tower_type)
local spec = get_tower_spec(tower_type) local spec = get_tower_spec(tower_type)
tower.cost = spec.cost tower.cost = spec.cost
tower.range = spec.range tower.range = spec.range
tower.fire_rate = spec.fire_rate tower.fire_rate = spec.fire_rate
tower.last_shot_time = -spec.fire_rate
tower.last_shot_time = -spec.fire_rate -- lets the tower fire immediately upon being placed
tower.size = spec.size tower.size = spec.size
if tower.size == 0 then if tower.size == 0 then
tower.hexes = { tower.hex } tower.hexes = { tower.hex }
@ -466,7 +489,7 @@ function make_and_register_tower(hex, tower_type)
tower.height = spec.height tower.height = spec.height
for _,h in pairs(tower.hexes) do for _,h in pairs(tower.hexes) do
local tile = state.map.get(h.x, h.y)
local tile = map_get(state.map, h.x, h.y)
tile.elevation = tile.elevation + tower.height tile.elevation = tile.elevation + tower.height
end end
@ -474,7 +497,7 @@ function make_and_register_tower(hex, tower_type)
tower.props.z = tower.height tower.props.z = tower.height
end end
register_entity(TOWERS, tower)
register_entity(state.towers, tower)
return tower return tower
end end
@ -486,13 +509,13 @@ function build_tower(hex, tower_type)
end end
function delete_all_towers() function delete_all_towers()
for tower_index,tower in pairs(TOWERS) do
if tower then delete_entity(TOWERS, tower_index) end
for tower_index,tower in pairs(state.towers) do
if tower then delete_entity(state.towers, tower_index) end
end end
end end
function do_tower_updates() function do_tower_updates()
for tower_index,tower in pairs(TOWERS) do
for tower_index,tower in pairs(state.towers) do
if tower and tower.update then if tower and tower.update then
tower.update(tower, tower_index) tower.update(tower, tower_index)
end end

Loading…
Cancel
Save