diff --git a/data/towers.lua b/data/towers.lua index 42124de..89c1f83 100644 --- a/data/towers.lua +++ b/data/towers.lua @@ -64,288 +64,3 @@ | | | | | --------------------------| -------- | -------------------------------------------------------------- | ]] - -return { - { - 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 = false, - }, - { - 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 = { - { - 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 - - - -- 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 - }, - { - 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 = { - { - 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 - 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 - }, - { - 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 = { - { - 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 - 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 - }, - { - 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, - update = 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, - update = 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, - 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 - 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/lib/color.lua b/lib/color.lua index 6ca58c9..50785b6 100644 --- a/lib/color.lua +++ b/lib/color.lua @@ -24,6 +24,8 @@ COLORS = { SUNRAY = vec4(228/255, 179/255, 99/255, 1), GREEN_YELLOW = vec4(204/255, 255/255, 102/255, 1), BLUE = vec4(50/255, 50/255, 180/255, 1), - MAGENTA = vec4(183/255, 0/255, 213/255, 1) + MAGENTA = vec4(183/255, 0/255, 213/255, 1), + + TAN1 = vec4(255/255, 216/255, 150/255, 1) } diff --git a/lib/extra.lua b/lib/extra.lua index 392abf1..1ce37b7 100644 --- a/lib/extra.lua +++ b/lib/extra.lua @@ -19,6 +19,10 @@ function math.lerp(v1, v2, t) return v1 * t + v2 * (1 - t) end +-- manually found the smallest number, doesn't make sense to me why, but hey it's one less than a power of two which is probably significant +-- pretty sure IEEE-754's smallest value is less than this, 32bit or 64bit +math.SMALLEST_NUMBER_ABOVE_0 = 2 ^ (-1023) + -- don't use this with sparse arrays function table.rchoice(t) return t[math.floor(math.random() * #t) + 1] diff --git a/lib/gui.lua b/lib/gui.lua index 1152602..6d7203b 100644 --- a/lib/gui.lua +++ b/lib/gui.lua @@ -93,16 +93,24 @@ function gui_make_button(args) return scene end -function gui_textfield( - position, - dimensions, - max, - disallowed_chars +-- args { +-- position vec2 +-- dimensions vec2 +-- max number +-- padding number +-- validate function(string) -> bool +-- onchange function(new_value) -> void +-- } +function gui_make_textfield( + args ) + local args = args or {} + local position = args.position or vec2(0) + local dimensions = args.dimensions or vec2(100, 40) + local validate = args.validate or function(string) return true end local width, height = dimensions.x, dimensions.y - local disallowed_chars = disallowed_chars or {} - local max = max or 10 - local padding = padding or 6 + local max = args.max or 10 + local padding = args.padding or 6 local half_width = width/2 local half_height = height/2 @@ -114,13 +122,26 @@ function gui_textfield( local back_rect = am.rect(x1 - padding/2, y1, x2, y2 + padding/2, vec4(0, 0, 0, 1)) local front_rect = am.rect(x1, y1, x2, y2, vec4(0.4)) + local function blink_cursor(cursor) + while true do + am.wait(am.delay(0.4)) + cursor.color = vec4(0) + am.wait(am.delay(0.4)) + cursor.color = vec4(0, 0, 0, 1) + end + end + local group = am.group{ back_rect, front_rect, - am.translate(-width/2 + padding, 0) ^ am.scale(2) ^ am.text("", vec4(0, 0, 0, 1), "left"), - am.translate(-width/2 + padding, -8) ^ am.line(vec2(0, 0), vec2(16, 0), 2, vec4(0, 0, 0, 1)) + am.translate(position + vec2(-width/2 + padding, 0)) ^ am.group( + am.scale(2) ^ am.text("", vec4(0, 0, 0, 1), "left"), + (am.translate(0, -8) ^ am.line(vec2(0, 0), vec2(16, 0), 2, vec4(0, 0, 0, 1)):action(coroutine.create(blink_cursor))):tag"cursor" + ) } + group"text".text = ""; + group:action(function(self) local keys = win:keys_pressed() if #keys == 0 then return end @@ -192,8 +213,10 @@ function gui_textfield( -- @NOTE this doesn't preserve the order of chars in the array so if -- someone presses a the key "a" then the backspace key in the same frame, in that order -- the backspace occurs first - self"text".text = self"text".text:sub(1, self"text".text:len() - 1) - + if self"text".text:len() ~= 0 then + self"text".text = self"text".text:sub(1, self"text".text:len() - 1) + self"cursor".position2d = self"cursor".position2d - vec2(9 * 2, 0) + end elseif k == "tab" then -- @TODO @@ -206,15 +229,20 @@ function gui_textfield( end for _,c in pairs(chars) do - if not disallowed_chars[c] then + if validate(self"text".text .. c) then if self"text".text:len() <= max then self"text".text = self"text".text .. c + self"cursor".position2d = self"cursor".position2d + vec2(9 * 2, 0) end end end end) - return group + function get_value() + return group"text".text + end + + return group, get_value end function gui_open_modal() diff --git a/lib/random.lua b/lib/random.lua index 738e0b2..11282a8 100644 --- a/lib/random.lua +++ b/lib/random.lua @@ -1,43 +1,56 @@ --- seed the random number generator with the current time -math.randomseed(os.clock()) +-- https://stackoverflow.com/a/32387452/12464892 +local function bitwise_and(a, b) + local result = 0 + local bit = 1 + while a > 0 and b > 0 do + if a % 2 == 1 and b % 2 == 1 then + result = result + bit + end + bit = bit * 2 -- shift left + a = math.floor(a/2) -- shift-right + b = math.floor(b/2) + end + return result +end -- https://stackoverflow.com/a/20177466/12464892 ---local A1, A2 = 727595, 798405 -- 5^17=D20*A1+A2 ---local D20, D40 = 1048576, 1099511627776 -- 2^20, 2^40 ---local X1, X2 = 0, 1 ---local function rand() --- local U = X2*A2 --- local V = (X1*A2 + X2*A1) % D20 --- V = (V*D20 + U) % D40 --- X1 = math.floor(V/D20) --- X2 = V - X1*D20 --- return V/D40 ---end --- ---local SEED_BOUNDS = 2^20 - 1 ---math.randomseed = function(seed) --- local v = math.clamp(math.abs(seed), 0, SEED_BOUNDS) --- X1 = v --- X2 = v + 1 ---end +local A1, A2 = 727595, 798405 -- 5^17=D20*A1+A2 +local D20, D40 = 1048576, 1099511627776 -- 2^20, 2^40 +local X1, X2 = 0, 1 +local function rand() + local U = X2*A2 + local V = (X1*A2 + X2*A1) % D20 + V = (V*D20 + U) % D40 + X1 = math.floor(V/D20) + X2 = V - X1*D20 + return V/D40 +end +local SEED_BOUNDS = 2^20 - 1 +math.randomseed = function(seed) + RANDOM_CALLS_COUNT = 0 --- to enable allowing the random number generator's state to be restored post-load (game-deserialize), --- we count the number of times we call math.random(), and on deserialize, seed the random --- number generator, and then discard |count| calls. -local R = math.random + -- 0 <= X1 <= 2^20-1, 1 <= X2 <= 2^20-1 (must be odd!) + -- ensure the number is odd, and within the bounds of + local seed = bitwise_and(seed, 1) + local v = math.clamp(math.abs(seed), 0, SEED_BOUNDS) + X1 = v + X2 = v + 1 +end RANDOM_CALLS_COUNT = 0 + +local R = math.random local function random(n, m) RANDOM_CALLS_COUNT = RANDOM_CALLS_COUNT + 1 if n then if m then - return R(n, m) + return (rand() + n) * m else - return R(n) + return rand() * n end else - return R() + return rand() end end @@ -74,3 +87,7 @@ end -- return k - 1 --end +-- seed the random number generator with the current time +-- os.clock() is better if the program has been running for a little bit. +math.randomseed(os.time()) + diff --git a/main.lua b/main.lua index 356d22e..d45bc37 100644 --- a/main.lua +++ b/main.lua @@ -113,6 +113,7 @@ function main_scene(do_backdrop, do_logo) end end group:append(hex_backdrop) + else group:append( pack_texture_into_sprite(TEXTURES.CURTAIN, win.width, win.height) @@ -149,11 +150,25 @@ function main_scene(do_backdrop, do_logo) group:append(logo) end + local seed_textfield, get_seed_textfield_value = gui_make_textfield{ + position = vec2(win.left + 150, 50), + dimensions = vec2(200, 40), + max = 9, + validate = function(string) + return not string.match(string, "%D") + end, + } + group:append( + seed_textfield + ) + local main_scene_options = { false, { texture = TEXTURES.NEW_GAME_HEX, - action = game_init + action = function() + game_init(nil, tonumber(get_seed_textfield_value())) + end }, false, false, @@ -192,10 +207,9 @@ function main_scene(do_backdrop, do_logo) false } - group:append(make_scene_menu(main_scene_options)) + group:append(make_scene_menu(main_scene_options, "main_menu")) group:action(main_action) - return group end @@ -272,8 +286,7 @@ function switch_context(scene, action) end function init() - load_entity_specs() - + init_entity_specs() switch_context(main_scene(true, true)) end diff --git a/res/img/farm.jpeg b/res/img/farm.jpeg new file mode 100644 index 0000000..1198a1f Binary files /dev/null and b/res/img/farm.jpeg differ diff --git a/src/entity.lua b/src/entity.lua index 44f4413..d8c5dd9 100644 --- a/src/entity.lua +++ b/src/entity.lua @@ -8,10 +8,8 @@ entity structure: update - function - runs every frame with itself and its index in some array as an argument node - node - scene graph node - should be initialized by caller after, though all entities have a node - type - enum - sub type - unset if 'basic' entity + type - enum - sub type props - table - table of properties specific to this entity subtype - - ... - any - a bunch of other shit depending on what entity type it is } --]] function make_basic_entity(hex, update_f, position) @@ -32,8 +30,6 @@ function make_basic_entity(hex, update_f, position) end entity.update = update_f - entity.node = false -- set by caller - entity.type = false -- set by caller entity.props = {} return entity @@ -58,10 +54,6 @@ function do_entity_updates() do_projectile_updates() end -function load_entity_specs() - resolve_tower_specs("data/towers.lua") -end - function entity_basic_devectored_copy(entity) local copy = table.shallow_copy(entity) copy.position = { copy.position.x, copy.position.y } @@ -78,3 +70,7 @@ function entity_basic_json_parse(json_string) return entity end +function init_entity_specs() + init_tower_specs() +end + diff --git a/src/game.lua b/src/game.lua index 09590ca..9bf38da 100644 --- a/src/game.lua +++ b/src/game.lua @@ -154,6 +154,7 @@ end local function game_deserialize(json_string) local new_game_state = am.parse_json(json_string) + log(new_game_state.RANDOM_CALLS_COUNT) if new_game_state.version ~= version then gui_alert("loading incompatible old save data.\nstarting a fresh game instead.", nil, 10) @@ -161,6 +162,15 @@ local function game_deserialize(json_string) end new_game_state.map = random_map(new_game_state.seed) + -- after generating a random map, the random number generator is seeded with the map's seed + -- additionally, we keep track of how many times we make calls to math.random during runtime + -- in order to restore the state of the random number generator on game deserialize, we first + -- seed it with the same seed used in the original state. then, we discard N calls, where N + -- is the number of calls we counted since seeding the generator last time. + for i = 0, new_game_state.RANDOM_CALLS_COUNT do + math.random() + end + log(RANDOM_CALLS_COUNT) new_game_state.world = make_hex_grid_scene(new_game_state.map, true) new_game_state.seed = nil @@ -206,6 +216,7 @@ end local function game_serialize() local serialized = table.shallow_copy(game_state) serialized.version = version + serialized.RANDOM_CALLS_COUNT = RANDOM_CALLS_COUNT serialized.seed = game_state.map.seed serialized.map = nil -- we re-generate the entire map from the seed on de-serialize @@ -218,7 +229,6 @@ local function game_serialize() -- -- this is dumb and if i forsaw this i would have probably used float arrays instead of vectors -- (the scene graph bit makes sense though) - serialized.towers = {} for i,t in pairs(game_state.towers) do if t then @@ -562,7 +572,7 @@ local function make_game_toolbelt() toolbelt("toolbelt_select_square"):action(am.tween(0.1, { position2d = new_position })) end - win.scene:replace("cursor", get_tower_cursor(tower_type):tag"cursor") + win.scene:replace("cursor", get_tower_cursor(tower_type)) play_sfx(SOUNDS.SELECT1) else @@ -571,7 +581,7 @@ local function make_game_toolbelt() -- de-selecting currently selected tower if any toolbelt("toolbelt_select_square").hidden = true - win.scene:replace("cursor", make_hex_cursor_node(0, COLORS.TRANSPARENT3):tag"cursor") + win.scene:replace("cursor", make_hex_cursor_node(0, COLORS.TRANSPARENT3)) end end @@ -652,7 +662,7 @@ local function game_scene() local scene = am.group( am.scale(1):tag"world_scale" ^ game_state.world, - am.translate(HEX_GRID_CENTER):tag"cursor_translate" ^ make_hex_cursor_node(0, COLORS.TRANSPARENT3):tag"cursor", + am.translate(HEX_GRID_CENTER):tag"cursor_translate" ^ make_hex_cursor_node(0, COLORS.TRANSPARENT3), score, money, wave_timer, @@ -698,7 +708,7 @@ function make_hex_cursor_node(radius, color_f, action_f, min_radius) group:action(action_f) end - return group + return group:tag"cursor" end function update_score(diff) game_state.score = game_state.score + diff end @@ -724,7 +734,7 @@ function game_save() gui_alert("succesfully saved!") end -function game_init(saved_state) +function game_init(saved_state, seed) if saved_state then game_state = game_deserialize(saved_state) @@ -739,7 +749,7 @@ function game_init(saved_state) -- but you don't have a built tower cursor node, so hovering a buildable tile throws an error select_tower_type(nil) else - game_state = get_initial_game_state() + game_state = get_initial_game_state(seed) end game = true diff --git a/src/grid.lua b/src/grid.lua index 9a02ee5..15fc750 100644 --- a/src/grid.lua +++ b/src/grid.lua @@ -64,7 +64,7 @@ function map_elevation_to_tile_type(elevation) elseif elevation < 0.5 then -- med-high elevation return "Ground - Dirt" - elseif elevation < 1 then -- high elevation + elseif elevation <= 1 then -- high elevation return "Mountain" else @@ -134,7 +134,7 @@ end function building_tower_breaks_flow_field(tower_type, hex) local original_elevations = {} local all_impassable = true - local hexes = hex_spiral_map(hex, get_tower_size(tower_type)) + local hexes = hex_spiral_map(hex, get_tower_size(tower_type) - 1) for _,h in pairs(hexes) do local tile = hex_map_get(game_state.map, h) @@ -178,7 +178,7 @@ function map_elevation_to_color(elevation) elseif elevation < 0.5 then -- med-high elevation return math.lerp(COLORS.DIRT, COLORS.GRASS, elevation + 0.5){ a = (elevation + 1.6) / 2 + 0.3 } - elseif elevation < 1 then -- high elevation + elseif elevation <= 1 then -- high elevation return COLORS.MOUNTAIN{ ra = elevation } else diff --git a/src/hexyz.lua b/src/hexyz.lua index 6c5d5e2..d5260a3 100644 --- a/src/hexyz.lua +++ b/src/hexyz.lua @@ -285,8 +285,9 @@ function hex_map_set(map, hex, y, v) end -- Returns Unordered Parallelogram-Shaped Map of |width| and |height| with Simplex Noise +-- note that this is the default behavior if you just iterate two dimensions and place hexes naively function hex_parallelogram_map(width, height, seed) - local seed = seed or math.random(width * height) + local seed = seed or os.time() local map = {} for i = 0, width - 1 do @@ -327,7 +328,7 @@ end -- Returns Unordered Triangular (Equilateral) Map of |size| with Simplex Noise function hex_triangular_map(size, seed) - local seed = seed or math.random(size * math.cos(size) / 2) + local seed = seed or os.time() local map = {} for i = 0, size do @@ -365,11 +366,25 @@ function hex_triangular_map(size, seed) }}) end +function hex_hexagonal_map_noise(i, j, radius, seed) + local idelta = i / radius + local jdelta = j / radius + local noise = 0 + + for oct = 1, 6 do + local f = 2/3^oct + local l = 2^oct + local pos = vec2(idelta + seed * radius, jdelta + seed * radius) + + noise = noise + f * math.simplex(pos * l) + end + + return noise +end + -- Returns Unordered Hexagonal Map of |radius| with Simplex Noise function hex_hexagonal_map(radius, seed) - -- @NOTE usually i try and generate a seed within the range of the area of the map, but for lua's math.random starts to exhibit some really weird behavior - -- when you seed it with a high integer value, so I changed 'radius^2' to just 'radius' here. - local seed = seed or math.random(math.floor(2 * math.pi * radius)) + local seed = seed or os.time() local size = 0 local map = {} @@ -380,20 +395,7 @@ function hex_hexagonal_map(radius, seed) local j2 = math.min(radius, -i + radius) for j = j1, j2 do - - -- Calculate Noise - local idelta = i / radius - local jdelta = j / radius - local noise = 0 - - for oct = 1, 6 do - local f = 2/3^oct - local l = 2^oct - local pos = vec2(idelta + seed * radius, jdelta + seed * radius) - - noise = noise + f * math.simplex(pos * l) - end - map[i][j] = noise + map[i][j] = hex_hexagonal_map_noise(i, j, radius, seed) size = size + 1 end end @@ -415,10 +417,25 @@ function hex_hexagonal_map(radius, seed) }}) end +function hex_rectangular_map_noise(i, j, width, height, seed) + local idelta = i / width + local jdelta = j / height + local noise = 0 + + for oct = 1, 6 do + local f = 2/3^oct + local l = 2^oct + local pos = vec2(idelta + seed * width, jdelta + seed * height) + noise = noise + f * math.simplex(pos * l) + end + + return noise +end + -- Returns Unordered Rectangular Map of |width| and |height| with Simplex Noise function hex_rectangular_map(width, height, orientation, seed, do_generate_noise) local orientation = orientation or HEX_DEFAULT_ORIENTATION - local seed = seed or math.random(width * height) + local seed = seed or os.time() local map = {} if orientation == HEX_ORIENTATION.FLAT then @@ -426,24 +443,13 @@ function hex_rectangular_map(width, height, orientation, seed, do_generate_noise map[i] = {} for j = 0, height - 1 do + local noise = 0 if do_generate_noise then - -- begin to calculate noise - local idelta = i / width - local jdelta = j / height - local noise = 0 - - for oct = 1, 6 do - local f = 2/3^oct - local l = 2^oct - local pos = vec2(idelta + seed * width, jdelta + seed * height) - noise = noise + f * math.simplex(pos * l) - end - j = j - math.floor(i/2) -- this is what makes it rectangular - hex_map_set(map, i, j, noise) - else - j = j - math.floor(i/2) -- this is what makes it rectangular - hex_map_set(map, i, j, 0) + noise = hex_rectangular_map_noise(i, j, width, height, seed) end + + j = j - math.floor(i/2) -- this is what makes it rectangular + hex_map_set(map, i, j, noise) end end elseif orientation == HEX_ORIENTATION.POINTY then diff --git a/src/map-editor.lua b/src/map-editor.lua index 2957297..4c9a0f4 100644 --- a/src/map-editor.lua +++ b/src/map-editor.lua @@ -121,7 +121,7 @@ function map_editor_action() -- fine tune tile's elevation with mouse wheel local mouse_wheel_delta = win:mouse_wheel_delta().y / 100 if map_editor_state.selected_tile and mouse_wheel_delta ~= 0 then - map_editor_state.selected_tile.elevation = math.clamp(map_editor_state.selected_tile.elevation + mouse_wheel_delta, -1, 1) + map_editor_state.selected_tile.elevation = math.clamp(map_editor_state.selected_tile.elevation + mouse_wheel_delta, -1, 1 - math.SMALLEST_NUMBER_ABOVE_0) --map_editor_state.selected_tile.node("circle").color = map_elevation_color(map_editor_state.selected_tile.elevation) end end diff --git a/src/mob.lua b/src/mob.lua index 2299d85..5db50b3 100644 --- a/src/mob.lua +++ b/src/mob.lua @@ -263,7 +263,7 @@ local function update_mob_spooder(mob, mob_index) if mob.frame_target then -- do movement - -- it's totally possible that the target we have was invalidated by a tower placed this frame, + -- it's possible that the target we have was invalidated by a tower placed this frame, -- or between when we last calculated this target and now -- check for that now if mob_can_pass_through(mob, mob.frame_target) then diff --git a/src/projectile.lua b/src/projectile.lua index afaff76..3242f59 100644 --- a/src/projectile.lua +++ b/src/projectile.lua @@ -81,7 +81,6 @@ local function update_projectile_shell(projectile, projectile_index) projectile.node.position2d = projectile.position projectile.hex = pixel_to_hex(projectile.position, vec2(HEX_SIZE)) - projectile.props.z = projectile.props.z - SHELL_GRAVITY * am.delta_time -- check if we hit something -- get a list of hexes that could have something we could hit on them @@ -106,14 +105,6 @@ local function update_projectile_shell(projectile, projectile_index) end end - local tile = hex_map_get(game_state.map, projectile.hex) - if tile and tile.elevation >= projectile.props.z then - --do_explode = true - - elseif projectile.props.z <= 0 then - --do_explode = true - end - if do_explode then for index,mob in pairs(mobs) do local damage = (1 / (math.distance(mob.position, projectile.position) / (HEX_PIXEL_WIDTH * 2))) * projectile.damage diff --git a/src/tower.lua b/src/tower.lua index a1be60d..3fc96d7 100644 --- a/src/tower.lua +++ b/src/tower.lua @@ -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