diff --git a/conf.lua b/conf.lua index 2b6ab61..71317e2 100644 --- a/conf.lua +++ b/conf.lua @@ -2,7 +2,7 @@ title = "hexyz" author = "nick hayashi" shortname = "hexyz" -version = "0.2.0" +version = "0.3.0" support_email = "" copyright_message = "Copyright © 2021 Nick Hayashi" diff --git a/data/towers.lua b/data/towers.lua new file mode 100644 index 0000000..85ddf41 --- /dev/null +++ b/data/towers.lua @@ -0,0 +1,291 @@ + +--[[ + the following is a list of tower specifications, which are declarations of a variety of properties describing what a tower is, and how it functions + this a lua file. a quick run-down of what writing code in lua looks like: https://www.amulet.xyz/doc/#lua-primer + + each tower spec is a lua table. lua tables are the thing that you use to represent bundles of data (both arrays and hashtables are represented by tables) + + the format of the bundles in our case are described below. + some propreties are optional. required properties are marked with an asterisk (*), and are generally included at the top of the list. + + # TOWER SPEC TABLE + | --------------------------| -------- | -------------------------------------------------------------- | + | property name, required* | datatype | general description / details | + | --------------------------| -------- | -------------------------------------------------------------- | + | name* | string | exact one-line display name text of the tower | + | placement_rules_text* | string | one-line description of the placement rules for this tower | + | short_description* | string | one-line description of the nature of this tower | + | texture* | userdata | @TODO | + | icon_texture* | userdata | @TODO | + | cost* | number | the starting cost of placing this tower | + | | | | + | weapons* | table | an array of weapons. | + | | | order matters - two weapons share a 'choke' value, and both | + | | | could acquire a target in a frame, the first one is choosen. | + | | | | + | placement_rules | table | @TODO | + | | | | + | | | | + | | | | + | | | | + | | | | + | | | | + | update_f | function | default value is complicated @TODO | + | grow_f | function | default value is false/nil. @TODO | + | size | number | default value of 1, which means the tower occupies one hex. | + | height | number | default value of 1. height is relevant for mob pathing and | + | | | projectile collision | + | | | | + | --------------------------| -------- | -------------------------------------------------------------- | + + # WEAPON TABLE + | --------------------------| -------- | -------------------------------------------------------------- | + | property name, required* | datatype | general description / details | + | --------------------------| -------- | -------------------------------------------------------------- | + | type | number | sometimes, instead of specifying everything for a weapon, it's | + | | | convenient to refer to a base type. if this is provided all of | + | | | the weapon's other fields will be initialized to preset values | + | | | and any other values you provide with the weapon spec will | + | | | overwrite those preset values. | + | | | if you provide a value here, all other properties become | + | | | optional. | + | | | values you can provide, and what they mean: | + | | | @TODO | + | | | | + | fire_rate* | number | 'shots' per second, if the weapon has a valid target | + | range* | number | max distance (in hexes) at which this weapon acquires targets | + | | | | + | min-range | number | default of 0. min distance (in hexes) at which this weapon acquires targets | + | target_acquisition_f | function | default value is complicated @TODO | + | choke | number | default of false/nil. @TODO | + | | | | + | --------------------------| -------- | -------------------------------------------------------------- | +]] + +return { + { + 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 = false, + placement_rules = { + } + }, + { + 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, + range = 4, + fire_rate = 0.5, + update = function(tower, tower_index) + if not tower.target_index then + -- we should try and acquire a target + 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 + end + + -- 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 + if not game_state.mobs[tower.target_index] then + -- the target we have was invalidated + tower.target_index = false + + else + -- the target we have is valid + local mob = game_state.mobs[tower.target_index] + local vector = math.normalize(mob.position - tower.position) + + if (game_state.time - tower.last_shot_time) > tower.fire_rate then + local projectile = make_and_register_projectile( + tower.hex, + PROJECTILE_TYPE.BULLET, + vector + ) + + tower.last_shot_time = game_state.time + play_sfx(SOUNDS.HIT1) + end + + -- 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 + }, + { + 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, + range = 6, + fire_rate = 4, + update = function(tower, tower_index) + if not tower.target_index then + -- we don't have a target + 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 + end + + -- 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 + -- @NOTE don't compare to false, empty indexes appear on game reload + if not game_state.mobs[tower.target_index] then + -- the target we have was invalidated + tower.target_index = false + + else + -- the target we have is valid + local mob = game_state.mobs[tower.target_index] + local vector = math.normalize(mob.position - tower.position) + + if (game_state.time - tower.last_shot_time) > tower.fire_rate then + local projectile = make_and_register_projectile( + tower.hex, + PROJECTILE_TYPE.SHELL, + vector + ) + + -- @HACK, the projectile will explode if it encounters something taller than it, + -- but the tower it spawns on quickly becomes taller than it, so we just pad it + -- if it's not enough the shell explodes before it leaves its spawning hex + projectile.props.z = tower.props.z + 0.1 + + tower.last_shot_time = game_state.time + play_sfx(SOUNDS.EXPLOSION2) + end + + -- 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 + }, + { + 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, + range = 9, + fire_rate = 3, + update = function(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 + end + else + if not game_state.mobs[tower.target_index] then + tower.target_index = false + + elseif (game_state.time - tower.last_shot_time) > tower.fire_rate then + local mob = game_state.mobs[tower.target_index] + + make_and_register_projectile( + tower.hex, + PROJECTILE_TYPE.LASER, + math.normalize(mob.position - tower.position) + ) + + tower.last_shot_time = game_state.time + vplay_sfx(SOUNDS.LASER2) + end + end + end + }, + { + 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, + update = false + }, + { + 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, + update = false + }, + { + 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, + target_aquisition_f = function(tower, tower_index) + + end, + update = 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 + }, +} diff --git a/main.lua b/main.lua index f7de517..b8132e1 100644 --- a/main.lua +++ b/main.lua @@ -109,6 +109,7 @@ require "src/tower" require "src/map-editor" + local sound_toggle_node_tag = "sound_on_off_icon" local function make_sound_toggle_node(on) local sprite diff --git a/src/extra.lua b/src/extra.lua index ab79e92..cb63ad9 100644 --- a/src/extra.lua +++ b/src/extra.lua @@ -1,9 +1,13 @@ +-- utility functions that don't below elsewhere go here, +-- especially if they would be at home on the global 'math' or 'table' variables, or are otherwise extensions of standard lua features +-- try to avoid too much amulet specific stuff, but vector types are probably ok. function fprofile(f, ...) local t1 = am.current_time() local result = { f(...) } - log("%f", am.current_time() - t1) - return unpack(result) + local time = am.current_time() - t1 + --log("%f", time) + return time, unpack(result) end function math.wrapf(float, range) diff --git a/src/game.lua b/src/game.lua index 85a564c..ca959cd 100644 --- a/src/game.lua +++ b/src/game.lua @@ -2,7 +2,7 @@ game = false -- flag to tell if there is a game running game_state = {} -local game_scene_options = { +local game_scene_menu_options = { false, { texture = TEXTURES.NEW_GAME_HEX, @@ -70,6 +70,7 @@ local function get_initial_game_state(seed) world = world, -- the root scene graph node for the game 'world' ui = nil, -- unused, root scene graph node for the 'ui' stuff + frame_start_time = 0, -- timestamp in seconds that this current frame began on time = 0, -- real time since the *current* game started in seconds score = 0, -- current game score money = STARTING_MONEY, -- current money @@ -148,7 +149,7 @@ end local function game_pause() win.scene("game").paused = true - win.scene:append(make_scene_menu(game_scene_options)) + win.scene:append(make_scene_menu(game_scene_menu_options)) end local function game_deserialize(json_string) @@ -251,6 +252,7 @@ local function game_pause_menu() end local function game_action(scene) + game_state.frame_start_time = am.current_time() if game_state.score < 0 then game_end() return true @@ -413,6 +415,8 @@ local function game_action(scene) win.scene("money").text = string.format("MONEY: $%d", game_state.money) win.scene("wave_timer").text = get_wave_timer_text() win.scene("top_right_display").text = get_top_right_display_text(hex, evenq, centered_evenq, game_state.selected_top_right_display_type) + + check_if_can_collect_garbage_for_free(game_state.frame_start_time, 60) end local function make_game_toolbelt() @@ -491,19 +495,8 @@ local function make_game_toolbelt() tower_select_square.hidden = true toolbelt:append(tower_select_square) - -- it's important these are in the same order as integers assigned to the enum - -- TOWER_TYPE, and none are missing. - local toolbelt_options = { - TOWER_TYPE.WALL, - TOWER_TYPE.GATTLER, - TOWER_TYPE.HOWITZER, - TOWER_TYPE.REDEYE, - TOWER_TYPE.MOAT, - TOWER_TYPE.RADAR, - TOWER_TYPE.LIGHTHOUSE, - } local toolbelt_buttons = {} - for i,v in pairs(toolbelt_options) do + for i = 0, #TOWER_SPECS do local button, rect = toolbelt_button(i) table.insert(toolbelt_buttons, { node = button, rect = rect }) toolbelt:append(button) @@ -715,6 +708,7 @@ function game_end() game_state = {} game = false + collectgarbage("restart") end function game_save() @@ -743,5 +737,6 @@ function game_init(saved_state) game = true win.scene:remove("game") win.scene:append(game_scene()) + collectgarbage("stop") end diff --git a/src/map-editor.lua b/src/map-editor.lua index fc0cc59..8e205bb 100644 --- a/src/map-editor.lua +++ b/src/map-editor.lua @@ -7,7 +7,7 @@ local map_editor_state = { selected_tile = false } -local map_editor_scene_options = { +local map_editor_scene_menu_options = { false, { texture = TEXTURES.NEW_GAME_HEX, @@ -82,7 +82,7 @@ function map_editor_action() if win:key_pressed"escape" then win.scene("map_editor").paused = true - win.scene:append(make_scene_menu(map_editor_scene_options)) + win.scene:append(make_scene_menu(map_editor_scene_menu_options)) end if win:mouse_down"left" then diff --git a/src/tower.lua b/src/tower.lua index 5bb6923..b61442e 100644 --- a/src/tower.lua +++ b/src/tower.lua @@ -1,103 +1,37 @@ -TOWER_TYPE = { - WALL = 1, - GATTLER = 2, - HOWITZER = 3, - REDEYE = 4, - -- = 5, - MOAT = 5, - -- = 7, - RADAR = 6, - -- = 9, - LIGHTHOUSE = 7 -} - -local TOWER_SPECS = { - [TOWER_TYPE.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, - size = 0, - height = 1, - }, - [TOWER_TYPE.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, - range = 4, - fire_rate = 0.5, - size = 0, - height = 1, - }, - [TOWER_TYPE.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, - range = 6, - fire_rate = 4, - size = 0, - height = 1, - }, - [TOWER_TYPE.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, - range = 9, - fire_rate = 3, - size = 0, - height = 1, - }, - [TOWER_TYPE.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, - size = 0, - height = -1, - }, - [TOWER_TYPE.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, - size = 0, - height = 1, - }, - [TOWER_TYPE.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, - size = 0, - height = 1, - }, -} +do + -- load tower data + local tfile = am.load_script("data/towers.lua") + local error_message + if tfile then + local status, result = pcall(tfile) + + 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(result) + if type_ ~= "table" then + error_message = "tower spec file should return a table, but we got " .. type_ + goto cleanup + end + + TOWER_SPECS = result + else + -- runtime error - including syntax errors + error_message = result + goto cleanup + end + else + -- file system related error - couldn't load the file + error_message = "couldn't load the file" + end + + ::cleanup:: + if error_message then + log(error_message) + -- @TODO no matter what fucked up, we should load defaults + end +end function get_tower_spec(tower_type) return TOWER_SPECS[tower_type] @@ -130,29 +64,34 @@ function get_tower_size(tower_type) return TOWER_SPECS[tower_type].size 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) - if tower_type == TOWER_TYPE.REDEYE then + -- @TODO move to tower spec + if tower_type == 4 then return make_tower_sprite(tower_type) - elseif tower_type == TOWER_TYPE.GATTLER then + 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 == TOWER_TYPE.HOWITZER then + 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 == TOWER_TYPE.LIGHTHOUSE then + elseif tower_type == 7 then return am.group{ make_tower_sprite(tower_type), am.particles2d{ @@ -178,20 +117,20 @@ function make_tower_node(tower_type) warmup_time = 5 } } - elseif tower_type == TOWER_TYPE.WALL then + elseif tower_type == 1 then return am.circle(vec2(0), HEX_SIZE, COLORS.VERY_DARK_GRAY{a=0.75}, 6) - elseif tower_type == TOWER_TYPE.MOAT then + elseif tower_type == 5 then return am.circle(vec2(0), HEX_SIZE, COLORS.WATER{a=1}, 6) - elseif tower_type == TOWER_TYPE.RADAR then + elseif tower_type == 6 then return make_tower_sprite(tower_type) end end do local tower_cursors = {} - for _,i in pairs(TOWER_TYPE) do + for i = 1, #TOWER_SPECS do local tower_sprite = make_tower_node(i) tower_sprite.color = COLORS.TRANSPARENT @@ -221,186 +160,6 @@ do end end -local function update_tower_redeye(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 - end - else - if not game_state.mobs[tower.target_index] then - tower.target_index = false - - elseif (game_state.time - tower.last_shot_time) > tower.fire_rate then - local mob = game_state.mobs[tower.target_index] - - make_and_register_projectile( - tower.hex, - PROJECTILE_TYPE.LASER, - math.normalize(mob.position - tower.position) - ) - - tower.last_shot_time = game_state.time - vplay_sfx(SOUNDS.LASER2) - end - end -end - -local function update_tower_gattler(tower, tower_index) - if not tower.target_index then - -- we should try and acquire a target - 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 - end - - -- 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 - if not game_state.mobs[tower.target_index] then - -- the target we have was invalidated - tower.target_index = false - - else - -- the target we have is valid - local mob = game_state.mobs[tower.target_index] - local vector = math.normalize(mob.position - tower.position) - - if (game_state.time - tower.last_shot_time) > tower.fire_rate then - local projectile = make_and_register_projectile( - tower.hex, - PROJECTILE_TYPE.BULLET, - vector - ) - - tower.last_shot_time = game_state.time - play_sfx(SOUNDS.HIT1) - end - - -- 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 - -local function update_tower_howitzer(tower, tower_index) - if not tower.target_index then - -- we don't have a target - 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 - end - - -- 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 - -- @NOTE don't compare to false, empty indexes appear on game reload - if not game_state.mobs[tower.target_index] then - -- the target we have was invalidated - tower.target_index = false - - else - -- the target we have is valid - local mob = game_state.mobs[tower.target_index] - local vector = math.normalize(mob.position - tower.position) - - if (game_state.time - tower.last_shot_time) > tower.fire_rate then - local projectile = make_and_register_projectile( - tower.hex, - PROJECTILE_TYPE.SHELL, - vector - ) - - -- @HACK, the projectile will explode if it encounters something taller than it, - -- but the tower it spawns on quickly becomes taller than it, so we just pad it - -- if it's not enough the shell explodes before it leaves its spawning hex - projectile.props.z = tower.props.z + 0.1 - - tower.last_shot_time = game_state.time - play_sfx(SOUNDS.EXPLOSION2) - end - - -- 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 - -local function update_tower_lighthouse(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. - - --[[ - local area = spiral_map(tower.hex, tower.range) - for _,h in pairs(area) do - local node = game_state.map[h.x][h.y].node"circle" - local initial_color = node.color - - local d = math.distance(h, tower.hex) - local target_color = COLORS.SUNRAY{ a = 1/(d/tower.range) + 0.9 } - node:late_action(am.series{ - am.tween(node, 0.3, { color = target_color }), - am.tween(node, 0.3, { color = initial_color }) - }) - end - ]] - end - end - end - end -end - -local function get_tower_update_function(tower_type) - if tower_type == TOWER_TYPE.REDEYE then - return update_tower_redeye - - elseif tower_type == TOWER_TYPE.GATTLER then - return update_tower_gattler - - elseif tower_type == TOWER_TYPE.HOWITZER then - return update_tower_howitzer - - elseif tower_type == TOWER_TYPE.LIGHTHOUSE then - return update_tower_lighthouse - end -end - function tower_serialize(tower) local serialized = entity_basic_devectored_copy(tower)