Browse Source

stuff

master
Nicholas Hayashi 4 years ago
parent
commit
570af1b536
  1. 35
      NOTES.md
  2. 14
      color.lua
  3. 27
      main.lua
  4. BIN
      res/moat1.png
  5. BIN
      res/wall_closed.png
  6. 114
      src/entity.lua
  7. 3
      src/extra.lua
  8. 13
      src/geometry.lua
  9. 37
      src/grid.lua
  10. 107
      src/hexyz.lua
  11. 143
      src/mob.lua
  12. 88
      src/projectile.lua
  13. 102
      src/tower.lua
  14. 1
      texture.lua

35
NOTES.md

@ -0,0 +1,35 @@
todoooos & notes
@TODO test optimizing pathfinding via breadth first search/djikstra
i think i want more or less no such thing as 'impassable' terrain
all mobs always can always traverse everything, though they initially will give the impression that certain tiles are impassable by prefering certain tiles.
the illusion is likely to be broken when you attempt to fully wall-off an area, and mobs begin deciding to climb over mountains or swim through lakes. this will come at great cost to them, but they will be capable of it
MAP RESOURCES
- spawn diamonds or special floating resources that give you bonuses for building on, whether it's score, money, or boosting the effectiveness of the tower you place on top, etc.
- killing certain mobs may cause these resources to spawn on the hex they died on
towers:
0 - wall
some fraction of the height of the tallest mountain
makes mob pathing more difficult
upgrades:
- +height - making the tower taller makes it more difficult/costly for mobs to climb over it
- spikes - mobs take damage when climbing
1 - moat
some fraction of the depth of the deepest lake
makes mob pathing more difficult
upgrades:
- +depth - making the moat deeper makes it more difficult/costly for mobs to swim through it
- alligators - mobs take damage while swimming

14
color.lua

@ -6,24 +6,26 @@ COLORS = {
-- tones -- tones
WHITE = vec4(0.8, 0.8, 0.7, 1), WHITE = vec4(0.8, 0.8, 0.7, 1),
BLACK = vec4(0, 0, 0.02, 1), BLACK = vec4(0, 0, 0.02, 1),
VERY_DARK_GRAY = vec4(45/255, 45/255, 35/255, 1),
TRUE_BLACK = vec4(0, 0, 0, 1), TRUE_BLACK = vec4(0, 0, 0, 1),
-- non-standard hues -- non-standard hues
WATER = vec4(0.12, 0.3, 0.3, 1), WATER = vec4(0.12, 0.3, 0.3, 1),
GRASS = vec4(0.10, 0.25, 0.10, 1), GRASS = vec4(0.10, 0.25, 0.10, 1),
DIRT = vec4(0.22, 0.20, 0.10, 1), DIRT = vec4(0.22, 0.20, 0.10, 1),
MOUNTAIN = vec4(0.45, 0.30, 0.20, 1),
MOUNTAIN = vec4(0.95, 0.30, 0.20, 1),
-- hues -- hues
MAGENTA = vec4(1, 0, 1, 1),
TEAL = vec4(16/255, 126/255, 124/244, 1),
YALE_BLUE = vec4(4/255, 75/255, 127/255, 1),
SUNRAY = vec4(228/255, 179/255, 99/255, 1),
MAGENTA = vec4( 1, 0, 1, 1),
TEAL = vec4( 16/255, 126/255, 124/244, 1),
YALE_BLUE = vec4( 4/255, 75/255, 127/255, 1),
OLIVE = vec4(111/255, 124/254, 18/255, 1), OLIVE = vec4(111/255, 124/254, 18/255, 1),
LIGHT_CYAN = vec4(224/255, 251/255, 252/255, 1), LIGHT_CYAN = vec4(224/255, 251/255, 252/255, 1),
PALE_SILVER = vec4(193/255, 178/255, 171/255, 1), PALE_SILVER = vec4(193/255, 178/255, 171/255, 1),
CLARET = vec4(139/255, 30/255, 63/255, 1), CLARET = vec4(139/255, 30/255, 63/255, 1),
BISTRO = vec4(73/255, 44/255, 29/255, 1),
DEEP_SPACE_SPARKLE = vec4(61/255, 90/255, 108/255, 1),
BISTRO = vec4( 73/255, 44/255, 29/255, 1),
DEEP_SPACE_SPARKLE = vec4( 61/255, 90/255, 108/255, 1),
WHEAT = vec4(225/255, 202/255, 150/255, 1) WHEAT = vec4(225/255, 202/255, 150/255, 1)
} }

27
main.lua

@ -25,7 +25,8 @@ WIN = am.window{
width = 1920, width = 1920,
height = 1080, height = 1080,
title = "hexyz", title = "hexyz",
highdpi = true,
letterbox = true
} }
OFF_SCREEN = vec2(WIN.width * 2) -- arbitrary pixel position that is garunteed to be off screen OFF_SCREEN = vec2(WIN.width * 2) -- arbitrary pixel position that is garunteed to be off screen
@ -40,6 +41,9 @@ MOUSE = false -- position of the mouse at the start of every frame, if an action
MUSIC_VOLUME = 0.1 MUSIC_VOLUME = 0.1
SFX_VOLUME = 0.1 SFX_VOLUME = 0.1
-- game stuff
SELECTED_TOWER_TYPE = TOWER_TYPE.REDEYE
-- top right display types -- top right display types
local TRDTS = { local TRDTS = {
NOTHING = -1, NOTHING = -1,
@ -52,6 +56,11 @@ local TRDTS = {
} }
local TRDT = TRDTS.SEED local TRDT = TRDTS.SEED
local function select_tower(tower_type)
SELECTED_TOWER_TYPE = tower_type
WIN.scene"tower_tooltip".text = tower_type_tostring(tower_type)
end
local function game_action(scene) local function game_action(scene)
if SCORE < 0 then game_end() end if SCORE < 0 then game_end() end
@ -72,12 +81,18 @@ local function game_action(scene)
if WIN:mouse_pressed"left" then if WIN:mouse_pressed"left" then
if hot and is_buildable(hex, tile, nil) then if hot and is_buildable(hex, tile, nil) then
make_and_register_tower(hex)
make_and_register_tower(hex, SELECTED_TOWER_TYPE)
end end
end end
if WIN:key_pressed"escape" then game_end()
elseif WIN:key_pressed"f1" then TRDT = (TRDT + 1) % #table.keys(TRDTS)
if WIN:key_pressed"escape" then
game_end()
elseif WIN:key_pressed"f1" then
TRDT = (TRDT + 1) % #table.keys(TRDTS)
elseif WIN:key_pressed"tab" then
select_tower((SELECTED_TOWER_TYPE + 1) % #table.keys(TOWER_TYPE))
end end
if tile and hot then if tile and hot then
@ -129,7 +144,10 @@ end
local function toolbelt() local function toolbelt()
local toolbelt_height = hex_height(HEX_SIZE) * 2 local toolbelt_height = hex_height(HEX_SIZE) * 2
local tower_tooltip = am.translate(WIN.left + 10, WIN.bottom + toolbelt_height + 10)
^ am.text(tower_type_tostring(SELECTED_TOWER_TYPE), "left"):tag"tower_tooltip"
local toolbelt = am.group{ local toolbelt = am.group{
tower_tooltip,
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)
} }
@ -154,6 +172,7 @@ function game_scene()
local coords = am.translate(WIN.right - 10, WIN.top - 20) ^ am.text("", "right", "top"):tag"coords" local coords = am.translate(WIN.right - 10, WIN.top - 20) ^ am.text("", "right", "top"):tag"coords"
local hex_cursor = am.circle(OFF_SCREEN, HEX_SIZE, COLORS.TRANSPARENT, 6):tag"hex_cursor" local hex_cursor = am.circle(OFF_SCREEN, HEX_SIZE, COLORS.TRANSPARENT, 6):tag"hex_cursor"
local curtain = am.rect(WIN.left, WIN.bottom, WIN.right, WIN.top, COLORS.TRUE_BLACK) local curtain = am.rect(WIN.left, WIN.bottom, WIN.right, WIN.top, COLORS.TRUE_BLACK)
curtain:action(coroutine.create(function() curtain:action(coroutine.create(function()
am.wait(am.tween(curtain, 3, { color = vec4(0) }, am.ease.out(am.ease.hyperbola))) am.wait(am.tween(curtain, 3, { color = vec4(0) }, am.ease.out(am.ease.hyperbola)))

BIN
res/moat1.png

After

Width: 137  |  Height: 137  |  Size: 1.8 KiB

BIN
res/wall_closed.png

Before

Width: 138  |  Height: 138  |  Size: 2.0 KiB

After

Width: 100  |  Height: 88  |  Size: 1.9 KiB

114
src/entity.lua

@ -1,77 +1,81 @@
ENTITY_TYPE = {
ENTITY = 0,
MOB = 1,
TOWER = 2,
PROJECTILE = 3
}
MOBS = {}
TOWERS = {}
PROJECTILES = {}
ENTITIES = {}
-- entity structure:
-- {
-- TOB - number - time of birth, const
-- hex - vec2 - current occupied hex, if any
-- position - vec2 - current pixel position of it's translate (forced parent) node
-- update - function - runs every frame with itself and its index as an argument
-- node - node - scene graph node
-- }
--
-- mob(entity) structure:
-- {
-- path - 2d table - map of hexes to other hexes, forms a path
-- speed - number - multiplier on distance travelled per frame, up to the update function to use correctly
-- bounty - number - score bonus you get when this mob is killed
-- hurtbox_radius - number -
-- }
--
-- tower(entity) structure:
-- {
-- -- @NOTE these should probably be wrapped in a 'weapon' struct or something, so towers can have multiple weapons
-- range - number - distance it can shoot
-- last_shot_time - number - timestamp (seconds) of last time it shot
-- target_index - number - index of entity it is currently shooting
-- }
--
-- bullet/projectile structure
-- {
-- vector - vec2 - normalized vector of the current direction of this projectile
-- velocity - number - multplier on distance travelled per frame
-- damage - number - guess
-- hitbox_radius - number - hitboxes are circles
-- }
--
function make_and_register_entity(type_, hex, node, update)
--[[
entity structure:
{
TOB - number - time of birth, const
hex - vec2 - current occupied hex, if any
position - vec2 - current pixel position of it's translate (forced parent) node
update - function - runs every frame with itself and its index as an argument
node - node - scene graph node
}
--]]
function make_basic_entity(hex, node, update, position)
local entity = {} local entity = {}
entity.type = type_
entity.TOB = TIME entity.TOB = TIME
-- usually you'll provide a hex and not a position, and the entity will spawn in the center
-- of the hex. if you want an entity to exist not at the center of a hex, you can provide a
-- pixel position instead
if position then
entity.position = position
entity.hex = pixel_to_hex(entity.position)
else
entity.hex = hex entity.hex = hex
entity.position = hex_to_pixel(hex) entity.position = hex_to_pixel(hex)
entity.update = update or function() log("unimplemented update function!") end
end
entity.update = update
entity.node = am.translate(entity.position) ^ node entity.node = am.translate(entity.position) ^ node
table.insert(ENTITIES, entity)
WORLD:append(entity.node)
return entity return entity
end end
function delete_all_entities()
for index,entity in pairs(ENTITIES) do
delete_entity(index)
end
function register_entity(t, entity)
table.insert(t, entity)
WORLD:append(entity.node)
end
ENTITIES = {}
-- |t| is the source table, probably MOBS, TOWERS, or PROJECTILES
function delete_entity(t, index)
if not t then log("splat!") end
WORLD:remove(t[index].node)
t[index] = false -- leave empty indexes so other entities can learn that this entity was deleted
end end
function delete_entity(index)
WORLD:remove(ENTITIES[index].node)
ENTITIES[index] = nil -- leave empty indexes so other entities can learn that this entity was deleted
function delete_all_entities()
for mob_index,mob in pairs(MOBS) do
delete_entity(MOBS, mob_index)
end
for tower_index,tower in pairs(TOWERS) do
delete_entity(TOWERS, tower_index)
end
for projectile_index,projectile in pairs(PROJECTILES) do
delete_entity(PROJECTILES, projectile_index)
end
end end
function do_entity_updates() function do_entity_updates()
for index,entity in pairs(ENTITIES) do
entity.update(entity, index)
for mob_index,mob in pairs(MOBS) do
if mob and mob.update then
mob.update(mob, mob_index)
end
end
for tower_index,tower in pairs(TOWERS) do
if tower and tower.update then
tower.update(tower, tower_index)
end
end
for projectile_index,projectile in pairs(PROJECTILES) do
if projectile and projectile.update then
projectile.update(projectile, projectile_index)
end
end end
end end

3
src/extra.lua

@ -1,4 +1,7 @@
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)

13
src/geometry.lua

@ -1,7 +1,18 @@
function circles_intersect(center1, center2, radius1, radius2) function circles_intersect(center1, center2, radius1, radius2)
return (((center1.x - center2.x)^2 + (center1.y - center2.y)^2)^0.5) <= (radius1 + radius2)
local c1, c2, r1, r2 = center1, center2, radius1, radius2
local d = math.distance(center1, center2)
local radii_sum = r1 + r2
-- touching
if d == radii_sum then return 1
-- not touching or intersecting
elseif d > radii_sum then return false
-- intersecting
else return 2
end
end end
function point_in_rect(point, rect) function point_in_rect(point, rect)

37
src/grid.lua

@ -1,6 +1,9 @@
-- distance from hex centerpoint to any vertex
HEX_SIZE = 20 HEX_SIZE = 20
HEX_PIXEL_SIZE = vec2(hex_width(HEX_SIZE, ORIENTATION.FLAT)
, hex_height(HEX_SIZE, ORIENTATION.FLAT))
-- with 1920x1080, this is the minimal dimensions to cover the screen (65x33) -- with 1920x1080, this is the minimal dimensions to cover the screen (65x33)
-- @NOTE added 2 cell padding, because we terraform the very outer edge and it looks ugly -- @NOTE added 2 cell padding, because we terraform the very outer edge and it looks ugly
@ -10,26 +13,29 @@ HEX_GRID_HEIGHT = 35
HEX_GRID_DIMENSIONS = vec2(HEX_GRID_WIDTH, HEX_GRID_HEIGHT) HEX_GRID_DIMENSIONS = vec2(HEX_GRID_WIDTH, HEX_GRID_HEIGHT)
-- leaving y == 0 makes this the center in hex coordinates -- leaving y == 0 makes this the center in hex coordinates
HEX_GRID_CENTER = vec2(math.floor(HEX_GRID_DIMENSIONS.x/2), 0)
HEX_GRID_CENTER = vec2(math.floor(HEX_GRID_WIDTH/2)
, 0)
-- math.floor(HEX_GRID_HEIGHT/2))
-- index is hex coordinates [x][y] -- index is hex coordinates [x][y]
-- { { elevation, node, etc. } } -- { { elevation, node, etc. } }
HEX_MAP = {} HEX_MAP = {}
local function grid_pixel_dimensions()
do
local hhs = hex_horizontal_spacing(HEX_SIZE) local hhs = hex_horizontal_spacing(HEX_SIZE)
local hvs = hex_vertical_spacing(HEX_SIZE) local hvs = hex_vertical_spacing(HEX_SIZE)
-- number of 'spacings' on the grid == number of cells - 1 -- number of 'spacings' on the grid == number of cells - 1
return vec2((HEX_GRID_WIDTH - 1) * hhs
GRID_PIXEL_DIMENSIONS = vec2((HEX_GRID_WIDTH - 1) * hhs
, (HEX_GRID_HEIGHT - 1) * hvs) , (HEX_GRID_HEIGHT - 1) * hvs)
end end
GRID_PIXEL_DIMENSIONS = grid_pixel_dimensions()
-- amulet puts 0,0 in the middle of the screen
-- transform coordinates by this to pretend 0,0 is elsewhere
WORLDSPACE_COORDINATE_OFFSET = -GRID_PIXEL_DIMENSIONS/2 WORLDSPACE_COORDINATE_OFFSET = -GRID_PIXEL_DIMENSIONS/2
-- the outer edges of the map are not interactable, most action occurs in the center
HEX_GRID_INTERACTABLE_REGION_PADDING = 4 HEX_GRID_INTERACTABLE_REGION_PADDING = 4
function is_interactable(tile, evenq) function is_interactable(tile, evenq)
return point_in_rect(evenq, { return point_in_rect(evenq, {
x1 = HEX_GRID_INTERACTABLE_REGION_PADDING, x1 = HEX_GRID_INTERACTABLE_REGION_PADDING,
@ -56,18 +62,17 @@ function color_at(elevation)
elseif elevation < 1 then -- high elevation elseif elevation < 1 then -- high elevation
return COLORS.MOUNTAIN{ ra = elevation } return COLORS.MOUNTAIN{ ra = elevation }
else
log('bad elevation'); return vec4(0)
end end
end end
-- hex_neighbours returns all coordinate positions that could be valid for a map extending infinite in all directions
-- grid_neighbours only gets you the neighbours that are actually in the grid
function grid_neighbours(map, hex)
return table.filter(hex_neighbours(hex), function(_hex)
return map.get(_hex.x, _hex.y)
end)
function grid_heuristic(source, target)
return math.distance(source, target)
end
function grid_cost(from, to)
local t1, t2 = HEX_MAP.get(from.x, from.y), HEX_MAP.get(to.x, to.y)
--local baseline = math.log(math.abs(1 - t1.elevation) + math.abs(1 - t2.elevation))
return math.abs(t1.elevation - t2.elevation) --+ baseline
end end
function random_map(seed) function random_map(seed)
@ -86,9 +91,9 @@ function random_map(seed)
else else
-- scale noise to be closer to 0 the closer we are to the center -- scale noise to be closer to 0 the closer we are to the center
-- @NOTE i don't know if this 100% of the time makes the center tile passable, but it probably does 99.9+% of the time
-- @NOTE i don't know if this 100% of the time makes the center tile passable, but it seems to 99.9+% of the time
local nx, ny = evenq.x/HEX_GRID_WIDTH - 0.5, -evenq.y/HEX_GRID_HEIGHT - 0.5 local nx, ny = evenq.x/HEX_GRID_WIDTH - 0.5, -evenq.y/HEX_GRID_HEIGHT - 0.5
local d = math.sqrt(nx^2 + ny^2) / math.sqrt(0.5)
local d = (nx^2 + ny^2)^0.5 / 0.5^0.5
noise = noise * d^0.125 -- arbitrary, seems to work good noise = noise * d^0.125 -- arbitrary, seems to work good
end end

107
src/hexyz.lua

@ -311,7 +311,12 @@ function parallelogram_map(width, height, seed)
get = function(x, y) return map_get(map, x, y) end, get = function(x, y) return map_get(map, x, y) end,
set = function(x, y, v) return map_set(map, x, y, v) 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, 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
traverse = function(callback) return map_traverse(map, callback) end,
neighbours = function(hex)
return table.filter(hex_neighbours(hex), function(_hex)
return map.get(_hex.x, _hex.y)
end)
end
}}) }})
end end
@ -344,7 +349,12 @@ function triangular_map(size, seed)
get = function(x, y) return map_get(map, x, y) end, get = function(x, y) return map_get(map, x, y) end,
set = function(x, y, v) return map_set(map, x, y, v) 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, 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
traverse = function(callback) return map_traverse(map, callback) end,
neighbours = function(hex)
return table.filter(hex_neighbours(hex), function(_hex)
return map.get(_hex.x, _hex.y)
end)
end
}}) }})
end end
@ -382,7 +392,12 @@ function hexagonal_map(radius, seed)
get = function(x, y) return map_get(map, x, y) end, get = function(x, y) return map_get(map, x, y) end,
set = function(x, y, v) return map_set(map, x, y, v) 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, 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
traverse = function(callback) return map_traverse(map, callback) end,
neighbours = function(hex)
return table.filter(hex_neighbours(hex), function(_hex)
return map.get(_hex.x, _hex.y)
end)
end
}}) }})
end end
@ -418,7 +433,12 @@ function rectangular_map(width, height, seed)
get = function(x, y) return map_get(map, x, y) end, get = function(x, y) return map_get(map, x, y) end,
set = function(x, y, v) return map_set(map, x, y, v) 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, 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
traverse = function(callback) return map_traverse(map, callback) end,
neighbours = function(hex)
return table.filter(hex_neighbours(hex), function(_hex)
return map.get(_hex.x, _hex.y)
end)
end
}}) }})
end end
@ -426,43 +446,78 @@ end
-- PATHFINDING -- PATHFINDING
--[[ @TODO bad breadth first
local frontier = { _tower.hex }
local history = {}
history[_tower.hex.x] = {}
history[_tower.hex.x][_tower.hex.y] = true
function breadth_first(map, start)
local frontier = {}
frontier[1] = { start }
local distance = {}
distance[start.x] = {}
distance[start.x][start.y] = 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(grid_neighbours(HEX_MAP, _tower.hex)) do
if not (history[neighbour.x] and history[neighbour.x][neighbour.y]) then
local mob = mob_on_hex(neighbour)
if mob then
_tower.target = mob
break
end
for _,neighbour in pairs(map.neighbours(current)) do
local d = map_get(distance, neighbour.x, neighbour.y)
if not d then
table.insert(frontier, neighbour) table.insert(frontier, neighbour)
map_set(distance, neighbour.x, neighbour.y, d + 1)
end
end end
end end
if _tower.target then
log(_tower.target)
return distance
end
function dijkstra(map, start, goal, cost_f)
local frontier = {}
frontier = { hex = start, priority = 0 }
local came_from = {}
came_from[start.x] = {}
came_from[start.x][start.y] = false
local cost_so_far = {}
cost_so_far[start.x] = {}
cost_so_far[start.x][start.y] = 0
while not (#frontier == 0) do
local current = table.remove(frontier, 1)
if current.hex == goal then
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(current.hex, neighbour)
local neighbour_cost = map_get(cost_so_far, neighbour.x, neighbour.y)
if not neighbour_cost or new_cost < neighbour_cost then
map_set(cost_so_far, neighbour.x, neighbour.y, new_cost)
local priority = new_cost
table.insert(frontier, { hex = neighbour, priority = priority })
map_set(came_from, neighbour.x, neighbour.y, current)
end
end
end end
]]
return came_from
end
-- generic A* pathfinding -- generic A* pathfinding
-- --
-- |heuristic| has the form:
-- function(source, target) -- source and target are vec2's
-- return some numeric value
--
-- |cost_f| has the form:
-- function (from, to) -- from and to are vec2's
-- return some numeric value
--
-- returns a map that has map[hex.x][hex.y] = { hex = vec2, priority = number }, -- 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, -- 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| -- as well as 'made_it' a bool that tells you if we were successful in reaching |goal|
function Astar(map, start, goal, neighbour_f, heuristic_f, cost_f)
local neighbour_f = neighbour_f or function(map, hex) return hex_neighbours(hex) end
local heuristic_f = heuristic_f or math.distance
local cost_f = cost_f or function(from, to) return 1 end
function Astar(map, start, goal, heuristic, cost_f)
local path = {} local path = {}
path[start.x] = {} path[start.x] = {}
path[start.x][start.y] = false path[start.x][start.y] = false
@ -483,13 +538,13 @@ function Astar(map, start, goal, neighbour_f, heuristic_f, cost_f)
break break
end end
for _,next_ in pairs(neighbour_f(map, 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(current.hex, next_) local new_cost = map_get(path_so_far, current.hex.x, current.hex.y) + cost_f(current.hex, next_)
local next_cost = map_get(path_so_far, next_.x, next_.y) local next_cost = 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) map_set(path_so_far, next_.x, next_.y, new_cost)
local priority = new_cost + heuristic_f(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) map_set(path, next_.x, next_.y, current)
end end

143
src/mob.lua

@ -1,34 +1,81 @@
--[[
mob(entity) structure:
{
path - 2d table - map of hexes to other hexes, forms a path
speed - number - multiplier on distance travelled per frame, up to the update function to use correctly
bounty - number - score bonus you get when this mob is killed
hurtbox_radius - number -
}
--]]
-- distance from hex centerpoint to nearest edge
MOB_SIZE = hex_height(HEX_SIZE, ORIENTATION.FLAT) / 2
function mobs_on_hex(hex)
local t = {}
for mob_index,mob in pairs(MOBS) do
if mob and mob.hex == hex then
table.insert(t, mob_index, mob)
end
end
return t
end
-- @NOTE returns i,v in the table -- @NOTE returns i,v in the table
function mob_on_hex(hex) function mob_on_hex(hex)
return table.find(ENTITIES, function(entity)
return entity.type == ENTITY_TYPE.MOB and entity.hex == hex
return table.find(MOBS, function(mob)
return mob and mob.hex == hex
end) end)
end end
function mob_die(mob, entity_index)
function mob_die(mob, mob_index)
WORLD:action(vplay_sound(SOUNDS.EXPLOSION1)) WORLD:action(vplay_sound(SOUNDS.EXPLOSION1))
delete_entity(entity_index)
--WORLD:append(mob_death_explosion(mob))
delete_entity(MOBS, mob_index)
end
function mob_death_explosion(mob)
local t = 0.5
return am.particles2d{
source_pos = mob.position,
source_pos_var = vec2(mob.hurtbox_radius),
max_particles = 25,
start_size = mob.hurtbox_radius/10,
start_size_var = mob.hurtbox_radius/15,
end_size = 0,
angle = 0,
angle_var = math.pi,
speed = 105,
speed_var = 55,
life = t * 0.8,
life_var = t * 0.2,
start_color = COLORS.CLARET,
start_color_var = COLORS.DIRT,
end_color = COLORS.DIRT,
end_color_var = COLORS.CLARET,
damping = 0.3
}:action(coroutine.create(function(self)
am.wait(am.delay(t))
WORLD:remove(self)
end))
end end
function do_hit_mob(mob, damage, index)
function do_hit_mob(mob, damage, mob_index)
mob.health = mob.health - damage mob.health = mob.health - damage
if mob.health < 1 then
if mob.health <= 0 then
update_score(mob.bounty) update_score(mob.bounty)
mob_die(mob, index)
mob_die(mob, mob_index)
end end
end end
function check_for_broken_mob_pathing(hex) function check_for_broken_mob_pathing(hex)
for _,entity in pairs(ENTITIES) do
if entity.type == ENTITY_TYPE.MOB and entity.path[hex.x] and entity.path[hex.x][hex.y] then
--local pathfinder = coroutine.create(function()
entity.path = get_mob_path(entity, HEX_MAP, entity.hex, HEX_GRID_CENTER)
--end)
--coroutine.resume(pathfinder)
for _,mob in pairs(MOBS) do
if mob and mob.path[hex.x] and mob.path[hex.x][hex.y] then
mob.path = get_mob_path(mob, HEX_MAP, mob.hex, HEX_GRID_CENTER)
end end
end end
end end
@ -43,22 +90,7 @@ end
-- try reducing map size by identifying key nodes (inflection points) -- try reducing map size by identifying key nodes (inflection points)
-- there are performance hits everytime we spawn a mob and it's Astar's fault -- there are performance hits everytime we spawn a mob and it's Astar's fault
function get_mob_path(mob, map, start, goal) function get_mob_path(mob, map, start, goal)
return Astar(map, goal, start,
-- neighbour function
function(map, hex)
return table.filter(grid_neighbours(map, hex), function(_hex)
return mob_can_pass_through(mob, _hex)
end)
end,
-- heuristic function
math.distance,
-- cost function
function(from, to)
return math.abs(map.get(from.x, from.y).elevation - map.get(to.x, to.y).elevation)
end
)
return Astar(map, goal, start, grid_heuristic, grid_cost)
end end
-- @FIXME there's a bug here where the position of the spawn hex is sometimes 1 closer to the center than we want -- @FIXME there's a bug here where the position of the spawn hex is sometimes 1 closer to the center than we want
@ -91,58 +123,53 @@ local function get_spawn_hex()
return spawn_hex return spawn_hex
end end
local function make_and_register_mob()
local mob = make_and_register_entity(
-- type
ENTITY_TYPE.MOB,
-- hex spawn position
get_spawn_hex(),
local function mob_update(mob, mob_index)
mob.hex = pixel_to_hex(mob.position)
-- node
am.scale(2)
^ am.rotate(TIME)
^ pack_texture_into_sprite(TEX_MOB1_1, 20, 20),
-- update
function(_mob, _mob_index)
_mob.hex = pixel_to_hex(_mob.position)
local frame_target = _mob.path[_mob.hex.x] and _mob.path[_mob.hex.x][_mob.hex.y]
local frame_target = mob.path[mob.hex.x] and mob.path[mob.hex.x][mob.hex.y]
if frame_target then if frame_target then
_mob.position = _mob.position + math.normalize(hex_to_pixel(frame_target.hex) - _mob.position) * _mob.speed
_mob.node.position2d = _mob.position
mob.position = mob.position + math.normalize(hex_to_pixel(frame_target.hex) - mob.position) * mob.speed
mob.node.position2d = mob.position
else else
if _mob.hex == HEX_GRID_CENTER then
update_score(-_mob.health)
mob_die(_mob, _mob_index)
if mob.hex == HEX_GRID_CENTER then
update_score(-mob.health)
mob_die(mob, mob_index)
else else
log("stuck") log("stuck")
end end
end end
-- passive animation
--[[ passive animation
if math.random() < 0.01 then if math.random() < 0.01 then
_mob.node"rotate":action(am.tween(0.3, { angle = _mob.node"rotate".angle + math.pi*3 }))
mob.node"rotate":action(am.tween(0.3, { angle = mob.node"rotate".angle + math.pi*3 }))
else else
_mob.node"rotate".angle = math.wrapf(_mob.node"rotate".angle + am.delta_time, math.pi*2)
end
mob.node"rotate".angle = math.wrapf(mob.node"rotate".angle + am.delta_time, math.pi*2)
end end
--]]
end
local function make_and_register_mob()
local mob = make_basic_entity(
get_spawn_hex(),
am.circle(vec2(0), MOB_SIZE, COLORS.SUNRAY),
mob_update
) )
mob.path = get_mob_path(mob, HEX_MAP, mob.hex, HEX_GRID_CENTER) mob.path = get_mob_path(mob, HEX_MAP, mob.hex, HEX_GRID_CENTER)
mob.health = 10 mob.health = 10
mob.speed = 1 mob.speed = 1
mob.bounty = 5 mob.bounty = 5
mob.hurtbox_radius = 100
mob.hurtbox_radius = MOB_SIZE
register_entity(MOBS, mob)
end end
local SPAWN_CHANCE = 100 local SPAWN_CHANCE = 100
function do_mob_spawning() function do_mob_spawning()
--if WIN:key_pressed"space" then --if WIN:key_pressed"space" then
if math.random(SPAWN_CHANCE) == 1 then if math.random(SPAWN_CHANCE) == 1 then
--if #MOBS < 1 then
make_and_register_mob() make_and_register_mob()
end end
end end

88
src/projectile.lua

@ -1,45 +1,81 @@
function make_and_register_projectile(hex, vector, velocity, damage, hitbox_radius)
local projectile = make_and_register_entity(
-- type
ENTITY_TYPE.PROJECTILE,
--[[
bullet/projectile(entity) structure
{
vector - vec2 - normalized vector of the current direction of this projectile
velocity - number - multplier on distance travelled per frame
damage - number - guess
hitbox_radius - number - hitboxes are circles
}
--]]
hex,
function projectile_update(projectile, projectile_index)
projectile.position = projectile.position + projectile.vector * projectile.velocity
projectile.node.position2d = projectile.position
projectile.hex = pixel_to_hex(projectile.position)
-- node
am.circle(vec2(0), hitbox_radius - 1, COLORS.CLARET),
-- check if we're out of bounds
if not point_in_rect(projectile.position + WORLDSPACE_COORDINATE_OFFSET, {
x1 = WIN.left,
y1 = WIN.bottom,
x2 = WIN.right,
y2 = WIN.top
}) then
delete_entity(PROJECTILES, projectile_index)
return true
end
-- update function
function(_projectile, _projectile_index)
_projectile.position = _projectile.position + vector * velocity
_projectile.node.position2d = _projectile.position
_projectile.hex = pixel_to_hex(_projectile.position)
-- check if we hit something
-- get a list of hexes that could have something we could hit on them
local search_hexes = spiral_map(projectile.hex, 1)
local hit_mob_count = 0
local hit_mobs = {}
for _,hex in pairs(search_hexes) do
local mob_index,mob = mob_on_hex(_projectile.hex)
-- check if there's a mob on the hex
for mob_index,mob in pairs(mobs_on_hex(hex)) do
if mob and circles_intersect(mob.position if mob and circles_intersect(mob.position
, _projectile.position
, projectile.position
, mob.hurtbox_radius , mob.hurtbox_radius
, _projectile.hitbox_radius) then
, projectile.hitbox_radius) then
table.insert(hit_mobs, mob_index, mob)
hit_mob_count = hit_mob_count + 1
end
end
end
do_hit_mob(mob, _projectile.damage, mob_index)
delete_entity(_projectile_index)
WORLD:action(vplay_sound(SOUNDS.HIT1))
-- we didn't hit anyone
if hit_mob_count == 0 then return end
elseif not point_in_rect(_projectile.position + WORLDSPACE_COORDINATE_OFFSET, {
x1 = WIN.left,
y1 = WIN.bottom,
x2 = WIN.right,
y2 = WIN.top
}) then
delete_entity(_projectile_index)
-- we could have hit multiple, (optionally) find the closest
local closest_mob_index, closest_mob = next(hit_mobs, nil)
local closest_d = math.distance(closest_mob.position, projectile.position)
for _mob_index,mob in pairs(hit_mobs) do
local d = math.distance(mob.position, projectile.position)
if d < closest_d then
closest_mob_index = _mob_index
closest_mob = mob
closest_d = d
end end
end end
)
-- hit the mob, delete ourselves, affect the world
do_hit_mob(closest_mob, projectile.damage, closest_mob_index)
delete_entity(PROJECTILES, projectile_index)
WORLD:action(vplay_sound(SOUNDS.HIT1))
end
function make_and_register_projectile(hex, vector, velocity, damage, hitbox_radius)
local projectile = make_basic_entity(hex
, am.line(vector, vector*hitbox_radius, 3, COLORS.CLARET)
, projectile_update)
projectile.vector = vector projectile.vector = vector
projectile.velocity = velocity projectile.velocity = velocity
projectile.damage = damage projectile.damage = damage
projectile.hitbox_radius = hitbox_radius projectile.hitbox_radius = hitbox_radius
register_entity(PROJECTILES, projectile)
end end

102
src/tower.lua

@ -1,53 +1,97 @@
TOWER_TYPE = {
REDEYE = 0,
WALL = 1,
MOAT = 2,
}
function tower_type_tostring(type_)
if type_ == TOWER_TYPE.REDEYE then
return "Redeye Tower"
elseif type_ == TOWER_TYPE.WALL then
return "Wall"
elseif type_ == TOWER_TYPE.MOAT then
return "Moat"
end
end
--[[
tower(entity) structure:
{
-- @NOTE these should probably be wrapped in a 'weapon' struct or something, so towers can have multiple weapons
range - number - distance it can shoot
last_shot_time - number - timestamp (seconds) of last time it shot
target_index - number - index of entity it is currently shooting
}
--]]
function is_buildable(hex, tile, tower) function is_buildable(hex, tile, tower)
local blocked = mob_on_hex(hex) local blocked = mob_on_hex(hex)
return not blocked and is_passable(tile) return not blocked and is_passable(tile)
end end
function make_and_register_tower(hex)
local tower = make_and_register_entity(
-- type
ENTITY_TYPE.TOWER,
-- spawning hex
hex,
-- node
pack_texture_into_sprite(TEX_TOWER2, 45, 34),
-- update function
function(_tower, _tower_index)
if not _tower.target_index then
for index,entity in pairs(ENTITIES) do
if entity and entity.type == ENTITY_TYPE.MOB then
local d = math.distance(entity.hex, _tower.hex)
if d <= _tower.range then
_tower.target_index = index
function update_tower_redeye(tower, tower_index)
if not tower.target_index then
for index,mob in pairs(MOBS) do
if mob then
local d = math.distance(mob.position, tower.position) / (HEX_SIZE * 2)
if d <= tower.range then
tower.target_index = index
break break
end end
end end
end end
else else
if ENTITIES[_tower.target_index] == nil then
_tower.target_index = false
if MOBS[tower.target_index] == false then
tower.target_index = false
elseif (TIME - _tower.last_shot_time) > 1 then
local entity = ENTITIES[_tower.target_index]
elseif (TIME - tower.last_shot_time) > 1 then
local mob = MOBS[tower.target_index]
make_and_register_projectile( make_and_register_projectile(
_tower.hex,
math.normalize(hex_to_pixel(entity.hex) - _tower.position),
tower.hex,
math.normalize(hex_to_pixel(mob.hex) - tower.position),
15, 15,
5, 5,
4
10
) )
_tower.last_shot_time = TIME
_tower.node:action(vplay_sound(SOUNDS.LASER2))
tower.last_shot_time = TIME
tower.node:action(vplay_sound(SOUNDS.LASER2))
end end
end end
end
local function make_tower_sprite(tower_type)
if tower_type == TOWER_TYPE.REDEYE then
return pack_texture_into_sprite(TEX_TOWER2, HEX_PIXEL_SIZE.x, HEX_PIXEL_SIZE.y)
elseif tower_type == TOWER_TYPE.WALL then
--return pack_texture_into_sprite(TEX_WALL_CLOSED, HEX_PIXEL_SIZE.x, HEX_PIXEL_SIZE.y)
return am.circle(vec2(0), HEX_SIZE, COLORS.VERY_DARK_GRAY, 6)
elseif tower_type == TOWER_TYPE.MOAT then
--return pack_texture_into_sprite(TEX_MOAT1, HEX_PIXEL_SIZE.x, HEX_PIXEL_SIZE.y)
return am.circle(vec2(0), HEX_SIZE, COLORS.YALE_BLUE, 6)
end end
end
local function modify_terrain_by_tower_type(tower_type, hex)
end
local function get_tower_update_function(tower_type)
if tower_type == TOWER_TYPE.REDEYE then
return update_tower_redeye
end
end
function make_and_register_tower(hex, tower_type)
local tower = make_basic_entity(
hex,
make_tower_sprite(tower_type),
get_tower_update_function(tower_type)
) )
tower.range = 10 tower.range = 10
@ -57,5 +101,7 @@ function make_and_register_tower(hex)
-- make this cell impassable -- make this cell impassable
HEX_MAP[hex.x][hex.y].elevation = 2 HEX_MAP[hex.x][hex.y].elevation = 2
check_for_broken_mob_pathing(hex) check_for_broken_mob_pathing(hex)
register_entity(TOWERS, tower)
end end

1
texture.lua

@ -6,6 +6,7 @@ function load_textures()
TEX_ARROW = am.texture2d("res/arrow.png") TEX_ARROW = am.texture2d("res/arrow.png")
TEX_WALL_CLOSED = am.texture2d("res/wall_closed.png") TEX_WALL_CLOSED = am.texture2d("res/wall_closed.png")
TEX_MOAT1 = am.texture2d("res/moat1.png")
TEX_TOWER1 = am.texture2d("res/tower1.png") TEX_TOWER1 = am.texture2d("res/tower1.png")
TEX_TOWER2 = am.texture2d("res/tower2.png") TEX_TOWER2 = am.texture2d("res/tower2.png")

Loading…
Cancel
Save