Browse Source

waves

master
Nicholas Hayashi 4 years ago
parent
commit
518ecaf5d2
  1. BIN
      res/Sprite-0001.aseprite
  2. BIN
      res/cannon1.png
  3. BIN
      res/mob_spooder.png
  4. 60
      src/game.lua
  5. 4
      src/grid.lua
  6. 104
      src/mob.lua
  7. 67
      src/tower.lua
  8. 2
      texture.lua

BIN
res/Sprite-0001.aseprite

BIN
res/cannon1.png

After

Width: 64  |  Height: 64  |  Size: 292 B

BIN
res/mob_spooder.png

After

Width: 40  |  Height: 40  |  Size: 558 B

60
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,

4
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)

104
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

67
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)

2
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)

Loading…
Cancel
Save