From 518ecaf5d2cd248409358be6469df3d09da2de13 Mon Sep 17 00:00:00 2001 From: Nicholas Hayashi Date: Thu, 11 Feb 2021 13:40:25 -0500 Subject: [PATCH] waves --- res/Sprite-0001.aseprite | Bin 845 -> 656 bytes res/cannon1.png | Bin 0 -> 292 bytes res/mob_spooder.png | Bin 0 -> 558 bytes src/game.lua | 60 +++++++++++++++++++--- src/grid.lua | 4 -- src/mob.lua | 104 +++++++++++++++++++++++++++++++-------- src/tower.lua | 67 ++++++++++++++----------- texture.lua | 2 + 8 files changed, 176 insertions(+), 61 deletions(-) create mode 100644 res/cannon1.png create mode 100644 res/mob_spooder.png diff --git a/res/Sprite-0001.aseprite b/res/Sprite-0001.aseprite index 55a59b0bfa099817af3027bce24514216bfcbcb3..14dfa047dd9eacfd095c34cf8e14c318b42b2c78 100644 GIT binary patch delta 153 zcmX@hHi4CC0@FmMI!1wwjU9~j?F9siaE&%5>^N9?R#R+ z!@ni^1?LnIpMQ&!t8O%&_lsK1e$>!z>x;|=km<%7<;Yn!+E>`DG?Z~M4*!?Hx9KXyr{cP(UUYnInwyuy)pn}H!jz{VE< DV8}UL delta 343 zcmV-d0jU0v1dV2eW%cg1aC$OZ`#kyCRa&idoz1-K=emXil|C&G*Zx6Q(!S z+V`$k+u01c=XQoXb30bv>wfcz#QXidbZ^Pd^VW7!z13U2vHw@r_o#o1kT& z$rLZ!qv4gB*%nRjl$&kP@J^XVqh`ulqBkeA#F~@2dh_-EV=d7e*}TLW)tq9DX4bGq pF>6}An=Pzf-PTr5UWC<)8EIv@S}c diff --git a/res/cannon1.png b/res/cannon1.png new file mode 100644 index 0000000000000000000000000000000000000000..be0fad2cbf016ffa3ada4758ed5b7f828db8bb41 GIT binary patch literal 292 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D$|?s>X6hE&XX zdu=ywvw=XvL*;4(0poxr*0at`aJ6CUPH8hKoZciU5Xe@j6(GdHFBtdezZUo0^UpW4 zb^vul!R`mTu?GZJ9h;h*eR=&Wc{pnV2S?;kDS4A9uCHFD^>t*j~Ps`f*LM2eS=trx%=E_Ct^f d<|>A=N34mPx$=t)FDR9J=Wm^)6xKoExijgV+kLG?6GDnGX2&|XfIe-T2X0^(2 z|I_Pc6UC4U*nzVWMN9B?a%2Hgg1gf;-L36meYFh6fp6(4>2r&6;5p#L_7Eo6W0Rn(> z0U9vAI+7J6S5j_G`RrYk;B;Lznw~>{mMu>v+56;+=M3x(7Dyf&%jMI#2Q{w&51cLK zv9-%2xu!c8bqrixHFi>8qbZ8#s^t_w9hnq>zla^SS!(TIL?E596N+O0`z9T;^eygn z_hMtZM1ZB^`(QFVm|Mg{t7sk^Lr1_wb$WpSm374&v|)HJ%zQ5oCHp>}+7a7Qz_bAY zj0ec4q-ePS`)m|xG;`YVMc9DK`k+gJvlY>Nn%S8}i1Xkfy4h6%9I%gnXo~XGA3xd_ w!`NLcZh&u4p29VwGUm(RuenrBcHS)3FCJ~*$NU852LJ#707*qoM6N<$f_aJd$p8QV literal 0 HcmV?d00001 diff --git a/src/game.lua b/src/game.lua index d5e2e0c..d7349e0 100644 --- a/src/game.lua +++ b/src/game.lua @@ -25,7 +25,7 @@ local TRDTS = { local function get_initial_game_state(seed) local STARTING_MONEY = 10000 -- 2014 - local map, world = random_map(2014) + local map, world = random_map() return { map = map, -- map of hex coords map[x][y] to some stuff at that location @@ -38,6 +38,9 @@ local function get_initial_game_state(seed) money = STARTING_MONEY, -- current money current_wave = 0, + time_until_next_wave = 15, + time_until_next_break = 0, + spawning = false, selected_tower_type = false, selected_toolbelt_button = 9, @@ -45,6 +48,14 @@ local function get_initial_game_state(seed) } end +local function get_wave_timer_text() + if state.spawning then + return string.format("wave over: %.2f", state.time_until_next_break) + else + return string.format("next wave: %.2f", state.time_until_next_wave) + end +end + local function get_top_right_display_text(hex, evenq, centered_evenq, display_type) local str = "" if display_type == TRDTS.CENTERED_EVENQ then @@ -85,7 +96,7 @@ end function do_day_night_cycle() local tstep = (math.sin(state.time * am.delta_time) + 1) / 100 - state.world"negative_mask".color = vec4(tstep){a=1} + --state.world"negative_mask".color = vec4(tstep){a=1} end local function game_pause() @@ -113,6 +124,14 @@ local function game_pause() end) end +local function get_wave_time(current_wave) + return 30 +end + +local function get_break_time(current_wave) + return 15 +end + local function game_action(scene) if state.score < 0 then game_end() return true end @@ -120,6 +139,26 @@ local function game_action(scene) state.time = state.time + am.delta_time state.score = state.score + am.delta_time + if state.spawning then + state.time_until_next_break = state.time_until_next_break - am.delta_time + + if state.time_until_next_break <= 0 then + state.time_until_next_break = 0 + + state.spawning = false + state.time_until_next_wave = get_wave_time(state.current_wave) + end + else + state.time_until_next_wave = state.time_until_next_wave - am.delta_time + + if state.time_until_next_wave <= 0 then + state.time_until_next_wave = 0 + + state.spawning = true + state.time_until_next_break = get_break_time(state.current_wave) + end + end + local mouse = WIN:mouse_position() local hex = pixel_to_hex(mouse - WORLDSPACE_COORDINATE_OFFSET) local rounded_mouse = hex_to_pixel(hex) + WORLDSPACE_COORDINATE_OFFSET @@ -165,9 +204,9 @@ local function game_action(scene) if WIN:mouse_pressed"middle" then WIN.scene("world_scale").scale2d = vec2(1) - else + elseif WIN:key_down"lctrl" then local mwd = WIN:mouse_wheel_delta() - WIN.scene("world_scale").scale2d = WIN.scene("world_scale").scale2d + vec2(mwd) / 100 + WIN.scene("world_scale").scale2d = WIN.scene("world_scale").scale2d + vec2(mwd.y) / 100 end if WIN:key_pressed"escape" then @@ -204,9 +243,6 @@ local function game_action(scene) do_gui_updates() do_day_night_cycle() - WIN.scene("top_right_display").text = get_top_right_display_text(hex, evenq, centered_evenq, state.selected_top_right_display_type) - WIN.scene("score").text = string.format("SCORE: %.2f", state.score) - WIN.scene("money").text = string.format("MONEY: %d", state.money) if interactable then WIN.scene("cursor").hidden = false @@ -218,6 +254,11 @@ local function game_action(scene) else WIN.scene("cursor").hidden = true end + + WIN.scene("score").text = string.format("SCORE: %.2f", state.score) + WIN.scene("money").text = string.format("MONEY: %d", 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, state.selected_top_right_display_type) end local function make_game_toolbelt() @@ -378,6 +419,10 @@ function game_scene() local money = am.translate(WIN.left + 10, WIN.top - 40) ^ am.text("", "left"):tag"money" + local wave_timer = am.translate(0, WIN.top - 30) + ^ am.scale(1.5) + ^ am.text(string.format("Next Wave: %d", state.time_until_next_wave)):tag"wave_timer" + local top_right_display = am.translate(WIN.right - 10, WIN.top - 20) ^ am.text("", "right", "top"):tag"top_right_display" @@ -393,6 +438,7 @@ function game_scene() am.translate(OFF_SCREEN):tag"cursor_translate" ^ make_hex_cursor(0, COLORS.TRANSPARENT), score, money, + wave_timer, top_right_display, make_game_toolbelt(), curtain, diff --git a/src/grid.lua b/src/grid.lua index 5704ca1..6237e5b 100644 --- a/src/grid.lua +++ b/src/grid.lua @@ -115,10 +115,6 @@ function apply_flow_field(map, flow_field, world) end end - -- dijkstra doesn't set the priority of the center, do it ourselves - map[HEX_GRID_CENTER.x][HEX_GRID_CENTER.y].priority = -1 - overlay_group:append(am.translate(hex_to_pixel(vec2(HEX_GRID_CENTER.x, HEX_GRID_CENTER.y))) - ^ am.text(string.format("%.1f", -10))) if world then overlay_group.hidden = flow_field_hidden world:append(overlay_group) diff --git a/src/mob.lua b/src/mob.lua index c8faf3d..bbd5001 100644 --- a/src/mob.lua +++ b/src/mob.lua @@ -4,6 +4,7 @@ MOBS = {} MOB_TYPE = { BEEPER = 1, + SPOODER = 2 } MAX_MOB_SIZE = hex_height(HEX_SIZE, ORIENTATION.FLAT) / 2 @@ -11,6 +12,12 @@ MOB_SIZE = MAX_MOB_SIZE MOB_SPECS = { [MOB_TYPE.BEEPER] = { + health = 30, + speed = 8, + bounty = 15, + hurtbox_radius = MOB_SIZE/2 + }, + [MOB_TYPE.SPOODER] = { health = 20, speed = 10, bounty = 5, @@ -73,24 +80,32 @@ end function make_mob_node(mob_type, mob) local healthbar = am.group{ - am.rect(-HEALTHBAR_WIDTH/2, -HEALTHBAR_HEIGHT/2, HEALTHBAR_WIDTH/2, HEALTHBAR_HEIGHT/2, COLORS.VERY_DARK_GRAY), + --am.rect(-HEALTHBAR_WIDTH/2, -HEALTHBAR_HEIGHT/2, HEALTHBAR_WIDTH/2, HEALTHBAR_HEIGHT/2, COLORS.VERY_DARK_GRAY), am.rect(-HEALTHBAR_WIDTH/2, -HEALTHBAR_HEIGHT/2, HEALTHBAR_WIDTH/2, HEALTHBAR_HEIGHT/2, COLORS.GREEN_YELLOW) } healthbar.hidden = true - return am.group{ - am.rotate(state.time) - ^ pack_texture_into_sprite(TEXTURES.MOB_BEEPER, MOB_SIZE, MOB_SIZE), - am.translate(0, -10) - ^ healthbar - } + if mob_type == MOB_TYPE.BEEPER then + return am.group{ + am.rotate(state.time) + ^ pack_texture_into_sprite(TEXTURES.MOB_BEEPER, MOB_SIZE, MOB_SIZE), + am.translate(0, -10) + ^ healthbar + } + elseif mob_type == MOB_TYPE.SPOODER then + return am.group{ + am.rotate(0) + ^ pack_texture_into_sprite(TEXTURES.MOB_SPOODER, MOB_SIZE, MOB_SIZE), + am.translate(0, -10) + ^ healthbar + } + end end function get_mob_path(mob, map, start, goal) return Astar(map, goal, start, grid_heuristic, grid_cost) end --- @FIXME there's a bug here where the position of the spawn hex is sometimes 1 closer to the center than we want local function get_spawn_hex() -- ensure we spawn on an random tile along the map's edges local roll = math.random(HEX_GRID_WIDTH * 2 + HEX_GRID_HEIGHT * 2) - 1 @@ -115,7 +130,7 @@ local function get_spawn_hex() return hex end -local function update_mob(mob, mob_index) +local function resolve_frame_target_for_mob(mob) local last_frame_hex = mob.hex mob.hex = pixel_to_hex(mob.position) @@ -161,7 +176,8 @@ local function update_mob(mob, mob_index) if not tile.priority then -- if there's no stored priority, that should mean it's the center tile - mob.frame_target = n + -- in which case, it should be the best target + lowest_cost_hex = n break end @@ -179,21 +195,48 @@ local function update_mob(mob, mob_index) end end end +end + +local function update_mob_spooder(mob, mob_index) + resolve_frame_target_for_mob(mob) + + if mob.frame_target then + -- do movement + -- it's totally 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 + local from = state.map.get(mob.hex.x, mob.hex.y) + local to = state.map.get(mob.frame_target.x, mob.frame_target.y) + local rate = (math.abs(from.elevation - to.elevation) * 100) * mob.speed * am.delta_time + + mob.position = mob.position + math.normalize(hex_to_pixel(mob.frame_target) - mob.position) * rate + mob.node.position2d = mob.position + else + mob.frame_target = false + end + else + log('no target') + end - if mob.frame_target and mob.frame_target == last_frame_hex then - log('backpedaling') + -- passive animation + if math.random() < 0.1 then + mob.node"rotate":action(am.tween(0.3, { angle = math.random(math.rad(0, -180))})) end +end + +local function update_mob_beeper(mob, mob_index) + resolve_frame_target_for_mob(mob) - -- do movement if mob.frame_target then + -- do movement -- it's totally 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 local from = state.map.get(mob.hex.x, mob.hex.y) local to = state.map.get(mob.frame_target.x, mob.frame_target.y) - -- @FIXME changing this '4' constant to '1' breaks mob pathing and I don't know why - local rate = 4 * mob.speed * am.delta_time - math.abs(from.elevation - to.elevation) + local rate = (4 * mob.speed - math.abs(to.elevation - from.elevation)) * am.delta_time mob.position = mob.position + math.normalize(hex_to_pixel(mob.frame_target) - mob.position) * rate mob.node.position2d = mob.position @@ -212,21 +255,40 @@ local function update_mob(mob, mob_index) end end +local function get_mob_update_function(mob_type) + if mob_type == MOB_TYPE.BEEPER then + return update_mob_beeper + + elseif mob_type == MOB_TYPE.SPOODER then + return update_mob_spooder + end +end + +local function grow_mob_health(mob_type, spec_health, time) + return spec_health * math.log(time) +end +local function grow_mob_speed(mob_type, spec_speed, time) + return spec_speed +end +local function grow_mob_bounty(mob_type, spec_speed, time) + return spec_speed * math.log(time) +end + local function make_and_register_mob(mob_type) local mob = make_basic_entity( get_spawn_hex(), make_mob_node(mob_type), - update_mob + get_mob_update_function(mob_type) ) mob.type = mob_type local spec = get_mob_spec(mob_type) - mob.health = spec.health - mob.speed = spec.speed - mob.bounty = spec.bounty + mob.health = grow_mob_health(mob_type, spec.health, state.time) + mob.speed = grow_mob_speed(mob_type, spec.speed, state.time) + mob.bounty = grow_mob_bounty(mob_type, spec.bounty, state.time) mob.hurtbox_radius = spec.hurtbox_radius - mob.healthbar = mob.node:child(1):child(2):child(1) + mob.healthbar = mob.node:child(1):child(2):child(1) -- lmao register_entity(MOBS, mob) return mob @@ -235,7 +297,7 @@ end local SPAWN_CHANCE = 25 function do_mob_spawning() --if WIN:key_pressed"space" then - if math.random(SPAWN_CHANCE) == 1 then + if state.spawning and math.random(SPAWN_CHANCE) == 1 then --if #MOBS < 1 then make_and_register_mob(MOB_TYPE.BEEPER) end diff --git a/src/tower.lua b/src/tower.lua index c2d5a7f..d63fc9c 100644 --- a/src/tower.lua +++ b/src/tower.lua @@ -18,7 +18,7 @@ TOWER_TYPE = { TOWER_SPECS = { [TOWER_TYPE.WALL] = { name = "Wall", - placement_rules_text = "Place on grass or dirt", + placement_rules_text = "Place on Ground", short_description = "Restricts movement", texture = TEXTURES.TOWER_WALL, icon_texture = TEXTURES.TOWER_WALL_ICON, @@ -27,9 +27,9 @@ TOWER_SPECS = { fire_rate = 2, }, [TOWER_TYPE.HOWITZER] = { - name = "HOWITZER", + name = "Howitzer", placement_rules_text = "Place on non-Water", - short_description = "Fires artillery. Range and cost increase with elevation of terrain underneath.", + short_description = "Fires artillery. Range increases with elevation of terrain underneath.", texture = TEXTURES.TOWER_HOWITZER, icon_texture = TEXTURES.TOWER_HOWITZER_ICON, cost = 20, @@ -48,7 +48,7 @@ TOWER_SPECS = { }, [TOWER_TYPE.MOAT] = { name = "Moat", - placement_rules_text = "Place on grass or dirt", + placement_rules_text = "Place on Ground", short_description = "Restricts movement", texture = TEXTURES.TOWER_MOAT, icon_texture = TEXTURES.TOWER_MOAT_ICON, @@ -58,7 +58,7 @@ TOWER_SPECS = { }, [TOWER_TYPE.RADAR] = { name = "Radar", - placement_rules_text = "Place on any non-water", + placement_rules_text = "Place on any non-Water", short_description = "Provides information about incoming waves.", texture = TEXTURES.TOWER_RADAR, icon_texture = TEXTURES.TOWER_RADAR_ICON, @@ -68,7 +68,7 @@ TOWER_SPECS = { }, [TOWER_TYPE.LIGHTHOUSE] = { name = "Lighthouse", - placement_rules_text = "Place next to - but not on - Water or Moats", + 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, @@ -147,10 +147,12 @@ local function make_tower_node(tower_type) return make_tower_sprite(tower_type) elseif tower_type == TOWER_TYPE.HOWITZER then - return make_tower_sprite(tower_type) - + return am.group{ + make_tower_sprite(tower_type), + am.rotate(0) ^ am.sprite("res/cannon1.png") + } elseif tower_type == TOWER_TYPE.LIGHTHOUSE then - return am.group( + return am.group{ make_tower_sprite(tower_type), am.particles2d{ source_pos = vec2(0, 12), @@ -174,12 +176,12 @@ local function make_tower_node(tower_type) max_particles = 200, warmup_time = 5 } - ) + } elseif tower_type == TOWER_TYPE.WALL then return am.circle(vec2(0), HEX_SIZE, COLORS.VERY_DARK_GRAY, 6) elseif tower_type == TOWER_TYPE.MOAT then - return am.circle(vec2(0), HEX_SIZE, (COLORS.WATER){a=1}, 6) + return am.circle(vec2(0), HEX_SIZE, COLORS.WATER{a=1}, 6) elseif tower_type == TOWER_TYPE.RADAR then return make_tower_sprite(tower_type) @@ -226,7 +228,7 @@ function tower_type_is_buildable_on(hex, tile, tower_type) if tower_type == TOWER_TYPE.HOWITZER then if not mobs_blocking and towers_blocking then blocked = false - for _,tower in pairs(TOWERS) do + for _,tower in pairs(blocking_towers) do if tower.type ~= TOWER_TYPE.WALL then blocked = true break @@ -238,7 +240,7 @@ function tower_type_is_buildable_on(hex, tile, tower_type) elseif tower_type == TOWER_TYPE.REDEYE then if not mobs_blocking and towers_blocking then blocked = false - for _,tower in pairs(TOWERS) do + for _,tower in pairs(blocking_towers) do if tower.type ~= TOWER_TYPE.WALL then blocked = true break @@ -314,22 +316,28 @@ function update_tower_howitzer(tower, tower_index) elseif (state.time - tower.last_shot_time) > tower.fire_rate then local mob = MOBS[tower.target_index] - - local projectile = make_and_register_projectile( - tower.hex, - PROJECTILE_TYPE.SHELL, - math.normalize(mob.position - tower.position) - ) - - -- @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 = state.time - play_sfx(SOUNDS.EXPLOSION2) + local theta = math.atan((tower.hex.y - mob.hex.y)/(tower.hex.x - mob.hex.x)) + + if (theta - tower.node("rotate").angle) < 0.1 then + local projectile = make_and_register_projectile( + tower.hex, + PROJECTILE_TYPE.SHELL, + math.normalize(mob.position - tower.position) + ) + + -- @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 = state.time + play_sfx(SOUNDS.EXPLOSION2) + end + else + tower.node("rotate").angle = tower.node("rotate").angle + state.time * 0.5 end end + tower.node("rotate").angle = math.wrapf(tower.node("rotate").angle, math.pi*2) end function update_tower_lighthouse(tower, tower_index) @@ -368,7 +376,7 @@ function update_tower_lighthouse(tower, tower_index) end end -local TOWER_HEIGHT = 0.6 +local TOWER_HEIGHT = 1 function make_and_register_tower(hex, tower_type) local tower = make_basic_entity( hex, @@ -409,7 +417,8 @@ function make_and_register_tower(hex, tower_type) tile.elevation = tile.elevation + TOWER_HEIGHT end - return register_entity(TOWERS, tower) + register_entity(TOWERS, tower) + return tower end function build_tower(hex, tower_type) diff --git a/texture.lua b/texture.lua index 831fdea..e947869 100644 --- a/texture.lua +++ b/texture.lua @@ -21,6 +21,7 @@ TEXTURES = { TOWER_WALL = load_texture("res/tower_wall.png"), TOWER_WALL_ICON = load_texture("res/tower_wall_icon.png"), TOWER_HOWITZER = load_texture("res/tower_howitzer.png"), + CANNON1 = load_texture("res/cannon1.png"), TOWER_HOWITZER_ICON = load_texture("res/tower_howitzer_icon.png"), TOWER_REDEYE = load_texture("res/tower_redeye.png"), TOWER_REDEYE_ICON = load_texture("res/tower_redeye_icon.png"), @@ -33,6 +34,7 @@ TEXTURES = { -- mob stuff MOB_BEEPER = load_texture("res/mob_beeper.png"), + MOB_SPOODER = load_texture("res/mob_spooder.png"), } function pack_texture_into_sprite(texture, width, height)