From aae662bef130f05c3a2fa71dde223de75fc0d78e Mon Sep 17 00:00:00 2001 From: Nicholas Hayashi Date: Fri, 6 Aug 2021 21:55:56 -0400 Subject: [PATCH] load towers from specfile, build broken --- data/towers.lua | 92 +++++++++++++++---- main.lua | 4 +- src/entity.lua | 4 +- src/extra.lua | 2 +- src/game.lua | 23 +++-- src/memory.lua | 2 +- src/tower.lua | 228 ++++++++++++++++++++++++++---------------------- 7 files changed, 218 insertions(+), 137 deletions(-) diff --git a/data/towers.lua b/data/towers.lua index 85ddf41..9d44e44 100644 --- a/data/towers.lua +++ b/data/towers.lua @@ -23,12 +23,16 @@ | | | 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 | + | placement_f | function | | | | | | | | | | | | | | | | | | | | | + | visual_range | number | when the tower has multiple weapons, what range represents the | + | | | overall range of the tower. default is calculated on load as | + | | | the largest range among the weapons the tower has. | + | min_visual_range | number | same as above but the largest minimum range among weapons | | | | | | update_f | function | default value is complicated @TODO | | grow_f | function | default value is false/nil. @TODO | @@ -73,8 +77,6 @@ return { range = 0, fire_rate = 2, update = false, - placement_rules = { - } }, { name = "Gattler", @@ -83,20 +85,17 @@ return { texture = TEXTURES.TOWER_GATTLER, icon_texture = TEXTURES.TOWER_GATTLER_ICON, cost = 20, - range = 4, - fire_rate = 0.5, + weapons = { + { + range = 4, + fire_rate = 0.5, + projectile_type = 3, + } + }, 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) @@ -138,8 +137,42 @@ return { texture = TEXTURES.TOWER_HOWITZER, icon_texture = TEXTURES.TOWER_HOWITZER_ICON, cost = 50, - range = 6, - fire_rate = 4, + weapons = { + { + range = 6, + fire_rate = 4, + projectile_type = 1, + } + }, + 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 = function(tower, tower_index) if not tower.target_index then -- we don't have a target @@ -199,8 +232,16 @@ return { texture = TEXTURES.TOWER_REDEYE, icon_texture = TEXTURES.TOWER_REDEYE_ICON, cost = 75, - range = 9, - fire_rate = 3, + weapons = { + { + range = 9, + fire_rate = 3, + projectile_type = 2, + } + }, + placement_f = function(blocked, has_water, has_mountain, has_ground, hex) + return not blocked and has_mountain + end, update = function(tower, tower_index) if not tower.target_index then for index,mob in pairs(game_state.mobs) do @@ -263,8 +304,20 @@ return { cost = 150, range = 7, fire_rate = 1, - target_aquisition_f = function(tower, tower_index) + 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 = function(tower, tower_index) -- check if there's a mob on a hex in our perimeter @@ -289,3 +342,4 @@ return { end }, } + diff --git a/main.lua b/main.lua index b8132e1..74e8e8c 100644 --- a/main.lua +++ b/main.lua @@ -98,6 +98,7 @@ require "texture" -- require "src/entity" require "src/extra" +require "src/memory" require "src/geometry" require "src/hexyz" require "src/game" @@ -108,8 +109,7 @@ require "src/projectile" 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/entity.lua b/src/entity.lua index af3d942..4de468f 100644 --- a/src/entity.lua +++ b/src/entity.lua @@ -14,7 +14,7 @@ entity structure: ... - any - a bunch of other shit depending on what entity type it is } --]] -function make_basic_entity(hex, update, position) +function make_basic_entity(hex, update_f, position) local entity = {} entity.TOB = game_state.time @@ -31,7 +31,7 @@ function make_basic_entity(hex, update, position) entity.position = hex_to_pixel(hex, vec2(HEX_SIZE)) end - entity.update = update + entity.update = update_f entity.node = false -- set by caller entity.type = false -- set by caller entity.props = {} diff --git a/src/extra.lua b/src/extra.lua index cb63ad9..c3ec57b 100644 --- a/src/extra.lua +++ b/src/extra.lua @@ -1,6 +1,6 @@ -- 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. +-- try to avoid *too* much amulet specific stuff, but vector types are probably ok. function fprofile(f, ...) local t1 = am.current_time() diff --git a/src/game.lua b/src/game.lua index ca959cd..5c54601 100644 --- a/src/game.lua +++ b/src/game.lua @@ -496,7 +496,7 @@ local function make_game_toolbelt() toolbelt:append(tower_select_square) local toolbelt_buttons = {} - for i = 0, #TOWER_SPECS do + for i = 1, #TOWER_SPECS do local button, rect = toolbelt_button(i) table.insert(toolbelt_buttons, { node = button, rect = rect }) toolbelt:append(button) @@ -677,14 +677,25 @@ end -- this is a stupid name, it just returns a scene node group of hexagons in a hexagonal shape centered at 0,0, of size |radius| -- |color_f| can be a function that takes a hex and returns a color, or just a color -- optionally, |action_f| is a function that operates on the group node every frame -function make_hex_cursor_node(radius, color_f, action_f) +function make_hex_cursor_node(radius, color_f, action_f, min_radius) local color = type(color_f) == "userdata" and color_f or nil - local map = hex_spiral_map(vec2(0), radius) local group = am.group() - for _,h in pairs(map) do - local hexagon = am.circle(hex_to_pixel(h, vec2(HEX_SIZE)), HEX_SIZE, color or color_f(h), 6) - group:append(hexagon) + if not min_radius then + local map = hex_spiral_map(vec2(0), radius) + + for _,h in pairs(map) do + local hexagon = am.circle(hex_to_pixel(h, vec2(HEX_SIZE)), HEX_SIZE, color or color_f(h), 6) + group:append(hexagon) + end + else + for i = min_radius, radius do + local map = hex_ring_map(vec2(0), i) + for _,h in pairs(map) do + local hexagon = am.circle(hex_to_pixel(h, vec2(HEX_SIZE)), HEX_SIZE, color or color_f(h), 6) + group:append(hexagon) + end + end end if action_f then diff --git a/src/memory.lua b/src/memory.lua index b02883b..5fb860b 100644 --- a/src/memory.lua +++ b/src/memory.lua @@ -13,7 +13,7 @@ function run_garbage_collector_cycle() garbage_collector_average_cycle_time = total / #garbage_collector_cycle_timing_history end -function check_if_can_collect_garbage_for_free(frame_start_time, min_goal_fps) +function check_if_can_collect_garbage_for_free(frame_start_time, min_fps) -- often this will be polled at the end of a frame to see if we're running fast or slow, -- and if we have some time to kill before the start of the next frame, we could maybe run gc. -- diff --git a/src/tower.lua b/src/tower.lua index b61442e..3bc3217 100644 --- a/src/tower.lua +++ b/src/tower.lua @@ -1,38 +1,129 @@ -do - -- load tower data - local tfile = am.load_script("data/towers.lua") +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) + 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 + +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 + + 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 + end +end + +function default_tower_update_f(tower, tower_index) +end + +function resolve_tower_specs(spec_file_path) + local spec_file = am.load_script(spec_file_path) local error_message - if tfile then - local status, result = pcall(tfile) + 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(result) + local type_ = type(tower_specs) if type_ ~= "table" then error_message = "tower spec file should return a table, but we got " .. type_ - goto cleanup end - TOWER_SPECS = result + -- 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 + end + + if not tower_spec.update_f then + tower_spec.update_f = default_tower_update_f + 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 + end + if not w.target_acquisition_f then + w.target_acquisition_f = default_weapon_target_acquisition_f + end + end + + if not tower_spec.placement_f then + tower_spec.placement_f = default_tower_placement_f + 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 + 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 + end + end + tower_spec.min_visual_range = largest_minimum_range + end + end + + return tower_specs else -- runtime error - including syntax errors error_message = result - goto cleanup end else - -- file system related error - couldn't load the file + -- filesystem/permissions 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 + log(error_message) + -- @TODO no matter what fucked up, we should load defaults + return {} end + +-- load tower spec file +TOWER_SPECS = resolve_tower_specs("data/towers.lua") + + + function get_tower_spec(tower_type) return TOWER_SPECS[tower_type] end @@ -130,7 +221,7 @@ end do local tower_cursors = {} - for i = 1, #TOWER_SPECS do + for i,tower_spec in pairs(TOWER_SPECS) do local tower_sprite = make_tower_node(i) tower_sprite.color = COLORS.TRANSPARENT @@ -150,7 +241,7 @@ do end) tower_cursors[i] = am.group{ - make_hex_cursor_node(get_tower_range(i), vec4(0), coroutine_), + make_hex_cursor_node(tower_spec.visual_range, vec4(0), coroutine_, tower_spec.min_visual_range), tower_sprite } end @@ -210,7 +301,7 @@ 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 + -- you can't build anything in the center, probably ever? if hex == HEX_GRID_CENTER then return false end local blocking_towers = {} @@ -218,6 +309,7 @@ function tower_type_is_buildable_on(hex, tile, tower_type) 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))) do table.merge(blocking_towers, towers_on_hex(h)) @@ -243,113 +335,37 @@ function tower_type_is_buildable_on(hex, tile, tower_type) local mobs_blocking = table.count(blocking_mobs) ~= 0 local blocked = mobs_blocking or towers_blocking - if tower_type == TOWER_TYPE.GATTLER then - local has_mountain_neighbour = false - local has_non_wall_non_moat_tower_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_mountain_neighbour = true - break - end - end - return not (blocked or has_water or has_mountain) - - elseif tower_type == TOWER_TYPE.HOWITZER then - 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) - - elseif tower_type == TOWER_TYPE.REDEYE then - return not blocked - and has_mountain - - elseif tower_type == TOWER_TYPE.LIGHTHOUSE then - 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 - - elseif tower_type == TOWER_TYPE.WALL then - return not blocked and tile_is_medium_elevation(tile) - - elseif tower_type == TOWER_TYPE.MOAT then - return not blocked and tile_is_medium_elevation(tile) - end + 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, - get_tower_update_function(tower_type) + spec.update_f ) + table.merge(tower, spec) + tower.type = tower_type tower.node = am.translate(tower.position) ^ make_tower_node(tower_type) - local spec = get_tower_spec(tower_type) - tower.cost = spec.cost - tower.range = spec.range - tower.fire_rate = spec.fire_rate - tower.last_shot_time = -spec.fire_rate -- lets the tower fire immediately upon being placed - tower.size = spec.size - if tower.size == 0 then + 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 + end + + if tower.size == 1 then tower.hexes = { tower.hex } else - tower.hexes = hex_spiral_map(tower.hex, tower.size) + tower.hexes = hex_spiral_map(tower.hex, tower.size - 1) end - tower.height = spec.height + -- should we be permuting the map here? 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 - if tower.type == TOWER_TYPE.HOWITZER then - tower.props.z = tower.height - - elseif tower.type == TOWER_TYPE.GATTLER then - tower.props.z = tower.height - - elseif tower.type == TOWER_TYPE.LIGHTHOUSE then - tower.perimeter = hex_ring_map(tower.hex, tower.range) - end - register_entity(game_state.towers, tower) return tower end