You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
648 lines
24 KiB
648 lines
24 KiB
|
|
local function default_tower_placement_f(blocked, has_water, has_mountain, has_ground, hex)
|
|
return not (blocked or has_water or has_mountain)
|
|
end
|
|
|
|
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)
|
|
|
|
for _,w in pairs(tower.weapons) do
|
|
if d <= w.range then
|
|
tower.target_index = index
|
|
return
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
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
|
|
-- 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
|
|
|
|
local function default_tower_weapon_target_acquirer_f(tower, tower_index)
|
|
end
|
|
|
|
TOWER_SPECS = {}
|
|
TOWER_TYPE = {}
|
|
local function make_tower_sprite(t)
|
|
return pack_texture_into_sprite(t.texture, HEX_PIXEL_WIDTH, HEX_PIXEL_HEIGHT)
|
|
end
|
|
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,
|
|
update_f = false,
|
|
make_node_f = function(self, hex)
|
|
local group = am.group(am.circle(vec2(0), HEX_SIZE, COLORS.VERY_DARK_GRAY, 6))
|
|
if not hex then
|
|
-- should only happen when making the hex-cursor for the wall
|
|
return group
|
|
end
|
|
|
|
local lines = am.rotate(math.rad(-30)) ^ am.group()
|
|
for i,n in pairs(hex_neighbours(hex)) do
|
|
local no_walls_adjacent = true
|
|
|
|
for _,t in pairs(towers_on_hex(n)) do
|
|
if t.type == TOWER_TYPE.WALL then
|
|
no_walls_adjacent = false
|
|
break
|
|
end
|
|
end
|
|
|
|
if no_walls_adjacent then
|
|
local p1 = hex_corner_offset(vec2(0), i)
|
|
local j = i == 6 and 1 or i + 1
|
|
local p2 = hex_corner_offset(vec2(0), j)
|
|
lines:append(
|
|
am.line(p1, p2, HEX_SIZE/4, COLORS.VERY_DARK_GRAY/vec4(vec3((i % 2) == 0 and 2 or 4), 1))
|
|
)
|
|
end
|
|
end
|
|
group:append(lines)
|
|
|
|
return group
|
|
end
|
|
},
|
|
{
|
|
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,
|
|
height = 2,
|
|
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,
|
|
height = 2,
|
|
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
|
|
|
|
return true
|
|
end)
|
|
|
|
if has_non_wall_non_moat_tower_neighbour then
|
|
break
|
|
end
|
|
|
|
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
|
|
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
|
|
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,
|
|
height = 2,
|
|
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 = function(tower, tower_index)
|
|
if not tower.target_index then
|
|
-- 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
|
|
},
|
|
{
|
|
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,
|
|
height = -1,
|
|
make_node_f = function(self)
|
|
return am.circle(vec2(0), HEX_SIZE, COLORS.WATER{a=1}, 6)
|
|
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,
|
|
height = 2,
|
|
make_node_f = function(self, hex)
|
|
if hex then
|
|
self.perimeter = hex_spiral_map(hex, 6)
|
|
end
|
|
|
|
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
|
|
end
|
|
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
|
|
end
|
|
end
|
|
},
|
|
{
|
|
id = "FARM",
|
|
name = "Farm",
|
|
placement_rules_text = "Place on Ground",
|
|
short_description = "Increases income gained over time. Mobs can trample farms, reducing their income (never completely nullifying it).",
|
|
texture = TEXTURES.TOWER_FARM,
|
|
icon_texture = TEXTURES.TOWER_FARM_ICON,
|
|
cost = 50,
|
|
size = 2,
|
|
height = 0,
|
|
props = {
|
|
income = 1
|
|
},
|
|
make_node_f = function(self)
|
|
local quads = am.quads(2*7, {"vert", "vec2", "uv", "vec2", "color", "vec4"})
|
|
|
|
local map = hex_spiral_map(vec2(0), 1)
|
|
for _,h in pairs(map) do
|
|
make_hex_quads_node(quads, hex_to_pixel(h))
|
|
end
|
|
|
|
return am.blend("alpha") ^ am.use_program(make_hex_shader_program_node()) ^ am.bind{ texture = TEXTURES.TOWER_FARM } ^ quads
|
|
end,
|
|
update_f = function(tower, tower_index)
|
|
for _,h in pairs(tower.hexes) do
|
|
for _,m in pairs(mobs_on_hex(h)) do
|
|
if m then
|
|
tower.props.income = tower.props.income * 0.7
|
|
end
|
|
end
|
|
end
|
|
game_state.money = game_state.money + tower.props.income * am.delta_time
|
|
end
|
|
}
|
|
}
|
|
|
|
-- 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
|
|
|
|
if not t.size then t.size = 1 end
|
|
if not t.height then t.height = 1 end
|
|
|
|
if not t.update_f then
|
|
-- is this good?
|
|
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 = {}
|
|
while true do
|
|
for _,n in node:child_pairs() do
|
|
table.insert(flash_on, am.tween(n, 1, { color = vec4(0.4) }))
|
|
table.insert(flash_off, am.tween(n, 1, { color = vec4(0) }))
|
|
end
|
|
am.wait(am.parallel(flash_on))
|
|
am.wait(am.parallel(flash_off))
|
|
flash_on = {}
|
|
flash_off = {}
|
|
end
|
|
end)
|
|
|
|
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
|
|
):tag"cursor"
|
|
|
|
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)
|
|
|
|
for i,h in pairs(tower.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)
|
|
|
|
-- @HACK weapons.last_shot_time field gets overrided with this merge, which we don't want
|
|
local weapons = tower.weapons
|
|
table.merge(tower, get_tower_spec(tower.type))
|
|
tower.weapons = weapons
|
|
|
|
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) ^ tower.make_node_f(tower)
|
|
|
|
return tower
|
|
end
|
|
|
|
-- note that the table returned has towers at their index into the global towers table,
|
|
-- so #t will often not work correctly (the table will often be sparse)
|
|
function towers_on_hex(hex)
|
|
local t = {}
|
|
for tower_index,tower in pairs(game_state.towers) do
|
|
if tower then
|
|
for _,h in pairs(tower.hexes) do
|
|
if h == hex then
|
|
table.insert(t, tower_index, tower)
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return t
|
|
end
|
|
|
|
function tower_on_hex(hex)
|
|
return table.find(game_state.towers, function(tower)
|
|
for _,h in pairs(tower.hexes) do
|
|
if h == hex then return true end
|
|
end
|
|
end)
|
|
end
|
|
|
|
function tower_type_is_buildable_on(hex, tile, tower_type)
|
|
-- this function gets polled a lot, and sometimes with nil/false tower types
|
|
if not tower_type then return false end
|
|
|
|
-- you can't build anything in the center, probably ever?
|
|
if hex == HEX_GRID_CENTER then return false end
|
|
|
|
local blocking_towers = {}
|
|
local blocking_mobs = {}
|
|
local has_water = false
|
|
local has_mountain = false
|
|
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) - 1)) do
|
|
if h == HEX_GRID_CENTER then return false end
|
|
|
|
table.merge(blocking_towers, towers_on_hex(h))
|
|
table.merge(blocking_mobs, mobs_on_hex(h))
|
|
|
|
local tile = hex_map_get(game_state.map, h)
|
|
-- this should always be true, unless it is possible to place a tower
|
|
-- where part of the tower overflows the edge of the map
|
|
if tile then
|
|
if is_water_elevation(tile.elevation) then
|
|
has_water = true
|
|
|
|
elseif is_mountain_elevation(tile.elevation) then
|
|
has_mountain = true
|
|
|
|
else
|
|
has_ground = true
|
|
end
|
|
end
|
|
end
|
|
|
|
local towers_blocking = table.count(blocking_towers) ~= 0
|
|
local mobs_blocking = table.count(blocking_mobs) ~= 0
|
|
local blocked = mobs_blocking or towers_blocking
|
|
|
|
return tower_spec.placement_f(blocked, has_water, has_mountain, has_ground, hex)
|
|
end
|
|
|
|
function make_and_register_tower(hex, tower_type)
|
|
local spec = get_tower_spec(tower_type)
|
|
local tower = make_basic_entity(
|
|
hex,
|
|
spec.update_f,
|
|
false,
|
|
math.clamp(spec.height, 0, 10)
|
|
)
|
|
|
|
table.merge(tower, spec)
|
|
tower.type = tower_type
|
|
tower.node = am.translate(tower.position) ^ tower.make_node_f(tower, hex)
|
|
|
|
-- 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
|
|
tower.hexes = hex_spiral_map(tower.hex, tower.size - 1)
|
|
end
|
|
|
|
register_entity(game_state.towers, tower)
|
|
return tower
|
|
end
|
|
|
|
function build_tower(hex, tower_type)
|
|
local tower = make_and_register_tower(hex, tower_type)
|
|
|
|
-- building a wall can change the adjancencies between towers, which affects what we should render
|
|
-- check for that now
|
|
for _,t in pairs(game_state.towers) do
|
|
if t ~= tower and t.type == TOWER_TYPE.WALL then
|
|
t.node:replace("group", t.make_node_f(t, t.hex))
|
|
end
|
|
end
|
|
|
|
-- modify the hexes the tower sits atop to be impassable (actually just taller by the tower's height value)
|
|
for _,h in pairs(tower.hexes) do
|
|
local tile = hex_map_get(game_state.map, h.x, h.y)
|
|
tile.elevation = tile.elevation + tower.height
|
|
end
|
|
|
|
vplay_sfx(SOUNDS.EXPLOSION4)
|
|
|
|
return tower
|
|
end
|
|
|
|
function do_tower_updates()
|
|
for tower_index,tower in pairs(game_state.towers) do
|
|
if tower and tower.update then
|
|
tower.update_f(tower, tower_index)
|
|
end
|
|
end
|
|
end
|
|
|