@ -1,235 +1,389 @@
function default_tower_placement_f ( blocked , has_water , has_mountain , has_ground , hex )
local function default_tower_placement_f ( blocked , has_water , has_mountain , has_ground , hex )
return not ( blocked or has_water or has_mountain )
end
function default_weapon_target_acquisition_f ( tower , tower_index )
local function default_weapon_target_acquisition_f ( tower , tower_index )
for index , mob in pairs ( game_state.mobs ) do
if mob then
local d = math.distance ( mob.hex , tower.hex )
if d <= tower.range then
tower.target_index = index
break
for _ , w in pairs ( tower.weapons ) do
if d <= w.range then
tower.target_index = index
return
end
end
end
end
end
function default_tower_target_acquisition_f ( tower , tower_index )
-- first, find out if a tower even *should*, acquire a target.
-- a tower should try and acquire a target if atleast one of its weapons that could be shooting, isn't
local function default_handle_target_f ( tower , tower_index , mob )
-- the target we have is valid
local vector = math.normalize ( mob.position - tower.position )
for _ , w in pairs ( tower.weapons ) do
if ( game_state.time - w.last_shot_time ) > w.fire_rate then
local projectile = make_and_register_projectile (
tower.hex ,
w.projectile_type ,
vector
)
w.last_shot_time = game_state.time
play_sfx ( w.hit_sound )
end
end
end
local function default_tower_update_f ( tower , tower_index )
if not tower.target_index then
for index , mob in pairs ( game_state.mobs ) do
if mob then
local d = math.distance ( mob.hex , tower.hex )
if d <= tower.range then
tower.target_index = index
break
end
end
-- try and acquire a target
default_weapon_target_acquisition_f ( tower , tower_index )
else
-- check if our current target is invalidated
local mob = game_state.mobs [ tower.target_index ]
if not mob then
tower.target_index = false
else
-- do what we should do with the target
default_handle_target_f ( tower , tower_index , mob )
end
end
end
function default_tower_update _f ( tower , tower_index )
local function default_tower_weapon_target_acquirer _f ( tower , tower_index )
end
-- load tower spec file
TOWER_SPECS = { }
TOWER_TYPE = { }
function get_tower_spec ( tower_type )
return TOWER_SPECS [ tower_type ]
end
function get_tower_name ( tower_type )
return TOWER_SPECS [ tower_type ] . name
end
function get_tower_placement_rules_text ( tower_type )
return TOWER_SPECS [ tower_type ] . placement_rules_text
end
function get_tower_short_description ( tower_type )
return TOWER_SPECS [ tower_type ] . short_description
local function make_tower_sprite ( t )
return pack_texture_into_sprite ( t.texture , HEX_PIXEL_WIDTH , HEX_PIXEL_HEIGHT )
end
function get_tower_texture ( tower_type )
return TOWER_SPECS [ tower_type ] . texture
end
function get_tower_icon_texture ( tower_type )
return TOWER_SPECS [ tower_type ] . icon_texture
end
function get_tower_cost ( tower_type )
return TOWER_SPECS [ tower_type ] . cost
end
function get_tower_range ( tower_type )
return TOWER_SPECS [ tower_type ] . range
end
function get_tower_fire_rate ( tower_type )
return TOWER_SPECS [ tower_type ] . fire_rate
end
function get_tower_size ( tower_type )
return TOWER_SPECS [ tower_type ] . size
end
function resolve_tower_specs ( spec_file_path )
local spec_file = am.load_script ( spec_file_path )
local error_message
if spec_file then
local status , tower_specs = pcall ( spec_file )
if status then
-- lua managed to run the file without syntax/runtime errors
-- it's not garunteed to be what we want yet. check:
local type_ = type ( tower_specs )
if type_ ~= " table " then
error_message = " tower spec file should return a table, but we got " .. type_
function init_tower_specs ( )
local base_tower_specs = {
{
id = " WALL " ,
name = " Wall " ,
placement_rules_text = " Place on Ground " ,
short_description = " Restricts movement, similar to a mountain. " ,
texture = TEXTURES.TOWER_WALL ,
icon_texture = TEXTURES.TOWER_WALL_ICON ,
cost = 10 ,
range = 0 ,
fire_rate = 2 ,
update_f = false ,
make_node_f = function ( self )
return am.circle ( vec2 ( 0 ) , HEX_SIZE , COLORS.TAN1 { a = 0.6 } , 6 )
end
-- if we're here, then we're going to assume the spec file is valid, no matter how weird it is
-- last thing to do before returning is fill in missing default values
for i , tower_spec in pairs ( tower_specs ) do
if not tower_spec.size then
tower_spec.size = 1
end
if not tower_spec.height then
tower_spec.height = 1
} ,
{
id = " GATTLER " ,
name = " Gattler " ,
placement_rules_text = " Place on Ground " ,
short_description = " Short-range, fast-fire rate single-target tower. " ,
texture = TEXTURES.TOWER_GATTLER ,
icon_texture = TEXTURES.TOWER_GATTLER_ICON ,
cost = 20 ,
weapons = {
{
projectile_type = PROJECTILE_TYPE.BULLET ,
range = 4 ,
fire_rate = 0.5 ,
hit_sound = SOUNDS.HIT1 ,
}
} ,
make_node_f = function ( self )
return am.group (
am.circle ( vec2 ( 0 ) , HEX_SIZE - 4 , COLORS.VERY_DARK_GRAY , 5 ) ,
am.rotate ( game_state.time or 0 )
^ pack_texture_into_sprite ( self.texture , HEX_PIXEL_HEIGHT * 1.5 , HEX_PIXEL_WIDTH * 2 , COLORS.GREEN_YELLOW )
)
end ,
update_f = function ( tower , tower_index )
if not tower.target_index then
-- we should try and acquire a target
default_weapon_target_acquisition_f ( tower , tower_index )
-- passive animation
tower.node ( " rotate " ) . angle = math.wrapf ( tower.node ( " rotate " ) . angle + 0.1 * am.delta_time , math.pi * 2 )
else
-- should have a target, so we should try and shoot it
local mob = game_state.mobs [ tower.target_index ]
if not mob then
-- the target we have was invalidated
tower.target_index = false
else
default_handle_target_f ( tower , tower_index , mob )
-- point the cannon at the dude
local theta = math.rad ( 90 ) - math.atan ( ( tower.position . y - mob.position . y ) / ( tower.position . x - mob.position . x ) )
local diff = tower.node ( " rotate " ) . angle - theta
tower.node ( " rotate " ) . angle = - theta + math.pi / 2
end
end
end
} ,
{
id = " HOWITZER " ,
name = " Howitzer " ,
placement_rules_text = " Place on Ground, with a 1 space gap between other towers and mountains - walls/moats don't count. " ,
short_description = " Medium-range, medium fire-rate area of effect artillery tower. " ,
texture = TEXTURES.TOWER_HOWITZER ,
icon_texture = TEXTURES.TOWER_HOWITZER_ICON ,
cost = 50 ,
weapons = {
{
projectile_type = PROJECTILE_TYPE.SHELL ,
range = 6 ,
fire_rate = 4 ,
hit_sound = SOUNDS.EXPLOSION2
}
} ,
make_node_f = function ( self )
return am.group (
am.circle ( vec2 ( 0 ) , HEX_SIZE - 4 , COLORS.VERY_DARK_GRAY , 6 ) ,
am.rotate ( game_state.time or 0 ) ^ am.group (
pack_texture_into_sprite ( self.texture , HEX_PIXEL_HEIGHT * 1.5 , HEX_PIXEL_WIDTH * 2 ) -- CHONK
)
)
end ,
placement_f = function ( blocked , has_water , has_mountain , has_ground , hex )
local has_mountain_neighbour = false
local has_non_wall_non_moat_tower_neighbour = false
for _ , h in pairs ( hex_neighbours ( hex ) ) do
local towers = towers_on_hex ( h )
local wall_on_hex = false
has_non_wall_non_moat_tower_neighbour = table.find ( towers , function ( tower )
if tower.type == TOWER_TYPE.WALL then
wall_on_hex = true
return false
elseif tower.type == TOWER_TYPE.MOAT then
return false
end
if not tower_spec.update_f then
tower_spec.update_f = default_tower_update_f
end
return true
end )
if not tower_spec.weapons then
tower_spec.weapons = { }
end
for i , w in pairs ( tower_spec.weapons ) do
if not w.min_range then
w.min_range = 0
if has_non_wall_non_moat_tower_neighbour then
break
end
if not w.target_acquisition_f then
w.target_acquisition_f = default_weapon_target_acquisition_f
local tile = hex_map_get ( game_state.map , h )
if not wall_on_hex and tile and tile.elevation >= 0.5 then
has_mountain_neighbour = true
break
end
end
if not tower_spec.placement_f then
tower_spec.placement_f = default_tower_placement_f
return not ( blocked or has_water or has_mountain or has_mountain_neighbour or has_non_wall_non_moat_tower_neighbour )
end ,
update_f = function ( tower , tower_index )
if not tower.target_index then
default_weapon_target_acquisition_f ( tower , tower_index )
-- passive animation
tower.node ( " rotate " ) . angle = math.wrapf ( tower.node ( " rotate " ) . angle + 0.1 * am.delta_time , math.pi * 2 )
else
-- we should have a target
local mob = game_state.mobs [ tower.target_index ]
if not mob then
-- the target we have was invalidated
tower.target_index = false
else
default_handle_target_f ( tower , tower_index , mob )
-- point the cannon at the dude
local theta = math.rad ( 90 ) - math.atan ( ( tower.position . y - mob.position . y ) / ( tower.position . x - mob.position . x ) )
local diff = tower.node ( " rotate " ) . angle - theta
tower.node ( " rotate " ) . angle = - theta + math.pi / 2
end
end
-- resolve a tower's visual range - if not provided we should use the largest range among weapons it has
if not tower_spec.visual_range then
local largest_range = 0
for i , w in pairs ( tower_spec.weapons ) do
if w.range > largest_range then
largest_range = w.range
end
end
} ,
{
id = " REDEYE " ,
name = " Redeye " ,
placement_rules_text = " Place on Mountains. " ,
short_description = " Long-range, penetrating high-velocity laser tower. " ,
texture = TEXTURES.TOWER_REDEYE ,
icon_texture = TEXTURES.TOWER_REDEYE_ICON ,
cost = 75 ,
weapons = {
{
projectile_type = PROJECTILE_TYPE.LASER ,
range = 9 ,
fire_rate = 3 ,
hit_sound = SOUNDS.LASER2
}
} ,
make_node_f = function ( self )
return make_tower_sprite ( self )
end ,
placement_f = function ( blocked , has_water , has_mountain , has_ground , hex )
return not blocked and has_mountain
end ,
update_f = default_tower_update_f
} ,
{
id = " MOAT " ,
name = " Moat " ,
placement_rules_text = " Place on Ground " ,
short_description = " Restricts movement, similar to water. " ,
texture = TEXTURES.TOWER_MOAT ,
icon_texture = TEXTURES.TOWER_MOAT_ICON ,
cost = 10 ,
range = 0 ,
fire_rate = 2 ,
height = - 1 ,
make_node_f = function ( self )
return am.circle ( vec2 ( 0 ) , HEX_SIZE , COLORS.WATER { a = 1 } , 6 )
end ,
update_f = false
} ,
{
id = " RADAR " ,
name = " Radar " ,
placement_rules_text = " n/a " ,
short_description = " Doesn't do anything right now :( " ,
texture = TEXTURES.TOWER_RADAR ,
icon_texture = TEXTURES.TOWER_RADAR_ICON ,
cost = 100 ,
range = 0 ,
fire_rate = 1 ,
make_node_f = function ( self )
return make_tower_sprite ( self )
end ,
update_f = false
} ,
{
id = " LIGHTHOUSE " ,
name = " Lighthouse " ,
placement_rules_text = " Place on Ground, adjacent to Water or Moats " ,
short_description = " Attracts nearby mobs; temporarily redirects their path " ,
texture = TEXTURES.TOWER_LIGHTHOUSE ,
icon_texture = TEXTURES.TOWER_LIGHTHOUSE_ICON ,
cost = 150 ,
range = 7 ,
fire_rate = 1 ,
make_node_f = function ( self )
return am.group (
make_tower_sprite ( self ) ,
am.particles2d {
source_pos = vec2 ( 0 , 12 ) ,
source_pos_var = vec2 ( 2 ) ,
start_size = 1 ,
start_size_var = 1 ,
end_size = 1 ,
end_size_var = 1 ,
angle = 0 ,
angle_var = math.pi ,
speed = 1 ,
speed_var = 2 ,
life = 10 ,
life_var = 1 ,
start_color = COLORS.WHITE ,
start_color_var = vec4 ( 0.1 , 0.1 , 0.1 , 1 ) ,
end_color = COLORS.SUNRAY ,
end_color_var = vec4 ( 0.1 ) ,
emission_rate = 4 ,
start_particles = 4 ,
max_particles = 200 ,
warmup_time = 5
}
)
end ,
placement_f = function ( blocked , has_water , has_mountain , has_ground , hex )
local has_water_neighbour = false
for _ , h in pairs ( hex_neighbours ( hex ) ) do
local tile = hex_map_get ( game_state.map , h )
if tile and tile.elevation < - 0.5 then
has_water_neighbour = true
break
end
tower_spec.visual_range = largest_range
end
-- do the same for the minimum visual range
if not tower_spec.min_visual_range then
local largest_minimum_range = 0
for i , w in pairs ( tower_spec.weapons ) do
if w.min_range > largest_minimum_range then
largest_minimum_range = w.min_range
return not blocked
and not has_mountain
and not has_water
and has_water_neighbour
end ,
update_f = function ( tower , tower_index )
-- check if there's a mob on a hex in our perimeter
for _ , h in pairs ( tower.perimeter ) do
local mobs = mobs_on_hex ( h )
for _ , m in pairs ( mobs ) do
if not m.path and not m.seen_lighthouse then
-- @TODO only attract the mob if its frame target (direction vector)
-- is within some angle range...? if the mob is heading directly away from the tower, then
-- the lighthouse shouldn't do much
local path , made_it = hex_Astar ( game_state.map , tower.hex , m.hex , grid_neighbours , grid_cost , grid_heuristic )
if made_it then
m.path = path
m.seen_lighthouse = true -- right now mobs don't care about lighthouses if they've already seen one.
end
end
end
tower_spec.min_visual_range = largest_minimum_range
end
end
TOWER_SPECS = tower_specs
for i , t in pairs ( TOWER_SPECS ) do
TOWER_TYPE [ t.id ] = i
end
build_tower_cursors ( )
return
else
-- runtime error - including syntax errors
error_message = result
end
else
-- filesystem/permissions related error - couldn't load the file
error_message = " couldn't load the file "
end
log ( error_message )
-- @TODO no matter what fucked up, we should load defaults
TOWER_SPECS = { }
build_tower_cursors ( )
end
local function default_tower_weapon_target_acquirer ( tower , tower_index )
end
local function make_tower_sprite ( tower_type )
return pack_texture_into_sprite ( get_tower_texture ( tower_type ) , HEX_PIXEL_WIDTH , HEX_PIXEL_HEIGHT )
end
function make_tower_node ( tower_type )
-- @TODO move to tower spec
if tower_type == 4 then
return make_tower_sprite ( tower_type )
elseif tower_type == 2 then
return am.group {
am.circle ( vec2 ( 0 ) , HEX_SIZE - 4 , COLORS.VERY_DARK_GRAY , 5 ) ,
am.rotate ( game_state.time or 0 )
^ pack_texture_into_sprite ( TEXTURES.TOWER_HOWITZER , HEX_PIXEL_HEIGHT * 1.5 , HEX_PIXEL_WIDTH * 2 , COLORS.GREEN_YELLOW )
}
elseif tower_type == 3 then
return am.group {
am.circle ( vec2 ( 0 ) , HEX_SIZE - 4 , COLORS.VERY_DARK_GRAY , 6 ) ,
am.rotate ( game_state.time or 0 ) ^ am.group {
pack_texture_into_sprite ( TEXTURES.TOWER_HOWITZER , HEX_PIXEL_HEIGHT * 1.5 , HEX_PIXEL_WIDTH * 2 ) -- CHONK
}
}
elseif tower_type == 7 then
return am.group {
make_tower_sprite ( tower_type ) ,
am.particles2d {
source_pos = vec2 ( 0 , 12 ) ,
source_pos_var = vec2 ( 2 ) ,
start_size = 1 ,
start_size_var = 1 ,
end_size = 1 ,
end_size_var = 1 ,
angle = 0 ,
angle_var = math.pi ,
speed = 1 ,
speed_var = 2 ,
life = 10 ,
life_var = 1 ,
start_color = COLORS.WHITE ,
start_color_var = vec4 ( 0.1 , 0.1 , 0.1 , 1 ) ,
end_color = COLORS.SUNRAY ,
end_color_var = vec4 ( 0.1 ) ,
emission_rate = 4 ,
start_particles = 4 ,
max_particles = 200 ,
warmup_time = 5
}
}
elseif tower_type == 1 then
return am.circle ( vec2 ( 0 ) , HEX_SIZE , COLORS.VERY_DARK_GRAY { a = 0.75 } , 6 )
}
elseif tower_type == 5 then
return am.circle ( vec2 ( 0 ) , HEX_SIZE , COLORS.WATER { a = 1 } , 6 )
-- initialize the tower cursors (what you see when you select a tower and hover over buildable hexes)
local tower_cursors = { }
for i , t in pairs ( base_tower_specs ) do
TOWER_TYPE [ t.id ] = i
elseif tower_type == 6 then
return make_tower_sprite ( tower_type )
end
end
if not t.size then t.size = 1 end
if not t.height then t.height = 1 end
function build_tower_cursors ( )
local tower_cursors = { }
for i , tower_spec in pairs ( TOWER_SPECS ) do
local tower_sprite = make_tower_node ( i )
tower_sprite.color = COLORS.TRANSPARENT3
if not t.update_f then
t.update_f = default_tower_update_f
end
if not t.placement_f then
t.placement_f = default_tower_placement_f
end
if not t.weapons then
t.weapons = { }
end
-- resolve missing fields among weapons the tower has, as well as
-- the tower's visual range - if not provided we should use the largest range among weapons it has
local largest_range = 0
local largest_minimum_range = 0
for i , w in pairs ( t.weapons ) do
if not w.min_range then
w.min_range = 0
end
if not w.target_acquisition_f then
w.target_acquisition_f = default_weapon_target_acquisition_f
end
if w.range > largest_range then
largest_range = w.range
end
if w.min_range > largest_minimum_range then
largest_minimum_range = w.min_range
end
end
if not t.min_visual_range then
t.min_visual_range = largest_minimum_range
end
if not t.visual_range then
t.visual_range = largest_range
end
-- build tower cursors
local coroutine_ = coroutine.create ( function ( node )
local flash_on = { }
local flash_off = { }
@ -245,17 +399,30 @@ function build_tower_cursors()
end
end )
tower_cursors [ i ] = am.group {
make_hex_cursor_node ( tower_spec.visual_range , vec4 ( 0 ) , coroutine_ , tower_spec.min_visual_range ) ,
local tower_sprite = t.make_node_f ( t )
tower_sprite.color = COLORS.TRANSPARENT3
tower_cursors [ i ] = am.group (
make_hex_cursor_node ( t.visual_range - 1 , vec4 ( 0 ) , coroutine_ , t.min_visual_range - 1 ) ,
tower_sprite
}
end
) : tag " cursor "
function get_tower_cursor ( tower_type )
return tower_cursors [ tower_type ]
function get_tower_cursor ( tower_type )
return tower_cursors [ tower_type ]
end
end
TOWER_SPECS = base_tower_specs
end
function get_tower_spec ( tower_type ) return TOWER_SPECS [ tower_type ] end
function get_tower_name ( tower_type ) return TOWER_SPECS [ tower_type ] . name end
function get_tower_cost ( tower_type ) return TOWER_SPECS [ tower_type ] . cost end
function get_tower_size ( tower_type ) return TOWER_SPECS [ tower_type ] . size end
function get_tower_icon_texture ( tower_type ) return TOWER_SPECS [ tower_type ] . icon_texture end
function get_tower_placement_rules_text ( tower_type ) return TOWER_SPECS [ tower_type ] . placement_rules_text end
function get_tower_short_description ( tower_type ) return TOWER_SPECS [ tower_type ] . short_description end
function get_tower_update_function ( tower_type ) return TOWER_SPECS [ tower_type ] . update_f end
function tower_serialize ( tower )
local serialized = entity_basic_devectored_copy ( tower )
@ -268,13 +435,14 @@ end
function tower_deserialize ( json_string )
local tower = entity_basic_json_parse ( json_string )
table.merge ( tower , get_tower_spec ( tower.type ) )
for i , h in pairs ( tower.hexes ) do
tower.hexes [ i ] = vec2 ( tower.hexes [ i ] [ 1 ] , tower.hexes [ i ] [ 2 ] )
end
tower.update = get_tower_update_function ( tower.type )
tower.node = am.translate ( tower.position ) ^ make_ tower_node( tower.type )
tower.node = am.translate ( tower.position ) ^ tower.make _node_f ( tower )
return tower
end
@ -316,7 +484,7 @@ function tower_type_is_buildable_on(hex, tile, tower_type)
local has_ground = false
local tower_spec = get_tower_spec ( tower_type )
for _ , h in pairs ( hex_spiral_map ( hex , get_tower_size ( tower_type ) ) ) do
for _ , h in pairs ( hex_spiral_map ( hex , get_tower_size ( tower_type ) - 1 ) ) do
table.merge ( blocking_towers , towers_on_hex ( h ) )
table.merge ( blocking_mobs , mobs_on_hex ( h ) )
@ -351,14 +519,16 @@ function make_and_register_tower(hex, tower_type)
)
table.merge ( tower , spec )
tower.type = tower_type
tower.node = am.translate ( tower.position ) ^ make_ tower_node( tower_type )
tower.node = am.translate ( tower.position ) ^ tower.make _node_f ( tower )
for i , w in pairs ( tower.weapons ) do
w.last_shot_time = - tower.weapons [ i ] . fire_rate -- lets the tower fire immediately upon being placed
-- initialize each weapons' last shot time to the negation of the fire rate -
-- this lets the tower fire immediately upon being placed
for _ , w in pairs ( tower.weapons ) do
w.last_shot_time = - w.fire_rate
end
-- set the tower's hexes - a list of hexes which the tower sits atop
if tower.size == 1 then
tower.hexes = { tower.hex }
else
@ -385,7 +555,7 @@ end
function do_tower_updates ( )
for tower_index , tower in pairs ( game_state.towers ) do
if tower and tower.update then
tower.update ( tower , tower_index )
tower.update_f ( tower , tower_index )
end
end
end