Browse Source

stuff

master
Nicholas Hayashi 4 years ago
parent
commit
570af1b536
  1. 35
      NOTES.md
  2. 18
      color.lua
  3. 27
      main.lua
  4. BIN
      res/moat1.png
  5. BIN
      res/wall_closed.png
  6. 118
      src/entity.lua
  7. 3
      src/extra.lua
  8. 13
      src/geometry.lua
  9. 39
      src/grid.lua
  10. 107
      src/hexyz.lua
  11. 161
      src/mob.lua
  12. 92
      src/projectile.lua
  13. 126
      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

18
color.lua

@ -6,24 +6,26 @@ COLORS = {
-- tones
WHITE = vec4(0.8, 0.8, 0.7, 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),
-- non-standard hues
WATER = vec4(0.12, 0.3, 0.3, 1),
GRASS = vec4(0.10, 0.25, 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
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),
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),
LIGHT_CYAN = vec4(224/255, 251/255, 252/255, 1),
PALE_SILVER = vec4(193/255, 178/255, 171/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),
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),
WHEAT = vec4(225/255, 202/255, 150/255, 1)
}

27
main.lua

@ -25,7 +25,8 @@ WIN = am.window{
width = 1920,
height = 1080,
title = "hexyz",
highdpi = true,
letterbox = true
}
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
SFX_VOLUME = 0.1
-- game stuff
SELECTED_TOWER_TYPE = TOWER_TYPE.REDEYE
-- top right display types
local TRDTS = {
NOTHING = -1,
@ -52,6 +56,11 @@ local TRDTS = {
}
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)
if SCORE < 0 then game_end() end
@ -72,12 +81,18 @@ local function game_action(scene)
if WIN:mouse_pressed"left" 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
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
if tile and hot then
@ -129,7 +144,10 @@ end
local function toolbelt()
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{
tower_tooltip,
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 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)
curtain:action(coroutine.create(function()
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

118
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 = {}
entity.type = type_
entity.TOB = TIME
entity.hex = hex
entity.position = hex_to_pixel(hex)
entity.update = update or function() log("unimplemented update function!") end
-- 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.position = hex_to_pixel(hex)
end
entity.update = update
entity.node = am.translate(entity.position) ^ node
table.insert(ENTITIES, entity)
WORLD:append(entity.node)
return entity
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
-- |t| is the source table, probably MOBS, TOWERS, or PROJECTILES
function delete_entity(t, index)
if not t then log("splat!") end
ENTITIES = {}
WORLD:remove(t[index].node)
t[index] = false -- leave empty indexes so other entities can learn that this entity was deleted
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
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

3
src/extra.lua

@ -1,4 +1,7 @@
function booltostring(bool)
return bool and "true" or "false"
end
function math.wrapf(float, range)
return float - range * math.floor(float / range)

13
src/geometry.lua

@ -1,7 +1,18 @@
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
function point_in_rect(point, rect)

39
src/grid.lua

@ -1,6 +1,9 @@
-- distance from hex centerpoint to any vertex
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)
-- @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)
-- 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]
-- { { elevation, node, etc. } }
HEX_MAP = {}
local function grid_pixel_dimensions()
do
local hhs = hex_horizontal_spacing(HEX_SIZE)
local hvs = hex_vertical_spacing(HEX_SIZE)
-- number of 'spacings' on the grid == number of cells - 1
return vec2((HEX_GRID_WIDTH - 1) * hhs
, (HEX_GRID_HEIGHT - 1) * hvs)
GRID_PIXEL_DIMENSIONS = vec2((HEX_GRID_WIDTH - 1) * hhs
, (HEX_GRID_HEIGHT - 1) * hvs)
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
-- the outer edges of the map are not interactable, most action occurs in the center
HEX_GRID_INTERACTABLE_REGION_PADDING = 4
function is_interactable(tile, evenq)
return point_in_rect(evenq, {
x1 = HEX_GRID_INTERACTABLE_REGION_PADDING,
@ -56,18 +62,17 @@ function color_at(elevation)
elseif elevation < 1 then -- high elevation
return COLORS.MOUNTAIN{ ra = elevation }
else
log('bad elevation'); return vec4(0)
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
function random_map(seed)
@ -86,9 +91,9 @@ function random_map(seed)
else
-- 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 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
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,
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
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
@ -344,7 +349,12 @@ function triangular_map(size, 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
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
@ -382,7 +392,12 @@ function hexagonal_map(radius, 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
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
@ -418,7 +433,12 @@ function rectangular_map(width, height, 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
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
@ -426,43 +446,78 @@ end
-- 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
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)
map_set(distance, neighbour.x, neighbour.y, d + 1)
end
end
end
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
if _tower.target then
log(_tower.target)
while not (#frontier == 0) do
local current = table.remove(frontier, 1)
if current.hex == goal then
break
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
]]
return came_from
end
-- 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 },
-- 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, 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 = {}
path[start.x] = {}
path[start.x][start.y] = false
@ -483,13 +538,13 @@ function Astar(map, start, goal, neighbour_f, heuristic_f, cost_f)
break
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 next_cost = map_get(path_so_far, next_.x, next_.y)
if not next_cost or new_cost < next_cost then
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 })
map_set(path, next_.x, next_.y, current)
end

161
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
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
function mob_die(mob, entity_index)
function mob_die(mob, mob_index)
WORLD:action(vplay_sound(SOUNDS.EXPLOSION1))
delete_entity(entity_index)
--WORLD:append(mob_death_explosion(mob))
delete_entity(MOBS, mob_index)
end
function do_hit_mob(mob, damage, index)
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
function do_hit_mob(mob, damage, mob_index)
mob.health = mob.health - damage
if mob.health < 1 then
if mob.health <= 0 then
update_score(mob.bounty)
mob_die(mob, index)
mob_die(mob, mob_index)
end
end
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
@ -43,22 +90,7 @@ end
-- 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
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
-- @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
end
local function make_and_register_mob()
local mob = make_and_register_entity(
-- type
ENTITY_TYPE.MOB,
local function mob_update(mob, mob_index)
mob.hex = pixel_to_hex(mob.position)
-- hex spawn position
get_spawn_hex(),
local frame_target = mob.path[mob.hex.x] and mob.path[mob.hex.x][mob.hex.y]
-- 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]
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
else
if _mob.hex == HEX_GRID_CENTER then
update_score(-_mob.health)
mob_die(_mob, _mob_index)
else
log("stuck")
end
end
-- passive animation
if math.random() < 0.01 then
_mob.node"rotate":action(am.tween(0.3, { angle = _mob.node"rotate".angle + math.pi*3 }))
else
_mob.node"rotate".angle = math.wrapf(_mob.node"rotate".angle + am.delta_time, math.pi*2)
end
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
else
if mob.hex == HEX_GRID_CENTER then
update_score(-mob.health)
mob_die(mob, mob_index)
else
log("stuck")
end
end
--[[ passive animation
if math.random() < 0.01 then
mob.node"rotate":action(am.tween(0.3, { angle = mob.node"rotate".angle + math.pi*3 }))
else
mob.node"rotate".angle = math.wrapf(mob.node"rotate".angle + am.delta_time, math.pi*2)
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.health = 10
mob.speed = 1
mob.bounty = 5
mob.hurtbox_radius = 100
mob.hurtbox_radius = MOB_SIZE
register_entity(MOBS, mob)
end
local SPAWN_CHANCE = 100
function do_mob_spawning()
--if WIN:key_pressed"space" then
if math.random(SPAWN_CHANCE) == 1 then
--if #MOBS < 1 then
make_and_register_mob()
end
end

92
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
, _projectile.position
, projectile.position
, mob.hurtbox_radius
, _projectile.hitbox_radius) then
do_hit_mob(mob, _projectile.damage, mob_index)
delete_entity(_projectile_index)
WORLD:action(vplay_sound(SOUNDS.HIT1))
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)
, projectile.hitbox_radius) then
table.insert(hit_mobs, mob_index, mob)
hit_mob_count = hit_mob_count + 1
end
end
)
end
-- we didn't hit anyone
if hit_mob_count == 0 then return end
-- 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
-- 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.velocity = velocity
projectile.damage = damage
projectile.hitbox_radius = hitbox_radius
register_entity(PROJECTILES, projectile)
end

126
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)
local blocked = mob_on_hex(hex)
return not blocked and is_passable(tile)
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
break
end
end
end
else
if ENTITIES[_tower.target_index] == nil then
_tower.target_index = false
elseif (TIME - _tower.last_shot_time) > 1 then
local entity = ENTITIES[_tower.target_index]
make_and_register_projectile(
_tower.hex,
math.normalize(hex_to_pixel(entity.hex) - _tower.position),
15,
5,
4
)
_tower.last_shot_time = TIME
_tower.node:action(vplay_sound(SOUNDS.LASER2))
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
end
end
end
else
if MOBS[tower.target_index] == false then
tower.target_index = false
elseif (TIME - tower.last_shot_time) > 1 then
local mob = MOBS[tower.target_index]
make_and_register_projectile(
tower.hex,
math.normalize(hex_to_pixel(mob.hex) - tower.position),
15,
5,
10
)
tower.last_shot_time = TIME
tower.node:action(vplay_sound(SOUNDS.LASER2))
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
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
@ -57,5 +101,7 @@ function make_and_register_tower(hex)
-- make this cell impassable
HEX_MAP[hex.x][hex.y].elevation = 2
check_for_broken_mob_pathing(hex)
register_entity(TOWERS, tower)
end

1
texture.lua

@ -6,6 +6,7 @@ function load_textures()
TEX_ARROW = am.texture2d("res/arrow.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_TOWER2 = am.texture2d("res/tower2.png")

Loading…
Cancel
Save