diff --git a/CLAUDE.md b/CLAUDE.md index 0f92afa..b6f3435 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -95,13 +95,16 @@ table.sort(cells, distance_to_corner) ``` **Every new `record` type must have a `.new()` constructor.** -Do not construct records ad-hoc by setting fields inline at the call site. Define a `RecordName.new(...)` function that initialises all fields and returns the fully constructed value. +Do not construct records ad-hoc by setting fields inline at the call site. Define a `RecordName.new(...)` function that initialises all fields and returns the fully constructed value. Always use `setmetatable({}, { __index = RecordName })` as the base, even for records with no methods, to keep construction consistent. **Avoid the ternary idiom for non-trivial conditions.** `x and a or b` is acceptable only for genuinely simple one-liners. For anything with logic or multiple parts, use `if/elseif/end`. -**Comments explain why, not what — and must be concrete.** -The code already says what is happening. A comment should explain why this logic exists and consider give the concrete scenario it handles. Avoid abstract labels; be explicit over implicit. +**Prefer structure over comments.** +Before writing a comment, ask whether the same clarity can be achieved by extracting a well-named function, choosing a more descriptive variable name, or modelling the code after what it actually represents. A name like `split_preserving_trailing_newline` communicates intent without a comment; a type named `Rewrite` tells you more than `FormatResult` ever could. Comments are a last resort, not a first instinct. + +**When a comment is necessary, explain why — concretely.** +The code already says what is happening. A comment should explain why this logic exists and give the concrete scenario it handles. Avoid abstract labels; be explicit over implicit. **Never use `as` casts or `any` to work around type errors.** Do not cast with `as` or widen to `any` to make the type checker accept code. The only exception is repeating a cast pattern that already exists in the codebase for the same reason. Any new use requires explicit permission from the user — and it should be assumed permission will not be granted unless the circumstances are extraordinary. diff --git a/Makefile b/Makefile index 9cdd152..2a17827 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ include $(wildcard .env) -SRCS := $(shell find src -type f) +SRCS := $(shell find src -type f -name "*.tl") SRCS_LINT := $(patsubst src/%, ./src/%, $(SRCS)) TRANSPILED := $(patsubst src/%.tl, dist/%.lua, $(SRCS)) ASSETS := $(shell find assets -type f) diff --git a/src/bombs/bombs.tl b/src/bombs/bombs.tl index f5e7503..248ccbd 100644 --- a/src/bombs/bombs.tl +++ b/src/bombs/bombs.tl @@ -1,24 +1,24 @@ +local explosions = require("bombs.explosions") local animations = require("engine.animations") local collision = require("engine.collision") -local maps_entities = require("level_layout.maps.entities") -local explosions = require("bombs.explosions") -local map_entities = require("level_layout.map_entities") -local grid = require("level_layout.grid") +local points = require("engine.geometry.points") +local rectangles = require("engine.geometry.rectangles") +local vectors = require("engine.geometry.vectors") local hitboxes = require("engine.hitboxes") -local itertools = require("itertools") local logger = require("engine.logger") local physics = require("engine.physics") +local itertools = require("itertools") +local grid = require("level_layout.grid") +local map_entities = require("level_layout.map_entities") +local maps_entities = require("level_layout.maps.entities") local players = require("players") -local points = require("engine.geometry.points") -local rectangles = require("engine.geometry.rectangles") -local vectors = require("engine.geometry.vectors") local z_index = require("z_index") local type Animation = animations.Animation local type Explodable = require("bombs.explodable") local type Explosions = explosions.Explosions -local type Kinematic = require("kinematic") local type Entity = require("engine.entity") +local type Kinematic = require("kinematic") local type Group = collision.Group local type Player = players.Player local type ReadableGroup = collision.ReadableGroup @@ -31,7 +31,6 @@ local type Goal = maps_entities.Goal local type ConveyorBelt = maps_entities.ConveyorBelt -- Bomb -- - local BOMB_TIMER_START = 2.5 local bomb_shadow: Image local bomb_image: Image @@ -48,17 +47,17 @@ local record Bomb is Entity, Kinematic, Explodable end function Bomb.new(position: Point, range: integer, hand_back: function()): Bomb - local self: Bomb = setmetatable({}, { __index = Bomb }) - self.position = position - self.z_index = z_index.BELOW - self.velocity = vectors.new(0, 0) - self.range = range - self.timer = BOMB_TIMER_START - self.hand_back = hand_back - self.has_been_kicked = false - self.stopping = false - self.animation = animations.new(bomb_image, 16, 0.25) - return self + local self: Bomb = setmetatable({}, {__index = Bomb}) + self.position = position + self.z_index = z_index.BELOW + self.velocity = vectors.new(0, 0) + self.range = range + self.timer = BOMB_TIMER_START + self.hand_back = hand_back + self.has_been_kicked = false + self.stopping = false + self.animation = animations.new(bomb_image, 16, 0.25) + return self end function Bomb:get_position(): Point @@ -74,7 +73,7 @@ function Bomb:hitbox(): Rectangle end function Bomb:movementbox(): Rectangle - return hitboxes.new_block(self.position) + return hitboxes.new_block(self.position) end function Bomb:blow_up() @@ -82,7 +81,7 @@ function Bomb:blow_up() end function Bomb:hit_with_bomb() - -- Does nothing + -- Does nothing end function Bomb:get_goal(): Goal @@ -117,7 +116,8 @@ function Bomb:_trigger_belt(direction?: Vector): ConveyorBelt | nil for _, belt in ipairs(belts) do if self.position:distance_to(grid.snap_to(self.position)) <= 3 then local snap_pos = grid.snap_to(self.position) - if snap_pos:equal(belt.position) and not belt.direction:same_direction(bomb_move_dir) then + if snap_pos:equal(belt.position) + and not belt.direction:same_direction(bomb_move_dir) then return belt end end @@ -135,17 +135,21 @@ local function moving_outside_of_map(test_hitbox: Rectangle): boolean return not grid.area():contains(test_hitbox) and not bomb_in_goal end -local function moving_into_new_block(movementbox: Rectangle, direction: Vector): Rectangle | nil +local function moving_into_new_block( + movementbox: Rectangle, direction: Vector +): Rectangle | nil local snaps_to = grid.snap_to(movementbox:top_left()) local new_movementbox = movementbox:shift(direction) - for i=-1, 1 do - for j=-1, 1 do - local close_block = hitboxes.new_block(points.new( - snaps_to.x + i * grid.TILE_SIZE, - snaps_to.y + j * grid.TILE_SIZE - )) - if new_movementbox:overlap(close_block) and not movementbox:overlap(close_block) then + for i = -1, 1 do + for j = -1, 1 do + local close_block = hitboxes.new_block( + points.new( + snaps_to.x + i * grid.TILE_SIZE, snaps_to.y + j * grid.TILE_SIZE + ) + ) + if new_movementbox:overlap(close_block) + and not movementbox:overlap(close_block) then return close_block end end @@ -169,21 +173,28 @@ local record CanMoveBomb end function CanMoveBomb.new(new_position: Point, in_direction: Vector): CanMoveBomb - local self: CanMoveBomb = setmetatable({}, { __index = CanMoveBomb }) - self.yes = true - self.new_position = new_position - self.in_direction = in_direction - self.will_hit = nil - self.should_stop = false - return self + local self: CanMoveBomb = setmetatable({}, {__index = CanMoveBomb}) + self.yes = true + self.new_position = new_position + self.in_direction = in_direction + self.will_hit = nil + self.should_stop = false + return self end function CanMoveBomb:no(): CanMoveBomb - self.yes = false - return self + self.yes = false + return self end -function Bomb:can_move(direction: Vector, occupies: ReadableGroup, players_: ReadableGroup, bombs: ReadableGroup, lookup: Lookup, call_level?: integer): CanMoveBomb +function Bomb:can_move( + direction: Vector, + occupies: ReadableGroup, + players_: ReadableGroup, + bombs: ReadableGroup, + lookup: Lookup, + call_level?: integer +): CanMoveBomb local function not_this_bomb(occupied: Kinematic): boolean return occupied ~= self end @@ -191,9 +202,15 @@ function Bomb:can_move(direction: Vector, occupies: ReadableGroup, pl local test_hitbox = self:movementbox():shift(direction) local old_hitbox = self:movementbox() local can_move_bomb = CanMoveBomb.new(test_hitbox:top_left(), direction:normalize()) - local close_occupies = lookup:in_box_and_group(old_hitbox:expand(grid.TILE_SIZE), occupies) - local close_players = lookup:in_box_and_group(old_hitbox:expand(grid.TILE_SIZE), players_) - local close_bombs = lookup:in_box_and_group(old_hitbox:expand(grid.TILE_SIZE), bombs) + local close_occupies = lookup:in_box_and_group( + old_hitbox:expand(grid.TILE_SIZE), occupies + ) + local close_players = lookup:in_box_and_group( + old_hitbox:expand(grid.TILE_SIZE), players_ + ) + local close_bombs = lookup:in_box_and_group( + old_hitbox:expand(grid.TILE_SIZE), bombs + ) close_bombs = itertools.filter(close_bombs, not_this_bomb) local close_interacts: {Kinematic} = itertools.combine(close_bombs, close_players) @@ -205,7 +222,7 @@ function Bomb:can_move(direction: Vector, occupies: ReadableGroup, pl end local belt_trigger = self:_trigger_belt(direction) - if not belt_trigger is nil then + if not (belt_trigger is nil) then direction = belt_trigger.direction:scale(direction:length()) can_move_bomb.in_direction = direction:normalize() end @@ -218,7 +235,9 @@ function Bomb:can_move(direction: Vector, occupies: ReadableGroup, pl return entity:hitbox():overlap(hitbox:shrink(3)) end - local function overlaps_entity_significantly(hitbox: Rectangle, entities: {Kinematic}): boolean + local function overlaps_entity_significantly( + hitbox: Rectangle, entities: {Kinematic} + ): boolean for _, occupied in ipairs(entities) do if overlaps_significantly(occupied, hitbox) then return true @@ -236,8 +255,10 @@ function Bomb:can_move(direction: Vector, occupies: ReadableGroup, pl -- the bomb would otherwise have moved inot new block tile, then we help the -- side kick by snapping the projected position/hitbox to corridor. This means -- a side kick can be done without things aligning exactly! - local mock_dt = 1/60 -- TODO: Need to pass in 'dt' in here to properly calculate this - local passing = moving_into_new_block(self:movementbox(), self.velocity:scale(mock_dt)) + local mock_dt = 1 / 60 -- TODO: Need to pass in 'dt' in here to properly calculate this + local passing = moving_into_new_block( + self:movementbox(), self.velocity:scale(mock_dt) + ) if self.velocity:orthogonal(direction) and passing then if direction.x == 0 then test_hitbox.x = grid.snap_to_x(test_hitbox.x) @@ -259,7 +280,9 @@ function Bomb:can_move(direction: Vector, occupies: ReadableGroup, pl for _, bomb in ipairs(close_bombs) do if overlaps_significantly(bomb, test_hitbox) then - if bomb:can_move(direction, occupies, players_, bombs, lookup, next_call_level).yes then + if bomb:can_move( + direction, occupies, players_, bombs, lookup, next_call_level + ).yes then can_move_bomb.will_hit = bomb end end @@ -273,11 +296,11 @@ function Bomb:can_move(direction: Vector, occupies: ReadableGroup, pl end local new_block = moving_into_new_block(self:movementbox(), direction) - if not new_block is nil then + if not (new_block is nil) then if self.stopping then return can_move_bomb:no() end - + for _, occupied in ipairs(close_occupies) do if occupied:movementbox():overlap(test_hitbox) then return can_move_bomb:no() @@ -286,7 +309,9 @@ function Bomb:can_move(direction: Vector, occupies: ReadableGroup, pl for _, bomb in ipairs(close_bombs) do if overlaps_significantly(bomb, new_block) then - if bomb:can_move(direction, occupies, players_, bombs, lookup, next_call_level).yes then + if bomb:can_move( + direction, occupies, players_, bombs, lookup, next_call_level + ).yes then can_move_bomb.will_hit = bomb end end @@ -300,12 +325,18 @@ function Bomb:can_move(direction: Vector, occupies: ReadableGroup, pl return can_move_bomb end -function Bomb:update(dt: number, occupies: ReadableGroup, players_: ReadableGroup, bombs: ReadableGroup, lookup: Lookup) +function Bomb:update( + dt: number, + occupies: ReadableGroup, + players_: ReadableGroup, + bombs: ReadableGroup, + lookup: Lookup +) self.timer = self.timer - dt - + if self.velocity:length() > 0 then local belt_trigger = self:_trigger_belt() - if not belt_trigger is nil then + if not (belt_trigger is nil) then self.position = belt_trigger.position self.velocity = belt_trigger.direction:scale(self.velocity:length()) end @@ -327,7 +358,7 @@ function Bomb:update(dt: number, occupies: ReadableGroup, players_: R end local bomb_hit = can_move.will_hit - if not bomb_hit is nil then + if not (bomb_hit is nil) then bomb_hit:kick(dx:normalize()) end if can_move.should_stop then @@ -344,8 +375,14 @@ local record ThrownBomb is Entity trajectory: ProjectileTrajectory end -function ThrownBomb.new(delegate: Bomb, target: Point, start_height: number, throw_height: number, throw_time: number): ThrownBomb - local self: ThrownBomb = setmetatable({}, { __index = ThrownBomb }) +function ThrownBomb.new( + delegate: Bomb, + target: Point, + start_height: number, + throw_height: number, + throw_time: number +): ThrownBomb + local self: ThrownBomb = setmetatable({}, {__index = ThrownBomb}) self.delegate = delegate self.z_index = z_index.ABOVE self.target = target @@ -356,7 +393,9 @@ function ThrownBomb.new(delegate: Bomb, target: Point, start_height: number, thr end function ThrownBomb:bounce() - self.target = self:get_position():move(self.trajectory:get_direction():scale(grid.TILE_SIZE)) + self.target = self:get_position():move( + self.trajectory:get_direction():scale(grid.TILE_SIZE) + ) self.trajectory = physics.calc_trajectory( 0, 0, grid.TILE_SIZE * 0.8, 0.25, self.delegate.position, self.target ) @@ -398,7 +437,6 @@ function ThrownBomb:update(dt: number) end -- Bombs -- - local record Bombs on_ground: Group in_air: Group @@ -413,7 +451,7 @@ function bombs.load_assets() end function bombs.new(): Bombs - local self: Bombs = setmetatable({}, { __index = Bombs }) + local self: Bombs = setmetatable({}, {__index = Bombs}) self.on_ground = collision.new_group() self.in_air = collision.new_group() self.explosions = explosions.new() @@ -438,7 +476,7 @@ function Bombs:place_bomb(player: Player, occupied: {Entity}) if player.bombs == 0 then return end - + local hand_back_bomb = function() player.bombs = player.bombs + 1 end local bomb = Bomb.new(position, player.fire, hand_back_bomb) self.on_ground:add(bomb) @@ -491,21 +529,26 @@ function Bombs:update( for _, bomb in pairs(self.in_air.set) do bomb:update(dt) if bomb:has_landed() then - local hitting_entities = lookup:collisions_between(collision.new_group({bomb}), bombs_bounce_on) + local hitting_entities = lookup:collisions_between( + collision.new_group({bomb}), bombs_bounce_on + ) local bomb_bouncing = false for _, hitting_entity in pairs(hitting_entities) do hitting_entity:hit_with_bomb() bomb_bouncing = true end - if not grid.area():contains(bomb:hitbox()) and not bomb.delegate:get_goal() then + if not grid.area():contains(bomb:hitbox()) + and not bomb.delegate:get_goal() then bomb_bouncing = true end if bomb_bouncing then if grid.outside_visible_area(bomb:hitbox()) then -- Bomb outside seen map, loops around - bomb.delegate.position = grid.outside_on_opposite_side(bomb:get_position()) + bomb.delegate.position = grid.outside_on_opposite_side( + bomb:get_position() + ) bomb.trajectory:reverse() end @@ -520,7 +563,6 @@ function Bombs:update( self.explosions:update(dt, blockers, explodables, lookup) end - local function draw_bomb_shadow(bomb: Bomb) lutro.graphics.draw(bomb_shadow, bomb.position.x - 1, bomb.position.y - 8) end diff --git a/src/bombs/explosions.tl b/src/bombs/explosions.tl index cbaebd7..2384a54 100644 --- a/src/bombs/explosions.tl +++ b/src/bombs/explosions.tl @@ -1,12 +1,12 @@ local animations = require("engine.animations") local audio = require("engine.audio") local collision = require("engine.collision") -local grid = require("level_layout.grid") -local hitboxes = require("engine.hitboxes") -local images = require("engine.images") local points = require("engine.geometry.points") -local vectors = require("engine.geometry.vectors") local rectangles = require("engine.geometry.rectangles") +local vectors = require("engine.geometry.vectors") +local hitboxes = require("engine.hitboxes") +local images = require("engine.images") +local grid = require("level_layout.grid") local z_index = require("z_index") local type Animation = animations.Animation @@ -33,9 +33,7 @@ local explosion_small: Source local explosion_medium: Source local explosion_large: Source - -- Explosion -- - local record ExplosionAnimations right_end: Animation left_end: Animation @@ -61,8 +59,13 @@ local record ExplosionTile is Entity type: ExplosionTileType end -function ExplosionTile.new(position: Point, reduction: integer, is_last: boolean, explosion_type: ExplosionTileType): ExplosionTile - local self: ExplosionTile = setmetatable({}, { __index = ExplosionTile }) +function ExplosionTile.new( + position: Point, + reduction: integer, + is_last: boolean, + explosion_type: ExplosionTileType +): ExplosionTile + local self: ExplosionTile = setmetatable({}, {__index = ExplosionTile}) self.position = position self.z_index = z_index.LEVEL self.reduction = reduction @@ -95,7 +98,7 @@ local record Explosion end function Explosion.new(position: Point, range: number, delay?: number): Explosion - local self: Explosion = setmetatable({}, { __index = Explosion }) + local self: Explosion = setmetatable({}, {__index = Explosion}) self.position = position self.blocked_at = {} self.range = range @@ -108,9 +111,11 @@ function Explosion.new(position: Point, range: number, delay?: number): Explosio left_end = animations.new(explosion_end_left_image, 16, FRAME_DURATION), top_end = animations.new(explosion_end_top_image, 16, FRAME_DURATION), bottom_end = animations.new(explosion_end_bottom_image, 16, FRAME_DURATION), - middle_horizontal = animations.new(explosion_horizontal_image, 16, FRAME_DURATION), + middle_horizontal = animations.new( + explosion_horizontal_image, 16, FRAME_DURATION + ), middle_vertical = animations.new(explosion_vertical_image, 16, FRAME_DURATION), - center = animations.new(explosion_center_image, 16, FRAME_DURATION) + center = animations.new(explosion_center_image, 16, FRAME_DURATION), } return self @@ -134,18 +139,20 @@ function Explosion:has_reached_max_range(): boolean return self.duration >= self.range / self.speed end -function Explosion:new_tile(position: Point, is_last: boolean, explosion_type: ExplosionTileType): ExplosionTile +function Explosion:new_tile( + position: Point, is_last: boolean, explosion_type: ExplosionTileType +): ExplosionTile local function tile_reduction(): integer local min_reduction = 3 if not self:has_reached_max_range() then return min_reduction end - + local time_since_max_range = self.duration - self.range / self.speed local reduction = (1 + math.floor(time_since_max_range / FRAME_DURATION)) * 2 return math.max(reduction, min_reduction) end - + return ExplosionTile.new(position, tile_reduction(), is_last, explosion_type) end @@ -155,7 +162,9 @@ local enum BlockingResult "nil" end -function Explosion:_update_active_tiles(blockers: ReadableGroup, lookup: Lookup) +function Explosion:_update_active_tiles( + blockers: ReadableGroup, lookup: Lookup +) self.tiles = collision.new_group() -- Calculate how far the explosion should expanded @@ -175,7 +184,9 @@ function Explosion:_update_active_tiles(blockers: ReadableGroup, loo end end - local colliding_blockers = lookup:collisions_between(collision.new_group({tile}), blockers) + local colliding_blockers = lookup:collisions_between( + collision.new_group({tile}), blockers + ) for _, blocker in pairs(colliding_blockers) do if blocker.in_explosion then return "next" @@ -213,7 +224,9 @@ function Explosion:_update_active_tiles(blockers: ReadableGroup, loo return {next_tile} end - local remaining_tiles = propagate(next_pos, delta, distance_traveled + 1, explosion_type) + local remaining_tiles = propagate( + next_pos, delta, distance_traveled + 1, explosion_type + ) local is_last = #remaining_tiles == 0 local tile = self:new_tile(next_pos, is_last, explosion_type) @@ -234,7 +247,9 @@ function Explosion:_update_active_tiles(blockers: ReadableGroup, loo -- Propagate in all four directions for direction, delta in pairs(directions) do - for _, tile in ipairs(propagate(self.position, delta:scale(grid.TILE_SIZE), 1, direction)) do + for _, tile in ipairs( + propagate(self.position, delta:scale(grid.TILE_SIZE), 1, direction) + ) do self.tiles:add(tile) end end @@ -246,10 +261,18 @@ function Explosion:get_tile_animation(tile: ExplosionTile): Animation end if tile.is_last then - if tile.type == "right" then return self.animations.right_end end - if tile.type == "left" then return self.animations.left_end end - if tile.type == "up" then return self.animations.top_end end - if tile.type == "down" then return self.animations.bottom_end end + if tile.type == "right" then + return self.animations.right_end + end + if tile.type == "left" then + return self.animations.left_end + end + if tile.type == "up" then + return self.animations.top_end + end + if tile.type == "down" then + return self.animations.bottom_end + end end if tile.type == "right" or tile.type == "left" then @@ -281,7 +304,9 @@ function Explosion:update_end_animation(dt: number) self.animations.center:update(dt) end -function Explosion:update(dt: number, blockers: ReadableGroup, lookup: Lookup) +function Explosion:update( + dt: number, blockers: ReadableGroup, lookup: Lookup +) if self.delay > 0 then self.delay = self.delay - dt return @@ -304,7 +329,9 @@ local explosions = {Explosions = Explosions} function explosions.load_assets() local function get_explosion_image(tile: string): Image - local image = lutro.graphics.newImage("assets/bomb/explosion/explosion_" .. tile .. ".png") + local image = lutro.graphics.newImage( + "assets/bomb/explosion/explosion_" .. tile .. ".png" + ) images.set_alpha(image, 175) return image end @@ -318,7 +345,9 @@ function explosions.load_assets() explosion_center_image = get_explosion_image("center") local function get_explosion_audio(size: string): Source - local explosion_audio = audio.new_source_pool("assets/bomb/explosion/" .. size .. ".wav", "static", 5) + local explosion_audio = audio.new_source_pool( + "assets/bomb/explosion/" .. size .. ".wav", "static", 5 + ) explosion_audio:setVolume(0.4) explosion_audio:setPitch(0.25) return explosion_audio @@ -330,9 +359,9 @@ function explosions.load_assets() end function explosions.new(): Explosions - local self: Explosions = setmetatable({}, { __index = Explosions }) - self.on_ground = {} - return self + local self: Explosions = setmetatable({}, {__index = Explosions}) + self.on_ground = {} + return self end function Explosions:place_explosion(position: Point, range: number, delay?: number) @@ -341,7 +370,12 @@ function Explosions:place_explosion(position: Point, range: number, delay?: numb table.insert(self.on_ground, explosion) end -function Explosions:update(dt: number, blockers: ReadableGroup, explodables: ReadableGroup, lookup: Lookup) +function Explosions:update( + dt: number, + blockers: ReadableGroup, + explodables: ReadableGroup, + lookup: Lookup +) for i = #self.on_ground, 1, -1 do local explosion = self.on_ground[i] explosion:update(dt, blockers, lookup) diff --git a/src/bombs/lightning_bombs.tl b/src/bombs/lightning_bombs.tl index 59c9a91..5f0e330 100644 --- a/src/bombs/lightning_bombs.tl +++ b/src/bombs/lightning_bombs.tl @@ -1,19 +1,19 @@ +local lightnings = require("bombs.lightnings") local animations = require("engine.animations") local audio = require("engine.audio") local collision = require("engine.collision") -local grid = require("level_layout.grid") -local hitboxes = require("engine.hitboxes") -local lightnings = require("bombs.lightnings") -local players = require("players") local points = require("engine.geometry.points") local rectangles = require("engine.geometry.rectangles") local vectors = require("engine.geometry.vectors") +local hitboxes = require("engine.hitboxes") +local grid = require("level_layout.grid") +local players = require("players") local z_index = require("z_index") local type Animation = animations.Animation local type Explodable = require("bombs.explodable") -local type Kinematic = require("kinematic") local type Entity = require("engine.entity") +local type Kinematic = require("kinematic") local type Group = collision.Group local type Lightnings = lightnings.Lightnings local type Player = players.Player @@ -27,7 +27,6 @@ local lightning_bomb_image: Image local coild_1_effect: Image local coild_2_effect: Image - local record LightningBomb is Entity, Kinematic position: Point timer: number @@ -40,7 +39,7 @@ local record LightningBomb is Entity, Kinematic end function LightningBomb.new(position: Point, hand_back: function()): LightningBomb - local self: LightningBomb = setmetatable({}, { __index = LightningBomb }) + local self: LightningBomb = setmetatable({}, {__index = LightningBomb}) self.position = position self.z_index = z_index.BELOW self.range = 6 @@ -71,7 +70,7 @@ function LightningBomb:hitbox(): Rectangle end function LightningBomb:movementbox(): Rectangle - return hitboxes.new_block(self.position) + return hitboxes.new_block(self.position) end function LightningBomb:blow_up() @@ -79,7 +78,7 @@ function LightningBomb:blow_up() end function LightningBomb:hit_with_bomb() - -- Does nothing + -- Does nothing end function LightningBomb:is_complete(): boolean @@ -130,7 +129,7 @@ function lightning_bombs.load_assets() end function lightning_bombs.new(): LightningBombs - local self: LightningBombs = setmetatable({}, { __index = LightningBombs }) + local self: LightningBombs = setmetatable({}, {__index = LightningBombs}) self.on_ground = collision.new_group() self.lightnings = lightnings.new() return self @@ -154,14 +153,21 @@ function LightningBombs:place_lightning_bomb(player: Player, occupied: {Entity}) if player.lightning_bomb == 0 then return end - - local hand_back_bomb = function() player.lightning_bomb = player.lightning_bomb + 1 end + + local hand_back_bomb = function() + player.lightning_bomb = player.lightning_bomb + 1 + end local bomb = LightningBomb.new(position, hand_back_bomb) self.on_ground:add(bomb) player.lightning_bomb = player.lightning_bomb - 1 end -function LightningBombs:update(dt: number, blockers: ReadableGroup, explodables: ReadableGroup, lookup: Lookup) +function LightningBombs:update( + dt: number, + blockers: ReadableGroup, + explodables: ReadableGroup, + lookup: Lookup +) for _, bomb in pairs(self.on_ground.set) do bomb:update(dt) @@ -175,5 +181,4 @@ function LightningBombs:update(dt: number, blockers: ReadableGroup, self.lightnings:update(dt, blockers, explodables, lookup) end - return lightning_bombs diff --git a/src/bombs/lightnings.tl b/src/bombs/lightnings.tl index 5e5dc53..3885bcb 100644 --- a/src/bombs/lightnings.tl +++ b/src/bombs/lightnings.tl @@ -1,11 +1,11 @@ local animations = require("engine.animations") local audio = require("engine.audio") local collision = require("engine.collision") -local grid = require("level_layout.grid") -local hitboxes = require("engine.hitboxes") -local players = require("players") local points = require("engine.geometry.points") local rectangles = require("engine.geometry.rectangles") +local hitboxes = require("engine.hitboxes") +local grid = require("level_layout.grid") +local players = require("players") local z_index = require("z_index") local type Animation = animations.Animation @@ -36,7 +36,7 @@ local record LightningTile is Entity end function LightningTile.new(position: Point): LightningTile - local self: LightningTile = setmetatable({}, { __index = LightningTile }) + local self: LightningTile = setmetatable({}, {__index = LightningTile}) self.position = position self.z_index = z_index.LEVEL self.reduction = 4 @@ -65,7 +65,7 @@ local record Lightning end function Lightning.new(position: Point, range: number): Lightning - local self: Lightning = setmetatable({}, { __index = Lightning }) + local self: Lightning = setmetatable({}, {__index = Lightning}) self.position = position self.range = range self.tiles = collision.new_group() @@ -73,7 +73,7 @@ function Lightning.new(position: Point, range: number): Lightning self.speed = 8 self.animations = { initial = animations.new(lightning_initial_image, 16, FRAME_DURATION / 2), - ending = animations.new(lightning_ending_image, 16, FRAME_DURATION) + ending = animations.new(lightning_ending_image, 16, FRAME_DURATION), } return self @@ -92,10 +92,12 @@ function Lightning:has_reached_max_range(): boolean end local function clamp(val: number, min: number, max: number): number - return math.min(math.max(val, min), max) + return math.min(math.max(val, min), max) end -function Lightning:_update_active_tiles(blockers: ReadableGroup, lookup: Lookup) +function Lightning:_update_active_tiles( + blockers: ReadableGroup, lookup: Lookup +) self.tiles = collision.new_group() -- Calculate how far the lightning have expanded @@ -113,10 +115,10 @@ function Lightning:_update_active_tiles(blockers: ReadableGroup, loo table.insert(manhattan_points, points.new(x - offset, y - inv_offset)) table.insert(manhattan_points, points.new(x - inv_offset, y + offset)) end - - for ring=0, radius-1 do + + for ring = 0, radius - 1 do local radius_pixels = ring * grid.TILE_SIZE - for offset=0, ring do + for offset = 0, ring do local offset_pixels = offset * grid.TILE_SIZE add_points_for(offset_pixels, radius_pixels) end @@ -130,7 +132,9 @@ function Lightning:_update_active_tiles(blockers: ReadableGroup, loo return false end - for _, _ in pairs(lookup:collisions_between(collision.new_group({tile}), blockers)) do + for _, _ in pairs( + lookup:collisions_between(collision.new_group({tile}), blockers) + ) do return false end @@ -179,7 +183,9 @@ function Lightning:draw() end end -function Lightning:update(dt: number, blockers: ReadableGroup, lookup: Lookup) +function Lightning:update( + dt: number, blockers: ReadableGroup, lookup: Lookup +) self.duration = self.duration + dt self:_update_active_tiles(blockers, lookup) self:_get_animation():update(dt) @@ -193,17 +199,23 @@ end local lightnings = {Lightnings = Lightnings} function lightnings.load_assets() - lightning_initial_image = lutro.graphics.newImage("assets/lightning-bomb/explosion/explosion_center.png") - lightning_ending_image = lutro.graphics.newImage("assets/lightning-bomb/explosion/explosion_end.png") - - lightning_sound = audio.new_source_pool("assets/lightning-bomb/explosion/detonate.ogg", "static", 5) + lightning_initial_image = lutro.graphics.newImage( + "assets/lightning-bomb/explosion/explosion_center.png" + ) + lightning_ending_image = lutro.graphics.newImage( + "assets/lightning-bomb/explosion/explosion_end.png" + ) + + lightning_sound = audio.new_source_pool( + "assets/lightning-bomb/explosion/detonate.ogg", "static", 5 + ) lightning_sound:setPitch(0.75) end function lightnings.new(): Lightnings - local self: Lightnings = setmetatable({}, { __index = Lightnings }) - self.on_ground = {} - return self + local self: Lightnings = setmetatable({}, {__index = Lightnings}) + self.on_ground = {} + return self end function Lightnings:place_explosion(position: Point, range: number) @@ -212,7 +224,12 @@ function Lightnings:place_explosion(position: Point, range: number) table.insert(self.on_ground, explosion) end -function Lightnings:update(dt: number, blockers: ReadableGroup, explodables: ReadableGroup, lookup: Lookup) +function Lightnings:update( + dt: number, + blockers: ReadableGroup, + explodables: ReadableGroup, + lookup: Lookup +) for i = #self.on_ground, 1, -1 do local explosion = self.on_ground[i] diff --git a/src/characters.tl b/src/characters.tl index bf38683..e833874 100644 --- a/src/characters.tl +++ b/src/characters.tl @@ -1,16 +1,14 @@ - - local type CharacterName = enum "kotetsu" "honey" - "kinu" + "kinu" "milon" - "black" - "white" - "kabuki" - "master_higins" - "manjimaru" - "bonks" + "black" + "white" + "kabuki" + "master_higins" + "manjimaru" + "bonks" end local ALL_SPRITES: {CharacterName} = { @@ -41,10 +39,9 @@ local character_selections: {CharacterSelection} = {} local in_queue_to_join: {integer} = {} function CharacterSelection.new( - player_id: integer, - sprite: CharacterName + player_id: integer, sprite: CharacterName ): CharacterSelection - local self: CharacterSelection = setmetatable({}, { __index = CharacterSelection }) + local self: CharacterSelection = setmetatable({}, {__index = CharacterSelection}) self.player_id = player_id self.sprite = sprite return self @@ -75,17 +72,18 @@ function characters.get_unused(): CharacterName assert(#character_selections < #ALL_SPRITES, "All sprites are selected!") for _, sprite in ipairs(characters.get_names()) do - if characters.is_unused(sprite) then - return sprite - end + if characters.is_unused(sprite) then + return sprite + end end end function characters.new_selection( - player_id: integer, - sprite?: CharacterName + player_id: integer, sprite?: CharacterName ): CharacterSelection - local new_selection = CharacterSelection.new(player_id, sprite or characters.get_unused()) + local new_selection = CharacterSelection.new( + player_id, sprite or characters.get_unused() + ) table.insert(character_selections, new_selection) return new_selection end @@ -139,12 +137,12 @@ function characters.queue_selection_for(player_id: integer) end function characters.next_player_selecting(): integer - for i, player_joining in ipairs(in_queue_to_join) do - table.remove(in_queue_to_join, i) - return player_joining - end + for i, player_joining in ipairs(in_queue_to_join) do + table.remove(in_queue_to_join, i) + return player_joining + end - return nil + return nil end return characters diff --git a/src/clock_text.tl b/src/clock_text.tl index bc5feb3..7ef81d0 100644 --- a/src/clock_text.tl +++ b/src/clock_text.tl @@ -1,25 +1,29 @@ local clock_text = {} function clock_text.from_time(time_in_sec: number): string - time_in_sec = math.max(time_in_sec, 0) - local minutes = math.floor(time_in_sec / 60) - local seconds = math.floor(time_in_sec - minutes * 60) - local microsec = math.floor((time_in_sec - seconds) * 100) - local first_num = minutes - local second_num = seconds - local delimiter = ":" + time_in_sec = math.max(time_in_sec, 0) + local minutes = math.floor(time_in_sec / 60) + local seconds = math.floor(time_in_sec - minutes * 60) + local microsec = math.floor((time_in_sec - seconds) * 100) + local first_num = minutes + local second_num = seconds + local delimiter = ":" - if minutes == 0 then - first_num = seconds - second_num = microsec - delimiter = "\"" - end + if minutes == 0 then + first_num = seconds + second_num = microsec + delimiter = "\"" + end - local function left_pad(n: integer, pad_char: string): string - if n < 10 then return pad_char .. n else return tostring(n) end - end + local function left_pad(n: integer, pad_char: string): string + if n < 10 then + return pad_char .. n + else + return tostring(n) + end + end - return left_pad(first_num, " ") .. delimiter .. left_pad(second_num, "0") + return left_pad(first_num, " ") .. delimiter .. left_pad(second_num, "0") end return clock_text diff --git a/src/dino_animations.tl b/src/dino_animations.tl index d5ee004..27248b3 100644 --- a/src/dino_animations.tl +++ b/src/dino_animations.tl @@ -1,10 +1,10 @@ -local animations = require("engine.animations") local dinos = require("dinos") -local grid = require("level_layout.grid") -local images = require("engine.images") +local animations = require("engine.animations") local points = require("engine.geometry.points") local rectangles = require("engine.geometry.rectangles") local vectors = require("engine.geometry.vectors") +local images = require("engine.images") +local grid = require("level_layout.grid") local settings = require("settings") local z_index = require("z_index") @@ -23,261 +23,281 @@ local type DinoAnimations = {Direction: {DinoAction: {integer: Animation}}} local dino_images: DinoImages local pixel_maps : {DinoColor: {Color: Color}} = { - blue = {}, -- No mapping, default dino - pink = { - [{176, 224, 248}] = {248, 192, 248}, - [{72, 176, 248}] = {248, 112, 192}, - [{64, 120, 216}] = {208, 80, 136}, - [{0, 56, 200}] = {168, 32, 72}, - }, - purple = { - [{176, 224, 248}] = {216, 216, 248}, - [{72, 176, 248}] = {176, 144, 248}, - [{64, 120, 216}] = {128, 96, 176}, - [{0, 56, 200}] = {80, 64, 128}, - }, + blue = {}, -- No mapping, default dino + pink = { + [{176, 224, 248}] = {248, 192, 248}, + [{72, 176, 248}] = {248, 112, 192}, + [{64, 120, 216}] = {208, 80, 136}, + [{0, 56, 200}] = {168, 32, 72}, + }, + purple = { + [{176, 224, 248}] = {216, 216, 248}, + [{72, 176, 248}] = {176, 144, 248}, + [{64, 120, 216}] = {128, 96, 176}, + [{0, 56, 200}] = {80, 64, 128}, + }, green = { - [{176, 224, 248}] = {112, 248, 168}, - [{72, 176, 248}] = {24, 208, 40}, - [{64, 120, 216}] = {24, 152, 24}, - [{0, 56, 200}] = {0, 104, 16}, - }, + [{176, 224, 248}] = {112, 248, 168}, + [{72, 176, 248}] = {24, 208, 40}, + [{64, 120, 216}] = {24, 152, 24}, + [{0, 56, 200}] = {0, 104, 16}, + }, yellow = { - [{176, 224, 248}] = {248, 248, 200}, - [{72, 176, 248}] = {232, 232, 104}, - [{64, 120, 216}] = {224, 184, 80}, - [{0, 56, 200}] = {192, 136, 8}, - }, + [{176, 224, 248}] = {248, 248, 200}, + [{72, 176, 248}] = {232, 232, 104}, + [{64, 120, 216}] = {224, 184, 80}, + [{0, 56, 200}] = {192, 136, 8}, + }, } -local function dino_iamge(level: integer, action: DinoAction, direction: Direction, color: DinoColor): Image - local png_name = action .. "_" .. direction - if action == "crashed" then - level = 1 -- Only possible to crash with level 1 dino - end - if action == "slide" and level == 1 then - level = 2 -- Not possible to slide with level 1 dino - end - - if action == "dying" or action == "stunned" then - png_name = action - end - - local image = lutro.graphics.newImage("assets/tirras/level_" .. level .. "/" .. png_name .. ".png") - images.change_colors(image, pixel_maps[color]) - return image +local function dino_iamge( + level: integer, action: DinoAction, direction: Direction, color: DinoColor +): Image + local png_name = action .. "_" .. direction + if action == "crashed" then + level = 1 -- Only possible to crash with level 1 dino + end + if action == "slide" and level == 1 then + level = 2 -- Not possible to slide with level 1 dino + end + + if action == "dying" or action == "stunned" then + png_name = action + end + + local image = lutro.graphics.newImage( + "assets/tirras/level_" .. level .. "/" .. png_name .. ".png" + ) + images.change_colors(image, pixel_maps[color]) + return image end - -local function dino_images_for_level(action: DinoAction, direction: Direction, color: DinoColor): {integer: Image} - return { - [1] = dino_iamge(1, action, direction, color), - [2] = dino_iamge(2, action, direction, color), - [3] = dino_iamge(3, action, direction, color), - } +local function dino_images_for_level( + action: DinoAction, direction: Direction, color: DinoColor +): {integer: Image} + return { + [1] = dino_iamge(1, action, direction, color), + [2] = dino_iamge(2, action, direction, color), + [3] = dino_iamge(3, action, direction, color), + } end -local function dino_images_for_dir(direction: Direction, color: DinoColor): DinoImageByActionAndLevel - local dino_animations : DinoImageByActionAndLevel = { - walk = dino_images_for_level("walk", direction, color), - idle = dino_images_for_level("idle", direction, color), - dying = dino_images_for_level("dying", direction, color), - stunned = dino_images_for_level("stunned", direction, color), - crashed = dino_images_for_level("crashed", direction, color), - slide = dino_images_for_level("slide", direction, color), - jump = dino_images_for_level("jump", direction, color), - dash = dino_images_for_level("dash", direction, color), - roar = dino_images_for_level("roar", direction, color) - } - return dino_animations +local function dino_images_for_dir( + direction: Direction, color: DinoColor +): DinoImageByActionAndLevel + local dino_animations : DinoImageByActionAndLevel = { + walk = dino_images_for_level("walk", direction, color), + idle = dino_images_for_level("idle", direction, color), + dying = dino_images_for_level("dying", direction, color), + stunned = dino_images_for_level("stunned", direction, color), + crashed = dino_images_for_level("crashed", direction, color), + slide = dino_images_for_level("slide", direction, color), + jump = dino_images_for_level("jump", direction, color), + dash = dino_images_for_level("dash", direction, color), + roar = dino_images_for_level("roar", direction, color), + } + return dino_animations end -local function dino_images_for_color(color: DinoColor): {Direction : {DinoAction : {integer : Image}}} - return { - up = dino_images_for_dir("up", color), - down = dino_images_for_dir("down", color), - right = dino_images_for_dir("right", color), - left = dino_images_for_dir("left", color), - } +local function dino_images_for_color( + color: DinoColor +): {Direction: {DinoAction: {integer: Image}}} + return { + up = dino_images_for_dir("up", color), + down = dino_images_for_dir("down", color), + right = dino_images_for_dir("right", color), + left = dino_images_for_dir("left", color), + } end local function get_dino_images(): DinoImages - local dino_images_total : DinoImages = { - blue = dino_images_for_color("blue"), - pink = dino_images_for_color("pink"), - purple = dino_images_for_color("purple"), - green = dino_images_for_color("green"), - yellow = dino_images_for_color("yellow"), - } - return dino_images_total + local dino_images_total : DinoImages = { + blue = dino_images_for_color("blue"), + pink = dino_images_for_color("pink"), + purple = dino_images_for_color("purple"), + green = dino_images_for_color("green"), + yellow = dino_images_for_color("yellow"), + } + return dino_images_total end local record AnimatedDino dino: Dino - finished: boolean - previous_action: DinoAction - animations: DinoAnimations + finished: boolean + previous_action: DinoAction + animations: DinoAnimations end local function get_dash_fames(level: integer): {number} - if level == 3 then - return {0.1, 0.1} - else - return {0.1, 0.1, 0.1} - end + if level == 3 then + return {0.1, 0.1} + else + return {0.1, 0.1, 0.1} + end end local function new_dino_animations(color: DinoColor): DinoAnimations - local new_animations: DinoAnimations = {} - - local function create_animations(image: Image, action: DinoAction, level: integer): Animation - if action == "jump" then - return animations.new_with_frames(image, {0.1, 10000}) - elseif action == "crashed" then - return animations.new_with_frames(image, {0.2, 0.4, 0.2, 0.2, 10000}) - elseif action == "slide" then - return animations.new_with_frames(image, {0.1, 10000}) - elseif action == "roar" then - return animations.new_with_frames(image, {dinos.ROAR_BUILDUP_TIME, 10000}) - elseif action == "dash" then - return animations.new_with_frames(image, get_dash_fames(level)) - else - return animations.new(image, 34, 1 / 6) - end - end - - for direction, image_by_level_by_action in pairs(dino_images[color]) do - new_animations[direction] = {} - for action, image_by_level in pairs(image_by_level_by_action) do - new_animations[direction][action] = {} - for level, image in pairs(image_by_level) do - new_animations[direction][action][level] = create_animations(image, action, level) - end - end - end - - return new_animations + local new_animations: DinoAnimations = {} + + local function create_animations( + image: Image, action: DinoAction, level: integer + ): Animation + if action == "jump" then + return animations.new_with_frames(image, {0.1, 10000}) + elseif action == "crashed" then + return animations.new_with_frames(image, {0.2, 0.4, 0.2, 0.2, 10000}) + elseif action == "slide" then + return animations.new_with_frames(image, {0.1, 10000}) + elseif action == "roar" then + return animations.new_with_frames(image, {dinos.ROAR_BUILDUP_TIME, 10000}) + elseif action == "dash" then + return animations.new_with_frames(image, get_dash_fames(level)) + else + return animations.new(image, 34, 1 / 6) + end + end + + for direction, image_by_level_by_action in pairs(dino_images[color]) do + new_animations[direction] = {} + for action, image_by_level in pairs(image_by_level_by_action) do + new_animations[direction][action] = {} + for level, image in pairs(image_by_level) do + new_animations[direction][action][level] = create_animations( + image, action, level + ) + end + end + end + + return new_animations end function AnimatedDino.new(dino: Dino): AnimatedDino - local self: AnimatedDino = setmetatable({}, { __index = AnimatedDino }) - self.dino = dino - self.finished = false - self.previous_action = "idle" - self.animations = new_dino_animations(dino.color) - return self + local self: AnimatedDino = setmetatable({}, {__index = AnimatedDino}) + self.dino = dino + self.finished = false + self.previous_action = "idle" + self.animations = new_dino_animations(dino.color) + return self end function AnimatedDino:get_animation(): Animation - return self.animations[self.dino.direction][self.dino.action][self.dino.level] + return self.animations[self.dino.direction][self.dino.action][self.dino.level] end function AnimatedDino:update(dt: number) - local animation = self:get_animation() + local animation = self:get_animation() - if self.previous_action ~= self.dino.action then - animation:reset() - end + if self.previous_action ~= self.dino.action then + animation:reset() + end - animation:update(dt) - self.previous_action = self.dino.action + animation:update(dt) + self.previous_action = self.dino.action - if self.dino.action == "dying" and animation.loops > 0 then - self.finished = true - end + if self.dino.action == "dying" and animation.loops > 0 then + self.finished = true + end end local function block_to_anim_draw_pos(animation: Animation, pos: Point): Point - -- y: +1 is because of the sprite, look over when doing animations - return points.new( - pos.x + (grid.TILE_SIZE - animation.width) / 2, - pos.y - animation.height + grid.TILE_SIZE + 1) + -- y: +1 is because of the sprite, look over when doing animations + return points.new( + pos.x + (grid.TILE_SIZE - animation.width) / 2, + pos.y - animation.height + grid.TILE_SIZE + 1 + ) end local record MountAnimation is Entity - dino: Dino - animated_dino: AnimatedDino + dino: Dino + animated_dino: AnimatedDino end function MountAnimation.new(animated_dino: AnimatedDino, dino: Dino): MountAnimation - local self: MountAnimation = setmetatable({}, { __index = MountAnimation }) - self.z_index = z_index.LEVEL - self.dino = dino - self.animated_dino = animated_dino - return self + local self: MountAnimation = setmetatable({}, {__index = MountAnimation}) + self.z_index = z_index.LEVEL + self.dino = dino + self.animated_dino = animated_dino + return self end function MountAnimation:get_position(): Point - return self.dino.position + return self.dino.position end function MountAnimation:hitbox(): Rectangle - return self.dino:hitbox() + return self.dino:hitbox() end function MountAnimation:draw() - local animation = self.animated_dino:get_animation() - local draw_pos = block_to_anim_draw_pos(animation, self.dino.position) - animation:draw(draw_pos.x, draw_pos.y) + local animation = self.animated_dino:get_animation() + local draw_pos = block_to_anim_draw_pos(animation, self.dino.position) + animation:draw(draw_pos.x, draw_pos.y) end local record AnimatedDinos - mount_entity_by_dino: {Dino: MountAnimation} + mount_entity_by_dino: {Dino: MountAnimation} end -local dino_animations = {AnimatedDinos = AnimatedDinos, AnimatedDino = AnimatedDino, MountAnimation = MountAnimation} +local dino_animations = { + AnimatedDinos = AnimatedDinos, + AnimatedDino = AnimatedDino, + MountAnimation = MountAnimation, +} function dino_animations.load_assets() - dino_images = get_dino_images() + dino_images = get_dino_images() end function dino_animations.new(): AnimatedDinos - local self: AnimatedDinos = setmetatable({}, { __index = AnimatedDinos }) - self.mount_entity_by_dino = {} - return self + local self: AnimatedDinos = setmetatable({}, {__index = AnimatedDinos}) + self.mount_entity_by_dino = {} + return self end function AnimatedDinos:update(dt: number) - for dino, mount in pairs(self.mount_entity_by_dino) do - mount.animated_dino:update(dt) - if mount.animated_dino.finished then - self.mount_entity_by_dino[dino] = nil - end - end + for dino, mount in pairs(self.mount_entity_by_dino) do + mount.animated_dino:update(dt) + if mount.animated_dino.finished then + self.mount_entity_by_dino[dino] = nil + end + end end function AnimatedDinos:add_for(dino: Dino) - if dino.level == 0 then - return -- Dino was consumed into existing dino - end + if dino.level == 0 then + return -- Dino was consumed into existing dino + end - local animated = AnimatedDino.new(dino) - self.mount_entity_by_dino[dino] = MountAnimation.new(animated, dino) + local animated = AnimatedDino.new(dino) + self.mount_entity_by_dino[dino] = MountAnimation.new(animated, dino) end function AnimatedDinos:get_from(dino: Dino): Animation - local mount = self.mount_entity_by_dino[dino] - if mount then - return mount.animated_dino:get_animation() - end + local mount = self.mount_entity_by_dino[dino] + if mount then + return mount.animated_dino:get_animation() + end end -function AnimatedDinos:get_mount_entities_except(exclude: {Dino}): {Dino: MountAnimation} - local to_return: {Dino: MountAnimation} = {} - for dino, mount in pairs(self.mount_entity_by_dino) do - local is_excluded = false - for _, excluded_dino in ipairs(exclude) do - if excluded_dino == dino then - is_excluded = true - end - end - - if not is_excluded and not dino.hatching then - to_return[dino] = mount - end - end - - return to_return +function AnimatedDinos:get_mount_entities_except( + exclude: {Dino} +): {Dino: MountAnimation} + local to_return: {Dino: MountAnimation} = {} + for dino, mount in pairs(self.mount_entity_by_dino) do + local is_excluded = false + for _, excluded_dino in ipairs(exclude) do + if excluded_dino == dino then + is_excluded = true + end + end + + if not is_excluded and not dino.hatching then + to_return[dino] = mount + end + end + + return to_return end return dino_animations diff --git a/src/dino_inputs.tl b/src/dino_inputs.tl index 0aedbca..73083d8 100644 --- a/src/dino_inputs.tl +++ b/src/dino_inputs.tl @@ -1,11 +1,11 @@ local bombs = require("bombs.bombs") local collision = require("engine.collision") +local points = require("engine.geometry.points") +local vectors = require("engine.geometry.vectors") local hitboxes = require("engine.hitboxes") -local grid = require("level_layout.grid") local joysticks = require("engine.joysticks") +local grid = require("level_layout.grid") local players = require("players") -local vectors = require("engine.geometry.vectors") -local points = require("engine.geometry.points") local type Bomb = bombs.Bomb local type Bombs = bombs.Bombs @@ -18,84 +18,85 @@ local type Point = points.Point local dino_inputs = {} -local function handle_jump_input(dt: number, player: Player, blocks_jump: ReadableGroup, lookup: Lookup) - local function jump_is_possible(jump_to: Point): boolean - local jump_landing_area = hitboxes.new_block(jump_to) - - -- Collision is done with hitboxes but here we care about - -- movementboxes, check in an area around and then manually - -- check for movementbox overlap - local blocks_jumps_close = lookup:in_box_and_group( - jump_landing_area:expand(grid.TILE_SIZE), blocks_jump - ) - - for _, possible_jump_block in ipairs(blocks_jumps_close) do - if jump_landing_area:overlap(possible_jump_block:movementbox()) then - return false - end - end - - -- Finally check that jump area is contained in play area - return grid.area():contains(jump_landing_area) - end - - local function try_all_jumps_for(jump_direction: Vector): Point | nil - -- Try to jump as far as possible first - for nbr_of_blocks_to_jump=player:get_max_jump_dist(), 1, -1 do - local jump_to = player.position:move( - jump_direction:scale(grid.TILE_SIZE * nbr_of_blocks_to_jump) - ) - local jump_candidates = { - jump_to, grid.snap_to(jump_to) - } - - for _, jump_candiated in ipairs(jump_candidates) do - if jump_is_possible(jump_candiated) then - return jump_candiated - end - end - end - - return nil - end - - local function get_jump_directions(): {Vector} - local result: {Vector} = {} - - if lutro.joystick.isDown(player.id, joysticks.BUTTONS.DOWN) then - table.insert(result, vectors.new(0, 1)) - end - if lutro.joystick.isDown(player.id, joysticks.BUTTONS.UP) then - table.insert(result, vectors.new(0, -1)) - end - if lutro.joystick.isDown(player.id, joysticks.BUTTONS.RIGHT) then - table.insert(result, vectors.new(1, 0)) - end - if lutro.joystick.isDown(player.id, joysticks.BUTTONS.LEFT) then - table.insert(result, vectors.new(-1, 0)) - end - - return result - end - - local function get_jump_target(): Point | nil - for _, jump_direction in ipairs(get_jump_directions()) do - local jump_to = try_all_jumps_for(jump_direction) - if jump_to then - return jump_to - end - end - end - - if lutro.joystick.isDown(player.id, joysticks.BUTTONS.B) and player:can_jump() then - local dino = player.dino - dino.jump_powerup_for = dino.jump_powerup_for + dt - end - - if (joysticks.is_released(player.id, joysticks.BUTTONS.B) and player:can_jump()) or player:must_jump() then - local jump_to = get_jump_target() - player:jump_with_dino(jump_to) - end +local function handle_jump_input( + dt: number, player: Player, blocks_jump: ReadableGroup, lookup: Lookup +) + local function jump_is_possible(jump_to: Point): boolean + local jump_landing_area = hitboxes.new_block(jump_to) + + -- Collision is done with hitboxes but here we care about + -- movementboxes, check in an area around and then manually + -- check for movementbox overlap + local blocks_jumps_close = lookup:in_box_and_group( + jump_landing_area:expand(grid.TILE_SIZE), blocks_jump + ) + + for _, possible_jump_block in ipairs(blocks_jumps_close) do + if jump_landing_area:overlap(possible_jump_block:movementbox()) then + return false + end + end + + -- Finally check that jump area is contained in play area + return grid.area():contains(jump_landing_area) + end + + local function try_all_jumps_for(jump_direction: Vector): Point | nil + -- Try to jump as far as possible first + for nbr_of_blocks_to_jump = player:get_max_jump_dist(), 1, -1 do + local jump_to = player.position:move( + jump_direction:scale(grid.TILE_SIZE * nbr_of_blocks_to_jump) + ) + local jump_candidates = {jump_to, grid.snap_to(jump_to)} + + for _, jump_candiated in ipairs(jump_candidates) do + if jump_is_possible(jump_candiated) then + return jump_candiated + end + end + end + + return nil + end + + local function get_jump_directions(): {Vector} + local result: {Vector} = {} + + if lutro.joystick.isDown(player.id, joysticks.BUTTONS.DOWN) then + table.insert(result, vectors.new(0, 1)) + end + if lutro.joystick.isDown(player.id, joysticks.BUTTONS.UP) then + table.insert(result, vectors.new(0, -1)) + end + if lutro.joystick.isDown(player.id, joysticks.BUTTONS.RIGHT) then + table.insert(result, vectors.new(1, 0)) + end + if lutro.joystick.isDown(player.id, joysticks.BUTTONS.LEFT) then + table.insert(result, vectors.new(-1, 0)) + end + + return result + end + + local function get_jump_target(): Point | nil + for _, jump_direction in ipairs(get_jump_directions()) do + local jump_to = try_all_jumps_for(jump_direction) + if jump_to then + return jump_to + end + end + end + + if lutro.joystick.isDown(player.id, joysticks.BUTTONS.B) and player:can_jump() then + local dino = player.dino + dino.jump_powerup_for = dino.jump_powerup_for + dt + end + + if (joysticks.is_released(player.id, joysticks.BUTTONS.B) and player:can_jump()) + or player:must_jump() then + local jump_to = get_jump_target() + player:jump_with_dino(jump_to) + end end local function handle_dash_input(player: Player) @@ -104,79 +105,100 @@ local function handle_dash_input(player: Player) end end -local function handle_throw_input(dt: number, player: Player, bombs_: Bombs, bombs_group: ReadableGroup, lookup: Lookup) - local dino = player.dino - local powerup_time_interval = 0.1 - local block_from_player_dir = vectors.from_direction(player.direction):scale(grid.TILE_SIZE) - - local function get_position_throwing_from(): Point - local snaped_pos = grid.snap_to(player.position) - return snaped_pos:move(block_from_player_dir) - end - - local function must_throw(): boolean - return dino.throw_powerup_for > (dino.level * 2 - 1) * powerup_time_interval - end - - local function get_bomb_to_throw(throw_bomb_from: Point): Bomb | nil - local area_bomb_can_be_in = hitboxes.new_block(throw_bomb_from) - local bombs_to_throw = lookup:in_box_and_group(area_bomb_can_be_in, bombs_group) - for _, bomb_to_throw in ipairs(bombs_to_throw) do - if bomb_to_throw.velocity:length() == 0 then - return bomb_to_throw - end - end - - return nil - end - - if joysticks.is_released(player.id, joysticks.BUTTONS.B) or must_throw() then - local nbr_blocks_to_throw = math.floor(dino.throw_powerup_for / powerup_time_interval) + 2 - nbr_blocks_to_throw = math.min(nbr_blocks_to_throw, dino.level * 2) -- clamp max throw dist - - local throw_bomb_from = get_position_throwing_from() - local bomb_to_throw = get_bomb_to_throw(throw_bomb_from) - if not bomb_to_throw is nil then - local throw_to = throw_bomb_from:move(block_from_player_dir:scale(nbr_blocks_to_throw)) - bombs_:throw_existing(bomb_to_throw, throw_to) - end - dino.throw_powerup_for = 0 - elseif lutro.joystick.isDown(player.id, joysticks.BUTTONS.B) then - local bomb_to_throw = get_bomb_to_throw(get_position_throwing_from()) - if not bomb_to_throw is nil then - dino.throw_powerup_for = dino.throw_powerup_for + dt - end - else - dino.throw_powerup_for = 0 - end +local function handle_throw_input( + dt: number, + player: Player, + bombs_: Bombs, + bombs_group: ReadableGroup, + lookup: Lookup +) + local dino = player.dino + local powerup_time_interval = 0.1 + local block_from_player_dir = vectors.from_direction(player.direction):scale( + grid.TILE_SIZE + ) + + local function get_position_throwing_from(): Point + local snaped_pos = grid.snap_to(player.position) + return snaped_pos:move(block_from_player_dir) + end + + local function must_throw(): boolean + return dino.throw_powerup_for > (dino.level * 2 - 1) * powerup_time_interval + end + + local function get_bomb_to_throw(throw_bomb_from: Point): Bomb | nil + local area_bomb_can_be_in = hitboxes.new_block(throw_bomb_from) + local bombs_to_throw = lookup:in_box_and_group(area_bomb_can_be_in, bombs_group) + for _, bomb_to_throw in ipairs(bombs_to_throw) do + if bomb_to_throw.velocity:length() == 0 then + return bomb_to_throw + end + end + + return nil + end + + if joysticks.is_released(player.id, joysticks.BUTTONS.B) or must_throw() then + local nbr_blocks_to_throw = math.floor( + dino.throw_powerup_for / powerup_time_interval + ) + 2 + nbr_blocks_to_throw = math.min( + nbr_blocks_to_throw, dino.level * 2 + ) -- clamp max throw dist + + local throw_bomb_from = get_position_throwing_from() + local bomb_to_throw = get_bomb_to_throw(throw_bomb_from) + if not (bomb_to_throw is nil) then + local throw_to = throw_bomb_from:move( + block_from_player_dir:scale(nbr_blocks_to_throw) + ) + bombs_:throw_existing(bomb_to_throw, throw_to) + end + dino.throw_powerup_for = 0 + elseif lutro.joystick.isDown(player.id, joysticks.BUTTONS.B) then + local bomb_to_throw = get_bomb_to_throw(get_position_throwing_from()) + if not (bomb_to_throw is nil) then + dino.throw_powerup_for = dino.throw_powerup_for + dt + end + else + dino.throw_powerup_for = 0 + end end local function handle_stun_cry_input(player: Player) - if lutro.joystick.isDown(player.id, joysticks.BUTTONS.B) then - player:stun_cry_with_dino() - end + if lutro.joystick.isDown(player.id, joysticks.BUTTONS.B) then + player:stun_cry_with_dino() + end end local function handle_sonic_blast_input(player: Player) - if lutro.joystick.isDown(player.id, joysticks.BUTTONS.B) then - player:sonic_blast_with_dino() - end + if lutro.joystick.isDown(player.id, joysticks.BUTTONS.B) then + player:sonic_blast_with_dino() + end end -function dino_inputs.handle(dt: number, player: Player, bombs_: Bombs, blocks_jump: ReadableGroup, bombs_group: ReadableGroup, lookup: Lookup) +function dino_inputs.handle( + dt: number, + player: Player, + bombs_: Bombs, + blocks_jump: ReadableGroup, + bombs_group: ReadableGroup, + lookup: Lookup +) if player.dino then - if player.dino.color == "pink" then - handle_jump_input(dt, player, blocks_jump, lookup) - elseif player.dino.color == "green" then - handle_dash_input(player) - elseif player.dino.color == "blue" then - handle_throw_input(dt, player, bombs_, bombs_group, lookup) - elseif player.dino.color == "yellow" then - handle_stun_cry_input(player) - elseif player.dino.color == "purple" then - handle_sonic_blast_input(player) - end - end + if player.dino.color == "pink" then + handle_jump_input(dt, player, blocks_jump, lookup) + elseif player.dino.color == "green" then + handle_dash_input(player) + elseif player.dino.color == "blue" then + handle_throw_input(dt, player, bombs_, bombs_group, lookup) + elseif player.dino.color == "yellow" then + handle_stun_cry_input(player) + elseif player.dino.color == "purple" then + handle_sonic_blast_input(player) + end + end end return dino_inputs diff --git a/src/dinos.tl b/src/dinos.tl index eaa98f3..0820259 100644 --- a/src/dinos.tl +++ b/src/dinos.tl @@ -1,7 +1,7 @@ -local hitboxes = require("engine.hitboxes") -local vectors = require("engine.geometry.vectors") local points = require("engine.geometry.points") local rectangles = require("engine.geometry.rectangles") +local vectors = require("engine.geometry.vectors") +local hitboxes = require("engine.hitboxes") local settings = require("settings") local type Direction = vectors.Direction @@ -33,23 +33,21 @@ end local ROAR_BUILDUP_TIME = 0.4 local dinos = { - ROAR_BUILDUP_TIME = ROAR_BUILDUP_TIME, - DinoAction = DinoAction, - Dino = Dino + ROAR_BUILDUP_TIME = ROAR_BUILDUP_TIME, DinoAction = DinoAction, Dino = Dino } function dinos.new(position: Point, direction: Direction): Dino - local self: Dino = setmetatable({}, { __index = Dino }) - self.position = position - self.direction = direction - self.action = "idle" - local dino_choices = settings.get_enabled_dinos() - self.color = dino_choices[lutro.math.random(#dino_choices)] - self.level = 1 - self.hatching = true - self.jump_powerup_for = 0 - self.throw_powerup_for = 0 - return self + local self: Dino = setmetatable({}, {__index = Dino}) + self.position = position + self.direction = direction + self.action = "idle" + local dino_choices = settings.get_enabled_dinos() + self.color = dino_choices[lutro.math.random(#dino_choices)] + self.level = 1 + self.hatching = true + self.jump_powerup_for = 0 + self.throw_powerup_for = 0 + return self end function Dino:level_up() diff --git a/src/eggs.tl b/src/eggs.tl index 346b181..fc7cb1a 100644 --- a/src/eggs.tl +++ b/src/eggs.tl @@ -1,11 +1,11 @@ +local dinos = require("dinos") local animations = require("engine.animations") local collision = require("engine.collision") -local dinos = require("dinos") -local grid = require("level_layout.grid") -local hitboxes = require("engine.hitboxes") -local players = require("players") local points = require("engine.geometry.points") local rectangles = require("engine.geometry.rectangles") +local hitboxes = require("engine.hitboxes") +local grid = require("level_layout.grid") +local players = require("players") local z_index = require("z_index") local type Animation = animations.Animation @@ -30,7 +30,7 @@ local record Egg is Entity, Explodable end function Egg.new(position: Point): Egg - local self: Egg = setmetatable({}, { __index = Egg }) + local self: Egg = setmetatable({}, {__index = Egg}) self.position = position self.z_index = z_index.LEVEL self.breaking = false @@ -46,7 +46,7 @@ function Egg:get_position(): Point end function Egg:get_position_to_draw_egg(): Point - return points.new( + return points.new( self.position.x + (grid.TILE_SIZE - self.animation.width) / 2, self.position.y + grid.TILE_SIZE - self.animation.height ) @@ -63,7 +63,7 @@ function Egg:blow_up() end function Egg:hitbox(): Rectangle - return hitboxes.new_block(self.position):shrink(4) + return hitboxes.new_block(self.position):shrink(4) end function Egg:draw() @@ -72,7 +72,7 @@ function Egg:draw() end local draw_pos = self:get_position_to_draw_egg() - self.animation:draw(draw_pos.x, draw_pos.y) + self.animation:draw(draw_pos.x, draw_pos.y) end function Egg:update(dt: number) @@ -94,9 +94,9 @@ end local eggs = {Eggs = Eggs} function eggs.new(): Eggs - local self: Eggs = setmetatable({}, { __index = Eggs }) - self.on_ground = collision.new_group() - return self + local self: Eggs = setmetatable({}, {__index = Eggs}) + self.on_ground = collision.new_group() + return self end function Eggs:update(dt: number) @@ -112,14 +112,13 @@ function Eggs:spawn_at(position: Point) self.on_ground:add(Egg.new(position)) end - function Eggs:possible_hatcher(player: Player): Dino for _, egg in pairs(self.on_ground.set) do if player:hitbox():overlap(egg:hitbox()) then - if not egg.breaking and not egg.is_exploding then - egg:open() + if not egg.breaking and not egg.is_exploding then + egg:open() return dinos.new(egg.position, player.direction) - end + end end end diff --git a/src/engine/animations.tl b/src/engine/animations.tl index e6394cf..814677b 100644 --- a/src/engine/animations.tl +++ b/src/engine/animations.tl @@ -2,11 +2,10 @@ -- from https://lutro.libretro.com/doc/usefullibs.html local logging = require("engine.logger") - local record Animation - image: Image + image: Image timer: number - width: number + width: number height: number playing: boolean frame_durations: {number} @@ -18,7 +17,7 @@ end local animations = {Animation = Animation} function animations.new_with_frames(image: Image, frame_durations: {number}): Animation - local self: Animation = setmetatable({}, { __index = Animation }) + local self: Animation = setmetatable({}, {__index = Animation}) self.image = image self.timer = 0 self.width = image:getWidth() / #frame_durations @@ -36,11 +35,13 @@ function animations.new_with_frames(image: Image, frame_durations: {number}): An return self end -function animations.new(image: Image, width: number, frame_duration: number, idle_time?: number): Animation +function animations.new( + image: Image, width: number, frame_duration: number, idle_time?: number +): Animation local steps = image:getWidth() / width - + local frame_durations: {number} = {} - for _=1, steps do + for _ = 1, steps do table.insert(frame_durations, frame_duration) end frame_durations[1] = frame_durations[1] + (idle_time or 0) @@ -58,7 +59,7 @@ function Animation:scale_frame_durations(scale: number) for i, frame_duration in ipairs(self.frame_durations) do self.frame_durations[i] = frame_duration * scale self.total_time = self.total_time + self.frame_durations[i] - end + end end function Animation:_get_frame(): integer @@ -70,7 +71,7 @@ function Animation:_get_frame(): integer clamped_time = clamped_time - frame_duration end - logging.warning("Could not find frame for time, using last frame") + logging.warning("Could not find frame for time, using last frame") return #self.frame_durations end @@ -93,17 +94,9 @@ function Animation:draw(x: number, y: number) local sw = self.image:getWidth() local sh = self.image:getHeight() - local q = lutro.graphics.newQuad( - ((id-1)%(sw/tw))*tw, - 0, - tw, th, - sw, sh) - - lutro.graphics.draw( - self.image, - q, - x, y - ) + local q = lutro.graphics.newQuad(((id - 1) % (sw / tw)) * tw, 0, tw, th, sw, sh) + + lutro.graphics.draw(self.image, q, x, y) end return animations diff --git a/src/engine/audio.tl b/src/engine/audio.tl index f7cada9..ef1b3a9 100644 --- a/src/engine/audio.tl +++ b/src/engine/audio.tl @@ -2,13 +2,13 @@ local audio = {} local loaded_audio_sources: {Source} = {} local record SourcePool is Source - delegates: {Source} + delegates: {Source} end function SourcePool.new(delegates: {Source}): SourcePool - local self: SourcePool = setmetatable({}, { __index = SourcePool }) - self.delegates = delegates - return self + local self: SourcePool = setmetatable({}, {__index = SourcePool}) + self.delegates = delegates + return self end function SourcePool:setVolume(volume: number) @@ -73,9 +73,11 @@ end -- For audio that can be played many times at same time. -- Should only be used for short audio as memory consumption could be very hight otherwise. -function audio.new_source_pool(filename: string, source_type: SourceType, pool_size: integer): Source +function audio.new_source_pool( + filename: string, source_type: SourceType, pool_size: integer +): Source local in_pool: {Source} = {} - for _=1, pool_size do + for _ = 1, pool_size do local new_source = lutro.audio.newSource(filename, source_type) table.insert(in_pool, new_source) table.insert(loaded_audio_sources, new_source) diff --git a/src/engine/collision.tl b/src/engine/collision.tl index 0d84595..f479c5b 100644 --- a/src/engine/collision.tl +++ b/src/engine/collision.tl @@ -1,5 +1,5 @@ -local logger = require("engine.logger") local rectangles = require("engine.geometry.rectangles") +local logger = require("engine.logger") local settings = require("engine.settings") local type Entity = require("engine.entity") @@ -13,8 +13,12 @@ end local function get_indexes(hitbox: Rectangle): (integer, integer, integer, integer) local block_size = settings.block_size - local x_min, y_min = math.ceil(hitbox.x / block_size), math.ceil(hitbox.y / block_size) - local x_max, y_max = math.ceil((hitbox.x + hitbox.width) / block_size), math.ceil((hitbox.y + hitbox.height) / block_size) + local x_min, y_min = math.ceil(hitbox.x / block_size), math.ceil( + hitbox.y / block_size + ) + local x_max, y_max = math.ceil((hitbox.x + hitbox.width) / block_size), math.ceil( + (hitbox.y + hitbox.height) / block_size + ) if y_max >= LIMIT_Y_TILES then logger.warning("Tile out of bound, collision checking will not work") @@ -40,13 +44,17 @@ local record CombinedGroup is ReadableGroup sub_groups: {ReadableGroup} end -local collision = {Group = Group, Lookup = Lookup, CombinedGroup = CombinedGroup, ReadableGroup = ReadableGroup} +local collision = { + Group = Group, + Lookup = Lookup, + CombinedGroup = CombinedGroup, + ReadableGroup = ReadableGroup, +} function Group:get(object: any): T | nil return self.set[object] end - function Group:is_empty(): boolean return next(self.set) == nil end @@ -60,7 +68,9 @@ function Group:remove(entity: T) end function Group:rebuild(list: {T}) - for k in pairs(self.set) do self.set[k] = nil end + for k in pairs(self.set) do + self.set[k] = nil + end for _, entity in ipairs(list) do self.set[entity] = entity end @@ -86,7 +96,7 @@ function CombinedGroup:is_empty(): boolean end function collision.new_group(...: {T}): Group - local self: Group = setmetatable({}, { __index = Group }) + local self: Group = setmetatable({}, {__index = Group}) self.set = {} for _, entity_list in ipairs({...}) do @@ -98,7 +108,7 @@ function collision.new_group(...: {T}): Group end function collision.new_group_from_set(set: {any: T}): Group - local self: Group = setmetatable({}, { __index = Group }) + local self: Group = setmetatable({}, {__index = Group}) self.set = set return self @@ -108,7 +118,7 @@ end -- membership changes made by the owning code (add/remove on the source Group) are -- immediately visible through this view without any rebuild step. function collision.new_combined_group(...: {any: T}): CombinedGroup - local self: CombinedGroup = setmetatable({}, { __index = CombinedGroup }) + local self: CombinedGroup = setmetatable({}, {__index = CombinedGroup}) self.sub_groups = {} for _, set in ipairs({...}) do @@ -118,7 +128,7 @@ function collision.new_combined_group(...: {any: T}): CombinedGroup end function collision.new_lookup(...: {any: Entity}): Lookup - local self: Lookup = setmetatable({}, { __index = Lookup }) + local self: Lookup = setmetatable({}, {__index = Lookup}) self.map = {} for _, entity_set in ipairs({...}) do @@ -138,21 +148,24 @@ function collision.new_lookup(...: {any: Entity}): Lookup return self end -function Lookup:collisions_between(entities: Group, group: ReadableGroup): {K: T} +function Lookup:collisions_between( + entities: Group, group: ReadableGroup +): {K: T} local collides_with: {K: T} = {} if group:is_empty() then return collides_with end for _, entity in pairs(entities.set) do - local x_min, x_max, y_min, y_max = get_indexes(entity:hitbox()) + local x_min, x_max, y_min, y_max = get_indexes(entity:hitbox()) for i = x_min, x_max do for j = y_min, y_max do local index = index_2d_to_1d(i, j) for _, check_against in ipairs(self.map[index] or {}) do - if entity ~= check_against and entity:hitbox():overlap(check_against:hitbox()) then + if entity ~= check_against + and entity:hitbox():overlap(check_against:hitbox()) then local in_group = group:get(check_against) - if not in_group is nil then + if not (in_group is nil) then collides_with[entity] = in_group end end diff --git a/src/engine/geometry/points.tl b/src/engine/geometry/points.tl index 10922ea..dea85fb 100644 --- a/src/engine/geometry/points.tl +++ b/src/engine/geometry/points.tl @@ -3,42 +3,44 @@ local vectors = require("engine.geometry.vectors") local type Vector = vectors.Vector local record Point - x: number - y: number - metamethod __tostring: function(Point): string + x: number + y: number + metamethod __tostring: function(Point): string end local points = {Point = Point} local function point_to_string(p: Point): string - return tostring(p.x) .. ":" .. tostring(p.y) + return tostring(p.x) .. ":" .. tostring(p.y) end function points.new(x: number, y: number): Point - local self: Point = setmetatable({}, { __index = Point, __tostring = point_to_string }) - self.x = x or 0 - self.y = y or 0 - return self + local self: Point = setmetatable( + {}, {__index = Point, __tostring = point_to_string} + ) + self.x = x or 0 + self.y = y or 0 + return self end function Point:move(delta: Vector): Point - return points.new(self.x + delta.x, self.y + delta.y) + return points.new(self.x + delta.x, self.y + delta.y) end function Point:vector_to(other: Point): Vector - return vectors.new(other.x - self.x, other.y - self.y) + return vectors.new(other.x - self.x, other.y - self.y) end function Point:distance_to(other: Point): number - return math.sqrt((other.x - self.x) ^ 2 + (other.y - self.y) ^ 2) + return math.sqrt((other.x - self.x) ^ 2 + (other.y - self.y) ^ 2) end function Point:in_radius_of(other: Point, radius: number): boolean - return self:distance_to(other) < radius + return self:distance_to(other) < radius end function Point:equal(other: Point): boolean - return self.x == other.x and self.y == other.y + return self.x == other.x and self.y == other.y end return points diff --git a/src/engine/geometry/rectangles.tl b/src/engine/geometry/rectangles.tl index 2b0d1be..d0a31ce 100644 --- a/src/engine/geometry/rectangles.tl +++ b/src/engine/geometry/rectangles.tl @@ -5,97 +5,98 @@ local type Vector = vectors.Vector local type Point = points.Point local record Rectangle - x: number - y: number - width: number - height: number + x: number + y: number + width: number + height: number end local rectangles = {Rectangle = Rectangle} function rectangles.new(x: number, y: number, width: number, height: number): Rectangle - local self: Rectangle = setmetatable({}, { __index = Rectangle }) - self.x = x or 0 - self.y = y or 0 - self.width = width or 0 - self.height = height or 0 - return self + local self: Rectangle = setmetatable({}, {__index = Rectangle}) + self.x = x or 0 + self.y = y or 0 + self.width = width or 0 + self.height = height or 0 + return self end -local function interval_overlap(xmin1: number, xmax1: number, xmin2: number, xmax2: number): boolean - return xmax1 > xmin2 and xmax2 > xmin1 +local function interval_overlap( + xmin1: number, xmax1: number, xmin2: number, xmax2: number +): boolean + return xmax1 > xmin2 and xmax2 > xmin1 end function Rectangle:overlap(other: Rectangle): boolean - return interval_overlap(self.x, self.x + self.width, other.x, other.x + other.width) and - interval_overlap(self.y, self.y + self.height, other.y, other.y + other.height) + return interval_overlap(self.x, self.x + self.width, other.x, other.x + other.width) + and interval_overlap( + self.y, self.y + self.height, other.y, other.y + other.height + ) end -local function interval_contained(xmin1: number, xmax1: number, xmin2: number, xmax2: number): boolean - return xmin1 >= xmin2 and xmax1 <= xmax2 +local function interval_contained( + xmin1: number, xmax1: number, xmin2: number, xmax2: number +): boolean + return xmin1 >= xmin2 and xmax1 <= xmax2 end function Rectangle:contains(other: Rectangle): boolean - return interval_contained(other.x, other.x + other.width, self.x, self.x + self.width) and - interval_contained(other.y, other.y + other.height, self.y, self.y + self.height) + return interval_contained( + other.x, other.x + other.width, self.x, self.x + self.width + ) and interval_contained( + other.y, other.y + other.height, self.y, self.y + self.height + ) end --- Clamp a number in interval [min, max] local function clamp(val: number, min: number, max: number): number - return math.min(math.max(val, min), max) + return math.min(math.max(val, min), max) end function Rectangle:clamp(rec: Rectangle): Rectangle - return rectangles.new( - clamp(rec.x, self.x, self.x + self.width - rec.width), - clamp(rec.y, self.y, self.y + self.height - rec.height), - rec.width, - rec.height - ) + return rectangles.new( + clamp(rec.x, self.x, self.x + self.width - rec.width), + clamp(rec.y, self.y, self.y + self.height - rec.height), + rec.width, + rec.height + ) end function Rectangle:center(): Point - return points.new( - self.x + self.width / 2, - self.y + self.height / 2 - ) + return points.new(self.x + self.width / 2, self.y + self.height / 2) end --- Distance between rectangles from their centers function Rectangle:center_distance(other: Rectangle): number - return self:center():distance_to(other:center()) + return self:center():distance_to(other:center()) end function Rectangle:top_left(): Point - return points.new(self.x, self.y) + return points.new(self.x, self.y) end function Rectangle:shrink(shrink: number): Rectangle - return rectangles.new( - self.x + shrink, - self.y + shrink, - self.width - 2 * shrink, - self.height - 2 * shrink - ) + return rectangles.new( + self.x + shrink, + self.y + shrink, + self.width - 2 * shrink, + self.height - 2 * shrink + ) end function Rectangle:expand(padding: number): Rectangle - return rectangles.new( - self.x - padding, - self.y - padding, - self.width + 2 * padding, - self.height + 2 * padding - ) + return rectangles.new( + self.x - padding, + self.y - padding, + self.width + 2 * padding, + self.height + 2 * padding + ) end function Rectangle:shift(movement: Vector): Rectangle - local new_top_left = self:top_left():move(movement) - return rectangles.new( - new_top_left.x, - new_top_left.y, - self.width, - self.height - ) + local new_top_left = self:top_left():move(movement) + return rectangles.new(new_top_left.x, new_top_left.y, self.width, self.height) end return rectangles diff --git a/src/engine/geometry/vectors.tl b/src/engine/geometry/vectors.tl index 0f22605..ac165f8 100644 --- a/src/engine/geometry/vectors.tl +++ b/src/engine/geometry/vectors.tl @@ -1,89 +1,88 @@ local record Vector - x: number - y: number + x: number + y: number end local type Direction = enum - "up" - "down" - "right" - "left" + "up" + "down" + "right" + "left" end local vectors = {Vector = Vector, Direction = Direction} - function vectors.new(x: number, y: number): Vector - local self: Vector = setmetatable({}, { __index = Vector }) - self.x = x or 0 - self.y = y or 0 - return self + local self: Vector = setmetatable({}, {__index = Vector}) + self.x = x or 0 + self.y = y or 0 + return self end function vectors.from_direction(direction: Direction): Vector - if direction == "up" then - return vectors.new(0, -1) - elseif direction == "down" then - return vectors.new(0, 1) - elseif direction == "right" then - return vectors.new(1, 0) - elseif direction == "left" then - return vectors.new(-1, 0) - else - assert(false, "bad direction") - end + if direction == "up" then + return vectors.new(0, -1) + elseif direction == "down" then + return vectors.new(0, 1) + elseif direction == "right" then + return vectors.new(1, 0) + elseif direction == "left" then + return vectors.new(-1, 0) + else + assert(false, "bad direction") + end end function Vector:permutate_if(condition: boolean): Vector - if condition then - return vectors.new(self.y, self.x) - end + if condition then + return vectors.new(self.y, self.x) + end - return self + return self end function Vector:scale(val: number): Vector - return vectors.new(self.x * val, self.y * val) + return vectors.new(self.x * val, self.y * val) end function Vector:abs(): Vector - return vectors.new(math.abs(self.x), math.abs(self.y)) + return vectors.new(math.abs(self.x), math.abs(self.y)) end function Vector:length(): number - return math.sqrt(self.x ^ 2 + self.y ^ 2) + return math.sqrt(self.x ^ 2 + self.y ^ 2) end function Vector:normalize(): Vector - return self:scale(1 / self:length()) + return self:scale(1 / self:length()) end function Vector:flip(): Vector - return vectors.new(-self.x, -self.y) + return vectors.new(-self.x, -self.y) end function Vector:minus(other: Vector): Vector - return vectors.new(self.x - other.x, self.y - other.y) + return vectors.new(self.x - other.x, self.y - other.y) end function Vector:plus(other: Vector): Vector - return vectors.new(self.x + other.x, self.y + other.y) + return vectors.new(self.x + other.x, self.y + other.y) end function Vector:same_direction(other: Vector): boolean - local function same_sign(x: number, y: number): boolean - return x == y or x * y > 0 - end + local function same_sign(x: number, y: number): boolean + return x == y or x * y > 0 + end - return same_sign(self.x, other.x) and same_sign(self.y, other.y) + return same_sign(self.x, other.x) and same_sign(self.y, other.y) end function Vector:dot_product(other: Vector): number - return self.x * other.x + self.y * other.y + return self.x * other.x + self.y * other.y end function Vector:orthogonal(other: Vector): boolean - return self:dot_product(other) == 0 + return self:dot_product(other) == 0 end return vectors diff --git a/src/engine/hitboxes.tl b/src/engine/hitboxes.tl index 6764ec5..800f735 100644 --- a/src/engine/hitboxes.tl +++ b/src/engine/hitboxes.tl @@ -1,5 +1,5 @@ -local rectangles = require("engine.geometry.rectangles") local points = require("engine.geometry.points") +local rectangles = require("engine.geometry.rectangles") local settings = require("engine.settings") local type Point = points.Point @@ -8,7 +8,9 @@ local type Rectangle = rectangles.Rectangle local hitboxes = {} function hitboxes.new_block(position: Point): Rectangle - return rectangles.new(position.x, position.y, settings.block_size, settings.block_size) + return rectangles.new( + position.x, position.y, settings.block_size, settings.block_size + ) end function hitboxes.draw(rectangle: Rectangle) diff --git a/src/engine/images.tl b/src/engine/images.tl index 304ecb4..155a2a8 100644 --- a/src/engine/images.tl +++ b/src/engine/images.tl @@ -3,58 +3,58 @@ local type Color = {number, number, number} local images = {Color = Color} local function color_equal(c1: Color, c2: Color): boolean - return c1[1] == c2[1] and c1[2] == c2[2] and c1[3] == c2[3] + return c1[1] == c2[1] and c1[2] == c2[2] and c1[3] == c2[3] end function images.change_colors(image: Image, color_mapping: {Color: Color}) - local image_data = image:getData() - -- Using C index in getPixel API for some reason... - for i = 0, image:getWidth() - 1 do - for j = 0, image:getHeight() - 1 do - local r, g, b, alpha = image_data:getPixel(i, j) - if alpha ~= 0 then - for from_color, to_color in pairs(color_mapping) do - if color_equal({r, g, b}, from_color) then - r, g, b = table.unpack(to_color) - end - end - image_data:setPixel(i, j, r, g, b, alpha) - end - end - end + local image_data = image:getData() + -- Using C index in getPixel API for some reason... + for i = 0, image:getWidth() - 1 do + for j = 0, image:getHeight() - 1 do + local r, g, b, alpha = image_data:getPixel(i, j) + if alpha ~= 0 then + for from_color, to_color in pairs(color_mapping) do + if color_equal({r, g, b}, from_color) then + r, g, b = table.unpack(to_color) + end + end + image_data:setPixel(i, j, r, g, b, alpha) + end + end + end end ---- Scale 0-1 represent how far from current to max to increase color component +--- Scale 0-1 represent how far from current to max to increase color component local function increase_color(color: number, scale: number): number - return color + scale * (255 - color) + return color + scale * (255 - color) end function images.make_lighter(image: Image, scale: number) - local image_data = image:getData() - -- Using C index in getPixel API for some reason... - for i = 0, image:getWidth() - 1 do - for j = 0, image:getHeight() - 1 do - local r, g, b, alpha = image_data:getPixel(i, j) - if alpha ~= 0 then - r = increase_color(r, scale) - g = increase_color(g, scale) - b = increase_color(b, scale) - image_data:setPixel(i, j, r, g, b, alpha) - end - end - end + local image_data = image:getData() + -- Using C index in getPixel API for some reason... + for i = 0, image:getWidth() - 1 do + for j = 0, image:getHeight() - 1 do + local r, g, b, alpha = image_data:getPixel(i, j) + if alpha ~= 0 then + r = increase_color(r, scale) + g = increase_color(g, scale) + b = increase_color(b, scale) + image_data:setPixel(i, j, r, g, b, alpha) + end + end + end end function images.set_alpha(image: Image, alpha: number) local image_data = image:getData() - for i = 0, image:getWidth() - 1 do - for j = 0, image:getHeight() - 1 do - local r, g, b, old_alpha = image_data:getPixel(i, j) + for i = 0, image:getWidth() - 1 do + for j = 0, image:getHeight() - 1 do + local r, g, b, old_alpha = image_data:getPixel(i, j) if old_alpha ~= 0 then - image_data:setPixel(i, j, r, g, b, alpha) - end - end - end + image_data:setPixel(i, j, r, g, b, alpha) + end + end + end end -return images \ No newline at end of file +return images diff --git a/src/engine/joysticks.tl b/src/engine/joysticks.tl index 5e30fad..fa6c3f9 100644 --- a/src/engine/joysticks.tl +++ b/src/engine/joysticks.tl @@ -5,22 +5,22 @@ local down_for_states: {integer: {integer: number}} = {} local joysticks = { MAX_BUTTONS = 16, BUTTONS = { - B = 1, - Y = 2, - SELECT = 3, - START = 4, - UP = 5, - DOWN = 6, - LEFT = 7, - RIGHT = 8, - A = 9, - X = 10, - L = 11, - R = 12, - L2 = 13, - R2 = 14, - L3 = 15, - R3 = 16, + B = 1, + Y = 2, + SELECT = 3, + START = 4, + UP = 5, + DOWN = 6, + LEFT = 7, + RIGHT = 8, + A = 9, + X = 10, + L = 11, + R = 12, + L2 = 13, + R2 = 14, + L3 = 15, + R3 = 16, }, } @@ -42,12 +42,12 @@ end -- Used to check if a button was just pressed this frame (not held) by any player function joysticks.is_pressed_any(button: integer): boolean - for player_id=1, joysticks.maxplayers() do + for player_id = 1, joysticks.maxplayers() do if joysticks.is_pressed(player_id, button) then return true end end - + return false end @@ -59,19 +59,21 @@ function joysticks.is_released(player_id: integer, button: integer): boolean return not is_currently_down and was_previously_down end --- Used to check if a button has been pressed down -function joysticks.is_down_for(player_id: integer, button: integer, time: number): boolean +-- Used to check if a button has been pressed down +function joysticks.is_down_for( + player_id: integer, button: integer, time: number +): boolean return down_for_states[player_id][button] >= time end -- Used to check if a button has been pressed down by any player function joysticks.is_down_for_any(button: integer, time: number): boolean - for player_id=1, joysticks.maxplayers() do + for player_id = 1, joysticks.maxplayers() do if joysticks.is_down_for(player_id, button, time) then return true end end - + return false end diff --git a/src/engine/logger.tl b/src/engine/logger.tl index c0647a8..e69b688 100644 --- a/src/engine/logger.tl +++ b/src/engine/logger.tl @@ -7,11 +7,11 @@ function logger.setup(app_name: string) end function logger.info(msg: string) - print('[INFO] [' .. config_app_name .. ']: ' .. msg) + print("[INFO] [" .. config_app_name .. "]: " .. msg) end function logger.warning(msg: string) - print('[WARN] [' .. config_app_name .. ']: ' .. msg) + print("[WARN] [" .. config_app_name .. "]: " .. msg) end return logger diff --git a/src/engine/lutro.d.tl b/src/engine/lutro.d.tl index d0c997c..ca274ce 100644 --- a/src/engine/lutro.d.tl +++ b/src/engine/lutro.d.tl @@ -3,137 +3,171 @@ -- Similar work have been done for LÖVE :https://github.com/MikuAuahDark/love2d-tl/blob/master/love.d.tl. -- As Lutro is a subset of LÖVE, the Teal code there can be used as inspiration. For details on what -- exists in Lutro see https://github.com/libretro/lutro-status?tab=readme-ov-file#status. Otherwise, --- add to the interface here as needed when using new Lutro functionality. +-- add to the interface here as needed when using new Lutro functionality. global type Configuration = interface - width: integer - height: integer + width: integer + height: integer end global enum DrawMode - "fill" - "line" + "fill" + "line" end local type Object = interface end -local type Drawable = interface - is Object +local type Drawable = interface is Object end -global type Image = interface - is Drawable, Object - - getWidth: function(self: Image): number - getHeight: function(self: Image): number - getData: function(self: Image): ImageData +global type Image = interface is Drawable, Object + getWidth: function(self: Image): number + getHeight: function(self: Image): number + getData: function(self: Image): ImageData end -global type ImageData = interface - is Object - getPixel: function(self: ImageData, x: number, y: number): number, number, number, number - setPixel: function(self: ImageData, x: number, y: number, r: number, g: number, b: number, a: number) +global type ImageData = interface is Object + getPixel: function( + self: ImageData, x: number, y: number + ): number, number, number, number + setPixel: function( + self: ImageData, + x: number, + y: number, + r: number, + g: number, + b: number, + a: number + ) end -global type Font = interface - is Object - getWidth: function(self: Font, text: string): number +global type Font = interface is Object + getWidth: function(self: Font, text: string): number end -global type Quad = interface - is Object - getTextureDimensions: function(self: Quad): number, number - getViewport: function(self: Quad): number, number, number, number - setViewport: function(self: Quad, x: number, y: number, w: number, h: number, sw?: number, sh?: number) +global type Quad = interface is Object + getTextureDimensions: function(self: Quad): number, number + getViewport: function(self: Quad): number, number, number, number + setViewport: function( + self: Quad, x: number, y: number, w: number, h: number, sw?: number, sh?: number + ) end -global type Source = interface - is Object - setVolume: function(self: Source, volume: number) - setLooping: function(self: Source, loop: boolean) - setPitch: function(self: Source, pitch: number) - play: function(self: Source) - pause: function(self: Source) - stop: function(self: Source) - isPlaying: function(self: Source): boolean - isPaused: function(self: Source): boolean +global type Source = interface is Object + setVolume: function(self: Source, volume: number) + setLooping: function(self: Source, loop: boolean) + setPitch: function(self: Source, pitch: number) + play: function(self: Source) + pause: function(self: Source) + stop: function(self: Source) + isPlaying: function(self: Source): boolean + isPaused: function(self: Source): boolean end global type GamepadButton = enum - "a" - "b" - "x" - "y" - "up" - "down" - "right" - "left" - "r1" - "r2" - "r3" - "l1" - "l2" - "l3" - "start" - "select" + "a" + "b" + "x" + "y" + "up" + "down" + "right" + "left" + "r1" + "r2" + "r3" + "l1" + "l2" + "l3" + "start" + "select" end global type SourceType = enum - "static" - "stream" - "queue" + "static" + "stream" + "queue" end global record lutro - conf: function(t: Configuration) - load: function() - update: function(dt: number) - draw: function() - - featureflags: {string: boolean} | nil - - record graphics - clear: function() - print: function(text: string, x: number, y: number) - - setColor: function(r: number, g: number, b: number, lum?: number) - setBackgroundColor: function(r: number, g: number, b: number, lum?: number) - - newImageFont: function(filename: string, glyphs: string): Font - setFont: function(font: Font) - print: function(text: string, x: number, y: number) - printf: function(text: string, x: number, y: number, limit: number, align: string) - - newImage: function(filename: string): Image - newQuad: function(x: number, y: number, width: number, height: number, sw: number, sh: number): Quad - - draw: function(drawable: Drawable, x?: number, y?: number, r?: number, sx?: number, sy?: number, ox?: number, oy?: number, kx?: number, ky?: number) - draw: function(drawable: Drawable, quad: Quad, x: number, y: number, r?: number, sx?: number, sy?: number, ox?: number, oy?: number, kx?: number, ky?: number) - - rectangle: function(mode: DrawMode, x: number, y: number, width: number, height: number) - end - - record joystick - isDown: function(joystick: integer, button: integer): boolean - getJoystickCount: function(): integer - end - - record audio - newSource: function(filename: string, type: SourceType): Source - play: function(source: Source) - end - - record math - random: function(): number - random: function(max: integer): integer - random: function(min: integer, max: integer): integer - end - - record timer - getTime: function(): number - end - - gamepadpressed: function(button_nbr: integer, button: GamepadButton) - gamepadreleased: function(button_nbr: integer, button: GamepadButton) + conf: function(t: Configuration) + load: function() + update: function(dt: number) + draw: function() + + featureflags: {string: boolean} | nil + + record graphics + clear: function() + print: function(text: string, x: number, y: number) + + setColor: function(r: number, g: number, b: number, lum?: number) + setBackgroundColor: function(r: number, g: number, b: number, lum?: number) + + newImageFont: function(filename: string, glyphs: string): Font + setFont: function(font: Font) + print: function(text: string, x: number, y: number) + printf: function( + text: string, x: number, y: number, limit: number, align: string + ) + + newImage: function(filename: string): Image + newQuad: function( + x: number, y: number, width: number, height: number, sw: number, sh: number + ): Quad + + draw: function( + drawable: Drawable, + x?: number, + y?: number, + r?: number, + sx?: number, + sy?: number, + ox?: number, + oy?: number, + kx?: number, + ky?: number + ) + draw: function( + drawable: Drawable, + quad: Quad, + x: number, + y: number, + r?: number, + sx?: number, + sy?: number, + ox?: number, + oy?: number, + kx?: number, + ky?: number + ) + + rectangle: function( + mode: DrawMode, x: number, y: number, width: number, height: number + ) + end + + record joystick + isDown: function(joystick: integer, button: integer): boolean + getJoystickCount: function(): integer + end + + record audio + newSource: function(filename: string, type: SourceType): Source + play: function(source: Source) + end + + record math + random: function(): number + random: function(max: integer): integer + random: function(min: integer, max: integer): integer + end + + record timer + getTime: function(): number + end + + gamepadpressed: function(button_nbr: integer, button: GamepadButton) + gamepadreleased: function(button_nbr: integer, button: GamepadButton) end diff --git a/src/engine/physics.tl b/src/engine/physics.tl index e4788e9..6de4035 100644 --- a/src/engine/physics.tl +++ b/src/engine/physics.tl @@ -1,84 +1,96 @@ -local vectors = require("engine.geometry.vectors") local points = require("engine.geometry.points") +local vectors = require("engine.geometry.vectors") local type Vector = vectors.Vector local type Point = points.Point local record ProjectileTrajectory - height: number - end_height: number + height: number + end_height: number velocity: number lateral_velocity: Vector gravity: number - duration: number + duration: number end local physics = {ProjectileTrajectory = ProjectileTrajectory} function ProjectileTrajectory:in_air_position(ground_pos: Point): Point - return ground_pos:move(vectors.new(0, -self.height)) + return ground_pos:move(vectors.new(0, -self.height)) end function ProjectileTrajectory:get_direction(): Vector - return self.lateral_velocity:normalize() + return self.lateral_velocity:normalize() end function ProjectileTrajectory:reverse() - self.lateral_velocity:flip() + self.lateral_velocity:flip() end function ProjectileTrajectory:finished(): boolean - return self.duration <= 0 + return self.duration <= 0 end function ProjectileTrajectory:update(dt: number, ground_pos: Point): Point - self.duration = self.duration - dt - local new_position = ground_pos:move(self.lateral_velocity:scale(dt)) - self.height = self.height + self.velocity * dt - self.velocity = self.velocity - self.gravity * dt + self.duration = self.duration - dt + local new_position = ground_pos:move(self.lateral_velocity:scale(dt)) + self.height = self.height + self.velocity * dt + self.velocity = self.velocity - self.gravity * dt - return new_position + return new_position end -function physics.calc_init_projectile_velocity(h_diff: number, h_max: number, t_end: number): number - -- Derived from projectile equation, calculates initial velocity given the - -- projectile height (h_max) and the time the arc will take (t_end) and the height - -- difference of start and end position (h_diff). - return (2 / t_end) * (h_max + math.sqrt(h_max * (h_max + h_diff))) +function physics.calc_init_projectile_velocity( + h_diff: number, h_max: number, t_end: number +): number + -- Derived from projectile equation, calculates initial velocity given the + -- projectile height (h_max) and the time the arc will take (t_end) and the height + -- difference of start and end position (h_diff). + return (2 / t_end) * (h_max + math.sqrt(h_max * (h_max + h_diff))) end function physics.calc_gravity(init_velocity: number, h_max: number): number - -- Calculates gravity from how the projectile arc should look like. Not very physical - -- but making up gravity coffecients that makes things look good is hard. - return init_velocity * init_velocity / (2 * h_max) + -- Calculates gravity from how the projectile arc should look like. Not very physical + -- but making up gravity coffecients that makes things look good is hard. + return init_velocity * init_velocity / (2 * h_max) end function physics.new_trajectory( - start_height: number, velocity: number, lateral_velocity: Vector, gravity: number, duration: number + start_height: number, + velocity: number, + lateral_velocity: Vector, + gravity: number, + duration: number ): ProjectileTrajectory - local self: ProjectileTrajectory = setmetatable({}, { __index = ProjectileTrajectory }) - self.height = start_height + local self: ProjectileTrajectory = setmetatable( + {}, {__index = ProjectileTrajectory} + ) + self.height = start_height self.velocity = velocity self.lateral_velocity = lateral_velocity self.gravity = gravity - self.duration = duration - return self + self.duration = duration + return self end function physics.calc_trajectory( - start_height: number, end_height: number, h_max: number, duration: number, position: Point, target: Point + start_height: number, + end_height: number, + h_max: number, + duration: number, + position: Point, + target: Point ): ProjectileTrajectory local velocity = physics.calc_init_projectile_velocity( - math.abs(start_height - end_height), h_max, duration - ) - local gravity = physics.calc_gravity(velocity, h_max) - - local throw_vector = vectors.new( - target.x - position.x, - target.y - position.y + math.abs(start_height - end_height), h_max, duration ) + local gravity = physics.calc_gravity(velocity, h_max) + + local throw_vector = vectors.new(target.x - position.x, target.y - position.y) local lateral_velocity = throw_vector:scale(1 / duration) - return physics.new_trajectory(start_height, velocity, lateral_velocity, gravity, duration) + return physics.new_trajectory( + start_height, velocity, lateral_velocity, gravity, duration + ) end return physics diff --git a/src/engine/scenes.tl b/src/engine/scenes.tl index b62538b..eb187bc 100644 --- a/src/engine/scenes.tl +++ b/src/engine/scenes.tl @@ -20,7 +20,7 @@ local record MemorySample end function MemorySample.new(): MemorySample - local self: MemorySample = setmetatable({}, { __index = MemorySample }) + local self: MemorySample = setmetatable({}, {__index = MemorySample}) self.frame_count = 0 self.accumulated_kb = 0 return self @@ -59,7 +59,7 @@ local record CheckMemoryOfScene is Scene end function CheckMemoryOfScene.new(to_wrap: Scene): CheckMemoryOfScene - local self: CheckMemoryOfScene = setmetatable({}, { __index = CheckMemoryOfScene }) + local self: CheckMemoryOfScene = setmetatable({}, {__index = CheckMemoryOfScene}) self.to_wrap = to_wrap self.is_internal = true self.update_sample = MemorySample.new() @@ -92,10 +92,10 @@ local record WrapperScene is Scene end function WrapperScene.new(to_wrap: Scene): WrapperScene - local self: WrapperScene = setmetatable({}, { __index = WrapperScene }) - self.to_wrap = to_wrap - self.is_internal = true - return self + local self: WrapperScene = setmetatable({}, {__index = WrapperScene}) + self.to_wrap = to_wrap + self.is_internal = true + return self end function WrapperScene:load() @@ -122,7 +122,7 @@ local record PerformanceSample end function PerformanceSample.new(): PerformanceSample - local self: PerformanceSample = setmetatable({}, { __index = PerformanceSample }) + local self: PerformanceSample = setmetatable({}, {__index = PerformanceSample}) self.frame_count = 0 self.accumulated_seconds = 0 return self @@ -158,12 +158,14 @@ local record CheckPerformanceOfScene is Scene end function CheckPerformanceOfScene.new(to_wrap: Scene): CheckPerformanceOfScene - local self: CheckPerformanceOfScene = setmetatable({}, { __index = CheckPerformanceOfScene }) - self.to_wrap = to_wrap - self.is_internal = true - self.update_sample = PerformanceSample.new() - self.draw_sample = PerformanceSample.new() - return self + local self: CheckPerformanceOfScene = setmetatable( + {}, {__index = CheckPerformanceOfScene} + ) + self.to_wrap = to_wrap + self.is_internal = true + self.update_sample = PerformanceSample.new() + self.draw_sample = PerformanceSample.new() + return self end function CheckPerformanceOfScene:load() diff --git a/src/engine/settings.tl b/src/engine/settings.tl index c2ecfb1..e12eb73 100644 --- a/src/engine/settings.tl +++ b/src/engine/settings.tl @@ -6,7 +6,7 @@ local record EngineSettings end function EngineSettings.new(): EngineSettings - local self: EngineSettings = setmetatable({}, { __index = EngineSettings }) + local self: EngineSettings = setmetatable({}, {__index = EngineSettings}) self.draw_hitboxes = false self.log_scene_perf = false self.log_mem_usage = false diff --git a/src/engine/tracing.tl b/src/engine/tracing.tl index 832b1b5..d276e8d 100644 --- a/src/engine/tracing.tl +++ b/src/engine/tracing.tl @@ -1,4 +1,3 @@ - local interface Span name: string elapsed: number @@ -17,7 +16,7 @@ local current_level: SpanTiming = nil local tracing = {Span = Span} function SpanTiming.new(name: string, previous_level: SpanTiming): SpanTiming - local self: SpanTiming = setmetatable({}, { __index = SpanTiming }) + local self: SpanTiming = setmetatable({}, {__index = SpanTiming}) self.name = name self.previous_level = previous_level self.next_levels = {} @@ -30,7 +29,7 @@ local record SpanDummy is Span end function SpanDummy.new(): SpanDummy - local self: SpanDummy = setmetatable({}, { __index = SpanDummy }) + local self: SpanDummy = setmetatable({}, {__index = SpanDummy}) self.name = "dummy" self.elapsed = 1234 return self @@ -49,7 +48,7 @@ function SpanTiming:stop() current_level = self.previous_level end -local DUMMY_TRACE_SPAM = SpanDummy.new() +local DUMMY_TRACE_SPAM = SpanDummy.new() function tracing.start(name: string): Span if not current_level then @@ -59,7 +58,7 @@ function tracing.start(name: string): Span local span_next_level = SpanTiming.new(name, current_level) table.insert(current_level.next_levels, span_next_level) - + -- Then current level is next level until we stop/exit that level current_level = span_next_level return span_next_level @@ -76,7 +75,12 @@ end function SpanTiming:_reqursive_report(indentation: string): string local parts: {string} = {} - table.insert(parts, indentation .. self.name .. ": " .. string.format("%.1f", self.elapsed * 1000) .. "ms") + table.insert( + parts, + indentation .. self.name .. ": " .. string.format( + "%.1f", self.elapsed * 1000 + ) .. "ms" + ) for _, next_level in ipairs(self.next_levels) do table.insert(parts, next_level:_reqursive_report(indentation .. " ")) end diff --git a/src/fonts.tl b/src/fonts.tl index 2cfe29d..f710aa5 100644 --- a/src/fonts.tl +++ b/src/fonts.tl @@ -3,28 +3,28 @@ local points = require("engine.geometry.points") local type Point = points.Point local record ShadedFont - main: Font - shade: Font + main: Font + shade: Font end local record MenuFont - main: Font - outline: Font + main: Font + outline: Font end local fonts = {ShadedFont = ShadedFont, MenuFont = MenuFont} function fonts.new_shaded(main: Font, shade: Font): ShadedFont - local self: ShadedFont = setmetatable({}, { __index = ShadedFont }) - self.main = main - self.shade = shade - return self + local self: ShadedFont = setmetatable({}, {__index = ShadedFont}) + self.main = main + self.shade = shade + return self end function ShadedFont:draw(text: string, position: Point) - lutro.graphics.setFont(self.shade) - for dx=1, 0, -1 do - for dy=1, 0, -1 do + lutro.graphics.setFont(self.shade) + for dx = 1, 0, -1 do + for dy = 1, 0, -1 do if dx == 0 and dy == 0 then lutro.graphics.setFont(self.main) end @@ -35,54 +35,72 @@ end function fonts.new_shaded_numbers(): ShadedFont local font_symbols = " 0123456789:\"" - local clock_main = lutro.graphics.newImageFont("assets/imgfont/numbers.png", font_symbols) - local clock_shade = lutro.graphics.newImageFont("assets/imgfont/numbers_blue_tint.png", font_symbols) - return fonts.new_shaded(clock_main, clock_shade) + local clock_main = lutro.graphics.newImageFont( + "assets/imgfont/numbers.png", font_symbols + ) + local clock_shade = lutro.graphics.newImageFont( + "assets/imgfont/numbers_blue_tint.png", font_symbols + ) + return fonts.new_shaded(clock_main, clock_shade) end function MenuFont.new(main: Font, outline: Font): MenuFont - local self: MenuFont = setmetatable({}, { __index = MenuFont }) - self.main = main - self.outline = outline - return self + local self: MenuFont = setmetatable({}, {__index = MenuFont}) + self.main = main + self.outline = outline + return self end local FONT_SYMBOLS = " abcdefghijklmnopqrstuvwxyz0123456789.!'-:*" function fonts.new_menu_white(): MenuFont - local white_font = lutro.graphics.newImageFont("assets/imgfont/white.png", FONT_SYMBOLS) + local white_font = lutro.graphics.newImageFont( + "assets/imgfont/white.png", FONT_SYMBOLS + ) return MenuFont.new(white_font, nil) end function fonts.new_menu_black(): MenuFont - local black_font = lutro.graphics.newImageFont("assets/imgfont/black.png", FONT_SYMBOLS) + local black_font = lutro.graphics.newImageFont( + "assets/imgfont/black.png", FONT_SYMBOLS + ) return MenuFont.new(black_font, nil) end function fonts.new_menu_white_outlined(): MenuFont - local white_font = lutro.graphics.newImageFont("assets/imgfont/white.png", FONT_SYMBOLS) - local black_font = lutro.graphics.newImageFont("assets/imgfont/black.png", FONT_SYMBOLS) + local white_font = lutro.graphics.newImageFont( + "assets/imgfont/white.png", FONT_SYMBOLS + ) + local black_font = lutro.graphics.newImageFont( + "assets/imgfont/black.png", FONT_SYMBOLS + ) return MenuFont.new(white_font, black_font) end function fonts.new_menu_gray_outlined(): MenuFont - local gray_font = lutro.graphics.newImageFont("assets/imgfont/gray.png", FONT_SYMBOLS) - local black_font = lutro.graphics.newImageFont("assets/imgfont/black.png", FONT_SYMBOLS) + local gray_font = lutro.graphics.newImageFont( + "assets/imgfont/gray.png", FONT_SYMBOLS + ) + local black_font = lutro.graphics.newImageFont( + "assets/imgfont/black.png", FONT_SYMBOLS + ) return MenuFont.new(gray_font, black_font) end function MenuFont:print_centered(text: string, box_width: number, position: Point) if self.outline then lutro.graphics.setFont(self.outline) - for dx=-1, 1 do - for dy=-1, 1 do - lutro.graphics.printf(text, position.x + dx, position.y + dy, box_width, "center") + for dx = -1, 1 do + for dy = -1, 1 do + lutro.graphics.printf( + text, position.x + dx, position.y + dy, box_width, "center" + ) end end end lutro.graphics.setFont(self.main) - lutro.graphics.printf(text, position.x, position.y, box_width, "center") + lutro.graphics.printf(text, position.x, position.y, box_width, "center") end return fonts diff --git a/src/game/battle.tl b/src/game/battle.tl index 7a34989..3a94db8 100644 --- a/src/game/battle.tl +++ b/src/game/battle.tl @@ -1,46 +1,46 @@ -local audio = require("engine.audio") -local background = require("level_graphics.background") -local battle_end_scene = require("game.battle_end_scene") local bombs = require("bombs.bombs") +local explosions = require("bombs.explosions") +local lightning_bombs = require("bombs.lightning_bombs") +local lightnings = require("bombs.lightnings") local characters = require("characters") -local collision = require("engine.collision") local dino_animations = require("dino_animations") local dino_inputs = require("dino_inputs") local eggs = require("eggs") -local explosions = require("bombs.explosions") +local audio = require("engine.audio") +local collision = require("engine.collision") +local points = require("engine.geometry.points") +local hitboxes = require("engine.hitboxes") +local joysticks = require("engine.joysticks") +local scenes = require("engine.scenes") +local tracing = require("engine.tracing") +local battle_end_scene = require("game.battle_end_scene") local countdown = require("game.countdown") -local timer = require("game.timer") local pause_scene = require("game.pause_scene") -local grid = require("level_layout.grid") +local score_board = require("game.score_board") +local timer = require("game.timer") +local win_condition = require("game.win_condition") local hard_blocks = require("hard_blocks") -local hitboxes = require("engine.hitboxes") local items = require("items") local itertools = require("itertools") -local joysticks = require("engine.joysticks") +local background = require("level_graphics.background") local level_graphics = require("level_graphics.maps.init") +local grid = require("level_layout.grid") +local map_entities = require("level_layout.map_entities") local maps = require("level_layout.maps.init") -local lightnings = require("bombs.lightnings") -local lightning_bombs = require("bombs.lightning_bombs") local mad_bombers = require("mad_bombers") -local map_entities = require("level_layout.map_entities") local movement = require("movement") local player_animations = require("player_animations") local players = require("players") -local points = require("engine.geometry.points") local pressure_blocks = require("pressure_block") -local scenes = require("engine.scenes") -local score_board = require("game.score_board") local settings = require("settings") local soft_blocks = require("soft_blocks") -local tracing = require("engine.tracing") -local win_condition = require("game.win_condition") local type Bomb = bombs.Bomb local type Bombs = bombs.Bombs local type Eggs = eggs.Eggs local type Explodable = require("bombs.explodable") -local type Kinematic = require("kinematic") local type Entity = require("engine.entity") +local type Kinematic = require("kinematic") local type GameCountdown = countdown.GameCountdown local type HardBlocks = hard_blocks.HardBlocks local type Items = items.Items @@ -64,564 +64,612 @@ local type WinCondition = win_condition.WinCondition local music: Source local type BattleState = enum - "starting" - "ongoing" - "restarting" - "finished" + "starting" + "ongoing" + "restarting" + "finished" end local record Battle is Scene - score_board: ScoreBoard - player_selection: Scene - - hard_blocks: HardBlocks - non_hard_block_positions: {Point} - bombs: Bombs - lightning_bombs: LightningBombs - soft_blocks: SoftBlocks - mad_bombers: MadBombers - eggs: Eggs - items: Items - timer: GameTimer - team_by_player_id: {integer: integer} - players: {Player} - player_animations: PlayerAnimations - win_condition: WinCondition - state: BattleState - countdown: GameCountdown - pressure_blocks: PressureBlocks - tic_counter: integer - audio_paused: boolean - pressure_blocks_group: Group - bomb_entities_group: Group - alive_players_group: Group - bombs_bounce_on: CombinedGroup - explodables: CombinedGroup - blocks_bombs: CombinedGroup - blocks_jump: CombinedGroup - blockers: CombinedGroup - lightning_blockers: CombinedGroup - blocking_blocks_group: CombinedGroup + score_board: ScoreBoard + player_selection: Scene + + hard_blocks: HardBlocks + non_hard_block_positions: {Point} + bombs: Bombs + lightning_bombs: LightningBombs + soft_blocks: SoftBlocks + mad_bombers: MadBombers + eggs: Eggs + items: Items + timer: GameTimer + team_by_player_id: {integer: integer} + players: {Player} + player_animations: PlayerAnimations + win_condition: WinCondition + state: BattleState + countdown: GameCountdown + pressure_blocks: PressureBlocks + tic_counter: integer + audio_paused: boolean + pressure_blocks_group: Group + bomb_entities_group: Group + alive_players_group: Group + bombs_bounce_on: CombinedGroup + explodables: CombinedGroup + blocks_bombs: CombinedGroup + blocks_jump: CombinedGroup + blockers: CombinedGroup + lightning_blockers: CombinedGroup + blocking_blocks_group: CombinedGroup end function Battle:load() - self.players = {} - local character_selections = characters.get_selections() - local start_positions = map_entities.get_player_start_positions(#character_selections) - for i, character_selection in ipairs(character_selections) do - local created_player = players.new( - character_selection.player_id, - character_selection.sprite, - start_positions[i] - ) - created_player.team_id = self.team_by_player_id[created_player.id] - table.insert(self.players, created_player) - end - - self.bombs = bombs.new() - self.lightning_bombs = lightning_bombs.new() - local block_configs = map_entities.get_block_configs(self.players) - self.soft_blocks = soft_blocks.new(block_configs.soft) - self.mad_bombers = mad_bombers.new(self.bombs) - self.eggs = eggs.new() - self.items = items.new() - self.timer = timer.new() - self.player_animations = player_animations.new(self.players) - self.win_condition:reset() - self.state = "starting" - self.countdown = countdown.new() - self.pressure_blocks = pressure_blocks.new() - self.hard_blocks = hard_blocks.new(block_configs.hard) - self.non_hard_block_positions = block_configs.non_hard - self.tic_counter = 0 - self.audio_paused = false - music:stop() - self.pressure_blocks_group = collision.new_group_from_set(self.pressure_blocks.on_ground.set) - self.bomb_entities_group = collision.new_group_from_set(self.bombs.on_ground.set) - self.alive_players_group = collision.new_group(self.players) - self.bombs_bounce_on = collision.new_combined_group( - self.soft_blocks.on_ground.set, - self.hard_blocks.on_ground.set, - self.lightning_bombs.on_ground.set, - self.pressure_blocks.on_ground.set, - self.bombs.on_ground.set, - self.alive_players_group.set - ) - self.explodables = collision.new_combined_group( - self.alive_players_group.set, - self.bombs.on_ground.set, - self.eggs.on_ground.set, - self.items.on_ground.set - ) - self.blocks_bombs = collision.new_combined_group( - self.soft_blocks.on_ground.set, - self.hard_blocks.on_ground.set, - self.lightning_bombs.on_ground.set, - self.pressure_blocks.on_ground.set - ) - self.blocks_jump = collision.new_combined_group( - self.soft_blocks.on_ground.set, - self.hard_blocks.on_ground.set, - self.lightning_bombs.on_ground.set, - self.pressure_blocks.on_ground.set, - self.bombs.on_ground.set - ) - self.blockers = collision.new_combined_group( - self.eggs.on_ground.set, - self.items.on_ground.set, - self.soft_blocks.on_ground.set, - self.pressure_blocks.on_ground.set, - self.hard_blocks.on_ground.set - ) - self.lightning_blockers = collision.new_combined_group( - self.pressure_blocks.on_ground.set, - self.hard_blocks.on_ground.set - ) - self.blocking_blocks_group = collision.new_combined_group( - self.soft_blocks.on_ground.set, - self.pressure_blocks.on_ground.set, - self.hard_blocks.on_ground.set - ) + self.players = {} + local character_selections = characters.get_selections() + local start_positions = map_entities.get_player_start_positions( + #character_selections + ) + for i, character_selection in ipairs(character_selections) do + local created_player = players.new( + character_selection.player_id, + character_selection.sprite, + start_positions[i] + ) + created_player.team_id = self.team_by_player_id[created_player.id] + table.insert(self.players, created_player) + end + + self.bombs = bombs.new() + self.lightning_bombs = lightning_bombs.new() + local block_configs = map_entities.get_block_configs(self.players) + self.soft_blocks = soft_blocks.new(block_configs.soft) + self.mad_bombers = mad_bombers.new(self.bombs) + self.eggs = eggs.new() + self.items = items.new() + self.timer = timer.new() + self.player_animations = player_animations.new(self.players) + self.win_condition:reset() + self.state = "starting" + self.countdown = countdown.new() + self.pressure_blocks = pressure_blocks.new() + self.hard_blocks = hard_blocks.new(block_configs.hard) + self.non_hard_block_positions = block_configs.non_hard + self.tic_counter = 0 + self.audio_paused = false + music:stop() + self.pressure_blocks_group = collision.new_group_from_set( + self.pressure_blocks.on_ground.set + ) + self.bomb_entities_group = collision.new_group_from_set(self.bombs.on_ground.set) + self.alive_players_group = collision.new_group(self.players) + self.bombs_bounce_on = collision.new_combined_group( + self.soft_blocks.on_ground.set, + self.hard_blocks.on_ground.set, + self.lightning_bombs.on_ground.set, + self.pressure_blocks.on_ground.set, + self.bombs.on_ground.set, + self.alive_players_group.set + ) + self.explodables = collision.new_combined_group( + self.alive_players_group.set, + self.bombs.on_ground.set, + self.eggs.on_ground.set, + self.items.on_ground.set + ) + self.blocks_bombs = collision.new_combined_group( + self.soft_blocks.on_ground.set, + self.hard_blocks.on_ground.set, + self.lightning_bombs.on_ground.set, + self.pressure_blocks.on_ground.set + ) + self.blocks_jump = collision.new_combined_group( + self.soft_blocks.on_ground.set, + self.hard_blocks.on_ground.set, + self.lightning_bombs.on_ground.set, + self.pressure_blocks.on_ground.set, + self.bombs.on_ground.set + ) + self.blockers = collision.new_combined_group( + self.eggs.on_ground.set, + self.items.on_ground.set, + self.soft_blocks.on_ground.set, + self.pressure_blocks.on_ground.set, + self.hard_blocks.on_ground.set + ) + self.lightning_blockers = collision.new_combined_group( + self.pressure_blocks.on_ground.set, self.hard_blocks.on_ground.set + ) + self.blocking_blocks_group = collision.new_combined_group( + self.soft_blocks.on_ground.set, + self.pressure_blocks.on_ground.set, + self.hard_blocks.on_ground.set + ) end -function Battle.new(player_selection: Scene, score_board_: ScoreBoard, win_condition_: WinCondition): Battle - local self: Battle = setmetatable({}, { __index = Battle }) - self.team_by_player_id = {} - self.player_selection = player_selection - self.score_board = score_board_ - self.win_condition = win_condition_ - self:load() - return self +function Battle.new( + player_selection: Scene, score_board_: ScoreBoard, win_condition_: WinCondition +): Battle + local self: Battle = setmetatable({}, {__index = Battle}) + self.team_by_player_id = {} + self.player_selection = player_selection + self.score_board = score_board_ + self.win_condition = win_condition_ + self:load() + return self end function Battle:get_available_spots(alive_players: {Player}): {Point} - -- Return places an item/egg can drop. The spot needs to - -- be free and not too close to a player - local available_spots: {Point} = {} - - local occupied: {Entity} = itertools.sets_as_list( - self.eggs.on_ground.set, - self.items.on_ground.set, - self.soft_blocks.on_ground.set, - self.pressure_blocks.on_ground.set - ) - - local function valid_position(pos: Point): boolean - for _, player in ipairs(alive_players) do - if player.position:distance_to(pos) < grid.TILE_SIZE * 2 then - return false - end - end - - for _, entity in ipairs(occupied) do - if entity:get_position():equal(pos) then - return false - end - end - return true - end - - for _, pos in ipairs(self.non_hard_block_positions) do - if valid_position(pos) then - table.insert(available_spots, pos) - end - end - - return available_spots + -- Return places an item/egg can drop. The spot needs to + -- be free and not too close to a player + local available_spots: {Point} = {} + + local occupied: {Entity} = itertools.sets_as_list( + self.eggs.on_ground.set, + self.items.on_ground.set, + self.soft_blocks.on_ground.set, + self.pressure_blocks.on_ground.set + ) + + local function valid_position(pos: Point): boolean + for _, player in ipairs(alive_players) do + if player.position:distance_to(pos) < grid.TILE_SIZE * 2 then + return false + end + end + + for _, entity in ipairs(occupied) do + if entity:get_position():equal(pos) then + return false + end + end + return true + end + + for _, pos in ipairs(self.non_hard_block_positions) do + if valid_position(pos) then + table.insert(available_spots, pos) + end + end + + return available_spots end - function Battle:handle_bomb_drop_input(player: Player) - local placed_bombs: {Entity} = itertools.sets_as_list( - self.bombs.on_ground.set, self.lightning_bombs.on_ground.set - ) - if joysticks.is_pressed(player.id, joysticks.BUTTONS.R) then - self.bombs:place_bomb(player, placed_bombs) - end - - if joysticks.is_pressed(player.id, joysticks.BUTTONS.Y) then - self.lightning_bombs:place_lightning_bomb(player, placed_bombs) - end + local placed_bombs: {Entity} = itertools.sets_as_list( + self.bombs.on_ground.set, self.lightning_bombs.on_ground.set + ) + if joysticks.is_pressed(player.id, joysticks.BUTTONS.R) then + self.bombs:place_bomb(player, placed_bombs) + end + + if joysticks.is_pressed(player.id, joysticks.BUTTONS.Y) then + self.lightning_bombs:place_lightning_bomb(player, placed_bombs) + end end function Battle:get_player(player_id: integer): Player - for _, player in ipairs(self.players) do - if player_id == player.id then - return player - end - end + for _, player in ipairs(self.players) do + if player_id == player.id then + return player + end + end - return nil + return nil end function Battle:handle_pause_input(): boolean - if self.state == "finished" then - return - end - - local function toggle_if_playing(player_id: integer) - if not characters.exist_selection_for(player_id) then - settings.number_of_players = settings.number_of_players + 1 - if settings.player_selection == "off" then - characters.new_selection(player_id) - else - characters.queue_selection_for(player_id) - end - scenes.set(self.player_selection) - - if self.state == "starting" then - -- If someone joins while game is still starting we restart so - -- they can join and play directly. - self.state = "restarting" - end - else - settings.number_of_players = settings.number_of_players - 1 - characters.remove_selection_for(player_id) - self.score_board:remove_player(player_id) - local possible_player = self:get_player(player_id) - if possible_player then - possible_player:remove() - end - scenes.set(self, true) - end - end - - for player_id=1, joysticks.maxplayers() do - if joysticks.is_pressed(player_id, joysticks.BUTTONS.START) then - self.countdown = countdown.new() - self.audio_paused = true - audio.pause() - scenes.set(pause_scene.new( - self, player_id, characters.exist_selection_for(player_id), toggle_if_playing) - ) - return true - end - end - - return false + if self.state == "finished" then + return + end + + local function toggle_if_playing(player_id: integer) + if not characters.exist_selection_for(player_id) then + settings.number_of_players = settings.number_of_players + 1 + if settings.player_selection == "off" then + characters.new_selection(player_id) + else + characters.queue_selection_for(player_id) + end + scenes.set(self.player_selection) + + if self.state == "starting" then + -- If someone joins while game is still starting we restart so + -- they can join and play directly. + self.state = "restarting" + end + else + settings.number_of_players = settings.number_of_players - 1 + characters.remove_selection_for(player_id) + self.score_board:remove_player(player_id) + local possible_player = self:get_player(player_id) + if possible_player then + possible_player:remove() + end + scenes.set(self, true) + end + end + + for player_id = 1, joysticks.maxplayers() do + if joysticks.is_pressed(player_id, joysticks.BUTTONS.START) then + self.countdown = countdown.new() + self.audio_paused = true + audio.pause() + scenes.set( + pause_scene.new( + self, + player_id, + characters.exist_selection_for(player_id), + toggle_if_playing + ) + ) + return true + end + end + + return false end -local is_alive = function(player: Player): boolean - return player:is_alive() -end +local is_alive = function(player: Player): boolean return player:is_alive() end -local is_dead = function(player: Player): boolean - return not player:is_alive() -end +local is_dead = function(player: Player): boolean return not player:is_alive() end function Battle:check_if_finished(dt: number) - if self.state == "finished" then - return - end - - self.win_condition:update(dt, self.players) - - if self.timer:out_of_time() and not self.win_condition.is_standing_count then - self.state = "finished" - -- Time out and have not entered standing count => draw - music:stop() - scenes.set(battle_end_scene.new_for_timeout(self)) - end - - if self.win_condition.have_winner then - self.state = "finished" - music:stop() - scenes.set(battle_end_scene.new_for_winners( - self, self.win_condition.winners, self.score_board) - ) - elseif self.win_condition.is_draw then - self.state = "finished" - music:stop() - scenes.set(battle_end_scene.new_for_draw(self)) - end + if self.state == "finished" then + return + end + + self.win_condition:update(dt, self.players) + + if self.timer:out_of_time() and not self.win_condition.is_standing_count then + self.state = "finished" + -- Time out and have not entered standing count => draw + music:stop() + scenes.set(battle_end_scene.new_for_timeout(self)) + end + + if self.win_condition.have_winner then + self.state = "finished" + music:stop() + scenes.set( + battle_end_scene.new_for_winners( + self, self.win_condition.winners, self.score_board + ) + ) + elseif self.win_condition.is_draw then + self.state = "finished" + music:stop() + scenes.set(battle_end_scene.new_for_draw(self)) + end end -function Battle:_handle_stun_cry(lookup: Lookup, alive_players: ReadableGroup, player: Player) - local hit_players = lookup:in_box_and_group(player:get_stun_cry_hitbox(), alive_players) - for _, hit_player in ipairs(hit_players) do - if hit_player.action ~= "stunned" then - -- Stun cry only stun someone not already stunned - -- so not possible to perpetually stun in area - hit_player:stun_for(1.5) - end - end +function Battle:_handle_stun_cry( + lookup: Lookup, alive_players: ReadableGroup, player: Player +) + local hit_players = lookup:in_box_and_group( + player:get_stun_cry_hitbox(), alive_players + ) + for _, hit_player in ipairs(hit_players) do + if hit_player.action ~= "stunned" then + -- Stun cry only stun someone not already stunned + -- so not possible to perpetually stun in area + hit_player:stun_for(1.5) + end + end end -function Battle:_handle_sonic_blast(lookup: Lookup, bombs_group: ReadableGroup, soft_blocks_group: ReadableGroup, player: Player) - local hit_bombs = lookup:in_box_and_group(player:get_sonic_blast_hitbox(), bombs_group) - for _, hit_bomb in ipairs(hit_bombs) do - hit_bomb:blow_up() - end - - local hit_blocks = lookup:in_box_and_group(player:get_sonic_blast_hitbox(), soft_blocks_group) - for _, hit_block in ipairs(hit_blocks) do - hit_block:reveal() - end +function Battle:_handle_sonic_blast( + lookup: Lookup, + bombs_group: ReadableGroup, + soft_blocks_group: ReadableGroup, + player: Player +) + local hit_bombs = lookup:in_box_and_group( + player:get_sonic_blast_hitbox(), bombs_group + ) + for _, hit_bomb in ipairs(hit_bombs) do + hit_bomb:blow_up() + end + + local hit_blocks = lookup:in_box_and_group( + player:get_sonic_blast_hitbox(), soft_blocks_group + ) + for _, hit_block in ipairs(hit_blocks) do + hit_block:reveal() + end end function Battle:update(dt: number) - if self.state == "restarting" then - self:load() - end - - local is_paused = self:handle_pause_input() - if is_paused then - return - end - - self.countdown:update(dt) - if not self.countdown:finished() then - return - end - if self.state == "starting" then - self.state = "ongoing" - music:play() - end - if self.audio_paused then - self.audio_paused = false - audio.unpause() - end - - self.timer:update(dt) - self.pressure_blocks.sudden_death = self.timer:sudden_death() - - self.pressure_blocks:update(dt) - local alive_players = itertools.filter(self.players, is_alive) - local dead_players = itertools.filter(self.players, is_dead) - local trace_collision = tracing.start("collision_groups") - self.alive_players_group:rebuild(alive_players) - local lookup = collision.new_lookup( - self.soft_blocks.on_ground.set, - self.hard_blocks.on_ground.set, - self.eggs.on_ground.set, - self.items.on_ground.set, - self.bombs.on_ground.set, - self.lightning_bombs.on_ground.set, - self.pressure_blocks.on_ground.set, - self.alive_players_group.set - ) - trace_collision:stop() - - local trace_bombs = tracing.start("bombs") - self.bombs:update(dt, self.blockers, self.explodables, self.blocks_bombs, self.alive_players_group, self.bombs.on_ground, self.bombs_bounce_on, lookup) - self.lightning_bombs:update(dt, self.lightning_blockers, self.alive_players_group, lookup) - trace_bombs:stop() - - local trace_players = tracing.start("players") - for _, player in ipairs(alive_players) do - local trace_player_update = tracing.start("update") - player:update(dt) - trace_player_update:stop() - local trace_player_movement = tracing.start("movement") - if player:accepts_input() then - self:handle_bomb_drop_input(player) - local area_close_to_player = player:movementbox():expand(grid.TILE_SIZE / 2) - local close_blocks = lookup:in_box_and_group(area_close_to_player, self.blocking_blocks_group) - movement.set_position_and_dir_from_input( - player, dt, close_blocks, self.lightning_bombs, self.blocks_bombs, self.alive_players_group, self.bombs.on_ground, lookup, - maps.get_layout():has_ice() - ) - dino_inputs.handle(dt, player, self.bombs, self.blocks_jump, self.bombs.on_ground, lookup) - end - trace_player_movement:stop() - local trace_player_interaction = tracing.start("interaction") - - self.items:possible_pickup_by(player) - - local hatching_dino = self.eggs:possible_hatcher(player) - if hatching_dino then - player:jumps_onto(hatching_dino) - self.player_animations.dinos:add_for(hatching_dino) - end - - if player:is_stun_crying() then - self:_handle_stun_cry(lookup, self.alive_players_group, player) - end - if player:is_sonic_blasting() then - self:_handle_sonic_blast(lookup, self.bombs.on_ground, self.soft_blocks.on_ground, player) - end - trace_player_interaction:stop() - end - trace_players:stop() - - local trace_entities = tracing.start("entities") - local hit_players = lookup:collisions_between(self.alive_players_group, self.pressure_blocks_group) - for player, _ in pairs(hit_players) do - player:kill() - end - for _, player in ipairs(dead_players) do - if settings.game_rules.players.mad_bombers then - self.mad_bombers:ensure_exist_for(player.id, player.sprite) - end - if player.is_removed then - self.mad_bombers.by_player[player.id] = nil - end - end - - if settings.clear_mad_bombers(self.timer:sudden_death()) then - -- Remove all mad bombers - self.mad_bombers.by_player = {} - end - - self.mad_bombers:update(dt) - self.items:update(dt) - self.eggs:update(dt) - - self.tic_counter = self.tic_counter + 1 - if self.tic_counter % 120 == 0 then - -- Only consider generating these every 120 tic (2 seconds) - -- This also helps with performance! - local available_spots = self:get_available_spots(alive_players) - self.items:possible_drop(available_spots) - self.items:drop_items_of(dead_players, available_spots) - end - - self.bombs.on_ground:cull_if_hit_by(lookup, self.pressure_blocks_group) - self.lightning_bombs.on_ground:cull_if_hit_by(lookup, self.pressure_blocks_group) - self.soft_blocks.on_ground:cull_if_hit_by(lookup, self.pressure_blocks_group) - self.hard_blocks.on_ground:cull_if_hit_by(lookup, self.pressure_blocks_group) - self.items.on_ground:cull_if_hit_by(lookup, self.pressure_blocks_group) - self.items.on_ground:cull_if_hit_by(lookup, self.bomb_entities_group) - self.eggs.on_ground:cull_if_hit_by(lookup, self.pressure_blocks_group) - self.eggs.on_ground:cull_if_hit_by(lookup, self.bomb_entities_group) - self.player_animations:update(dt) - self.soft_blocks:update(dt) - for _, soft_block in pairs(self.soft_blocks.on_ground.set) do - if not soft_block.is_exploding then - -- Do nothing - elseif soft_block.item then - self.items:drop_at(soft_block.position, soft_block.item) - soft_block.item = nil - elseif soft_block.egg then - self.eggs:spawn_at(soft_block.position) - soft_block.egg = false - end - end - - self:check_if_finished(dt) - trace_entities:stop() + if self.state == "restarting" then + self:load() + end + + local is_paused = self:handle_pause_input() + if is_paused then + return + end + + self.countdown:update(dt) + if not self.countdown:finished() then + return + end + if self.state == "starting" then + self.state = "ongoing" + music:play() + end + if self.audio_paused then + self.audio_paused = false + audio.unpause() + end + + self.timer:update(dt) + self.pressure_blocks.sudden_death = self.timer:sudden_death() + + self.pressure_blocks:update(dt) + local alive_players = itertools.filter(self.players, is_alive) + local dead_players = itertools.filter(self.players, is_dead) + local trace_collision = tracing.start("collision_groups") + self.alive_players_group:rebuild(alive_players) + local lookup = collision.new_lookup( + self.soft_blocks.on_ground.set, + self.hard_blocks.on_ground.set, + self.eggs.on_ground.set, + self.items.on_ground.set, + self.bombs.on_ground.set, + self.lightning_bombs.on_ground.set, + self.pressure_blocks.on_ground.set, + self.alive_players_group.set + ) + trace_collision:stop() + + local trace_bombs = tracing.start("bombs") + self.bombs:update( + dt, + self.blockers, + self.explodables, + self.blocks_bombs, + self.alive_players_group, + self.bombs.on_ground, + self.bombs_bounce_on, + lookup + ) + self.lightning_bombs:update( + dt, self.lightning_blockers, self.alive_players_group, lookup + ) + trace_bombs:stop() + + local trace_players = tracing.start("players") + for _, player in ipairs(alive_players) do + local trace_player_update = tracing.start("update") + player:update(dt) + trace_player_update:stop() + local trace_player_movement = tracing.start("movement") + if player:accepts_input() then + self:handle_bomb_drop_input(player) + local area_close_to_player = player:movementbox():expand(grid.TILE_SIZE / 2) + local close_blocks = lookup:in_box_and_group( + area_close_to_player, self.blocking_blocks_group + ) + movement.set_position_and_dir_from_input( + player, + dt, + close_blocks, + self.lightning_bombs, + self.blocks_bombs, + self.alive_players_group, + self.bombs.on_ground, + lookup, + maps.get_layout():has_ice() + ) + dino_inputs.handle( + dt, player, self.bombs, self.blocks_jump, self.bombs.on_ground, lookup + ) + end + trace_player_movement:stop() + local trace_player_interaction = tracing.start("interaction") + + self.items:possible_pickup_by(player) + + local hatching_dino = self.eggs:possible_hatcher(player) + if hatching_dino then + player:jumps_onto(hatching_dino) + self.player_animations.dinos:add_for(hatching_dino) + end + + if player:is_stun_crying() then + self:_handle_stun_cry(lookup, self.alive_players_group, player) + end + if player:is_sonic_blasting() then + self:_handle_sonic_blast( + lookup, self.bombs.on_ground, self.soft_blocks.on_ground, player + ) + end + trace_player_interaction:stop() + end + trace_players:stop() + + local trace_entities = tracing.start("entities") + local hit_players = lookup:collisions_between( + self.alive_players_group, self.pressure_blocks_group + ) + for player, _ in pairs(hit_players) do + player:kill() + end + for _, player in ipairs(dead_players) do + if settings.game_rules.players.mad_bombers then + self.mad_bombers:ensure_exist_for(player.id, player.sprite) + end + if player.is_removed then + self.mad_bombers.by_player[player.id] = nil + end + end + + if settings.clear_mad_bombers(self.timer:sudden_death()) then + -- Remove all mad bombers + self.mad_bombers.by_player = {} + end + + self.mad_bombers:update(dt) + self.items:update(dt) + self.eggs:update(dt) + + self.tic_counter = self.tic_counter + 1 + if self.tic_counter % 120 == 0 then + -- Only consider generating these every 120 tic (2 seconds) + -- This also helps with performance! + local available_spots = self:get_available_spots(alive_players) + self.items:possible_drop(available_spots) + self.items:drop_items_of(dead_players, available_spots) + end + + self.bombs.on_ground:cull_if_hit_by(lookup, self.pressure_blocks_group) + self.lightning_bombs.on_ground:cull_if_hit_by(lookup, self.pressure_blocks_group) + self.soft_blocks.on_ground:cull_if_hit_by(lookup, self.pressure_blocks_group) + self.hard_blocks.on_ground:cull_if_hit_by(lookup, self.pressure_blocks_group) + self.items.on_ground:cull_if_hit_by(lookup, self.pressure_blocks_group) + self.items.on_ground:cull_if_hit_by(lookup, self.bomb_entities_group) + self.eggs.on_ground:cull_if_hit_by(lookup, self.pressure_blocks_group) + self.eggs.on_ground:cull_if_hit_by(lookup, self.bomb_entities_group) + self.player_animations:update(dt) + self.soft_blocks:update(dt) + for _, soft_block in pairs(self.soft_blocks.on_ground.set) do + if not soft_block.is_exploding then + -- Do nothing + elseif soft_block.item then + self.items:drop_at(soft_block.position, soft_block.item) + soft_block.item = nil + elseif soft_block.egg then + self.eggs:spawn_at(soft_block.position) + soft_block.egg = false + end + end + + self:check_if_finished(dt) + trace_entities:stop() end local function sort_entities_by_y_cord(players_to_sort: {Entity}) - local function compare_entity_y_pos(e1: Entity, e2: Entity): boolean - if e1.z_index ~= e2.z_index then - return e1.z_index < e2.z_index - end - local pos1 = e1:get_position() - local pos2 = e2:get_position() - if pos1.y ~= pos2.y then - return pos1.y < pos2.y - end - if pos1.x ~= pos2.x then - return pos1.x < pos2.x - end - -- tostring gives a stable memory-address-based order within a session, - -- preventing flickering for entities at identical positions. - return tostring(e1) < tostring(e2) - end - table.sort(players_to_sort, compare_entity_y_pos) + local function compare_entity_y_pos(e1: Entity, e2: Entity): boolean + if e1.z_index ~= e2.z_index then + return e1.z_index < e2.z_index + end + local pos1 = e1:get_position() + local pos2 = e2:get_position() + if pos1.y ~= pos2.y then + return pos1.y < pos2.y + end + if pos1.x ~= pos2.x then + return pos1.x < pos2.x + end + -- tostring gives a stable memory-address-based order within a session, + -- preventing flickering for entities at identical positions. + return tostring(e1) < tostring(e2) + end + table.sort(players_to_sort, compare_entity_y_pos) end function Battle:draw() - background.draw() - self.timer:draw() - self.countdown:draw() - - for _, block in pairs(self.pressure_blocks:falling()) do - -- falling blocks are rendered last on top of everything, - -- but their shadows have to be behind anything - block:draw_shadow() - end - - -- When jumping in place, draw jump shadows under all entities - self.player_animations:draw_vertical_jump_shadows() - -- Bomb on ground shadows should also be under all entities - self.bombs:draw_on_ground_shadows() - - self.score_board:draw() - self.bombs.explosions:draw() - self.lightning_bombs.lightnings:draw() - - local entities_to_draw: {Entity} = itertools.sets_as_list( - self.bombs.on_ground.set, - self.lightning_bombs.on_ground.set, - self.pressure_blocks.on_ground.set, - self.pressure_blocks:falling(), - self.items.on_ground.set, - self.hard_blocks.on_ground.set, - self.soft_blocks.on_ground.set, - self.eggs.on_ground.set, - self.bombs.in_air.set, - self.mad_bombers.by_player, - self.player_animations:get_all() - ) - - -- Need to render entities in the front last so they are on top of any other entity - sort_entities_by_y_cord(entities_to_draw) - - for _, entity in ipairs(entities_to_draw) do - entity:draw() - hitboxes.draw(entity:hitbox()) - end - - -- Bomb thrown in air, draw throw shadows over all entities - self.bombs:draw_in_air_shadows() - -- When jumping some distance, draw jump shadows over all entities - self.player_animations:draw_lateral_jump_shadows() - -- Put stun cry text above everything else (with up exception) - self.player_animations:draw_stun_cry_texts_not_up() - - local mobility_related: {Kinematic} = itertools.sets_as_list( - self.bombs.on_ground.set, - self.lightning_bombs.on_ground.set, - self.pressure_blocks.on_ground.set, - self.hard_blocks.on_ground.set, - self.soft_blocks.on_ground.set, - self.alive_players_group.set - ) - for _, entity in ipairs(mobility_related) do - hitboxes.draw_movement(entity:movementbox()) - end - for _, player in pairs(self.alive_players_group.set) do - if player:is_stun_crying() then - hitboxes.draw(player:get_stun_cry_hitbox()) - end - if player:is_sonic_blasting() then - hitboxes.draw(player:get_sonic_blast_hitbox()) - end - end - - self.timer:draw_hurry_text() + background.draw() + self.timer:draw() + self.countdown:draw() + + for _, block in pairs(self.pressure_blocks:falling()) do + -- falling blocks are rendered last on top of everything, + -- but their shadows have to be behind anything + block:draw_shadow() + end + + -- When jumping in place, draw jump shadows under all entities + self.player_animations:draw_vertical_jump_shadows() + -- Bomb on ground shadows should also be under all entities + self.bombs:draw_on_ground_shadows() + + self.score_board:draw() + self.bombs.explosions:draw() + self.lightning_bombs.lightnings:draw() + + local entities_to_draw: {Entity} = itertools.sets_as_list( + self.bombs.on_ground.set, + self.lightning_bombs.on_ground.set, + self.pressure_blocks.on_ground.set, + self.pressure_blocks:falling(), + self.items.on_ground.set, + self.hard_blocks.on_ground.set, + self.soft_blocks.on_ground.set, + self.eggs.on_ground.set, + self.bombs.in_air.set, + self.mad_bombers.by_player, + self.player_animations:get_all() + ) + + -- Need to render entities in the front last so they are on top of any other entity + sort_entities_by_y_cord(entities_to_draw) + + for _, entity in ipairs(entities_to_draw) do + entity:draw() + hitboxes.draw(entity:hitbox()) + end + + -- Bomb thrown in air, draw throw shadows over all entities + self.bombs:draw_in_air_shadows() + -- When jumping some distance, draw jump shadows over all entities + self.player_animations:draw_lateral_jump_shadows() + -- Put stun cry text above everything else (with up exception) + self.player_animations:draw_stun_cry_texts_not_up() + + local mobility_related: {Kinematic} = itertools.sets_as_list( + self.bombs.on_ground.set, + self.lightning_bombs.on_ground.set, + self.pressure_blocks.on_ground.set, + self.hard_blocks.on_ground.set, + self.soft_blocks.on_ground.set, + self.alive_players_group.set + ) + for _, entity in ipairs(mobility_related) do + hitboxes.draw_movement(entity:movementbox()) + end + for _, player in pairs(self.alive_players_group.set) do + if player:is_stun_crying() then + hitboxes.draw(player:get_stun_cry_hitbox()) + end + if player:is_sonic_blasting() then + hitboxes.draw(player:get_sonic_blast_hitbox()) + end + end + + self.timer:draw_hurry_text() end local battle = {Battle = Battle} function battle.load_assets() - eggs.load_assets() - items.load_assets() - timer.load_assets() - bombs.load_assets() - explosions.load_assets() - lightnings.load_assets() - lightning_bombs.load_assets() - pressure_blocks.load_assets() - level_graphics.load_assets() - mad_bombers.load_assets() - countdown.load_assets() - score_board.load_assets() - pause_scene.load_assets() - battle_end_scene.load_assets() - dino_animations.load_assets() - players.load_assets() - player_animations.load_assets() - - music = audio.new_source("assets/music/battle.ogg", "static") - music:setLooping(true) - music:setVolume(0.75) + eggs.load_assets() + items.load_assets() + timer.load_assets() + bombs.load_assets() + explosions.load_assets() + lightnings.load_assets() + lightning_bombs.load_assets() + pressure_blocks.load_assets() + level_graphics.load_assets() + mad_bombers.load_assets() + countdown.load_assets() + score_board.load_assets() + pause_scene.load_assets() + battle_end_scene.load_assets() + dino_animations.load_assets() + players.load_assets() + player_animations.load_assets() + + music = audio.new_source("assets/music/battle.ogg", "static") + music:setLooping(true) + music:setVolume(0.75) end -function battle.new(player_selection: Scene, score_board_: ScoreBoard, win_condition_: WinCondition): Battle - return Battle.new(player_selection, score_board_, win_condition_) +function battle.new( + player_selection: Scene, score_board_: ScoreBoard, win_condition_: WinCondition +): Battle + return Battle.new(player_selection, score_board_, win_condition_) end return battle diff --git a/src/game/battle_end_scene.tl b/src/game/battle_end_scene.tl index e15e4ad..42f956d 100644 --- a/src/game/battle_end_scene.tl +++ b/src/game/battle_end_scene.tl @@ -1,5 +1,5 @@ -local audio = require("engine.audio") local characters = require("characters") +local audio = require("engine.audio") local scenes = require("engine.scenes") local score_board = require("game.score_board") local winner_scene = require("game.winner_scene") @@ -25,7 +25,7 @@ end function BattleEndScene.new( game_scene: Scene, next_scene: Scene, sound: Source, scene_lenght: number ): BattleEndScene - local self: BattleEndScene = setmetatable({}, { __index = BattleEndScene }) + local self: BattleEndScene = setmetatable({}, {__index = BattleEndScene}) self.game_scene = game_scene self.next_scene = next_scene self.played_sound = false @@ -48,15 +48,11 @@ function battle_end_scene.new_for_winners( return BattleEndScene.new(game_scene, get_next_scene(), winner_sound, 3) end -function battle_end_scene.new_for_draw( - game_scene: Scene -): BattleEndScene +function battle_end_scene.new_for_draw(game_scene: Scene): BattleEndScene return BattleEndScene.new(game_scene, game_scene, draw_sound, 4) end -function battle_end_scene.new_for_timeout( - game_scene: Scene -): BattleEndScene +function battle_end_scene.new_for_timeout(game_scene: Scene): BattleEndScene return BattleEndScene.new(game_scene, game_scene, time_up_sound, 2) end @@ -68,7 +64,7 @@ function battle_end_scene.load_assets() end function BattleEndScene:draw() - self.game_scene:draw() + self.game_scene:draw() end function BattleEndScene:update(dt: number) diff --git a/src/game/countdown.tl b/src/game/countdown.tl index aab7174..b99d9d2 100644 --- a/src/game/countdown.tl +++ b/src/game/countdown.tl @@ -9,9 +9,9 @@ local READY_AT_TIME = 3 local SHOW_TIME_MARGIN = 1 local record GameCountdown - animation: Animation + animation: Animation have_played_sound: boolean - waited_for: number + waited_for: number end local ready_set_go_image: Image @@ -22,21 +22,21 @@ local start_jingle: Source local countdown = {GameCountdown = GameCountdown} function countdown.load_assets() - ready_set_go_image = lutro.graphics.newImage("assets/ready_set_go.png") - ready_set_go_frame_image = lutro.graphics.newImage("assets/ready_set_go_frame.png") - ready_set_go_sound = audio.new_source("assets/ready_set_go.wav", "static") - start_jingle = audio.new_source("assets/battle-start.ogg", "static") - start_jingle:setVolume(0.35) + ready_set_go_image = lutro.graphics.newImage("assets/ready_set_go.png") + ready_set_go_frame_image = lutro.graphics.newImage("assets/ready_set_go_frame.png") + ready_set_go_sound = audio.new_source("assets/ready_set_go.wav", "static") + start_jingle = audio.new_source("assets/battle-start.ogg", "static") + start_jingle:setVolume(0.35) end function countdown.new(): GameCountdown - local self: GameCountdown = setmetatable({}, { __index = GameCountdown }) - local image_width = 52 - self.animation = animations.new(ready_set_go_image, image_width, 1) - self.have_played_sound = false - self.waited_for = 0.0 - ready_set_go_sound:stop() - start_jingle:stop() + local self: GameCountdown = setmetatable({}, {__index = GameCountdown}) + local image_width = 52 + self.animation = animations.new(ready_set_go_image, image_width, 1) + self.have_played_sound = false + self.waited_for = 0.0 + ready_set_go_sound:stop() + start_jingle:stop() return self end @@ -45,23 +45,23 @@ function GameCountdown:finished(): boolean end function GameCountdown:update(dt: number) - self.waited_for = self.waited_for + dt - if self.animation.loops == 0 then - self.animation:update(dt) - end - if not self.have_played_sound then - start_jingle:play() - ready_set_go_sound:play() - self.have_played_sound = true - end + self.waited_for = self.waited_for + dt + if self.animation.loops == 0 then + self.animation:update(dt) + end + if not self.have_played_sound then + start_jingle:play() + ready_set_go_sound:play() + self.have_played_sound = true + end end function GameCountdown:draw() - if self.waited_for < READY_AT_TIME + SHOW_TIME_MARGIN then - local position = TIMER_POS - lutro.graphics.draw(ready_set_go_frame_image, position.x, position.y) - self.animation:draw(position.x, position.y) - end + if self.waited_for < READY_AT_TIME + SHOW_TIME_MARGIN then + local position = TIMER_POS + lutro.graphics.draw(ready_set_go_frame_image, position.x, position.y) + self.animation:draw(position.x, position.y) + end end return countdown diff --git a/src/game/pause_scene.tl b/src/game/pause_scene.tl index ecab16b..40ede91 100644 --- a/src/game/pause_scene.tl +++ b/src/game/pause_scene.tl @@ -1,13 +1,13 @@ local audio = require("engine.audio") -local fonts = require("fonts") -local images = require("engine.images") -local joysticks = require("engine.joysticks") local points = require("engine.geometry.points") local rectangles = require("engine.geometry.rectangles") +local images = require("engine.images") +local joysticks = require("engine.joysticks") local scenes = require("engine.scenes") +local fonts = require("fonts") local screen = require("screen") -local type MenuFont = fonts.MenuFont +local type MenuFont = fonts.MenuFont local type Scene = scenes.Scene local type Rectangle = rectangles.Rectangle @@ -29,10 +29,7 @@ local function get_pause_square(): Rectangle local height = 50 local width = 180 return rectangles.new( - (screen.WIDTH - width) / 2, - (screen.HEIGHT - height) / 2, - width, - height + (screen.WIDTH - width) / 2, (screen.HEIGHT - height) / 2, width, height ) end @@ -42,7 +39,7 @@ function pause_scene.new( is_playing: boolean, toggle_if_playing: function(integer) ): GamePauseScene - local self: GamePauseScene = setmetatable({}, { __index = GamePauseScene }) + local self: GamePauseScene = setmetatable({}, {__index = GamePauseScene}) self.paused_scene = paused_scene self.player_id = player_id self.is_playing = is_playing @@ -65,7 +62,7 @@ function GamePauseScene:_time_to_exit(): integer end function GamePauseScene:draw() - self.paused_scene:draw() + self.paused_scene:draw() local window = get_pause_square() lutro.graphics.draw(pause_menu_image, window.x, window.y) local header_pos = points.new(window.x, window.y + 10) @@ -82,16 +79,20 @@ function GamePauseScene:draw() local exit_msg_pos = points.new(window.x, window.y + 34) if self.exit_counter < 0.1 then - white_font:print_centered("hold select down to exit", window.width, exit_msg_pos) + white_font:print_centered( + "hold select down to exit", window.width, exit_msg_pos + ) else - white_font:print_centered("exiting game in " .. self:_time_to_exit(), window.width, exit_msg_pos) + white_font:print_centered( + "exiting game in " .. self:_time_to_exit(), window.width, exit_msg_pos + ) end end function GamePauseScene:update(dt: number) - if joysticks.is_pressed(self.player_id, joysticks.BUTTONS.START) then - scenes.set(self.paused_scene, true) - end + if joysticks.is_pressed(self.player_id, joysticks.BUTTONS.START) then + scenes.set(self.paused_scene, true) + end if joysticks.is_pressed(self.player_id, joysticks.BUTTONS.L) then self.toggle_if_playing(self.player_id) end diff --git a/src/game/score_board.tl b/src/game/score_board.tl index 50b65e7..39a05fc 100644 --- a/src/game/score_board.tl +++ b/src/game/score_board.tl @@ -1,9 +1,9 @@ local characters = require("characters") -local fonts = require("fonts") -local images = require("engine.images") local points = require("engine.geometry.points") local rectangles = require("engine.geometry.rectangles") local vectors = require("engine.geometry.vectors") +local images = require("engine.images") +local fonts = require("fonts") local screen = require("screen") local settings = require("settings") local teams = require("teams") @@ -19,14 +19,14 @@ local type TeamSelection = teams.TeamSelection local score_font: ShadedFont local avatar_images: {CharacterName: Image} -local COLOR_BLOCK_SIZE = 19 -local AVATAR_WIDTH = 18 -local SCORE_WIDTH = 17 -local X_START = 75 -local Y_START = 3 -local Y_SCORE_ADJUST = 5 -local AVATAR_SPACE_PADDING = 10 -local TOTAL_WIDTH = screen.WIDTH - X_START - AVATAR_SPACE_PADDING +local COLOR_BLOCK_SIZE = 19 +local AVATAR_WIDTH = 18 +local SCORE_WIDTH = 17 +local X_START = 75 +local Y_START = 3 +local Y_SCORE_ADJUST = 5 +local AVATAR_SPACE_PADDING = 10 +local TOTAL_WIDTH = screen.WIDTH - X_START - AVATAR_SPACE_PADDING local interface ScoreBoard draw: function(self) @@ -52,7 +52,7 @@ local record TeamScore end function TeamScore.new(team: Team, avatars: {CharacterSelection}): TeamScore - local self: TeamScore = setmetatable({}, { __index = TeamScore }) + local self: TeamScore = setmetatable({}, {__index = TeamScore}) self.team = team self.score = 0 self.avatars = avatars @@ -67,10 +67,14 @@ local record TeamsScoreBoard is ScoreBoard teams: {TeamScore} end -local score_board = {ScoreBoard = ScoreBoard, SinglesScoreBoard = SinglesScoreBoard, TeamsScoreBoard = TeamsScoreBoard} +local score_board = { + ScoreBoard = ScoreBoard, + SinglesScoreBoard = SinglesScoreBoard, + TeamsScoreBoard = TeamsScoreBoard, +} function PlayerScore.new(id: integer, avatar: CharacterName): PlayerScore - local self: PlayerScore = setmetatable({}, { __index = PlayerScore }) + local self: PlayerScore = setmetatable({}, {__index = PlayerScore}) self.id = id self.score = 0 self.avatar = avatar @@ -78,22 +82,22 @@ function PlayerScore.new(id: integer, avatar: CharacterName): PlayerScore end local function get_avatar_position(avatar_num: number, num_avatars: number): Point - local space_for_avatar = AVATAR_WIDTH + SCORE_WIDTH + local space_for_avatar = AVATAR_WIDTH + SCORE_WIDTH - local padding = (TOTAL_WIDTH - space_for_avatar * num_avatars) / (num_avatars + 1) - local x_pos = X_START + padding * avatar_num + space_for_avatar * (avatar_num - 1) - return points.new(x_pos, Y_START) + local padding = (TOTAL_WIDTH - space_for_avatar * num_avatars) / (num_avatars + 1) + local x_pos = X_START + padding * avatar_num + space_for_avatar * (avatar_num - 1) + return points.new(x_pos, Y_START) end function SinglesScoreBoard:draw() local score_adjust = vectors.new(AVATAR_WIDTH, Y_SCORE_ADJUST) for nbr, player in ipairs(self.players) do - local avatar_pos = get_avatar_position(nbr, #self.players) - local score_pos = avatar_pos:move(score_adjust) - lutro.graphics.draw(avatar_images[player.avatar], avatar_pos.x, avatar_pos.y) - score_font:draw(tostring(player.score), score_pos) - end + local avatar_pos = get_avatar_position(nbr, #self.players) + local score_pos = avatar_pos:move(score_adjust) + lutro.graphics.draw(avatar_images[player.avatar], avatar_pos.x, avatar_pos.y) + score_font:draw(tostring(player.score), score_pos) + end end function SinglesScoreBoard:add_point(player_id: integer) @@ -139,7 +143,7 @@ function SinglesScoreBoard:game_finished(): boolean end function score_board.new_for_singles(): SinglesScoreBoard - local self: SinglesScoreBoard = setmetatable({}, { __index = SinglesScoreBoard }) + local self: SinglesScoreBoard = setmetatable({}, {__index = SinglesScoreBoard}) self.players = {} return self end @@ -176,7 +180,7 @@ function TeamsScoreBoard:draw() local team_background = get_team_background_area(x, team) teams.draw_square(team.team, team_background) draw_team_boarder(team_background) - + for _, avatar in ipairs(team.avatars) do lutro.graphics.draw(avatar_images[avatar.sprite], x, Y_START) x = x + AVATAR_WIDTH @@ -221,12 +225,14 @@ end function TeamsScoreBoard:update_avatars(selections: {TeamSelection}) self.teams = {} for _, selection_group in ipairs(teams.grouped_selections(selections)) do - table.insert(self.teams, TeamScore.new(selection_group.team, selection_group.selections)) + table.insert( + self.teams, TeamScore.new(selection_group.team, selection_group.selections) + ) end end function score_board.new_for_teams(): TeamsScoreBoard - local self: TeamsScoreBoard = setmetatable({}, { __index = TeamsScoreBoard }) + local self: TeamsScoreBoard = setmetatable({}, {__index = TeamsScoreBoard}) self.teams = {} return self end diff --git a/src/game/timer.tl b/src/game/timer.tl index 43c1ca7..f03251d 100644 --- a/src/game/timer.tl +++ b/src/game/timer.tl @@ -1,16 +1,15 @@ -local audio = require("engine.audio") local clock_text = require("clock_text") -local fonts = require("fonts") -local itertools = require("itertools") +local audio = require("engine.audio") local points = require("engine.geometry.points") local vectors = require("engine.geometry.vectors") +local fonts = require("fonts") +local itertools = require("itertools") local screen = require("screen") local settings = require("settings") local type ShadedFont = fonts.ShadedFont local type Point = points.Point - local TIMER_POS = points.new(18, 8) local WARNING_MARGIN = 2 local TEXT_X_SPEED = vectors.new(-200, 0) @@ -18,11 +17,11 @@ local TEXT_SIN_PERIOD = 9 local TEXT_SIN_AMPLITUDE = 5 local record GameTimer - time_left: number - have_played_hurry_up_sound: boolean - hurry_text_position: Point - hurry_text_sin_pos: number - hurry_text_mid_countdown: number + time_left: number + have_played_hurry_up_sound: boolean + hurry_text_position: Point + hurry_text_sin_pos: number + hurry_text_mid_countdown: number end local clock_font: ShadedFont @@ -33,82 +32,84 @@ local hurry_up_sound: Source local timer = {GameTimer = GameTimer} function timer.new(): GameTimer - local self: GameTimer = setmetatable({}, { __index = GameTimer }) - self.time_left = settings.game_rules.timings.battle - self.have_played_hurry_up_sound = false - self.hurry_text_position = hurry_letters_start - self.hurry_text_sin_pos = math.pi - self.hurry_text_mid_countdown = 1 - return self + local self: GameTimer = setmetatable({}, {__index = GameTimer}) + self.time_left = settings.game_rules.timings.battle + self.have_played_hurry_up_sound = false + self.hurry_text_position = hurry_letters_start + self.hurry_text_sin_pos = math.pi + self.hurry_text_mid_countdown = 1 + return self end function timer.load_assets() - clock_font = fonts.new_shaded_numbers() - hurry_up_sound = audio.new_source("assets/hurry-up.wav", "static") - - local function img_letter(letter: string): Image - return lutro.graphics.newImage("assets/imgfont/" .. letter .. ".png") - end - - hurry_letters = itertools.map({"h", "u", "r", "r", "y", "!"}, img_letter) - local letter_box_width = 0.0 - for _, letter in ipairs(hurry_letters) do - letter_box_width = letter_box_width + letter:getWidth() - end - hurry_letters_mid = (screen.WIDTH - letter_box_width) / 2 - hurry_letters_start = points.new( - screen.WIDTH, - (screen.HEIGHT - hurry_letters[1]:getHeight()) / 2 - ) + clock_font = fonts.new_shaded_numbers() + hurry_up_sound = audio.new_source("assets/hurry-up.wav", "static") + + local function img_letter(letter: string): Image + return lutro.graphics.newImage("assets/imgfont/" .. letter .. ".png") + end + + hurry_letters = itertools.map({"h", "u", "r", "r", "y", "!"}, img_letter) + local letter_box_width = 0.0 + for _, letter in ipairs(hurry_letters) do + letter_box_width = letter_box_width + letter:getWidth() + end + hurry_letters_mid = (screen.WIDTH - letter_box_width) / 2 + hurry_letters_start = points.new( + screen.WIDTH, (screen.HEIGHT - hurry_letters[1]:getHeight()) / 2 + ) end function GameTimer:finished(): boolean - return self.time_left <= 0.0 + return self.time_left <= 0.0 end function GameTimer:sudden_death(): boolean - return self.time_left <= settings.game_rules.timings.sudden_death + return self.time_left <= settings.game_rules.timings.sudden_death end function GameTimer:warn_for_sudden_death(): boolean - return self.time_left - WARNING_MARGIN <= settings.game_rules.timings.sudden_death + return self.time_left - WARNING_MARGIN <= settings.game_rules.timings.sudden_death end function GameTimer:update(dt: number) - self.time_left = self.time_left - dt - if self:warn_for_sudden_death() and not self.have_played_hurry_up_sound then - self.have_played_hurry_up_sound = true - hurry_up_sound:play() - end - if self:warn_for_sudden_death() then - if self.hurry_text_position.x < hurry_letters_mid and self.hurry_text_mid_countdown > 0 then - self.hurry_text_mid_countdown = self.hurry_text_mid_countdown - dt - else - self.hurry_text_position = self.hurry_text_position:move(TEXT_X_SPEED:scale(dt)) - end - self.hurry_text_sin_pos = self.hurry_text_sin_pos + TEXT_SIN_PERIOD * dt - self.hurry_text_sin_pos = math.fmod(self.hurry_text_sin_pos, 2*math.pi) - end + self.time_left = self.time_left - dt + if self:warn_for_sudden_death() and not self.have_played_hurry_up_sound then + self.have_played_hurry_up_sound = true + hurry_up_sound:play() + end + if self:warn_for_sudden_death() then + if self.hurry_text_position.x < hurry_letters_mid + and self.hurry_text_mid_countdown > 0 then + self.hurry_text_mid_countdown = self.hurry_text_mid_countdown - dt + else + self.hurry_text_position = self.hurry_text_position:move( + TEXT_X_SPEED:scale(dt) + ) + end + self.hurry_text_sin_pos = self.hurry_text_sin_pos + TEXT_SIN_PERIOD * dt + self.hurry_text_sin_pos = math.fmod(self.hurry_text_sin_pos, 2 * math.pi) + end end function GameTimer:out_of_time(): boolean - return self.time_left < 0 + return self.time_left < 0 end function GameTimer:draw() - local text = clock_text.from_time(self.time_left) - clock_font:draw(text, TIMER_POS) + local text = clock_text.from_time(self.time_left) + clock_font:draw(text, TIMER_POS) end function GameTimer:draw_hurry_text() - local letter_position = self.hurry_text_position - local letter_sin_pos = self.hurry_text_sin_pos - for _, letter in ipairs(hurry_letters) do - local y_pos = letter_position.y + math.sin(letter_sin_pos) * TEXT_SIN_AMPLITUDE - lutro.graphics.draw(letter, letter_position.x, y_pos) - letter_position = letter_position:move(vectors.new(letter:getWidth(), 0)) - letter_sin_pos = letter_sin_pos + math.pi / 5 - end + local letter_position = self.hurry_text_position + local letter_sin_pos = self.hurry_text_sin_pos + for _, letter in ipairs(hurry_letters) do + local y_pos = letter_position.y + math.sin(letter_sin_pos) * TEXT_SIN_AMPLITUDE + lutro.graphics.draw(letter, letter_position.x, y_pos) + letter_position = letter_position:move(vectors.new(letter:getWidth(), 0)) + letter_sin_pos = letter_sin_pos + math.pi / 5 + end end return timer diff --git a/src/game/win_condition.tl b/src/game/win_condition.tl index 85b38d9..64dc7ea 100644 --- a/src/game/win_condition.tl +++ b/src/game/win_condition.tl @@ -1,6 +1,6 @@ local characters = require("characters") -local players = require("players") local score_board = require("game.score_board") +local players = require("players") local teams = require("teams") local type CharacterName = characters.CharacterName @@ -28,8 +28,10 @@ local record TeamsWinCondition is WinCondition teams_by_player_id: {integer: integer} end -local win_condition = {WinCondition = WinCondition, TeamsWinCondition = TeamsWinCondition} -local time_to_wait_to_end_game = 2.0 +local win_condition = { + WinCondition = WinCondition, TeamsWinCondition = TeamsWinCondition +} +local time_to_wait_to_end_game = 2.0 function SinglesWinCondition:update(dt: number, players_: {Player}) local alive_players: {Player} = {} @@ -44,7 +46,8 @@ function SinglesWinCondition:update(dt: number, players_: {Player}) self.is_standing_count = true end - if not self.have_winner and self.time_of_1_alive_player > time_to_wait_to_end_game then + if not self.have_winner + and self.time_of_1_alive_player > time_to_wait_to_end_game then local winner = alive_players[1] self.score_board:add_point(winner.id) self.have_winner = true @@ -63,7 +66,7 @@ function SinglesWinCondition:reset() end function win_condition.new_for_singles(score_board_: ScoreBoard): SinglesWinCondition - local self: SinglesWinCondition = setmetatable({}, { __index = SinglesWinCondition }) + local self: SinglesWinCondition = setmetatable({}, {__index = SinglesWinCondition}) self.score_board = score_board_ self:reset() return self @@ -88,7 +91,8 @@ function TeamsWinCondition:update(dt: number, players_: {Player}) self.is_standing_count = true end - if not self.have_winner and self.time_of_1_alive_team > time_to_wait_to_end_game then + if not self.have_winner + and self.time_of_1_alive_team > time_to_wait_to_end_game then for winning_team, _ in pairs(alive_teams) do self.score_board:add_point(winning_team) end @@ -104,11 +108,11 @@ function TeamsWinCondition:update(dt: number, players_: {Player}) end function TeamsWinCondition:reset() - self.time_of_1_alive_team = 0 - self.is_standing_count = false - self.is_draw = false - self.have_winner = false - self.winners = {} + self.time_of_1_alive_team = 0 + self.is_standing_count = false + self.is_draw = false + self.have_winner = false + self.winners = {} end function TeamsWinCondition:update_team_mapping(selections: {TeamSelection}) @@ -119,11 +123,11 @@ function TeamsWinCondition:update_team_mapping(selections: {TeamSelection}) end function win_condition.new_for_teams(score_board_: ScoreBoard): TeamsWinCondition - local self: TeamsWinCondition = setmetatable({}, { __index = TeamsWinCondition }) - self.score_board = score_board_ - self.teams_by_player_id = {} - self:reset() - return self + local self: TeamsWinCondition = setmetatable({}, {__index = TeamsWinCondition}) + self.score_board = score_board_ + self.teams_by_player_id = {} + self:reset() + return self end return win_condition diff --git a/src/game/winner_scene.tl b/src/game/winner_scene.tl index 159cfe8..60a44ea 100644 --- a/src/game/winner_scene.tl +++ b/src/game/winner_scene.tl @@ -1,6 +1,6 @@ +local characters = require("characters") local animations = require("engine.animations") local audio = require("engine.audio") -local characters = require("characters") local points = require("engine.geometry.points") local vectors = require("engine.geometry.vectors") local scenes = require("engine.scenes") @@ -13,14 +13,14 @@ local type Vector = vectors.Vector local type Point = points.Point local record GameWinnerScene is Scene - names: {CharacterName} - trophy: Animation - trophy_positions: {Point} - character_positions: {CharacterName: Point} - character_jump_velocity: Vector - sparks: {Vector: Animation} + names: {CharacterName} + trophy: Animation + trophy_positions: {Point} + character_positions: {CharacterName: Point} + character_jump_velocity: Vector + sparks: {Vector: Animation} have_played_sound: boolean - waited_for: number + waited_for: number end local trophy_image: Image @@ -32,106 +32,119 @@ local music: Source local winner_scene = {} function winner_scene.load_assets() - trophy_image = lutro.graphics.newImage("assets/trophy.png") - spark_1_image = lutro.graphics.newImage("assets/spark-1.png") - spark_2_image = lutro.graphics.newImage("assets/spark-2.png") - jump_animations = {} - for _, character_name in ipairs(characters.get_names()) do - jump_animations[character_name] = lutro.graphics.newImage( - "assets/" .. character_name .. "/foot/jump_down.png" - ) - end - music = audio.new_source("assets/music/winner.ogg", "static") - music:setVolume(0.75) + trophy_image = lutro.graphics.newImage("assets/trophy.png") + spark_1_image = lutro.graphics.newImage("assets/spark-1.png") + spark_2_image = lutro.graphics.newImage("assets/spark-2.png") + jump_animations = {} + for _, character_name in ipairs(characters.get_names()) do + jump_animations[character_name] = lutro.graphics.newImage( + "assets/" .. character_name .. "/foot/jump_down.png" + ) + end + music = audio.new_source("assets/music/winner.ogg", "static") + music:setVolume(0.75) end local function get_x_pos(nbr: integer, total: integer): number - local space = 40 - local total_space = space * (total - 1) - local y_start = (screen.WIDTH - total_space) / 2 - return y_start + space * (nbr - 1) + local space = 40 + local total_space = space * (total - 1) + local y_start = (screen.WIDTH - total_space) / 2 + return y_start + space * (nbr - 1) end function winner_scene.new(names: {CharacterName}): GameWinnerScene - local self: GameWinnerScene = setmetatable({}, { __index = GameWinnerScene }) - self.names = names - self.trophy = animations.new_with_frames(trophy_image, {2, 0.1, 0.1, 1, 0.1}) - self.trophy_positions = {} - self.character_positions = {} - for i, name in ipairs(names) do - local x_pos_adjust = jump_animations[name]:getWidth() / 2 - self.character_positions[name] = points.new(get_x_pos(i, #names) - x_pos_adjust, screen.HEIGHT) - table.insert(self.trophy_positions, points.new( - get_x_pos(i, #names) - self.trophy.width / 2, - (screen.HEIGHT - self.trophy.height) / 2 - )) - end - self.character_jump_velocity = vectors.new(0, -47) - self.have_played_sound = false - self.waited_for = 0.0 - - self.sparks = {} - local function add_random_spark(spark_image: Image, start_time: number): Vector, Animation - local spark = animations.new_with_frames(spark_image, {2.7, 0.2, 0.2, 0.2}) - spark.timer = (2.7 - start_time) + lutro.math.random() * 0.1 - local spark_dp = vectors.new(lutro.math.random(-1, 17), lutro.math.random(-1, 23)) - self.sparks[spark_dp] = spark - end - - for _=1, 4 do - add_random_spark(spark_1_image, 2.15) - add_random_spark(spark_2_image, 3.25) - end + local self: GameWinnerScene = setmetatable({}, {__index = GameWinnerScene}) + self.names = names + self.trophy = animations.new_with_frames(trophy_image, {2, 0.1, 0.1, 1, 0.1}) + self.trophy_positions = {} + self.character_positions = {} + for i, name in ipairs(names) do + local x_pos_adjust = jump_animations[name]:getWidth() / 2 + self.character_positions[name] = points.new( + get_x_pos(i, #names) - x_pos_adjust, screen.HEIGHT + ) + table.insert( + self.trophy_positions, + points.new( + get_x_pos(i, #names) - self.trophy.width / 2, + (screen.HEIGHT - self.trophy.height) / 2 + ) + ) + end + self.character_jump_velocity = vectors.new(0, -47) + self.have_played_sound = false + self.waited_for = 0.0 + + self.sparks = {} + local function add_random_spark( + spark_image: Image, start_time: number + ): Vector, Animation + local spark = animations.new_with_frames(spark_image, {2.7, 0.2, 0.2, 0.2}) + spark.timer = (2.7 - start_time) + lutro.math.random() * 0.1 + local spark_dp = vectors.new( + lutro.math.random(-1, 17), lutro.math.random(-1, 23) + ) + self.sparks[spark_dp] = spark + end + + for _ = 1, 4 do + add_random_spark(spark_1_image, 2.15) + add_random_spark(spark_2_image, 3.25) + end return self end function GameWinnerScene:_get_trophy_height_diff(): number - local period = 3 - local amplitude = 8 - return math.sin(self.waited_for * period) * amplitude + local period = 3 + local amplitude = 8 + return math.sin(self.waited_for * period) * amplitude end function GameWinnerScene:update(dt: number) - self.waited_for = self.waited_for + dt - - local gravity = vectors.new(0, 10):scale(dt) - self.character_jump_velocity = self.character_jump_velocity:plus(gravity) - for name, character_position in pairs(self.character_positions) do - self.character_positions[name] = character_position:move(self.character_jump_velocity:scale(dt)) - end - - self.trophy:update(dt) - for _, spark in pairs(self.sparks) do - spark:update(dt) - end - - if not music:isPlaying() then - music:play() - end - - if self.character_jump_velocity.y > -5 then - music:stop() - scenes.return_to_start() - end + self.waited_for = self.waited_for + dt + + local gravity = vectors.new(0, 10):scale(dt) + self.character_jump_velocity = self.character_jump_velocity:plus(gravity) + for name, character_position in pairs(self.character_positions) do + self.character_positions[name] = character_position:move( + self.character_jump_velocity:scale(dt) + ) + end + + self.trophy:update(dt) + for _, spark in pairs(self.sparks) do + spark:update(dt) + end + + if not music:isPlaying() then + music:play() + end + + if self.character_jump_velocity.y > -5 then + music:stop() + scenes.return_to_start() + end end function GameWinnerScene:draw() - lutro.graphics.setColor(72, 72, 72) - lutro.graphics.rectangle("fill", 0, 0, screen.WIDTH, screen.HEIGHT) - - for name, character_position in pairs(self.character_positions) do - lutro.graphics.draw(jump_animations[name], character_position.x, character_position.y) - end - - for _, trophy_position in ipairs(self.trophy_positions) do - local pos = trophy_position:move(vectors.new(0, self:_get_trophy_height_diff())) - self.trophy:draw(pos.x, pos.y) - for spark_dp, spark_anim in pairs(self.sparks) do - local spark_pos = pos:move(spark_dp) - spark_anim:draw(spark_pos.x, spark_pos.y) - end - end + lutro.graphics.setColor(72, 72, 72) + lutro.graphics.rectangle("fill", 0, 0, screen.WIDTH, screen.HEIGHT) + + for name, character_position in pairs(self.character_positions) do + lutro.graphics.draw( + jump_animations[name], character_position.x, character_position.y + ) + end + + for _, trophy_position in ipairs(self.trophy_positions) do + local pos = trophy_position:move(vectors.new(0, self:_get_trophy_height_diff())) + self.trophy:draw(pos.x, pos.y) + for spark_dp, spark_anim in pairs(self.sparks) do + local spark_pos = pos:move(spark_dp) + spark_anim:draw(spark_pos.x, spark_pos.y) + end + end end return winner_scene diff --git a/src/hard_blocks.tl b/src/hard_blocks.tl index a5e5f72..4d8c5cc 100644 --- a/src/hard_blocks.tl +++ b/src/hard_blocks.tl @@ -1,11 +1,11 @@ local animations = require("engine.animations") local collision = require("engine.collision") -local grid = require("level_layout.grid") -local hard_block_animations = require("level_graphics.hard_block_animations") -local hitboxes = require("engine.hitboxes") -local map_entities = require("level_layout.map_entities") local points = require("engine.geometry.points") local rectangles = require("engine.geometry.rectangles") +local hitboxes = require("engine.hitboxes") +local hard_block_animations = require("level_graphics.hard_block_animations") +local grid = require("level_layout.grid") +local map_entities = require("level_layout.map_entities") local z_index = require("z_index") local type Animation = animations.Animation @@ -18,22 +18,22 @@ local type Rectangle = rectangles.Rectangle local type HardBlockConfig = map_entities.HardBlockConfig local record HardBlock is Explodable, Kinematic, Entity - position: Point - in_explosion: boolean + position: Point + in_explosion: boolean animation: Animation end function HardBlock.new(position: Point, is_tall: boolean): HardBlock - local self: HardBlock = setmetatable({}, { __index = HardBlock }) - self.position = position - self.z_index = z_index.LEVEL - self.in_explosion = false - self.animation = hard_block_animations.get(is_tall) - return self + local self: HardBlock = setmetatable({}, {__index = HardBlock}) + self.position = position + self.z_index = z_index.LEVEL + self.in_explosion = false + self.animation = hard_block_animations.get(is_tall) + return self end function HardBlock:get_position(): Point - return self.position + return self.position end function HardBlock:draw() @@ -42,29 +42,29 @@ function HardBlock:draw() end function HardBlock:hit_with_bomb() - -- Does nothing + -- Does nothing end function HardBlock:blow_up() - -- Does nothing + -- Does nothing end function HardBlock:hitbox(): Rectangle - return hitboxes.new_block(self.position):shrink(4) + return hitboxes.new_block(self.position):shrink(4) end function HardBlock:movementbox(): Rectangle - return hitboxes.new_block(self.position) + return hitboxes.new_block(self.position) end local record HardBlocks - on_ground: Group + on_ground: Group end local hard_blocks = {HardBlock = HardBlock, HardBlocks = HardBlocks} function hard_blocks.new(configs: {HardBlockConfig}): HardBlocks - local self: HardBlocks = setmetatable({}, { __index = HardBlocks }) + local self: HardBlocks = setmetatable({}, {__index = HardBlocks}) self.on_ground = collision.new_group() for _, config in ipairs(configs) do self.on_ground:add(HardBlock.new(config.position, config.is_tall)) diff --git a/src/items.tl b/src/items.tl index 18a735a..8d61863 100644 --- a/src/items.tl +++ b/src/items.tl @@ -1,11 +1,11 @@ local animations = require("engine.animations") local audio = require("engine.audio") local collision = require("engine.collision") -local itertools = require("itertools") -local hitboxes = require("engine.hitboxes") -local players = require("players") local points = require("engine.geometry.points") local rectangles = require("engine.geometry.rectangles") +local hitboxes = require("engine.hitboxes") +local itertools = require("itertools") +local players = require("players") local settings = require("settings") local z_index = require("z_index") @@ -31,7 +31,7 @@ local record Item is Entity, Explodable end function Item.new(item_type: ItemType, position: Point): Item - local self: Item = setmetatable({}, { __index = Item }) + local self: Item = setmetatable({}, {__index = Item}) self.type = item_type self.position = position self.z_index = z_index.LEVEL @@ -49,7 +49,7 @@ function Item:blow_up() end function Item:hitbox(): Rectangle - return hitboxes.new_block(self.position):shrink(4) + return hitboxes.new_block(self.position):shrink(4) end function Item:draw() @@ -60,9 +60,9 @@ function Item:draw() local item_icon = item_icons[self.type] local y_draw = self.position.y - 9 local x_draw = self.position.x - 1 - frame_animation:draw(x_draw, y_draw) - lutro.graphics.draw(item_icon, x_draw, y_draw) - lutro.graphics.draw(item_shadow, x_draw, y_draw) + frame_animation:draw(x_draw, y_draw) + lutro.graphics.draw(item_icon, x_draw, y_draw) + lutro.graphics.draw(item_shadow, x_draw, y_draw) end function Item:is_destroyed(): boolean @@ -77,10 +77,10 @@ end local items = {Items = Items} function items.new(): Items - local self: Items = setmetatable({}, { __index = Items }) - self.on_ground = collision.new_group() - self.by_players = {} - return self + local self: Items = setmetatable({}, {__index = Items}) + self.on_ground = collision.new_group() + self.by_players = {} + return self end function Items:possible_drop(available_spots: {Point}) @@ -133,8 +133,10 @@ function Items:drop_items_of(players_: {Player}, available_spots: {Point}) self.by_players[player.id] = nil for i, item in ipairs(shuffled_items) do if i <= max_items_to_drop then - local spot_to_drop = available_spots[lutro.math.random(#available_spots)] - self:drop_at(spot_to_drop, item) + local spot_to_drop = available_spots[lutro.math.random( + #available_spots + )] + self:drop_at(spot_to_drop, item) end end end @@ -171,13 +173,14 @@ function Items:possible_pickup_by(player: Player) end end - function items.load_assets() take_item_sound = audio.new_source_pool("assets/take_item.wav", "static", 5) take_item_sound:setVolume(0.75) - frame_animation = animations.new(lutro.graphics.newImage("assets/items/frame.png"), 18, 1 / 30) - item_shadow = lutro.graphics.newImage("assets/items/shadow.png") - item_icons = { + frame_animation = animations.new( + lutro.graphics.newImage("assets/items/frame.png"), 18, 1 / 30 + ) + item_shadow = lutro.graphics.newImage("assets/items/shadow.png") + item_icons = { speed = lutro.graphics.newImage("assets/items/speed.png"), bomb = lutro.graphics.newImage("assets/items/bomb.png"), lightning_bomb = lutro.graphics.newImage("assets/items/lightning-bomb.png"), diff --git a/src/itertools.tl b/src/itertools.tl index c038d32..e4fa922 100644 --- a/src/itertools.tl +++ b/src/itertools.tl @@ -1,88 +1,91 @@ local record LoopIndex - start: integer - end_: integer - step: integer + start: integer + end_: integer + step: integer end local itertools = {} function itertools.new_loop(start: integer, end_: integer, step?: integer): LoopIndex - local self: LoopIndex = setmetatable({}, { __index = LoopIndex }) - self.start = start - self.end_ = end_ - self.step = step or 1 - return self + local self: LoopIndex = setmetatable({}, {__index = LoopIndex}) + self.start = start + self.end_ = end_ + self.step = step or 1 + return self end function LoopIndex:reverse(): LoopIndex - return itertools.new_loop(self.end_, self.start, -1 * self.step) + return itertools.new_loop(self.end_, self.start, -1 * self.step) end function LoopIndex:values(): function(): integer - local val = self.start - self.step - local function value_iterator(): integer - val = val + self.step - if (val <= self.end_ and self.step > 0) or (val >= self.end_ and self.step < 0) then - return val - end - end - - return value_iterator + local val = self.start - self.step + local function value_iterator(): integer + val = val + self.step + if (val <= self.end_ and self.step > 0) + or (val >= self.end_ and self.step < 0) then + return val + end + end + + return value_iterator end function itertools.double_loop_inner_loopback( - outer_range: {integer, integer, integer}, inner_range: {integer, integer, integer} + outer_range: {integer, integer, integer}, inner_range: {integer, integer, integer} ): function(): integer, integer - local outer = itertools.new_loop(outer_range[1], outer_range[2], outer_range[3]) - local inner = itertools.new_loop(inner_range[1], inner_range[2], inner_range[3]) - local values: {{integer, integer}} = {} - for j in outer:values() do - for i in inner:values() do - table.insert(values, {j, i}) - end - inner = inner:reverse() - end + local outer = itertools.new_loop(outer_range[1], outer_range[2], outer_range[3]) + local inner = itertools.new_loop(inner_range[1], inner_range[2], inner_range[3]) + local values: {{integer, integer}} = {} + for j in outer:values() do + for i in inner:values() do + table.insert(values, {j, i}) + end + inner = inner:reverse() + end local i = 0 local n = #values - return function (): integer, integer + return function(): integer, integer i = i + 1 - if i <= n then return values[i][1], values[i][2] end + if i <= n then + return values[i][1], values[i][2] + end end end function itertools.sets_as_list(...: {any: T}): {T} - local list: {T} = {} + local list: {T} = {} - for _, set in ipairs({...}) do - for _, elem in pairs(set) do - table.insert(list, elem) - end - end + for _, set in ipairs({...}) do + for _, elem in pairs(set) do + table.insert(list, elem) + end + end - return list + return list end function itertools.combine(...: {T}): {T} - local combined_list: {T} = {} - - for _, list in ipairs({...}) do - for _, elem in ipairs(list) do - table.insert(combined_list, elem) - end - end - - return combined_list + local combined_list: {T} = {} + + for _, list in ipairs({...}) do + for _, elem in ipairs(list) do + table.insert(combined_list, elem) + end + end + + return combined_list end function itertools.find(value: T, list: {T}): integer - for index, elem in ipairs(list) do - if elem == value then - return index - end - end + for index, elem in ipairs(list) do + if elem == value then + return index + end + end - assert(false, "Missing in list!") + assert(false, "Missing in list!") end function itertools.map(iterator: {T}, f: function(T): V): {V} @@ -96,22 +99,24 @@ end function itertools.filter(iterator: {T}, predicate: function(T): boolean): {T} local result = {} for _, element in ipairs(iterator) do - if predicate(element) then - table.insert(result, element) - end + if predicate(element) then + table.insert(result, element) + end end return result end function itertools.shuffle(t: {T}): {T} - -- https://stackoverflow.com/a/68486276 - local s = {} - for i = 1, #t do s[i] = t[i] end - for i = #t, 2, -1 do - local j = lutro.math.random(i) - s[i], s[j] = s[j], s[i] - end - return s + -- https://stackoverflow.com/a/68486276 + local s = {} + for i = 1, #t do + s[i] = t[i] + end + for i = #t, 2, -1 do + local j = lutro.math.random(i) + s[i], s[j] = s[j], s[i] + end + return s end return itertools diff --git a/src/level_graphics/background.tl b/src/level_graphics/background.tl index a0a39e9..c79bb18 100644 --- a/src/level_graphics/background.tl +++ b/src/level_graphics/background.tl @@ -1,5 +1,4 @@ -local init = require("level_graphics.maps.init") - +local init = require("level_graphics.maps.init") local background = {} diff --git a/src/level_graphics/hard_block_animations.tl b/src/level_graphics/hard_block_animations.tl index f68854d..ed315c4 100644 --- a/src/level_graphics/hard_block_animations.tl +++ b/src/level_graphics/hard_block_animations.tl @@ -1,12 +1,12 @@ local animations = require("engine.animations") -local init = require("level_graphics.maps.init") +local init = require("level_graphics.maps.init") local type Animation = animations.Animation local hard_block_animations = {} function hard_block_animations.get(is_tall: boolean): Animation - return init.get_graphics():get_hard_block(is_tall) + return init.get_graphics():get_hard_block(is_tall) end return hard_block_animations diff --git a/src/level_graphics/maps/factory.tl b/src/level_graphics/maps/factory.tl index 0f93de7..91a5f10 100644 --- a/src/level_graphics/maps/factory.tl +++ b/src/level_graphics/maps/factory.tl @@ -41,14 +41,18 @@ end function FactoryGraphics:load_assets() background = load_image("background.png") soft_block_animation = animations.new(load_image("soft-block.png"), 16, 0.1) - soft_block_revealed_animation = animations.new(load_image("soft-block-revealed.png"), 16, 0.1) + soft_block_revealed_animation = animations.new( + load_image("soft-block-revealed.png"), 16, 0.1 + ) soft_block_breaking_image = load_image("soft-block-breaking.png") hard_block_animations[false] = animations.new(load_image("hard-block.png"), 16, 1) - hard_block_animations[true] = animations.new(load_image("hard-block-tall.png"), 16, 1) + hard_block_animations[true] = animations.new( + load_image("hard-block-tall.png"), 16, 1 + ) end function factory.new_graphics(): FactoryGraphics - local self: FactoryGraphics = setmetatable({}, { __index = FactoryGraphics }) + local self: FactoryGraphics = setmetatable({}, {__index = FactoryGraphics}) return self end diff --git a/src/level_graphics/maps/football.tl b/src/level_graphics/maps/football.tl index 5be15ee..2904396 100644 --- a/src/level_graphics/maps/football.tl +++ b/src/level_graphics/maps/football.tl @@ -6,7 +6,7 @@ local type MapGraphics = require("level_graphics.maps.interfaces") local football = {} local background: Image -local soft_block_animation: Animation +local soft_block_animation: Animation local soft_block_revealed_animation: Animation local soft_block_breaking_image: Image local hard_block_animation: {boolean: Animation} = {} @@ -41,15 +41,19 @@ end function FootballGraphics:load_assets() background = load_image("background.png") soft_block_animation = animations.new(load_image("soft-block.png"), 16, 0.1) - soft_block_revealed_animation = animations.new(load_image("soft-block-revealed.png"), 16, 0.1) + soft_block_revealed_animation = animations.new( + load_image("soft-block-revealed.png"), 16, 0.1 + ) soft_block_breaking_image = load_image("soft-block-breaking.png") hard_block_animation[false] = animations.new(load_image("hard-block.png"), 16, 1) - hard_block_animation[true] = animations.new(load_image("hard-block-tall.png"), 16, 1) + hard_block_animation[true] = animations.new( + load_image("hard-block-tall.png"), 16, 1 + ) end function football.new_graphics(): FootballGraphics - local self: FootballGraphics = setmetatable({}, { __index = FootballGraphics }) + local self: FootballGraphics = setmetatable({}, {__index = FootballGraphics}) return self end -return football \ No newline at end of file +return football diff --git a/src/level_graphics/maps/ice_rink.tl b/src/level_graphics/maps/ice_rink.tl index 5d7340e..d535075 100644 --- a/src/level_graphics/maps/ice_rink.tl +++ b/src/level_graphics/maps/ice_rink.tl @@ -41,14 +41,18 @@ end function IceRinkGraphics:load_assets() background = load_image("background.png") soft_block_animation = animations.new(load_image("soft-block.png"), 16, 0.1) - soft_block_revealed_animation = animations.new(load_image("soft-block-revealed.png"), 16, 0.1) + soft_block_revealed_animation = animations.new( + load_image("soft-block-revealed.png"), 16, 0.1 + ) soft_block_breaking_image = load_image("soft-block-breaking.png") hard_block_animation[false] = animations.new(load_image("hard-block.png"), 16, 1) - hard_block_animation[true] = animations.new(load_image("hard-block-tall.png"), 16, 1) + hard_block_animation[true] = animations.new( + load_image("hard-block-tall.png"), 16, 1 + ) end function ice_rink.new_graphics(): IceRinkGraphics - local self: IceRinkGraphics = setmetatable({}, { __index = IceRinkGraphics }) + local self: IceRinkGraphics = setmetatable({}, {__index = IceRinkGraphics}) return self end diff --git a/src/level_graphics/maps/init.tl b/src/level_graphics/maps/init.tl index b5f9002..1131463 100644 --- a/src/level_graphics/maps/init.tl +++ b/src/level_graphics/maps/init.tl @@ -9,9 +9,9 @@ local type MapGraphics = require("level_graphics.maps.interfaces") local init = {} local map_graphics : {Map: MapGraphics} = { - football = football.new_graphics(), - factory = factory.new_graphics(), - ice_rink = ice_rink.new_graphics(), + football = football.new_graphics(), + factory = factory.new_graphics(), + ice_rink = ice_rink.new_graphics(), } function init.get_graphics(): MapGraphics diff --git a/src/level_layout/block_layouts.tl b/src/level_layout/block_layouts.tl index ce4ef08..6189c19 100644 --- a/src/level_layout/block_layouts.tl +++ b/src/level_layout/block_layouts.tl @@ -1,8 +1,8 @@ -local entities = require("level_layout.maps.entities") -local grid = require("level_layout.grid") -local itertools = require("itertools") local points = require("engine.geometry.points") local vectors = require("engine.geometry.vectors") +local itertools = require("itertools") +local grid = require("level_layout.grid") +local entities = require("level_layout.maps.entities") local settings = require("settings") local type BlockType = entities.BlockType @@ -55,14 +55,17 @@ local function player_exclusion_zone(occupies: {Kinematic}): {Point} return close_to_entity end -local function place_soft_blocks(layout: {integer: {integer: BlockType}}, exclusion_zone: {Point}) +local function place_soft_blocks( + layout: {integer: {integer: BlockType}}, exclusion_zone: {Point} +) local dim = grid.dimensions() local count = 0 while count < settings.game_rules.items.nbr_soft_blocks do local i = lutro.math.random(dim.x_max + 1) - 1 local j = lutro.math.random(dim.y_max + 1) - 1 local cell: BlockType = layout[i][j] - if cell == "none" and not contains_point(exclusion_zone, grid.get_tile_position(i, j)) then + if cell == "none" + and not contains_point(exclusion_zone, grid.get_tile_position(i, j)) then layout[i][j] = "soft" count = count + 1 end @@ -92,25 +95,38 @@ local record QuadrantCell j: integer end -function QuadrantCell.new(i: integer, j: integer, corner_x: integer, corner_y: integer): QuadrantCell - local self: QuadrantCell = setmetatable({}, { __index = QuadrantCell }) +function QuadrantCell.new( + i: integer, j: integer, corner_x: integer, corner_y: integer +): QuadrantCell + local self: QuadrantCell = setmetatable({}, {__index = QuadrantCell}) self.i = i self.j = j self.distance = math.abs(i - corner_x) + math.abs(j - corner_y) return self end -function QuadrantCell:can_place_soft_block(layout: {integer: {integer: BlockType}}, exclusion_zone: {Point}): boolean +function QuadrantCell:can_place_soft_block( + layout: {integer: {integer: BlockType}}, exclusion_zone: {Point} +): boolean return layout[self.i][self.j] == "none" and not contains_point(exclusion_zone, grid.get_tile_position(self.i, self.j)) end local function distance_to_corner(first: QuadrantCell, second: QuadrantCell): boolean - if first.distance ~= second.distance then return first.distance < second.distance end + if first.distance ~= second.distance then + return first.distance < second.distance + end return first.i < second.i end -local function quadrant_cells(x_min: integer, x_max: integer, y_min: integer, y_max: integer, corner_x: integer, corner_y: integer): {QuadrantCell} +local function quadrant_cells( + x_min: integer, + x_max: integer, + y_min: integer, + y_max: integer, + corner_x: integer, + corner_y: integer +): {QuadrantCell} local cells: {QuadrantCell} = {} for i = x_min, x_max do for j = y_min, y_max do @@ -131,17 +147,21 @@ function block_layouts.quadrant(occupies: {Kinematic}): {integer: {integer: Bloc local per_quadrant = math.floor(total / 4) local remainder = total - per_quadrant * 4 + -- fmt: off local top_left = quadrant_cells(1, x_mid - 1, 1, y_mid - 1, 1, 1 ) local top_right = quadrant_cells(x_mid + 1, dim.x_max - 1, 1, y_mid - 1, dim.x_max - 1, 1 ) local bottom_left = quadrant_cells(1, x_mid - 1, y_mid + 1, dim.y_max - 1, 1, dim.y_max - 1) local bottom_right = quadrant_cells(x_mid + 1, dim.x_max - 1, y_mid + 1, dim.y_max - 1, dim.x_max - 1, dim.y_max - 1) + -- fmt: on local all_quadrants = {top_left, top_right, bottom_left, bottom_right} for quadrant_index, cells in ipairs(all_quadrants) do local count = per_quadrant + (quadrant_index <= remainder and 1 or 0) local placed = 0 for _, cell in ipairs(cells) do - if placed >= count then break end + if placed >= count then + break + end if cell:can_place_soft_block(layout, exclusion_zone) then layout[cell.i][cell.j] = "soft" placed = placed + 1 diff --git a/src/level_layout/grid.tl b/src/level_layout/grid.tl index 044c403..565e1ea 100644 --- a/src/level_layout/grid.tl +++ b/src/level_layout/grid.tl @@ -1,81 +1,81 @@ -local logger = require("engine.logger") local points = require("engine.geometry.points") local rectangles = require("engine.geometry.rectangles") +local logger = require("engine.logger") local type Point = points.Point local type Rectangle = rectangles.Rectangle local PLAYER_AREA = rectangles.new(24, 32, 272, 176) local grid = { - TILE_SIZE = 16, + TILE_SIZE = 16, } local record GridDimensions - x_min: integer - x_max: integer - y_min: integer - y_max: integer + x_min: integer + x_max: integer + y_min: integer + y_max: integer end function grid.area(): Rectangle - return PLAYER_AREA + return PLAYER_AREA end function grid.dimensions(): GridDimensions - return { - x_min = 0, - x_max = 16, - y_min = 0, - y_max = 10 - } + return {x_min = 0, x_max = 16, y_min = 0, y_max = 10} end function grid.get_tile(i: integer, j: integer): Rectangle - return rectangles.new( - PLAYER_AREA.x + i * grid.TILE_SIZE, - PLAYER_AREA.y + j * grid.TILE_SIZE, - grid.TILE_SIZE, - grid.TILE_SIZE - ) + return rectangles.new( + PLAYER_AREA.x + i * grid.TILE_SIZE, + PLAYER_AREA.y + j * grid.TILE_SIZE, + grid.TILE_SIZE, + grid.TILE_SIZE + ) end function grid.get_tile_position(i: integer, j: integer): Point - return grid.get_tile(i, j):top_left() + return grid.get_tile(i, j):top_left() end --- Get local position for point in block, so {x, y} ∈ [0, TILE_SIZE). function grid.get_local_tile_position(global_position: Point): Point - local player_area = grid.area() - return points.new( - math.fmod(global_position.x - player_area.x, grid.TILE_SIZE), - math.fmod(global_position.y - player_area.y, grid.TILE_SIZE) - ) + local player_area = grid.area() + return points.new( + math.fmod(global_position.x - player_area.x, grid.TILE_SIZE), + math.fmod(global_position.y - player_area.y, grid.TILE_SIZE) + ) end function grid.get_random_tile(): Point local dim = grid.dimensions() - return grid.get_tile_position( - lutro.math.random(dim.x_max + 1) - 1, - lutro.math.random(dim.y_max + 1) - 1 + return grid.get_tile_position( + lutro.math.random(dim.x_max + 1) - 1, lutro.math.random(dim.y_max + 1) - 1 ) end function grid.snap_to_x(x: number): number - return math.floor((x - PLAYER_AREA.x) / grid.TILE_SIZE + 0.5) * grid.TILE_SIZE + PLAYER_AREA.x + return math.floor( + (x - PLAYER_AREA.x) / grid.TILE_SIZE + 0.5 + ) * grid.TILE_SIZE + PLAYER_AREA.x end function grid.snap_to_y(y: number): number - return math.floor((y - PLAYER_AREA.y) / grid.TILE_SIZE + 0.5) * grid.TILE_SIZE + PLAYER_AREA.y + return math.floor( + (y - PLAYER_AREA.y) / grid.TILE_SIZE + 0.5 + ) * grid.TILE_SIZE + PLAYER_AREA.y end function grid.snap_to(pos: Point): Point - return points.new(grid.snap_to_x(pos.x), grid.snap_to_y(pos.y)) + return points.new(grid.snap_to_x(pos.x), grid.snap_to_y(pos.y)) end function grid.outside_on_opposite_side(out_pos: Point): Point if out_pos.y < PLAYER_AREA.y then -- exiting top - return points.new(out_pos.x, PLAYER_AREA.y + PLAYER_AREA.height + grid.TILE_SIZE) + return points.new( + out_pos.x, PLAYER_AREA.y + PLAYER_AREA.height + grid.TILE_SIZE + ) elseif out_pos.y > PLAYER_AREA.y + PLAYER_AREA.height then -- exiting bottom return points.new(out_pos.x, PLAYER_AREA.y - grid.TILE_SIZE * 2) @@ -92,7 +92,7 @@ function grid.outside_on_opposite_side(out_pos: Point): Point end function grid.outside_visible_area(box: Rectangle): boolean - return not PLAYER_AREA:expand(grid.TILE_SIZE):contains(box) + return not PLAYER_AREA:expand(grid.TILE_SIZE):contains(box) end return grid diff --git a/src/level_layout/map_entities.tl b/src/level_layout/map_entities.tl index 2a8c7e7..4f95c0e 100644 --- a/src/level_layout/map_entities.tl +++ b/src/level_layout/map_entities.tl @@ -1,9 +1,9 @@ -local entities = require("level_layout.maps.entities") -local grid = require("level_layout.grid") -local itertools = require("itertools") local points = require("engine.geometry.points") -local settings = require("settings") +local itertools = require("itertools") +local grid = require("level_layout.grid") +local entities = require("level_layout.maps.entities") local maps = require("level_layout.maps.init") +local settings = require("settings") local type Kinematic = require("kinematic") local type Point = points.Point @@ -13,12 +13,12 @@ local type ItemType = settings.ItemType local type BlockType = entities.BlockType local record HardBlockConfig - position: Point + position: Point is_tall: boolean end local record SoftBlockConfig - position: Point + position: Point item: ItemType egg: boolean end @@ -30,21 +30,27 @@ local record BlockConfigs end function HardBlockConfig.new(position: Point, is_tall: boolean): HardBlockConfig - local self: HardBlockConfig = setmetatable({}, { __index = HardBlockConfig }) + local self: HardBlockConfig = setmetatable({}, {__index = HardBlockConfig}) self.position = position self.is_tall = is_tall return self end -function SoftBlockConfig.new(position: Point, item: ItemType, egg: boolean): SoftBlockConfig - local self: SoftBlockConfig = setmetatable({}, { __index = SoftBlockConfig }) +function SoftBlockConfig.new( + position: Point, item: ItemType, egg: boolean +): SoftBlockConfig + local self: SoftBlockConfig = setmetatable({}, {__index = SoftBlockConfig}) self.position = position self.item = item self.egg = egg return self end -local map_entities = {SoftBlockConfig = SoftBlockConfig, HardBlockConfig = HardBlockConfig, BlockConfigs = BlockConfigs} +local map_entities = { + SoftBlockConfig = SoftBlockConfig, + HardBlockConfig = HardBlockConfig, + BlockConfigs = BlockConfigs, +} local function assign_items_to_positions(blocks_positions: {Point}): {SoftBlockConfig} local item_settings = settings.game_rules.items @@ -52,12 +58,12 @@ local function assign_items_to_positions(blocks_positions: {Point}): {SoftBlockC -- Shuffle so items are assigned randomly to blocks blocks_positions = itertools.shuffle(blocks_positions) for item_type, quantity in pairs(item_settings.nbr_of_items) do - for _=1, math.min(quantity, #blocks_positions) do + for _ = 1, math.min(quantity, #blocks_positions) do local pos = table.remove(blocks_positions, 1) table.insert(soft, SoftBlockConfig.new(pos, item_type, false)) end end - for _=1, math.min(settings.game_rules.dinos.nbr_of_eggs, #blocks_positions) do + for _ = 1, math.min(settings.game_rules.dinos.nbr_of_eggs, #blocks_positions) do local pos = table.remove(blocks_positions, 1) table.insert(soft, SoftBlockConfig.new(pos, nil, true)) end @@ -67,8 +73,10 @@ local function assign_items_to_positions(blocks_positions: {Point}): {SoftBlockC return soft end -function BlockConfigs.new(hard: {HardBlockConfig}, soft: {SoftBlockConfig}, non_hard: {Point}): BlockConfigs - local self: BlockConfigs = setmetatable({}, { __index = BlockConfigs }) +function BlockConfigs.new( + hard: {HardBlockConfig}, soft: {SoftBlockConfig}, non_hard: {Point} +): BlockConfigs + local self: BlockConfigs = setmetatable({}, {__index = BlockConfigs}) self.hard = hard self.soft = soft self.non_hard = non_hard @@ -98,87 +106,83 @@ function map_entities.get_block_configs(occupied: {Kinematic}): BlockConfigs return BlockConfigs.new(hard, assign_items_to_positions(soft_positions), non_hard) end - function map_entities.get_right_side_pressure_blocks(): {Point} - local blocks: {Point} = {} + local blocks: {Point} = {} - for i, j in itertools.double_loop_inner_loopback({0, 3}, {0, 10}) do - table.insert(blocks, grid.get_tile_position(i, j)) - end + for i, j in itertools.double_loop_inner_loopback({0, 3}, {0, 10}) do + table.insert(blocks, grid.get_tile_position(i, j)) + end - for j, i in itertools.double_loop_inner_loopback({0, 1}, {4, 12}) do - table.insert(blocks, grid.get_tile_position(i, j)) - end + for j, i in itertools.double_loop_inner_loopback({0, 1}, {4, 12}) do + table.insert(blocks, grid.get_tile_position(i, j)) + end - return blocks + return blocks end function map_entities.get_left_side_pressure_blocks(): {Point} - local blocks: {Point} = {} + local blocks: {Point} = {} - for i, j in itertools.double_loop_inner_loopback({16, 13, -1}, {10, 0, -1}) do - table.insert(blocks, grid.get_tile_position(i, j)) - end + for i, j in itertools.double_loop_inner_loopback({16, 13, -1}, {10, 0, -1}) do + table.insert(blocks, grid.get_tile_position(i, j)) + end - for j, i in itertools.double_loop_inner_loopback({10, 9, -1}, {12, 4, -1}) do - table.insert(blocks, grid.get_tile_position(i, j)) - end + for j, i in itertools.double_loop_inner_loopback({10, 9, -1}, {12, 4, -1}) do + table.insert(blocks, grid.get_tile_position(i, j)) + end - return blocks + return blocks end - function map_entities.get_player_start_positions(number_of_players: integer): {Point} - local x_min = 0 - local x_max = 16 - local y_min = 0 - local y_max = 10 - - local corners = { - grid.get_tile_position(x_min, y_min), - grid.get_tile_position(x_max, y_max), - grid.get_tile_position(x_max, y_min), - grid.get_tile_position(x_min, y_max) - } - local center = { - grid.get_tile_position( - math.floor(x_max / 2), math.floor(y_max / 2) - ) - } - local inner_positions = { - grid.get_tile_position(x_min + 5, y_min + 2), - grid.get_tile_position(x_max - 5, y_max - 2), - grid.get_tile_position(x_max - 5, y_min + 2), - grid.get_tile_position(x_min + 5, y_max - 2) - } - - local function get_positions(): {Point} - if number_of_players == 5 then - -- Only if there are 5 players can you start dead in the middle - return itertools.combine(corners, center) - else - local extra_on_sides = { - grid.get_tile_position(x_min, math.floor(y_max / 2)), - grid.get_tile_position(x_max, math.floor(y_max / 2)) - } - local result = itertools.combine(corners, inner_positions, extra_on_sides) - return {table.unpack(result, 1, number_of_players)} - end - end - - if settings.game_rules.players.start_position_shuffle then - return itertools.shuffle(get_positions()) - else - return get_positions() - end + local x_min = 0 + local x_max = 16 + local y_min = 0 + local y_max = 10 + + local corners = { + grid.get_tile_position(x_min, y_min), + grid.get_tile_position(x_max, y_max), + grid.get_tile_position(x_max, y_min), + grid.get_tile_position(x_min, y_max), + } + local center = { + grid.get_tile_position(math.floor(x_max / 2), math.floor(y_max / 2)) + } + local inner_positions = { + grid.get_tile_position(x_min + 5, y_min + 2), + grid.get_tile_position(x_max - 5, y_max - 2), + grid.get_tile_position(x_max - 5, y_min + 2), + grid.get_tile_position(x_min + 5, y_max - 2), + } + + local function get_positions(): {Point} + if number_of_players == 5 then + -- Only if there are 5 players can you start dead in the middle + return itertools.combine(corners, center) + else + local extra_on_sides = { + grid.get_tile_position(x_min, math.floor(y_max / 2)), + grid.get_tile_position(x_max, math.floor(y_max / 2)), + } + local result = itertools.combine(corners, inner_positions, extra_on_sides) + return {table.unpack(result, 1, number_of_players)} + end + end + + if settings.game_rules.players.start_position_shuffle then + return itertools.shuffle(get_positions()) + else + return get_positions() + end end function map_entities.get_goals(): {Goal} - return maps.get_layout():get_goals() + return maps.get_layout():get_goals() end function map_entities.get_conveyor_belts(): {ConveyorBelt} - return maps.get_layout():get_conveyor_belts() + return maps.get_layout():get_conveyor_belts() end return map_entities diff --git a/src/level_layout/maps/entities.tl b/src/level_layout/maps/entities.tl index 4fc0649..e94f5a5 100644 --- a/src/level_layout/maps/entities.tl +++ b/src/level_layout/maps/entities.tl @@ -1,7 +1,7 @@ -local hitboxes = require("engine.hitboxes") -local vectors = require("engine.geometry.vectors") local points = require("engine.geometry.points") local rectangles = require("engine.geometry.rectangles") +local vectors = require("engine.geometry.vectors") +local hitboxes = require("engine.hitboxes") local type Vector = vectors.Vector local type Point = points.Point @@ -13,7 +13,7 @@ local record Goal end function Goal.new(position: Point, pair: Point): Goal - local self: Goal = setmetatable({}, { __index = Goal }) + local self: Goal = setmetatable({}, {__index = Goal}) self.position = position self.pair = pair return self @@ -29,7 +29,7 @@ local record ConveyorBelt end function ConveyorBelt.new(position: Point, direction: Vector): ConveyorBelt - local self: ConveyorBelt = setmetatable({}, { __index = ConveyorBelt }) + local self: ConveyorBelt = setmetatable({}, {__index = ConveyorBelt}) self.position = position self.direction = direction return self @@ -51,7 +51,9 @@ local type Kinematic = require("kinematic") local type MapLayout = interface get_conveyor_belts: function(self): {ConveyorBelt} get_goals: function(self): {Goal} - get_block_layout: function(self, occupies: {Kinematic}): {integer: {integer: BlockType}} + get_block_layout: function( + self, occupies: {Kinematic} + ): {integer: {integer: BlockType}} has_ice: function(self): boolean end diff --git a/src/level_layout/maps/factory.tl b/src/level_layout/maps/factory.tl index 599aee9..2088fbd 100644 --- a/src/level_layout/maps/factory.tl +++ b/src/level_layout/maps/factory.tl @@ -1,7 +1,7 @@ +local vectors = require("engine.geometry.vectors") local block_layouts = require("level_layout.block_layouts") -local entities = require("level_layout.maps.entities") local grid = require("level_layout.grid") -local vectors = require("engine.geometry.vectors") +local entities = require("level_layout.maps.entities") local type MapLayout = entities.MapLayout local type BlockType = entities.BlockType @@ -16,48 +16,52 @@ local record FactoryLayout is MapLayout end function FactoryLayout.new(): FactoryLayout - local self: FactoryLayout = setmetatable({}, { __index = FactoryLayout }) + local self: FactoryLayout = setmetatable({}, {__index = FactoryLayout}) return self end function FactoryLayout:get_goals(): {Goal} - return {} + return {} end function FactoryLayout:get_conveyor_belts(): {ConveyorBelt} - local function create_belt(i: integer, j: integer, dir: Direction): ConveyorBelt - return ConveyorBelt.new(grid.get_tile_position(i, j), vectors.from_direction(dir)) - end + local function create_belt(i: integer, j: integer, dir: Direction): ConveyorBelt + return ConveyorBelt.new( + grid.get_tile_position(i, j), vectors.from_direction(dir) + ) + end local dim = grid.dimensions() - return { - -- Outer ring - create_belt(dim.x_min, dim.y_min, "down"), - create_belt(dim.x_max, dim.y_max, "up"), - create_belt(dim.x_max, dim.y_min, "left"), - create_belt(dim.x_min, dim.y_max, "right"), - -- Middle ring - create_belt(dim.x_min + 4, dim.y_min + 2, "right"), - create_belt(dim.x_max - 4, dim.y_max - 2, "left"), - create_belt(dim.x_max - 4, dim.y_min + 2, "down"), - create_belt(dim.x_min + 4, dim.y_max - 2, "up"), - -- Inner ring - create_belt(dim.x_min + 6, dim.y_min + 4, "down"), - create_belt(dim.x_max - 6, dim.y_max - 4, "up"), - create_belt(dim.x_max - 6, dim.y_min + 4, "left"), - create_belt(dim.x_min + 6, dim.y_max - 4, "right"), - -- Middle up/down - create_belt(math.ceil(dim.x_max / 2), dim.y_min, "down"), - create_belt(math.ceil(dim.x_max / 2), dim.y_max, "up"), - } + return { + -- Outer ring + create_belt(dim.x_min, dim.y_min, "down"), + create_belt(dim.x_max, dim.y_max, "up"), + create_belt(dim.x_max, dim.y_min, "left"), + create_belt(dim.x_min, dim.y_max, "right"), + -- Middle ring + create_belt(dim.x_min + 4, dim.y_min + 2, "right"), + create_belt(dim.x_max - 4, dim.y_max - 2, "left"), + create_belt(dim.x_max - 4, dim.y_min + 2, "down"), + create_belt(dim.x_min + 4, dim.y_max - 2, "up"), + -- Inner ring + create_belt(dim.x_min + 6, dim.y_min + 4, "down"), + create_belt(dim.x_max - 6, dim.y_max - 4, "up"), + create_belt(dim.x_max - 6, dim.y_min + 4, "left"), + create_belt(dim.x_min + 6, dim.y_max - 4, "right"), + -- Middle up/down + create_belt(math.ceil(dim.x_max / 2), dim.y_min, "down"), + create_belt(math.ceil(dim.x_max / 2), dim.y_max, "up"), + } end -function FactoryLayout:get_block_layout(occupies: {Kinematic}): {integer: {integer: BlockType}} - return block_layouts.grid(occupies) +function FactoryLayout:get_block_layout( + occupies: {Kinematic} +): {integer: {integer: BlockType}} + return block_layouts.grid(occupies) end function FactoryLayout:has_ice(): boolean - return false + return false end return FactoryLayout.new() diff --git a/src/level_layout/maps/football.tl b/src/level_layout/maps/football.tl index 807b2dc..ce9d158 100644 --- a/src/level_layout/maps/football.tl +++ b/src/level_layout/maps/football.tl @@ -1,6 +1,6 @@ local block_layouts = require("level_layout.block_layouts") -local entities = require("level_layout.maps.entities") local grid = require("level_layout.grid") +local entities = require("level_layout.maps.entities") local type MapLayout = entities.MapLayout local type BlockType = entities.BlockType @@ -14,29 +14,31 @@ local record FootballLayout is MapLayout end function FootballLayout.new(): FootballLayout - local self: FootballLayout = setmetatable({}, { __index = FootballLayout }) + local self: FootballLayout = setmetatable({}, {__index = FootballLayout}) return self end function FootballLayout:get_goals(): {Goal} - return { - Goal.new(grid.get_tile_position(-1, 4), grid.get_tile_position(-1, 6)), - Goal.new(grid.get_tile_position(-1, 6), grid.get_tile_position(-1, 4)), - Goal.new(grid.get_tile_position(17, 4), grid.get_tile_position(17, 6)), - Goal.new(grid.get_tile_position(17, 6), grid.get_tile_position(17, 4)), - } + return { + Goal.new(grid.get_tile_position(-1, 4), grid.get_tile_position(-1, 6)), + Goal.new(grid.get_tile_position(-1, 6), grid.get_tile_position(-1, 4)), + Goal.new(grid.get_tile_position(17, 4), grid.get_tile_position(17, 6)), + Goal.new(grid.get_tile_position(17, 6), grid.get_tile_position(17, 4)), + } end function FootballLayout:get_conveyor_belts(): {ConveyorBelt} - return {} + return {} end -function FootballLayout:get_block_layout(occupies: {Kinematic}): {integer: {integer: BlockType}} - return block_layouts.grid(occupies) +function FootballLayout:get_block_layout( + occupies: {Kinematic} +): {integer: {integer: BlockType}} + return block_layouts.grid(occupies) end function FootballLayout:has_ice(): boolean - return false + return false end return FootballLayout.new() diff --git a/src/level_layout/maps/ice_rink.tl b/src/level_layout/maps/ice_rink.tl index 03dc0d3..130185e 100644 --- a/src/level_layout/maps/ice_rink.tl +++ b/src/level_layout/maps/ice_rink.tl @@ -13,7 +13,7 @@ local record IceRinkLayout is MapLayout end function IceRinkLayout.new(): IceRinkLayout - local self: IceRinkLayout = setmetatable({}, { __index = IceRinkLayout }) + local self: IceRinkLayout = setmetatable({}, {__index = IceRinkLayout}) return self end @@ -25,7 +25,9 @@ function IceRinkLayout:get_conveyor_belts(): {ConveyorBelt} return {} end -function IceRinkLayout:get_block_layout(occupies: {Kinematic}): {integer: {integer: BlockType}} +function IceRinkLayout:get_block_layout( + occupies: {Kinematic} +): {integer: {integer: BlockType}} return block_layouts.quadrant(occupies) end diff --git a/src/level_layout/maps/init.tl b/src/level_layout/maps/init.tl index 1ab984a..a26683e 100644 --- a/src/level_layout/maps/init.tl +++ b/src/level_layout/maps/init.tl @@ -1,6 +1,6 @@ local entities = require("level_layout.maps.entities") -local football = require("level_layout.maps.football") local factory = require("level_layout.maps.factory") +local football = require("level_layout.maps.football") local ice_rink = require("level_layout.maps.ice_rink") local settings = require("settings") @@ -8,9 +8,9 @@ local type Map = settings.Map local type MapLayout = entities.MapLayout local map_layouts : {Map: MapLayout} = { - football = football, - factory = factory, - ice_rink = ice_rink, + football = football, + factory = factory, + ice_rink = ice_rink, } local init = {} diff --git a/src/level_layout/rail.tl b/src/level_layout/rail.tl index c4f5597..80a41ba 100644 --- a/src/level_layout/rail.tl +++ b/src/level_layout/rail.tl @@ -1,6 +1,6 @@ -local grid = require("level_layout.grid") -local vectors = require("engine.geometry.vectors") local points = require("engine.geometry.points") +local vectors = require("engine.geometry.vectors") +local grid = require("level_layout.grid") local type Direction = vectors.Direction local type Vector = vectors.Vector @@ -16,15 +16,11 @@ local DISTANCE_TO_SIDE: {Direction: number} = { } local START_POSITION_OF_SIDE: {Direction: Point} = { up = PLAYER_AREA:top_left(), - right = PLAYER_AREA:top_left():move( - vectors.new(PLAYER_AREA.width, 0) - ), + right = PLAYER_AREA:top_left():move(vectors.new(PLAYER_AREA.width, 0)), down = PLAYER_AREA:top_left():move( vectors.new(PLAYER_AREA.width, PLAYER_AREA.height) ), - left = PLAYER_AREA:top_left():move( - vectors.new(0, PLAYER_AREA.height) - ), + left = PLAYER_AREA:top_left():move(vectors.new(0, PLAYER_AREA.height)), } local MOVE_DIRECTION_OF_SIDE: {Direction: Vector} = { up = vectors.new(1, 0), diff --git a/src/mad_bombers.tl b/src/mad_bombers.tl index c5f7391..25ddd86 100644 --- a/src/mad_bombers.tl +++ b/src/mad_bombers.tl @@ -1,13 +1,13 @@ -local animations = require("engine.animations") local bombs = require("bombs.bombs") local characters = require("characters") -local grid = require("level_layout.grid") -local hitboxes = require("engine.hitboxes") -local joysticks = require("engine.joysticks") +local animations = require("engine.animations") local points = require("engine.geometry.points") -local rail = require("level_layout.rail") local rectangles = require("engine.geometry.rectangles") local vectors = require("engine.geometry.vectors") +local hitboxes = require("engine.hitboxes") +local joysticks = require("engine.joysticks") +local grid = require("level_layout.grid") +local rail = require("level_layout.rail") local z_index = require("z_index") local type Animation = animations.Animation @@ -37,16 +37,16 @@ local HEAD_DIR_BY_SIDE: {Direction: Direction} = { } local type CartState = enum - "forward" -- Moving clockwise - "backward" -- Moving anti-clockwise - "idle" + "forward" -- Moving clockwise + "backward" -- Moving anti-clockwise + "idle" end local type CannonState = enum - "ready" - "pre-fire" - "firing" - "loading" + "ready" + "pre-fire" + "firing" + "loading" end local type CartAnimations = {Direction: {CartState: Animation}} @@ -60,8 +60,8 @@ local cart_firing_images: {Direction: Image} = {} local cart_heads: {CharacterName: {Direction: Image}} = {} local function cart_animation(image: Image): Animation - local nbr_steps = 2 - return animations.new(image, image:getWidth() / nbr_steps, 0.05) + local nbr_steps = 2 + return animations.new(image, image:getWidth() / nbr_steps, 0.05) end local function get_firing_animation(side: Direction): Animation @@ -99,7 +99,7 @@ end local record PlayerCart is Entity id: integer - position: number -- 1D position on cart rail + position: number -- 1D position on cart rail state: CartState previous_direction: CartState cannon: CannonState @@ -112,10 +112,8 @@ local record PlayerCart is Entity firing_animations: CartAnimations end -function PlayerCart.new( - id: integer, position: number, head: CharacterName -): PlayerCart - local self: PlayerCart = setmetatable({}, { __index = PlayerCart }) +function PlayerCart.new(id: integer, position: number, head: CharacterName): PlayerCart + local self: PlayerCart = setmetatable({}, {__index = PlayerCart}) self.id = id self.position = position self.z_index = z_index.ABOVE @@ -199,9 +197,9 @@ function PlayerCart:_get_draw_offset(): Vector end function PlayerCart:_get_x_y_draw_position(): Point - return rail.get_x_y_position(self.position):move( - self:_get_draw_offset() - ):move(self:_get_cart_image_padding()) + return rail.get_x_y_position(self.position):move(self:_get_draw_offset()):move( + self:_get_cart_image_padding() + ) end function PlayerCart:get_position(): Point @@ -223,7 +221,7 @@ function PlayerCart:draw() -- If not ready, head is blinking by only sometimes not rendering it below return end - + local side = rail.get_side(self.position) local head_image = cart_heads[self.head][HEAD_DIR_BY_SIDE[side]] local cart_paddings = self:_get_cart_image_padding():abs() @@ -250,18 +248,18 @@ function PlayerCart:draw() end end - local cart_half_size = vectors.new( - animation.width, animation.height - ):minus(total_padding()):scale(1/2) + local cart_half_size = vectors.new(animation.width, animation.height):minus( + total_padding() + ):scale(1 / 2) local head_pos = points.new( cart_pos.x + cart_half_size.x - head_image:getWidth() / 2, cart_pos.y + cart_paddings.y - head_image:getHeight() / 2 ):move(pixel_tweaks()) - + if side == "right" then -- Note, special case for the right hand side. This is because -- the right/left cart sprites are not centered with equal padding - -- on both sides. + -- on both sides. head_pos = head_pos:move(vectors.new(cart_paddings.x, 0)) end lutro.graphics.draw(head_image, head_pos.x, head_pos.y) @@ -315,7 +313,7 @@ end local mad_bombers = {MadBombers = MadBombers} function mad_bombers.new(bombs_: Bombs): MadBombers - local self: MadBombers = setmetatable({}, { __index = MadBombers }) + local self: MadBombers = setmetatable({}, {__index = MadBombers}) self.by_player = {} self.bombs = bombs_ return self @@ -361,12 +359,14 @@ function MadBombers:handle_cart_move_input(dt: number, cart: PlayerCart) if (side == "left" and down) or (side == "right" and up) then cart:set_state("backward") end - - if lutro.joystick.isDown(cart.id, joysticks.BUTTONS.R) and cart.cannon == "ready" then + + if lutro.joystick.isDown(cart.id, joysticks.BUTTONS.R) + and cart.cannon == "ready" then cart.cannon_powerup_for = cart.cannon_powerup_for + dt end - if joysticks.is_released(cart.id, joysticks.BUTTONS.R) or cart.cannon_powerup_for > 0.8 then + if joysticks.is_released(cart.id, joysticks.BUTTONS.R) + or cart.cannon_powerup_for > 0.8 then local fire_from, fire_len = cart:fire_cannon() if fire_from then local fire_dir = THROW_BOMB_DIRECTION_BY_SIDE[side] @@ -379,12 +379,11 @@ end function MadBombers:update(dt: number) for _, cart in pairs(self.by_player) do - self:handle_cart_move_input(dt, cart) - cart:update(dt) - end + self:handle_cart_move_input(dt, cart) + cart:update(dt) + end end - function mad_bombers.load_assets() local folder = "assets/carts/" diff --git a/src/main.tl b/src/main.tl index 04128a1..92d5533 100644 --- a/src/main.tl +++ b/src/main.tl @@ -1,45 +1,44 @@ -local battle = require("game.battle") -local grid = require("level_layout.grid") local joysticks = require("engine.joysticks") local logger = require("engine.logger") local scenes = require("engine.scenes") -local screen = require("screen") -local screens = require("menus.screens") local settings = require("engine.settings") +local tracing = require("engine.tracing") +local battle = require("game.battle") +local grid = require("level_layout.grid") +local screens = require("menus.screens") local start_screen = require("menus.start_screen") +local update_screen = require("menus.update_screen") local player_selection = require("player_selection") +local screen = require("screen") local team_selection = require("team_selection") -local update_screen = require("menus.update_screen") -local tracing = require("engine.tracing") - function lutro.conf(t: Configuration) - t.width = screen.WIDTH - t.height = screen.HEIGHT + t.width = screen.WIDTH + t.height = screen.HEIGHT end function lutro.load() - logger.setup("EXPLODELON") - settings.set_block_size(grid.TILE_SIZE) - local load_start = lutro.timer.getTime() - joysticks.setup() - screens.load_assets() - player_selection.load_assets() - team_selection.load_assets() - battle.load_assets() - update_screen.load_assets() - local load_time = lutro.timer.getTime() - load_start - logger.info("Loading assets at start took " .. load_time .. "s") - scenes.set_start(start_screen.new()) + logger.setup("EXPLODELON") + settings.set_block_size(grid.TILE_SIZE) + local load_start = lutro.timer.getTime() + joysticks.setup() + screens.load_assets() + player_selection.load_assets() + team_selection.load_assets() + battle.load_assets() + update_screen.load_assets() + local load_time = lutro.timer.getTime() - load_start + logger.info("Loading assets at start took " .. load_time .. "s") + scenes.set_start(start_screen.new()) end function lutro.update(dt: number) - tracing.reset() - scenes.get():update(dt) - joysticks.update(dt) + tracing.reset() + scenes.get():update(dt) + joysticks.update(dt) end function lutro.draw() - tracing.reset() - scenes.get():draw() + tracing.reset() + scenes.get():draw() end diff --git a/src/menus/choices.tl b/src/menus/choices.tl index e5a58f8..164e734 100644 --- a/src/menus/choices.tl +++ b/src/menus/choices.tl @@ -1,8 +1,8 @@ -local fonts = require("fonts") local points = require("engine.geometry.points") local joysticks = require("engine.joysticks") -local items = require("menus.items") local scenes = require("engine.scenes") +local fonts = require("fonts") +local items = require("menus.items") local screen = require("screen") local type MenuFont = fonts.MenuFont @@ -25,10 +25,15 @@ end local choices = {MenuChoice = MenuChoice} -function choices.new_boolean(menu_text: string, y_pos: integer, on_by_default: boolean, callback: function(boolean)): MenuChoiceImpl +function choices.new_boolean( + menu_text: string, + y_pos: integer, + on_by_default: boolean, + callback: function(boolean) +): MenuChoiceImpl local selection = items.new_boolean_selection(on_by_default) - local self: MenuChoiceImpl = setmetatable({}, { __index = MenuChoiceImpl }) + local self: MenuChoiceImpl = setmetatable({}, {__index = MenuChoiceImpl}) self.y_pos = y_pos self.selection = selection self.menu_text = menu_text @@ -41,8 +46,8 @@ function MenuChoiceImpl:update(_: Scene): boolean local function active_button(button: integer): boolean local time_until_fast_change = 0.3 - return joysticks.is_pressed_any(button) or - joysticks.is_down_for_any(button, time_until_fast_change) + return joysticks.is_pressed_any(button) + or joysticks.is_down_for_any(button, time_until_fast_change) end if active_button(joysticks.BUTTONS.RIGHT) then @@ -56,13 +61,16 @@ function MenuChoiceImpl:update(_: Scene): boolean end function MenuChoiceImpl:draw(font: MenuFont) - local line = MenuLine.new({ - MenuLineColumn.new("", 20), - MenuLineColumn.new(self.menu_text, 170), - MenuLineColumn.new("-", 30), - MenuLineColumn.new(self.selection:as_menu_string(), 50), - MenuLineColumn.new("", 50), - }, self.y_pos) + local line = MenuLine.new( + { + MenuLineColumn.new("", 20), + MenuLineColumn.new(self.menu_text, 170), + MenuLineColumn.new("-", 30), + MenuLineColumn.new(self.selection:as_menu_string(), 50), + MenuLineColumn.new("", 50), + }, + self.y_pos + ) local x_pos = 0.0 for _, column in ipairs(line.columns) do @@ -71,8 +79,13 @@ function MenuChoiceImpl:draw(font: MenuFont) end end -function choices.new_integer(menu_text: string, y_pos: integer, selection: MenuSelection, callback: function(integer)): MenuChoiceImpl - local self: MenuChoiceImpl = setmetatable({}, { __index = MenuChoiceImpl }) +function choices.new_integer( + menu_text: string, + y_pos: integer, + selection: MenuSelection, + callback: function(integer) +): MenuChoiceImpl + local self: MenuChoiceImpl = setmetatable({}, {__index = MenuChoiceImpl}) self.y_pos = y_pos self.selection = selection self.menu_text = menu_text @@ -80,8 +93,13 @@ function choices.new_integer(menu_text: string, y_pos: integer, selection: MenuS return self end -function choices.new_number(menu_text: string, y_pos: integer, selection: MenuSelection, callback: function(number)): MenuChoiceImpl - local self: MenuChoiceImpl = setmetatable({}, { __index = MenuChoiceImpl }) +function choices.new_number( + menu_text: string, + y_pos: integer, + selection: MenuSelection, + callback: function(number) +): MenuChoiceImpl + local self: MenuChoiceImpl = setmetatable({}, {__index = MenuChoiceImpl}) self.y_pos = y_pos self.selection = selection self.menu_text = menu_text @@ -89,8 +107,13 @@ function choices.new_number(menu_text: string, y_pos: integer, selection: MenuSe return self end -function choices.new_enum(menu_text: string, y_pos: integer, selection: MenuSelection, callback: function(T)): MenuChoiceImpl - local self: MenuChoiceImpl = setmetatable({}, { __index = MenuChoiceImpl }) +function choices.new_enum( + menu_text: string, + y_pos: integer, + selection: MenuSelection, + callback: function(T) +): MenuChoiceImpl + local self: MenuChoiceImpl = setmetatable({}, {__index = MenuChoiceImpl}) self.y_pos = y_pos self.selection = selection self.menu_text = menu_text @@ -117,8 +140,10 @@ function MenuAction:draw(font: MenuFont) font:print_centered(self.menu_text, screen.WIDTH, points.new(0, self.y_pos)) end -function choices.new_done(menu_text: string, y_pos: integer, callback: function(ctx: Scene)): MenuAction - local self: MenuAction = setmetatable({}, { __index = MenuAction }) +function choices.new_done( + menu_text: string, y_pos: integer, callback: function(ctx: Scene) +): MenuAction + local self: MenuAction = setmetatable({}, {__index = MenuAction}) self.y_pos = y_pos self.menu_text = menu_text self.on_select = callback diff --git a/src/menus/debug_screen.tl b/src/menus/debug_screen.tl index 2db80fe..4a0d9e2 100644 --- a/src/menus/debug_screen.tl +++ b/src/menus/debug_screen.tl @@ -1,7 +1,7 @@ local scenes = require("engine.scenes") +local settings = require("engine.settings") local choices = require("menus.choices") local screens = require("menus.screens") -local settings = require("engine.settings") local type Scene = scenes.Scene local type MenuScreen = screens.MenuScreen @@ -13,18 +13,27 @@ function debug_screen.new(previous_screen: Scene): MenuScreen scenes.set(previous_screen) end - return screens.new({ - choices.new_boolean( - "draw hitboxes", 50, settings.draw_hitboxes, settings.set_draw_hitboxs - ), - choices.new_boolean( - "log slow game loop", 70, settings.log_scene_perf, settings.set_log_scene_perf - ), - choices.new_boolean( - "log memory usage", 90, settings.log_mem_usage, settings.set_log_mem_usage - ), - choices.new_done("back", 180, done) - }, done) + return screens.new( + { + choices.new_boolean( + "draw hitboxes", 50, settings.draw_hitboxes, settings.set_draw_hitboxs + ), + choices.new_boolean( + "log slow game loop", + 70, + settings.log_scene_perf, + settings.set_log_scene_perf + ), + choices.new_boolean( + "log memory usage", + 90, + settings.log_mem_usage, + settings.set_log_mem_usage + ), + choices.new_done("back", 180, done), + }, + done + ) end return debug_screen diff --git a/src/menus/dino_setting_screen.tl b/src/menus/dino_setting_screen.tl index f1a4943..90bb194 100644 --- a/src/menus/dino_setting_screen.tl +++ b/src/menus/dino_setting_screen.tl @@ -1,8 +1,8 @@ local scenes = require("engine.scenes") -local settings = require("settings") local choices = require("menus.choices") local items = require("menus.items") local screens = require("menus.screens") +local settings = require("settings") local type Scene = scenes.Scene local type MenuScreen = screens.MenuScreen @@ -15,28 +15,48 @@ function dino_settings_screen.new(previous_screen: Scene): MenuScreen end local dino_settings = settings.game_rules.dinos - local nbr_eggs_selected = items.new_integer_selection(dino_settings.nbr_of_eggs, 0, 30) - return screens.new({ + local nbr_eggs_selected = items.new_integer_selection( + dino_settings.nbr_of_eggs, 0, 30 + ) + return screens.new( + { choices.new_integer( - "nbr. of eggs", 50, nbr_eggs_selected, settings.set_nbr_of_eggs - ), - choices.new_boolean( - "blue dinos", 65, dino_settings.enabled["blue"], settings.set_dino_enabled("blue") - ), - choices.new_boolean( - "pink dinos", 80, dino_settings.enabled["pink"], settings.set_dino_enabled("pink") - ), - choices.new_boolean( - "purple dinos", 95, dino_settings.enabled["purple"], settings.set_dino_enabled("purple") - ), - choices.new_boolean( - "green dinos", 110, dino_settings.enabled["green"], settings.set_dino_enabled("green") - ), - choices.new_boolean( - "yellow dinos", 125, dino_settings.enabled["yellow"], settings.set_dino_enabled("yellow") - ), - choices.new_done("back", 180, done) - }, done) + "nbr. of eggs", 50, nbr_eggs_selected, settings.set_nbr_of_eggs + ), + choices.new_boolean( + "blue dinos", + 65, + dino_settings.enabled["blue"], + settings.set_dino_enabled("blue") + ), + choices.new_boolean( + "pink dinos", + 80, + dino_settings.enabled["pink"], + settings.set_dino_enabled("pink") + ), + choices.new_boolean( + "purple dinos", + 95, + dino_settings.enabled["purple"], + settings.set_dino_enabled("purple") + ), + choices.new_boolean( + "green dinos", + 110, + dino_settings.enabled["green"], + settings.set_dino_enabled("green") + ), + choices.new_boolean( + "yellow dinos", + 125, + dino_settings.enabled["yellow"], + settings.set_dino_enabled("yellow") + ), + choices.new_done("back", 180, done), + }, + done + ) end return dino_settings_screen diff --git a/src/menus/game_rule_screen.tl b/src/menus/game_rule_screen.tl index fa38bcc..46884c7 100644 --- a/src/menus/game_rule_screen.tl +++ b/src/menus/game_rule_screen.tl @@ -1,13 +1,13 @@ local scenes = require("engine.scenes") local choices = require("menus.choices") -local screens = require("menus.screens") local dino_settings_screen = require("menus.dino_setting_screen") local ice_settings_screen = require("menus.ice_settings_screen") local item_spawn_screen = require("menus.item_drops_screen") local item_setting_screen = require("menus.item_setting_screen") +local load_rules_screen = require("menus.load_rules_screen") local player_setting_screen = require("menus.player_setting_screen") +local screens = require("menus.screens") local timing_settings_screen = require("menus.timing_settings_screen") -local load_rules_screen = require("menus.load_rules_screen") local type Scene = scenes.Scene local type MenuScreen = screens.MenuScreen @@ -40,31 +40,19 @@ function game_rule_screen.new(previous_screen: Scene): MenuScreen scenes.set(load_rules_screen.new(ctx, previous_screen)) end - return screens.new({ - choices.new_done( - "load ruleset", 50, enter_load_rules_screen - ), - choices.new_done( - "item rules", 70, enter_item_settings_screen - ), - choices.new_done( - "item drop rules", 90, enter_item_spawn_screen - ), - choices.new_done( - "dino rules", 110, enter_dino_settings_screen - ), - choices.new_done( - "player rules", 130, enter_player_settings_screen - ), - choices.new_done( - "timing rules", 150, enter_timing_settings_screen - ), - choices.new_done( - "ice rules", 170, enter_ice_settings_screen - ), - - choices.new_done("back", 200, done) - }, done) + return screens.new( + { + choices.new_done("load ruleset", 50, enter_load_rules_screen), + choices.new_done("item rules", 70, enter_item_settings_screen), + choices.new_done("item drop rules", 90, enter_item_spawn_screen), + choices.new_done("dino rules", 110, enter_dino_settings_screen), + choices.new_done("player rules", 130, enter_player_settings_screen), + choices.new_done("timing rules", 150, enter_timing_settings_screen), + choices.new_done("ice rules", 170, enter_ice_settings_screen), + choices.new_done("back", 200, done), + }, + done + ) end return game_rule_screen diff --git a/src/menus/ice_settings_screen.tl b/src/menus/ice_settings_screen.tl index a5bfa62..ed485e1 100644 --- a/src/menus/ice_settings_screen.tl +++ b/src/menus/ice_settings_screen.tl @@ -1,8 +1,8 @@ -local items = require("menus.items") local scenes = require("engine.scenes") -local settings = require("settings") local choices = require("menus.choices") +local items = require("menus.items") local screens = require("menus.screens") +local settings = require("settings") local type Scene = scenes.Scene local type MenuScreen = screens.MenuScreen @@ -19,7 +19,9 @@ function ice_settings_screen.new(previous_screen: Scene): MenuScreen local function find_index(options: {number}, target: number): integer for i, v in ipairs(options) do - if v == target then return i end + if v == target then + return i + end end return 1 end @@ -32,15 +34,21 @@ function ice_settings_screen.new(previous_screen: Scene): MenuScreen find_index(friction_options, ice.friction_factor), friction_options ) - return screens.new({ - choices.new_number( - "acceleration", 50, acceleration_selection, settings.set_ice_acceleration_factor - ), - choices.new_number( - "friction", 70, friction_selection, settings.set_ice_friction_factor - ), - choices.new_done("back", 180, done) - }, done) + return screens.new( + { + choices.new_number( + "acceleration", + 50, + acceleration_selection, + settings.set_ice_acceleration_factor + ), + choices.new_number( + "friction", 70, friction_selection, settings.set_ice_friction_factor + ), + choices.new_done("back", 180, done), + }, + done + ) end return ice_settings_screen diff --git a/src/menus/item_drops_screen.tl b/src/menus/item_drops_screen.tl index 44586aa..6533469 100644 --- a/src/menus/item_drops_screen.tl +++ b/src/menus/item_drops_screen.tl @@ -1,8 +1,8 @@ local scenes = require("engine.scenes") -local settings = require("settings") local choices = require("menus.choices") local items = require("menus.items") local screens = require("menus.screens") +local settings = require("settings") local type Scene = scenes.Scene local type MenuScreen = screens.MenuScreen @@ -16,7 +16,7 @@ function item_drops_screen.new(previous_screen: Scene): MenuScreen local current_idx_by_val: {number: integer} = {} local numbers_to_select_from: {number} = {} - for idx=1, 100 do + for idx = 1, 100 do local val = (idx - 1) * 0.01 -- Percentage resolution current_idx_by_val[val] = idx numbers_to_select_from[idx] = val @@ -27,29 +27,51 @@ function item_drops_screen.new(previous_screen: Scene): MenuScreen end local spawn_settings = settings.game_rules.items.spawn_rates - local nbr_bombs_selected = items.new_number_selection(get_start(spawn_settings.bomb), numbers_to_select_from) - local nbr_fires_selected = items.new_number_selection(get_start(spawn_settings.fire), numbers_to_select_from) - local nbr_speeds_selected = items.new_number_selection(get_start(spawn_settings.speed), numbers_to_select_from) - local nbr_kicks_selected = items.new_number_selection(get_start(spawn_settings.kick), numbers_to_select_from) - local nbr_lightning_bombs_selected = items.new_number_selection(get_start(spawn_settings.lightning_bomb), numbers_to_select_from) - return screens.new({ - choices.new_number( - "drop pct. of bombs", 50, nbr_bombs_selected, settings.set_spawn_of_bombs - ), - choices.new_number( - "drop pct. of fire", 65, nbr_fires_selected, settings.set_spawn_of_fire - ), - choices.new_number( - "drop pct. of speed", 80, nbr_speeds_selected, settings.set_spawn_of_speed - ), - choices.new_number( - "drop pct. of kicks", 95, nbr_kicks_selected, settings.set_spawn_of_kick - ), - choices.new_number( - "drop pct. of lightning-bombs", 110, nbr_lightning_bombs_selected, settings.set_spawn_of_lightning_bombs - ), - choices.new_done("back", 180, done) - }, done) + local nbr_bombs_selected = items.new_number_selection( + get_start(spawn_settings.bomb), numbers_to_select_from + ) + local nbr_fires_selected = items.new_number_selection( + get_start(spawn_settings.fire), numbers_to_select_from + ) + local nbr_speeds_selected = items.new_number_selection( + get_start(spawn_settings.speed), numbers_to_select_from + ) + local nbr_kicks_selected = items.new_number_selection( + get_start(spawn_settings.kick), numbers_to_select_from + ) + local nbr_lightning_bombs_selected = items.new_number_selection( + get_start(spawn_settings.lightning_bomb), numbers_to_select_from + ) + return screens.new( + { + choices.new_number( + "drop pct. of bombs", + 50, + nbr_bombs_selected, + settings.set_spawn_of_bombs + ), + choices.new_number( + "drop pct. of fire", 65, nbr_fires_selected, settings.set_spawn_of_fire + ), + choices.new_number( + "drop pct. of speed", + 80, + nbr_speeds_selected, + settings.set_spawn_of_speed + ), + choices.new_number( + "drop pct. of kicks", 95, nbr_kicks_selected, settings.set_spawn_of_kick + ), + choices.new_number( + "drop pct. of lightning-bombs", + 110, + nbr_lightning_bombs_selected, + settings.set_spawn_of_lightning_bombs + ), + choices.new_done("back", 180, done), + }, + done + ) end return item_drops_screen diff --git a/src/menus/item_setting_screen.tl b/src/menus/item_setting_screen.tl index bc4bff2..46ca3f4 100644 --- a/src/menus/item_setting_screen.tl +++ b/src/menus/item_setting_screen.tl @@ -1,8 +1,8 @@ local scenes = require("engine.scenes") -local settings = require("settings") local choices = require("menus.choices") local items = require("menus.items") local screens = require("menus.screens") +local settings = require("settings") local type Scene = scenes.Scene local type MenuScreen = screens.MenuScreen @@ -15,33 +15,54 @@ function item_settings_screen.new(previous_screen: Scene): MenuScreen end local item_settings = settings.game_rules.items - local nbr_soft_blocks_selected = items.new_integer_selection(item_settings.nbr_soft_blocks, 0, 120) - local nbr_bombs_selected = items.new_integer_selection(item_settings.nbr_of_items.bomb, 0, 30) - local nbr_fires_selected = items.new_integer_selection(item_settings.nbr_of_items.fire, 0, 30) - local nbr_speeds_selected = items.new_integer_selection(item_settings.nbr_of_items.speed, 0, 30) - local nbr_kicks_selected = items.new_integer_selection(item_settings.nbr_of_items.kick, 0, 30) - local nbr_lightning_bombs_selected = items.new_integer_selection(item_settings.nbr_of_items.lightning_bomb, 0, 30) - return screens.new({ - choices.new_integer( - "nbr. of soft blocks", 50, nbr_soft_blocks_selected, settings.set_nbr_soft_blocks - ), - choices.new_integer( - "nbr. of bombs", 65, nbr_bombs_selected, settings.set_nbr_of_bombs - ), - choices.new_integer( - "nbr. of lightning-bombs", 80, nbr_lightning_bombs_selected, settings.set_nbr_of_lightning_bombs - ), - choices.new_integer( - "nbr. of fire", 95, nbr_fires_selected, settings.set_nbr_of_fire - ), - choices.new_integer( - "nbr. of speed", 110, nbr_speeds_selected, settings.set_nbr_of_speed - ), - choices.new_integer( - "nbr. of kick", 125, nbr_kicks_selected, settings.set_nbr_of_kick - ), - choices.new_done("back", 180, done) - }, done) + local nbr_soft_blocks_selected = items.new_integer_selection( + item_settings.nbr_soft_blocks, 0, 120 + ) + local nbr_bombs_selected = items.new_integer_selection( + item_settings.nbr_of_items.bomb, 0, 30 + ) + local nbr_fires_selected = items.new_integer_selection( + item_settings.nbr_of_items.fire, 0, 30 + ) + local nbr_speeds_selected = items.new_integer_selection( + item_settings.nbr_of_items.speed, 0, 30 + ) + local nbr_kicks_selected = items.new_integer_selection( + item_settings.nbr_of_items.kick, 0, 30 + ) + local nbr_lightning_bombs_selected = items.new_integer_selection( + item_settings.nbr_of_items.lightning_bomb, 0, 30 + ) + return screens.new( + { + choices.new_integer( + "nbr. of soft blocks", + 50, + nbr_soft_blocks_selected, + settings.set_nbr_soft_blocks + ), + choices.new_integer( + "nbr. of bombs", 65, nbr_bombs_selected, settings.set_nbr_of_bombs + ), + choices.new_integer( + "nbr. of lightning-bombs", + 80, + nbr_lightning_bombs_selected, + settings.set_nbr_of_lightning_bombs + ), + choices.new_integer( + "nbr. of fire", 95, nbr_fires_selected, settings.set_nbr_of_fire + ), + choices.new_integer( + "nbr. of speed", 110, nbr_speeds_selected, settings.set_nbr_of_speed + ), + choices.new_integer( + "nbr. of kick", 125, nbr_kicks_selected, settings.set_nbr_of_kick + ), + choices.new_done("back", 180, done), + }, + done + ) end return item_settings_screen diff --git a/src/menus/items.tl b/src/menus/items.tl index 939ba85..dfb92aa 100644 --- a/src/menus/items.tl +++ b/src/menus/items.tl @@ -1,6 +1,5 @@ -local audio = require("engine.audio") local clock_text = require("clock_text") - +local audio = require("engine.audio") local record MenuLineColumn text: string @@ -8,7 +7,7 @@ local record MenuLineColumn end function MenuLineColumn.new(text: string, width: number): MenuLineColumn - local self: MenuLineColumn = setmetatable({}, { __index = MenuLineColumn }) + local self: MenuLineColumn = setmetatable({}, {__index = MenuLineColumn}) self.text = text self.width = width return self @@ -20,7 +19,7 @@ local record MenuLine end function MenuLine.new(columns: {MenuLineColumn}, y_pos: number): MenuLine - local self: MenuLine = setmetatable({}, { __index = MenuLine }) + local self: MenuLine = setmetatable({}, {__index = MenuLine}) self.columns = columns self.y_pos = y_pos return self @@ -34,10 +33,14 @@ local record MenuSelection convert: function(integer): T end -local menu_items = {MenuLineColumn = MenuLineColumn, MenuLine = MenuLine, MenuSelection = MenuSelection} +local menu_items = { + MenuLineColumn = MenuLineColumn, MenuLine = MenuLine, MenuSelection = MenuSelection +} -function menu_items.new_integer_selection(start: integer, min: integer, max: integer): MenuSelection - local self: MenuSelection = setmetatable({}, { __index = MenuSelection }) +function menu_items.new_integer_selection( + start: integer, min: integer, max: integer +): MenuSelection + local self: MenuSelection = setmetatable({}, {__index = MenuSelection}) self.value = start self.min = min or 0 self.max = max or 100 @@ -71,16 +74,18 @@ function MenuSelection:as_menu_string(): string return self.tostring(self.value) end -function menu_items.new_boolean_selection(on_by_default: boolean): MenuSelection +function menu_items.new_boolean_selection( + on_by_default: boolean +): MenuSelection local function value_as_on_off(value: integer): string if value == 1 then - return 'on' + return "on" else - return 'off' + return "off" end end local start_val = on_by_default and 1 or 2 - local self: MenuSelection = setmetatable({}, { __index = MenuSelection }) + local self: MenuSelection = setmetatable({}, {__index = MenuSelection}) self.value = start_val self.min = 1 self.max = 2 @@ -93,7 +98,7 @@ function menu_items.new_enum(choices: {T}): MenuSelection local function value_as_enum(value: integer): string return (tostring(choices[value]):gsub("_", " ")) end - local self: MenuSelection = setmetatable({}, { __index = MenuSelection }) + local self: MenuSelection = setmetatable({}, {__index = MenuSelection}) self.value = 1 self.min = 1 self.max = #choices @@ -102,12 +107,14 @@ function menu_items.new_enum(choices: {T}): MenuSelection return self end -function menu_items.new_time_selection(start_idx: integer, times: {number}): MenuSelection +function menu_items.new_time_selection( + start_idx: integer, times: {number} +): MenuSelection local function value_as_clock_time(index: integer): string local time = times[index] return clock_text.from_time(time) end - local self: MenuSelection = setmetatable({}, { __index = MenuSelection }) + local self: MenuSelection = setmetatable({}, {__index = MenuSelection}) self.value = start_idx self.min = 1 self.max = #times @@ -116,12 +123,14 @@ function menu_items.new_time_selection(start_idx: integer, times: {number}): Men return self end -function menu_items.new_number_selection(start_idx: integer, choices: {number}): MenuSelection +function menu_items.new_number_selection( + start_idx: integer, choices: {number} +): MenuSelection local function value_as_string(index: integer): string local val = choices[index] return tostring(val) end - local self: MenuSelection = setmetatable({}, { __index = MenuSelection }) + local self: MenuSelection = setmetatable({}, {__index = MenuSelection}) self.value = start_idx self.min = 1 self.max = #choices diff --git a/src/menus/load_rules_screen.tl b/src/menus/load_rules_screen.tl index a27a2cd..d4fbb1c 100644 --- a/src/menus/load_rules_screen.tl +++ b/src/menus/load_rules_screen.tl @@ -1,7 +1,7 @@ local scenes = require("engine.scenes") -local settings = require("settings") local choices = require("menus.choices") local screens = require("menus.screens") +local settings = require("settings") local type Scene = scenes.Scene local type MenuScreen = screens.MenuScreen @@ -44,18 +44,23 @@ function load_rules_screen.new(previous_screen: Scene, start_screen: Scene): Men settings.set_debug_dinos() scenes.set(start_screen) end - - return screens.new({ - choices.new_done("insanity", 50, load_insanity_rules), - choices.new_done("dino madness", 65, load_dino_madness_rules), - choices.new_done("bomb stack", 80, load_bomb_stack_rules), - choices.new_done("default", 95, load_default_rules), - choices.new_done("debug dinos", 110, load_debug_dino_rules), - choices.new_done("debug kick", 125, load_debug_kick_rules), - choices.new_done("debug lightning-bomb", 140, load_debug_lightning_bomb_rules), - choices.new_done("debug performance", 155, load_debug_performance_rules), - choices.new_done("back", 195, done) - }, done) + + return screens.new( + { + choices.new_done("insanity", 50, load_insanity_rules), + choices.new_done("dino madness", 65, load_dino_madness_rules), + choices.new_done("bomb stack", 80, load_bomb_stack_rules), + choices.new_done("default", 95, load_default_rules), + choices.new_done("debug dinos", 110, load_debug_dino_rules), + choices.new_done("debug kick", 125, load_debug_kick_rules), + choices.new_done( + "debug lightning-bomb", 140, load_debug_lightning_bomb_rules + ), + choices.new_done("debug performance", 155, load_debug_performance_rules), + choices.new_done("back", 195, done), + }, + done + ) end return load_rules_screen diff --git a/src/menus/player_setting_screen.tl b/src/menus/player_setting_screen.tl index 5bbe587..40babf6 100644 --- a/src/menus/player_setting_screen.tl +++ b/src/menus/player_setting_screen.tl @@ -1,8 +1,8 @@ -local items = require("menus.items") local scenes = require("engine.scenes") -local settings = require("settings") local choices = require("menus.choices") +local items = require("menus.items") local screens = require("menus.screens") +local settings = require("settings") local type Scene = scenes.Scene local type MenuScreen = screens.MenuScreen @@ -15,37 +15,69 @@ function player_settings_screen.new(previous_screen: Scene): MenuScreen end local player_settings = settings.game_rules.players - local start_speed_selected = items.new_integer_selection(player_settings.start_speed, 20, 100) - local start_with_bombs_selected = items.new_integer_selection(player_settings.start_with_bombs, 1, 20) - local start_with_lightning_bombs_selected = items.new_integer_selection(player_settings.start_with_lightning_bombs, 1, 20) - local start_with_fire_selected = items.new_integer_selection(player_settings.start_with_fire, 1, 20) - return screens.new({ - choices.new_boolean( - "start position random", 50, player_settings.start_position_shuffle, settings.set_start_position_shuffle - ), - choices.new_boolean( - "mad bombers", 65, player_settings.mad_bombers.enabled, settings.set_mad_bombers - ), - choices.new_boolean( - "mad bombers dont clear", 80, player_settings.mad_bombers.after_sudden_death, settings.set_mad_bombers_after_sudden_death - ), - choices.new_integer( - "start speed", 95, start_speed_selected, settings.set_start_speed - ), - choices.new_boolean( - "start with kick", 110, player_settings.start_with_kick, settings.set_start_with_kick - ), - choices.new_integer( - "start with bombs", 125, start_with_bombs_selected, settings.set_start_with_bombs - ), - choices.new_integer( - "start with lightning-bombs", 140, start_with_lightning_bombs_selected, settings.set_start_with_lightning_bombs - ), - choices.new_integer( - "start with fire", 155, start_with_fire_selected, settings.set_start_with_fire - ), - choices.new_done("back", 180, done) - }, done) + local start_speed_selected = items.new_integer_selection( + player_settings.start_speed, 20, 100 + ) + local start_with_bombs_selected = items.new_integer_selection( + player_settings.start_with_bombs, 1, 20 + ) + local start_with_lightning_bombs_selected = items.new_integer_selection( + player_settings.start_with_lightning_bombs, 1, 20 + ) + local start_with_fire_selected = items.new_integer_selection( + player_settings.start_with_fire, 1, 20 + ) + return screens.new( + { + choices.new_boolean( + "start position random", + 50, + player_settings.start_position_shuffle, + settings.set_start_position_shuffle + ), + choices.new_boolean( + "mad bombers", + 65, + player_settings.mad_bombers.enabled, + settings.set_mad_bombers + ), + choices.new_boolean( + "mad bombers dont clear", + 80, + player_settings.mad_bombers.after_sudden_death, + settings.set_mad_bombers_after_sudden_death + ), + choices.new_integer( + "start speed", 95, start_speed_selected, settings.set_start_speed + ), + choices.new_boolean( + "start with kick", + 110, + player_settings.start_with_kick, + settings.set_start_with_kick + ), + choices.new_integer( + "start with bombs", + 125, + start_with_bombs_selected, + settings.set_start_with_bombs + ), + choices.new_integer( + "start with lightning-bombs", + 140, + start_with_lightning_bombs_selected, + settings.set_start_with_lightning_bombs + ), + choices.new_integer( + "start with fire", + 155, + start_with_fire_selected, + settings.set_start_with_fire + ), + choices.new_done("back", 180, done), + }, + done + ) end return player_settings_screen diff --git a/src/menus/screens.tl b/src/menus/screens.tl index 4fba0c5..3ecbea8 100644 --- a/src/menus/screens.tl +++ b/src/menus/screens.tl @@ -1,9 +1,9 @@ local audio = require("engine.audio") -local choices = require("menus.choices") -local fonts = require("fonts") local joysticks = require("engine.joysticks") -local items = require("menus.items") local scenes = require("engine.scenes") +local fonts = require("fonts") +local choices = require("menus.choices") +local items = require("menus.items") local screen = require("screen") local type MenuFont = fonts.MenuFont @@ -17,7 +17,6 @@ local music: Source local ok_sound: Source local background: Image - local record MenuScreen is Scene menu_choices: {MenuChoice} menu_selection: MenuSelection @@ -27,8 +26,10 @@ end local screens = {MenuScreen = MenuScreen} -function screens.new(menu_choices: {MenuChoice}, go_back?: function(Scene), on_load?: function()): MenuScreen - local self: MenuScreen = setmetatable({}, { __index = MenuScreen }) +function screens.new( + menu_choices: {MenuChoice}, go_back?: function(Scene), on_load?: function() +): MenuScreen + local self: MenuScreen = setmetatable({}, {__index = MenuScreen}) self.menu_choices = menu_choices self.menu_selection = items.new_integer_selection(1, 1, #self.menu_choices) self.on_load = on_load or function() end @@ -58,7 +59,7 @@ function MenuScreen:update(_: number) ok_action = line:update(self) end end - + if joysticks.is_pressed_any(joysticks.BUTTONS.DOWN) then ok_action = self.menu_selection:increment() elseif joysticks.is_pressed_any(joysticks.BUTTONS.UP) then @@ -77,8 +78,8 @@ function MenuScreen:draw() lutro.graphics.setColor(200, 200, 200, 175) local x_pad = 20 local y_pad = 10 - lutro.graphics.rectangle( - "fill", x_pad, y_pad, screen.WIDTH - 2*x_pad, screen.HEIGHT - 2*y_pad + lutro.graphics.rectangle( + "fill", x_pad, y_pad, screen.WIDTH - 2 * x_pad, screen.HEIGHT - 2 * y_pad ) local function get_font(line_idx): MenuFont diff --git a/src/menus/start_screen.tl b/src/menus/start_screen.tl index 795bb2a..b587cf3 100644 --- a/src/menus/start_screen.tl +++ b/src/menus/start_screen.tl @@ -1,17 +1,16 @@ -local audio = require("engine.audio") local characters = require("characters") +local audio = require("engine.audio") +local joysticks = require("engine.joysticks") +local scenes = require("engine.scenes") +local itertools = require("itertools") local choices = require("menus.choices") local debug_screen = require("menus.debug_screen") local game_rule_screen = require("menus.game_rule_screen") local items = require("menus.items") -local itertools = require("itertools") -local joysticks = require("engine.joysticks") -local player_selection = require("player_selection") -local scenes = require("engine.scenes") local screens = require("menus.screens") -local settings = require("settings") local update_sceen = require("menus.update_screen") - +local player_selection = require("player_selection") +local settings = require("settings") local type Scene = scenes.Scene local type MenuScreen = screens.MenuScreen @@ -21,7 +20,9 @@ local type PlayerSelectionMethod = settings.PlayerSelectionMethod local start_screen = {} function start_screen.new(): MenuScreen - local nbr_player_selected = items.new_integer_selection(settings.number_of_players, 2, joysticks.maxplayers()) + local nbr_player_selected = items.new_integer_selection( + settings.number_of_players, 2, joysticks.maxplayers() + ) local choices_values: {PlayerSelectionMethod} = {"on", "off", "keep"} local player_selection_choice = items.new_enum(choices_values) local map_selection_choice = items.new_enum({"football", "factory", "ice_rink"}) @@ -38,7 +39,8 @@ function start_screen.new(): MenuScreen end scenes.set(player_selection.setup()) end - local function go_back(_: Scene) end -- Does nothing at start screen + local function go_back(_: Scene) + end -- Does nothing at start screen local function enter_debug_screen(ctx: Scene) scenes.set(debug_screen.new(ctx)) end @@ -65,20 +67,32 @@ function start_screen.new(): MenuScreen end local function on_load() nbr_player_selected.value = settings.number_of_players - player_selection_choice.value = itertools.find(settings.player_selection, choices_values) + player_selection_choice.value = itertools.find( + settings.player_selection, choices_values + ) update_player_selection_choices() end local menu_choices: {MenuChoice} = { - choices.new_integer("number of players", 50, nbr_player_selected, set_number_of_players), - choices.new_enum("player selection", 65, player_selection_choice, settings.set_player_selection), + choices.new_integer( + "number of players", 50, nbr_player_selected, set_number_of_players + ), + choices.new_enum( + "player selection", + 65, + player_selection_choice, + settings.set_player_selection + ), choices.new_boolean("teams", 80, settings.teams, settings.set_teams), choices.new_enum("map", 95, map_selection_choice, settings.set_map), choices.new_done("game rules", 110, enter_game_rules_screen), choices.new_done("debug settings", 125, enter_debug_screen), } if update_sceen.is_recalbox() then - table.insert(menu_choices, choices.new_done("check for updates", 140, enter_update_screen)) + table.insert( + menu_choices, + choices.new_done("check for updates", 140, enter_update_screen) + ) end table.insert(menu_choices, choices.new_done("start game", 180, done)) diff --git a/src/menus/timing_settings_screen.tl b/src/menus/timing_settings_screen.tl index b7b567b..79dc9ba 100644 --- a/src/menus/timing_settings_screen.tl +++ b/src/menus/timing_settings_screen.tl @@ -1,8 +1,8 @@ -local items = require("menus.items") local scenes = require("engine.scenes") -local settings = require("settings") local choices = require("menus.choices") +local items = require("menus.items") local screens = require("menus.screens") +local settings = require("settings") local type Scene = scenes.Scene local type MenuScreen = screens.MenuScreen @@ -16,7 +16,7 @@ function timing_settings_screen.new(previous_screen: Scene): MenuScreen local current_idx_by_time: {number: integer} = {} local times_to_select_from: {number} = {} - for idx=1, 100 do + for idx = 1, 100 do local time = 60 + (idx - 1) * 5 -- Second resolution current_idx_by_time[time] = idx times_to_select_from[idx] = time @@ -25,22 +25,34 @@ function timing_settings_screen.new(previous_screen: Scene): MenuScreen local timings = settings.game_rules.timings local battle_idx = current_idx_by_time[timings.battle] or 5 local sudden_death_idx = current_idx_by_time[timings.sudden_death] or 1 - local nbr_battles_selection = items.new_integer_selection(settings.game_rules.timings.nbr_battles, 1, 20) - local battle_time_selection = items.new_time_selection(battle_idx, times_to_select_from) - local sudden_death_time_selection = items.new_time_selection(sudden_death_idx, times_to_select_from) + local nbr_battles_selection = items.new_integer_selection( + settings.game_rules.timings.nbr_battles, 1, 20 + ) + local battle_time_selection = items.new_time_selection( + battle_idx, times_to_select_from + ) + local sudden_death_time_selection = items.new_time_selection( + sudden_death_idx, times_to_select_from + ) - return screens.new({ - choices.new_integer( - "nbr. battles", 50, nbr_battles_selection, settings.set_nbr_battles - ), - choices.new_number( - "battle time", 70, battle_time_selection, settings.set_battle_time - ), - choices.new_number( - "sudden death time", 90, sudden_death_time_selection, settings.set_sudden_death_time - ), - choices.new_done("back", 180, done) - }, done) + return screens.new( + { + choices.new_integer( + "nbr. battles", 50, nbr_battles_selection, settings.set_nbr_battles + ), + choices.new_number( + "battle time", 70, battle_time_selection, settings.set_battle_time + ), + choices.new_number( + "sudden death time", + 90, + sudden_death_time_selection, + settings.set_sudden_death_time + ), + choices.new_done("back", 180, done), + }, + done + ) end return timing_settings_screen diff --git a/src/menus/update_screen.tl b/src/menus/update_screen.tl index fc110b2..076cd25 100644 --- a/src/menus/update_screen.tl +++ b/src/menus/update_screen.tl @@ -1,10 +1,9 @@ -local fonts = require("fonts") local joysticks = require("engine.joysticks") local scenes = require("engine.scenes") +local fonts = require("fonts") local screen = require("screen") - -local type MenuFont = fonts.MenuFont +local type MenuFont = fonts.MenuFont local type Scene = scenes.Scene local ROM_DIR = "/recalbox/share/roms/lutro" @@ -13,8 +12,8 @@ local white_font: MenuFont local update_screen = {} local function capture(cmd: string): string - local f = assert(io.popen(cmd, 'r')) - local s = f:read('*a') + local f = assert(io.popen(cmd, "r")) + local s = f:read("*a") f:close() return s end @@ -26,13 +25,18 @@ local function run_script(script_name: string): (integer, string) end local function file_exists(name: string): boolean - local f = io.open(name,"r") - if f~=nil then io.close(f) return true else return false end + local f = io.open(name, "r") + if f ~= nil then + io.close(f) + return true + else + return false + end end local function file_size(name: string): integer - local f = io.open(name,"r") - if f~=nil then + local f = io.open(name, "r") + if f ~= nil then local size = f:seek("end") io.close(f) return size @@ -42,7 +46,7 @@ local function file_size(name: string): integer end local record UpdateCheckScene is Scene - ctx: Scene + ctx: Scene since_last_connection_check: number have_connection: boolean download_url: string @@ -53,7 +57,7 @@ local record UpdateCheckScene is Scene end function update_screen.new(ctx: Scene): UpdateCheckScene - local self: UpdateCheckScene = setmetatable({}, { __index = UpdateCheckScene }) + local self: UpdateCheckScene = setmetatable({}, {__index = UpdateCheckScene}) self.ctx = ctx self.since_last_connection_check = 0 self.have_connection = false @@ -83,7 +87,8 @@ function UpdateCheckScene:download_finished(): boolean end function UpdateCheckScene:update(dt: number) - if joysticks.is_pressed_any(joysticks.BUTTONS.L) or joysticks.is_pressed_any(joysticks.BUTTONS.A) then + if joysticks.is_pressed_any(joysticks.BUTTONS.L) + or joysticks.is_pressed_any(joysticks.BUTTONS.A) then scenes.set(self.ctx) return end @@ -119,17 +124,25 @@ local function show(left: string, right: string, y_cord: number) end function UpdateCheckScene:draw() - lutro.graphics.setColor(72, 72, 72) - lutro.graphics.rectangle("fill", 0, 0, screen.WIDTH, screen.HEIGHT) + lutro.graphics.setColor(72, 72, 72) + lutro.graphics.rectangle("fill", 0, 0, screen.WIDTH, screen.HEIGHT) local function connection_str(): string - if self.have_connection then return "ok" else return " " end + if self.have_connection then + return "ok" + else + return " " + end end show("checking connection... ", connection_str(), 40) local function is_up_to_date_str(): string - if self.is_up_to_date then return "yes" else return "no " end + if self.is_up_to_date then + return "yes" + else + return "no " + end end if self.have_connection then diff --git a/src/movement.tl b/src/movement.tl index 42275a7..d3ad7a3 100644 --- a/src/movement.tl +++ b/src/movement.tl @@ -1,14 +1,14 @@ local bombs = require("bombs.bombs") +local lightning_bombs = require("bombs.lightning_bombs") local collision = require("engine.collision") +local points = require("engine.geometry.points") +local rectangles = require("engine.geometry.rectangles") +local vectors = require("engine.geometry.vectors") +local joysticks = require("engine.joysticks") local itertools = require("itertools") local grid = require("level_layout.grid") -local joysticks = require("engine.joysticks") -local lightning_bombs = require("bombs.lightning_bombs") local players = require("players") local settings = require("settings") -local vectors = require("engine.geometry.vectors") -local points = require("engine.geometry.points") -local rectangles = require("engine.geometry.rectangles") local type Action = players.Action local type Bomb = bombs.Bomb @@ -24,80 +24,93 @@ local type Point = points.Point local type Rectangle = rectangles.Rectangle local record Movement - dxdy: Vector - dir: Direction + dxdy: Vector + dir: Direction end function Movement.new(dxdy: Vector, dir: Direction): Movement - local self: Movement = setmetatable({}, { __index = Movement }) - self.dir = dir - self.dxdy = dxdy - return self + local self: Movement = setmetatable({}, {__index = Movement}) + self.dir = dir + self.dxdy = dxdy + return self end function Movement.new_from_player(dt: number, player: Player): Movement - local self: Movement = setmetatable({}, { __index = Movement }) - self.dir = player.direction - self.dxdy = vectors.from_direction(self.dir):scale(dt * player:get_speed()) - return self + local self: Movement = setmetatable({}, {__index = Movement}) + self.dir = player.direction + self.dxdy = vectors.from_direction(self.dir):scale(dt * player:get_speed()) + return self end local record CheckedMovement - position: Point - spent: boolean - cuts_corner: boolean - constrained: boolean -- true if anything was touched: block, bomb, boundary, or corner cut - bombs_kicked: {{Bomb, Vector}} + position: Point + spent: boolean + cuts_corner: boolean + constrained: boolean -- true if anything was touched: block, bomb, boundary, or corner cut + bombs_kicked: {{Bomb, Vector}} end function CheckedMovement.new( - position: Point, spent: boolean, cuts_corner: boolean, bombs_kicked?: {{Bomb, Vector}} + position: Point, + spent: boolean, + cuts_corner: boolean, + bombs_kicked?: {{Bomb, Vector}} ): CheckedMovement - local self: CheckedMovement = setmetatable({}, { __index = CheckedMovement }) - self.position = position - self.spent = spent - self.cuts_corner = cuts_corner - self.constrained = not spent or cuts_corner - self.bombs_kicked = bombs_kicked or {} - return self + local self: CheckedMovement = setmetatable({}, {__index = CheckedMovement}) + self.position = position + self.spent = spent + self.cuts_corner = cuts_corner + self.constrained = not spent or cuts_corner + self.bombs_kicked = bombs_kicked or {} + return self end local record MovementWithCheck - dxdy: Vector - dir: Direction - check: CheckedMovement + dxdy: Vector + dir: Direction + check: CheckedMovement end -function MovementWithCheck.new(dxdy: Vector, dir: Direction, check: CheckedMovement): MovementWithCheck - local self: MovementWithCheck = setmetatable({}, { __index = MovementWithCheck }) - self.dir = dir - self.dxdy = dxdy - self.check = check - return self +function MovementWithCheck.new( + dxdy: Vector, dir: Direction, check: CheckedMovement +): MovementWithCheck + local self: MovementWithCheck = setmetatable({}, {__index = MovementWithCheck}) + self.dir = dir + self.dxdy = dxdy + self.check = check + return self end local function get_moves_from_input(player: Player, dt: number): {Movement} - local result: {Movement} = {} - - if lutro.joystick.isDown(player.id, joysticks.BUTTONS.DOWN) then - table.insert(result, Movement.new(vectors.new(0, dt * player:get_speed()), "down")) - end - if lutro.joystick.isDown(player.id, joysticks.BUTTONS.UP) then - table.insert(result, Movement.new(vectors.new(0, -dt * player:get_speed()), "up")) - end - if lutro.joystick.isDown(player.id, joysticks.BUTTONS.RIGHT) then - table.insert(result, Movement.new(vectors.new(dt * player:get_speed(), 0), "right")) - end - if lutro.joystick.isDown(player.id, joysticks.BUTTONS.LEFT) then - table.insert(result, Movement.new(vectors.new(-dt * player:get_speed(), 0), "left")) - end - - return result + local result: {Movement} = {} + + if lutro.joystick.isDown(player.id, joysticks.BUTTONS.DOWN) then + table.insert( + result, Movement.new(vectors.new(0, dt * player:get_speed()), "down") + ) + end + if lutro.joystick.isDown(player.id, joysticks.BUTTONS.UP) then + table.insert( + result, Movement.new(vectors.new(0, -dt * player:get_speed()), "up") + ) + end + if lutro.joystick.isDown(player.id, joysticks.BUTTONS.RIGHT) then + table.insert( + result, Movement.new(vectors.new(dt * player:get_speed(), 0), "right") + ) + end + if lutro.joystick.isDown(player.id, joysticks.BUTTONS.LEFT) then + table.insert( + result, Movement.new(vectors.new(-dt * player:get_speed(), 0), "left") + ) + end + + return result end --- Sign of a number represented as 1 or -1 local function sign_of(number: number): integer - return number > 0 and 1 or (number == 0 and 0 or -1) + return number > 0 and 1 or (number == 0 and 0 or -1) end --- Split movement that goes against block into parts for walking corner. @@ -107,443 +120,504 @@ end -- we are walking against the wall on the y-axis. If this is not the case -- the vector returned have to be permutated. local function around_corner_move( - movement_len: number, dist_to_corner: number, pos_and_block_lateral_diff: number + movement_len: number, dist_to_corner: number, pos_and_block_lateral_diff: number ): Vector - local movement_dist = math.abs(movement_len) - local movement_towards_corner = math.min(dist_to_corner, movement_dist) - local movement_down_corner = movement_dist - movement_towards_corner - - return vectors.new( - sign_of(pos_and_block_lateral_diff) * movement_towards_corner, - sign_of(movement_len) * movement_down_corner - ) + local movement_dist = math.abs(movement_len) + local movement_towards_corner = math.min(dist_to_corner, movement_dist) + local movement_down_corner = movement_dist - movement_towards_corner + + return vectors.new( + sign_of(pos_and_block_lateral_diff) * movement_towards_corner, + sign_of(movement_len) * movement_down_corner + ) end local function in_corridor_and_walking_against_wall( - position: Point, walk_dir: Direction + position: Point, walk_dir: Direction ): boolean - local local_pos = grid.get_local_tile_position(position) - local in_vertical_corridor = local_pos.x == 0 and local_pos.y > 0 - local in_horizontal_corridor = local_pos.y == 0 and local_pos.x > 0 + local local_pos = grid.get_local_tile_position(position) + local in_vertical_corridor = local_pos.x == 0 and local_pos.y > 0 + local in_horizontal_corridor = local_pos.y == 0 and local_pos.x > 0 - return (in_vertical_corridor and (walk_dir == "left" or walk_dir == "right")) or - (in_horizontal_corridor and (walk_dir == "up" or walk_dir == "down")) + return (in_vertical_corridor and (walk_dir == "left" or walk_dir == "right")) + or (in_horizontal_corridor and (walk_dir == "up" or walk_dir == "down")) end local function in_end_of_corridor_and_walking_out( - movement: MovementWithCheck, player_pos: Point + movement: MovementWithCheck, player_pos: Point ): boolean - local local_pos = grid.get_local_tile_position(player_pos) - - local function first_half_of_block(val: number): boolean - return val < grid.TILE_SIZE / 2 and val > 0 - end - local function second_half_of_block(val: number): boolean - return val > grid.TILE_SIZE / 2 - end - - return (movement.dir == "down" and second_half_of_block(local_pos.y)) or - (movement.dir == "up" and first_half_of_block(local_pos.y)) or - (movement.dir == "right" and second_half_of_block(local_pos.x)) or - (movement.dir == "left" and first_half_of_block(local_pos.x)) + local local_pos = grid.get_local_tile_position(player_pos) + + local function first_half_of_block(val: number): boolean + return val < grid.TILE_SIZE / 2 and val > 0 + end + local function second_half_of_block(val: number): boolean + return val > grid.TILE_SIZE / 2 + end + + return (movement.dir == "down" and second_half_of_block(local_pos.y)) + or (movement.dir == "up" and first_half_of_block(local_pos.y)) + or (movement.dir == "right" and second_half_of_block(local_pos.x)) + or (movement.dir == "left" and first_half_of_block(local_pos.x)) end local function restrict_player_movement( - player: Player, - movement: Vector, - blocks: {Kinematic}, - close_lightning_bombs: {LightningBomb}, - blocks_bombs: ReadableGroup, - close_bombs: {Bomb}, - players_: ReadableGroup, - bombs_: ReadableGroup, - lookup: Lookup + player: Player, + movement: Vector, + blocks: {Kinematic}, + close_lightning_bombs: {LightningBomb}, + blocks_bombs: ReadableGroup, + close_bombs: {Bomb}, + players_: ReadableGroup, + bombs_: ReadableGroup, + lookup: Lookup ): CheckedMovement - local margin_for_corner_assist = 8 - local new_position = player.position:move(movement) - local new_hitbox = player:movementbox():shift(movement) - - local function move_against_block( - movement_len: number, - pos_and_block_lateral_diff: number, - walk_along_y_axis: boolean - ): CheckedMovement - local dist_to_corner = grid.TILE_SIZE - math.abs(pos_and_block_lateral_diff) - - if dist_to_corner > margin_for_corner_assist then - return CheckedMovement.new(player.position, false, false) - end - - local along_wall = around_corner_move( - movement_len, dist_to_corner, pos_and_block_lateral_diff - ):permutate_if(walk_along_y_axis) - - -- Using 'new_position' instead of 'player_pos' here is not a bug, this - -- actually makes corner movement smoother and feel much better. - -- It also makes so you move over the corner, i.e 'cutting the corner' - return CheckedMovement.new(new_position:move(along_wall), true, true) - end - - local old_hitbox = player:movementbox() - local function walking_further_into_block(block: Rectangle): boolean - if not new_hitbox:overlap(block) then - return false - end - - local new_dist_to_1_ton_block = block:center_distance(new_hitbox) - local old_dist_to_1_ton_block = block:center_distance(old_hitbox) - return new_dist_to_1_ton_block < old_dist_to_1_ton_block - end - - local function check_block_collision(block_box: Rectangle): CheckedMovement - if movement.x == 0 then - return move_against_block(movement.y, player.position.x - block_box.x, false) - elseif movement.y == 0 then - return move_against_block(movement.x, player.position.y - block_box.y, true) - else - assert(false, "Bad movement vector") - end - end - - local bombs_player_is_inside: {Kinematic: Kinematic} = {} - for _, bomb in ipairs(close_bombs) do - if bomb.position:distance_to(player.position) <= grid.TILE_SIZE * (3 / 4) and not bomb.has_been_kicked then - bombs_player_is_inside[bomb] = bomb - end - end - for _, bomb in ipairs(close_lightning_bombs) do - if bomb.position:distance_to(player.position) <= grid.TILE_SIZE * (3 / 4) then - bombs_player_is_inside[bomb] = bomb - end - end - - -- Attempt a corner cut around the given hitbox. Returns the cut movement if - -- the path is clear of all blocks, bombs and lightning bombs, nil otherwise. - -- skip excludes the obstacle being cut around. bombs_player_is_inside are - -- also excluded since the player can freely overlap them during the cut. - local function try_corner_cut(hitbox: Rectangle, skip?: Kinematic): CheckedMovement | nil - local result = check_block_collision(hitbox) - if not result.cuts_corner then - return nil - end - local corner_cut_hitbox = player:movementbox():shift(player.position:vector_to(result.position)) - local all_obstacles = itertools.combine(blocks, close_bombs, close_lightning_bombs) - local function not_skipped(obstacle: Kinematic): boolean - return obstacle ~= skip and not bombs_player_is_inside[obstacle] - end - for _, obstacle in ipairs(itertools.filter(all_obstacles, not_skipped)) do - if corner_cut_hitbox:overlap(obstacle:movementbox()) then - return nil - end - end - return result - end - - for _, bomb in ipairs(close_bombs) do - local bomb_hitbox = bomb:movementbox() - if walking_further_into_block(bomb_hitbox) and not bombs_player_is_inside[bomb] then - -- If a bomb has been kicked then it is no longer to move - -- freely in far inside it. This is to stop moving through - -- bombs that are kicked against a player. - local corner_cut = try_corner_cut(bomb_hitbox, bomb) - if corner_cut then - return corner_cut - end - local kicked_bombs: {{Bomb, Vector}} = {} - if player.has_kick then - local kick_direction = movement:normalize() - local can_move = bomb:can_move( - kick_direction:scale(grid.TILE_SIZE), blocks_bombs, players_, bombs_, lookup - ) - if can_move.yes then - -- Walking into bomb and at edge of bomb then kick - table.insert(kicked_bombs, {bomb, can_move.in_direction}) - end - end - -- Not allowed to walk further into bomb if at edge - return CheckedMovement.new(player.position, false, false, kicked_bombs) - end - end - - for _, bomb in ipairs(close_lightning_bombs) do - local bomb_hitbox = bomb:movementbox() - if walking_further_into_block(bomb_hitbox) and not bombs_player_is_inside[bomb] then - local corner_cut = try_corner_cut(bomb_hitbox, bomb) - if corner_cut then - return corner_cut - end - -- Not allowed to walk further into bomb if at edge - return CheckedMovement.new(player.position, false, false) - end - end - - for _, block in ipairs(blocks) do - local block_box = block:movementbox() - if walking_further_into_block(block_box) then - local corner_cut = try_corner_cut(block_box, block) - if corner_cut then - return corner_cut - end - return CheckedMovement.new(player.position, false, false) - end - end - - if not grid.area():contains(new_hitbox) then - local clamped_position = grid.area():clamp(new_hitbox):top_left() - return CheckedMovement.new(clamped_position, false, false) - end - - return CheckedMovement.new(new_position, true, false) + local margin_for_corner_assist = 8 + local new_position = player.position:move(movement) + local new_hitbox = player:movementbox():shift(movement) + + local function move_against_block( + movement_len: number, + pos_and_block_lateral_diff: number, + walk_along_y_axis: boolean + ): CheckedMovement + local dist_to_corner = grid.TILE_SIZE - math.abs(pos_and_block_lateral_diff) + + if dist_to_corner > margin_for_corner_assist then + return CheckedMovement.new(player.position, false, false) + end + + local along_wall = around_corner_move( + movement_len, dist_to_corner, pos_and_block_lateral_diff + ):permutate_if(walk_along_y_axis) + + -- Using 'new_position' instead of 'player_pos' here is not a bug, this + -- actually makes corner movement smoother and feel much better. + -- It also makes so you move over the corner, i.e 'cutting the corner' + return CheckedMovement.new(new_position:move(along_wall), true, true) + end + + local old_hitbox = player:movementbox() + local function walking_further_into_block(block: Rectangle): boolean + if not new_hitbox:overlap(block) then + return false + end + + local new_dist_to_1_ton_block = block:center_distance(new_hitbox) + local old_dist_to_1_ton_block = block:center_distance(old_hitbox) + return new_dist_to_1_ton_block < old_dist_to_1_ton_block + end + + local function check_block_collision(block_box: Rectangle): CheckedMovement + if movement.x == 0 then + return move_against_block( + movement.y, player.position.x - block_box.x, false + ) + elseif movement.y == 0 then + return move_against_block(movement.x, player.position.y - block_box.y, true) + else + assert(false, "Bad movement vector") + end + end + + local bombs_player_is_inside: {Kinematic: Kinematic} = {} + for _, bomb in ipairs(close_bombs) do + if bomb.position:distance_to(player.position) <= grid.TILE_SIZE * (3 / 4) + and not bomb.has_been_kicked then + bombs_player_is_inside[bomb] = bomb + end + end + for _, bomb in ipairs(close_lightning_bombs) do + if bomb.position:distance_to(player.position) <= grid.TILE_SIZE * (3 / 4) then + bombs_player_is_inside[bomb] = bomb + end + end + + -- Attempt a corner cut around the given hitbox. Returns the cut movement if + -- the path is clear of all blocks, bombs and lightning bombs, nil otherwise. + -- skip excludes the obstacle being cut around. bombs_player_is_inside are + -- also excluded since the player can freely overlap them during the cut. + local function try_corner_cut( + hitbox: Rectangle, skip?: Kinematic + ): CheckedMovement | nil + local result = check_block_collision(hitbox) + if not result.cuts_corner then + return nil + end + local corner_cut_hitbox = player:movementbox():shift( + player.position:vector_to(result.position) + ) + local all_obstacles = itertools.combine( + blocks, close_bombs, close_lightning_bombs + ) + local function not_skipped(obstacle: Kinematic): boolean + return obstacle ~= skip and not bombs_player_is_inside[obstacle] + end + for _, obstacle in ipairs(itertools.filter(all_obstacles, not_skipped)) do + if corner_cut_hitbox:overlap(obstacle:movementbox()) then + return nil + end + end + return result + end + + for _, bomb in ipairs(close_bombs) do + local bomb_hitbox = bomb:movementbox() + if walking_further_into_block(bomb_hitbox) + and not bombs_player_is_inside[bomb] then + -- If a bomb has been kicked then it is no longer to move + -- freely in far inside it. This is to stop moving through + -- bombs that are kicked against a player. + local corner_cut = try_corner_cut(bomb_hitbox, bomb) + if corner_cut then + return corner_cut + end + local kicked_bombs: {{Bomb, Vector}} = {} + if player.has_kick then + local kick_direction = movement:normalize() + local can_move = bomb:can_move( + kick_direction:scale(grid.TILE_SIZE), + blocks_bombs, + players_, + bombs_, + lookup + ) + if can_move.yes then + -- Walking into bomb and at edge of bomb then kick + table.insert(kicked_bombs, {bomb, can_move.in_direction}) + end + end + -- Not allowed to walk further into bomb if at edge + return CheckedMovement.new(player.position, false, false, kicked_bombs) + end + end + + for _, bomb in ipairs(close_lightning_bombs) do + local bomb_hitbox = bomb:movementbox() + if walking_further_into_block(bomb_hitbox) + and not bombs_player_is_inside[bomb] then + local corner_cut = try_corner_cut(bomb_hitbox, bomb) + if corner_cut then + return corner_cut + end + -- Not allowed to walk further into bomb if at edge + return CheckedMovement.new(player.position, false, false) + end + end + + for _, block in ipairs(blocks) do + local block_box = block:movementbox() + if walking_further_into_block(block_box) then + local corner_cut = try_corner_cut(block_box, block) + if corner_cut then + return corner_cut + end + return CheckedMovement.new(player.position, false, false) + end + end + + if not grid.area():contains(new_hitbox) then + local clamped_position = grid.area():clamp(new_hitbox):top_left() + return CheckedMovement.new(clamped_position, false, false) + end + + return CheckedMovement.new(new_position, true, false) end - local function get_new_player_orientation( - player_dir: Direction, - directions: {Movement}, - blocks: {Kinematic}, - lightning_bombs_on_ground: ReadableGroup, - player: Player, - max_directions: integer, - blocks_bombs: ReadableGroup, - players_: ReadableGroup, - bombs_: ReadableGroup, - lookup: Lookup + player_dir: Direction, + directions: {Movement}, + blocks: {Kinematic}, + lightning_bombs_on_ground: ReadableGroup, + player: Player, + max_directions: integer, + blocks_bombs: ReadableGroup, + players_: ReadableGroup, + bombs_: ReadableGroup, + lookup: Lookup ): Direction, Point, boolean - - --- Get movement priority, lower number is higher priority. - -- For example walking out of corridor and cutting corner - -- have a low priority and should be tried last. This is - -- mainly to sort and handle all cases when the user press multiple - -- directions in a deterministic and expected way. - local function movement_prio(movement: MovementWithCheck): integer - if in_end_of_corridor_and_walking_out(movement, player.position) then - -- On ice, pressing down while sliding horizontally can put the player - -- slightly past the tile midpoint, which triggers - -- in_end_of_corridor_and_walking_out for the downward corner cut. - -- Without this split, the corner cut ties with the horizontal - -- movement at prio 4, the horizontal fires first, and the corner - -- cut is dropped by the constrained early-break. - if movement.check.cuts_corner then - return 4 - end - return 5 - elseif movement.check.cuts_corner then - return 3 - elseif in_corridor_and_walking_against_wall(player.position, movement.dir) then - return 2 - else - return 1 - end - end - - local function sort_directions_by_prio(to_sort: {MovementWithCheck}) - local function compare(m1: MovementWithCheck, m2: MovementWithCheck): boolean - return movement_prio(m1) < movement_prio(m2) - end - table.sort(to_sort, compare) - end - - local cur_position = player.position - local cur_direction = player_dir - local directions_checks: {MovementWithCheck} = {} - local close_bombs = lookup:in_box_and_group(player:movementbox():expand(grid.TILE_SIZE), bombs_) - local close_lightning_bombs = lookup:in_box_and_group(player:movementbox():expand(grid.TILE_SIZE), lightning_bombs_on_ground) - for _, direction in ipairs(directions) do - local check = restrict_player_movement( - player, direction.dxdy, blocks, close_lightning_bombs, blocks_bombs, close_bombs, players_, bombs_, lookup - ) - table.insert(directions_checks, MovementWithCheck.new(direction.dxdy, direction.dir, check)) - end - - sort_directions_by_prio(directions_checks) - - local function any_constrained(checks: {MovementWithCheck}): boolean - for _, dc in ipairs(checks) do - if dc.check.constrained then return true end - end - return false - end - - local constrained = any_constrained(directions_checks) - - local directions_applied = 0 - for _, direction in ipairs(directions_checks) do - local check = direction.check - -- Accumulate displacement so movements compose rather than overwrite - cur_position = cur_position:move(player.position:vector_to(check.position)) - cur_direction = direction.dir - for _, bomb_kicked in ipairs(check.bombs_kicked) do - local bomb = bomb_kicked[1] - local kick_direction = bomb_kicked[2] - bomb:kick(kick_direction) - end - if check.spent then - directions_applied = directions_applied + 1 - - if constrained or directions_applied >= max_directions then - break - end - end - end - return cur_direction, cur_position, constrained + --- Get movement priority, lower number is higher priority. + -- For example walking out of corridor and cutting corner + -- have a low priority and should be tried last. This is + -- mainly to sort and handle all cases when the user press multiple + -- directions in a deterministic and expected way. + local function movement_prio(movement: MovementWithCheck): integer + if in_end_of_corridor_and_walking_out(movement, player.position) then + -- On ice, pressing down while sliding horizontally can put the player + -- slightly past the tile midpoint, which triggers + -- in_end_of_corridor_and_walking_out for the downward corner cut. + -- Without this split, the corner cut ties with the horizontal + -- movement at prio 4, the horizontal fires first, and the corner + -- cut is dropped by the constrained early-break. + if movement.check.cuts_corner then + return 4 + end + return 5 + elseif movement.check.cuts_corner then + return 3 + elseif in_corridor_and_walking_against_wall(player.position, movement.dir) then + return 2 + else + return 1 + end + end + + local function sort_directions_by_prio(to_sort: {MovementWithCheck}) + local function compare(m1: MovementWithCheck, m2: MovementWithCheck): boolean + return movement_prio(m1) < movement_prio(m2) + end + table.sort(to_sort, compare) + end + + local cur_position = player.position + local cur_direction = player_dir + local directions_checks: {MovementWithCheck} = {} + local close_bombs = lookup:in_box_and_group( + player:movementbox():expand(grid.TILE_SIZE), bombs_ + ) + local close_lightning_bombs = lookup:in_box_and_group( + player:movementbox():expand(grid.TILE_SIZE), lightning_bombs_on_ground + ) + for _, direction in ipairs(directions) do + local check = restrict_player_movement( + player, + direction.dxdy, + blocks, + close_lightning_bombs, + blocks_bombs, + close_bombs, + players_, + bombs_, + lookup + ) + table.insert( + directions_checks, + MovementWithCheck.new(direction.dxdy, direction.dir, check) + ) + end + + sort_directions_by_prio(directions_checks) + + local function any_constrained(checks: {MovementWithCheck}): boolean + for _, dc in ipairs(checks) do + if dc.check.constrained then + return true + end + end + return false + end + + local constrained = any_constrained(directions_checks) + + local directions_applied = 0 + for _, direction in ipairs(directions_checks) do + local check = direction.check + -- Accumulate displacement so movements compose rather than overwrite + cur_position = cur_position:move(player.position:vector_to(check.position)) + cur_direction = direction.dir + for _, bomb_kicked in ipairs(check.bombs_kicked) do + local bomb = bomb_kicked[1] + local kick_direction = bomb_kicked[2] + bomb:kick(kick_direction) + end + if check.spent then + directions_applied = directions_applied + 1 + + if constrained or directions_applied >= max_directions then + break + end + end + end + return cur_direction, cur_position, constrained end local movement = {} local INVERTED_DIRECTIONS: {Direction: Direction} = { - up = "down", - down = "up", - left = "right", - right = "left" + up = "down", down = "up", left = "right", right = "left" } local function inputs_for_sliding(player: Player, move_inputs: {Movement}): boolean - for _, move in ipairs(move_inputs) do - if move.dir == INVERTED_DIRECTIONS[player.direction] then - return true - end - end + for _, move in ipairs(move_inputs) do + if move.dir == INVERTED_DIRECTIONS[player.direction] then + return true + end + end - return false + return false end -local function get_moves(player: Player, move_inputs: {Movement}, momentum: Movement): {Movement} - if player.action == "slide" then - return {momentum} - elseif player.action == "dash" then - if player.dino and player.dino.level >= 3 then - -- For level 3 dinos then check if direction is changed which is - -- allowed for highest level dino - - for _, move in ipairs(move_inputs) do - if move.dir ~= player.direction then - -- Also return move to continue with dash which will be used - -- if changing direction does not work - return {move, momentum} - end - end - end - - return {momentum} - else - return move_inputs - end +local function get_moves( + player: Player, move_inputs: {Movement}, momentum: Movement +): {Movement} + if player.action == "slide" then + return {momentum} + elseif player.action == "dash" then + if player.dino and player.dino.level >= 3 then + -- For level 3 dinos then check if direction is changed which is + -- allowed for highest level dino + for _, move in ipairs(move_inputs) do + if move.dir ~= player.direction then + -- Also return move to continue with dash which will be used + -- if changing direction does not work + return {move, momentum} + end + end + end + + return {momentum} + else + return move_inputs + end end local function is_diagonal(moves: {Movement}): boolean - local has_x = false - local has_y = false - for _, move in ipairs(moves) do - if move.dxdy.x ~= 0 then has_x = true end - if move.dxdy.y ~= 0 then has_y = true end - end - return has_x and has_y + local has_x = false + local has_y = false + for _, move in ipairs(moves) do + if move.dxdy.x ~= 0 then + has_x = true + end + if move.dxdy.y ~= 0 then + has_y = true + end + end + return has_x and has_y end local function any_move_has_direction(moves: {Movement}, dir: Direction): boolean - for _, move in ipairs(moves) do - if move.dir == dir then - return true - end - end - return false + for _, move in ipairs(moves) do + if move.dir == dir then + return true + end + end + return false end -local function direction_for_diagonal(moves: {Movement}, current_dir: Direction, computed_dir: Direction, constrained: boolean): Direction - if constrained then - return computed_dir - end - - if is_diagonal(moves) and any_move_has_direction(moves, current_dir) then - return current_dir - end - return computed_dir +local function direction_for_diagonal( + moves: {Movement}, + current_dir: Direction, + computed_dir: Direction, + constrained: boolean +): Direction + if constrained then + return computed_dir + end + + if is_diagonal(moves) and any_move_has_direction(moves, current_dir) then + return current_dir + end + return computed_dir end -local function direction_from_dominant_velocity(vel: Vector, current_dir: Direction): Direction - if math.abs(vel.x) > math.abs(vel.y) then - return vel.x > 0 and "right" or "left" - elseif math.abs(vel.y) > math.abs(vel.x) then - return vel.y > 0 and "down" or "up" - elseif vel.x ~= 0 then - return vel.x > 0 and "right" or "left" - elseif vel.y ~= 0 then - return vel.y > 0 and "down" or "up" - else - return current_dir - end +local function direction_from_dominant_velocity( + vel: Vector, current_dir: Direction +): Direction + if math.abs(vel.x) > math.abs(vel.y) then + return vel.x > 0 and "right" or "left" + elseif math.abs(vel.y) > math.abs(vel.x) then + return vel.y > 0 and "down" or "up" + elseif vel.x ~= 0 then + return vel.x > 0 and "right" or "left" + elseif vel.y ~= 0 then + return vel.y > 0 and "down" or "up" + else + return current_dir + end end -local function get_ice_moves(player: Player, dt: number, move_inputs: {Movement}): {Movement} - local max_speed = player:get_speed() - local ice = settings.game_rules.ice - local acceleration = max_speed * ice.acceleration_factor - local friction = max_speed * ice.friction_factor - - local function update_axis(v: number, pos: boolean, neg: boolean): number - if pos then - return math.min(max_speed, v + acceleration * dt) - elseif neg then - return math.max(-max_speed, v - acceleration * dt) - elseif math.abs(v) <= friction * dt then - return 0 - elseif v > 0 then - return v - friction * dt - else - return v + friction * dt - end - end - - local new_vel = vectors.new( - update_axis(player.ice_velocity.x, any_move_has_direction(move_inputs, "right"), any_move_has_direction(move_inputs, "left")), - update_axis(player.ice_velocity.y, any_move_has_direction(move_inputs, "down"), any_move_has_direction(move_inputs, "up")) - ) - player.ice_velocity = new_vel - - local result: {Movement} = {} - if new_vel.x ~= 0 then - local dir: Direction = new_vel.x > 0 and "right" or "left" - table.insert(result, Movement.new(vectors.new(new_vel.x * dt, 0), dir)) - end - if new_vel.y ~= 0 then - local dir: Direction = new_vel.y > 0 and "down" or "up" - table.insert(result, Movement.new(vectors.new(0, new_vel.y * dt), dir)) - end - return result +local function get_ice_moves( + player: Player, dt: number, move_inputs: {Movement} +): {Movement} + local max_speed = player:get_speed() + local ice = settings.game_rules.ice + local acceleration = max_speed * ice.acceleration_factor + local friction = max_speed * ice.friction_factor + + local function update_axis(v: number, pos: boolean, neg: boolean): number + if pos then + return math.min(max_speed, v + acceleration * dt) + elseif neg then + return math.max(-max_speed, v - acceleration * dt) + elseif math.abs(v) <= friction * dt then + return 0 + elseif v > 0 then + return v - friction * dt + else + return v + friction * dt + end + end + + local new_vel = vectors.new( + update_axis( + player.ice_velocity.x, + any_move_has_direction(move_inputs, "right"), + any_move_has_direction(move_inputs, "left") + ), + update_axis( + player.ice_velocity.y, + any_move_has_direction(move_inputs, "down"), + any_move_has_direction(move_inputs, "up") + ) + ) + player.ice_velocity = new_vel + + local result: {Movement} = {} + if new_vel.x ~= 0 then + local dir: Direction = new_vel.x > 0 and "right" or "left" + table.insert(result, Movement.new(vectors.new(new_vel.x * dt, 0), dir)) + end + if new_vel.y ~= 0 then + local dir: Direction = new_vel.y > 0 and "down" or "up" + table.insert(result, Movement.new(vectors.new(0, new_vel.y * dt), dir)) + end + return result end function movement.set_position_and_dir_from_input( - player: Player, - dt: number, - blocks: {Kinematic}, - lightning_bombs_: LightningBombs, - blocks_bombs: ReadableGroup, - players_: ReadableGroup, - bombs_: ReadableGroup, - lookup: Lookup, - ice_sliding: boolean + player: Player, + dt: number, + blocks: {Kinematic}, + lightning_bombs_: LightningBombs, + blocks_bombs: ReadableGroup, + players_: ReadableGroup, + bombs_: ReadableGroup, + lookup: Lookup, + ice_sliding: boolean ) - local move_inputs = get_moves_from_input(player, dt) - local momentum = Movement.new_from_player(dt, player) - local moves = get_moves(player, move_inputs, momentum) - local player_action: Action = #moves ~= 0 and "walk" or "idle" - - if ice_sliding then - moves = get_ice_moves(player, dt, moves) - end - - if player:can_slide() and inputs_for_sliding(player, move_inputs) then - player:slide_with_dino() - end - - local max_directions = (player.action == "slide" or player.action == "dash") and 1 or 2 - local new_dir, new_pos, constrained = get_new_player_orientation( - player.direction, moves, blocks, lightning_bombs_.on_ground, player, max_directions, blocks_bombs, players_, bombs_, lookup - ) - local dominant_dir = direction_from_dominant_velocity(player.ice_velocity, new_dir) - local updated_dir = direction_for_diagonal(moves, player.direction, dominant_dir, constrained) - - player:set_movement(new_pos, updated_dir) - player:set_action(player_action) + local move_inputs = get_moves_from_input(player, dt) + local momentum = Movement.new_from_player(dt, player) + local moves = get_moves(player, move_inputs, momentum) + local player_action: Action = #moves ~= 0 and "walk" or "idle" + + if ice_sliding then + moves = get_ice_moves(player, dt, moves) + end + + if player:can_slide() and inputs_for_sliding(player, move_inputs) then + player:slide_with_dino() + end + + local max_directions = (player.action == "slide" or player.action == "dash") and 1 + or 2 + local new_dir, new_pos, constrained = get_new_player_orientation( + player.direction, + moves, + blocks, + lightning_bombs_.on_ground, + player, + max_directions, + blocks_bombs, + players_, + bombs_, + lookup + ) + local dominant_dir = direction_from_dominant_velocity(player.ice_velocity, new_dir) + local updated_dir = direction_for_diagonal( + moves, player.direction, dominant_dir, constrained + ) + + player:set_movement(new_pos, updated_dir) + player:set_action(player_action) end return movement diff --git a/src/player_animations.tl b/src/player_animations.tl index 062dec2..fa9deed 100644 --- a/src/player_animations.tl +++ b/src/player_animations.tl @@ -1,13 +1,13 @@ -local animations = require("engine.animations") local characters = require("characters") -local dinos = require("dinos") local dino_animations = require("dino_animations") -local grid = require("level_layout.grid") -local images = require("engine.images") -local players = require("players") +local dinos = require("dinos") +local animations = require("engine.animations") local points = require("engine.geometry.points") local rectangles = require("engine.geometry.rectangles") local vectors = require("engine.geometry.vectors") +local images = require("engine.images") +local grid = require("level_layout.grid") +local players = require("players") local teams = require("teams") local z_index = require("z_index") @@ -34,571 +34,609 @@ local stun_cry_text_images: {Direction: {integer: Image}} local sprites: {CharacterName: SpriteImages} = {} local sprites_invulnerable: {CharacterName: SpriteImages} = {} local special_animation_frame_durations: {Action: number} = { - dying = 1 / 8, - stunned = 1 / 8, - crashed = 1 / 8, + dying = 1 / 8, + stunned = 1 / 8, + crashed = 1 / 8, } local function get_flag_pixel_map(team_index: integer): {Color: Color} - local team = teams.get(team_index) - local flag_base_color: Color = {154, 64, 64} - local flag_dark_color: Color = {134, 48, 48} - local flag_darker_color: Color = {94, 34, 34} - return { - [flag_base_color] = team.color, - [flag_dark_color] = team:color_shade(), - [flag_darker_color] = team:color_darker_shade() - } + local team = teams.get(team_index) + local flag_base_color: Color = {154, 64, 64} + local flag_dark_color: Color = {134, 48, 48} + local flag_darker_color: Color = {94, 34, 34} + return { + [flag_base_color] = team.color, + [flag_dark_color] = team:color_shade(), + [flag_darker_color] = team:color_darker_shade(), + } end local flag_pixel_maps: {integer: {Color: Color}} = { - [1] = get_flag_pixel_map(1), - [2] = get_flag_pixel_map(2), - [3] = get_flag_pixel_map(3), + [1] = get_flag_pixel_map(1), + [2] = get_flag_pixel_map(2), + [3] = get_flag_pixel_map(3), [4] = get_flag_pixel_map(4), } local function get_animation_frame_duration(action: Action): number - local default_frame_duration = 1 / 6 - return special_animation_frame_durations[action] or default_frame_duration + local default_frame_duration = 1 / 6 + return special_animation_frame_durations[action] or default_frame_duration end local function get_sprite_set( - character_asset: CharacterName, invulnerable: boolean + character_asset: CharacterName, invulnerable: boolean ): SpriteImages - if invulnerable then - return sprites_invulnerable[character_asset] - else - return sprites[character_asset] - end -end - -local function character_animation(character_asset: CharacterName, invulnerable: boolean): {PlayerState: {Action: {Direction: Animation}}} - local resolved_sprites = get_sprite_set(character_asset, invulnerable) - local character_animations: {PlayerState: {Action: {Direction: Animation}}} = {} - for state, by_action in pairs(resolved_sprites) do - character_animations[state] = {} - for action, by_direction in pairs(by_action) do - character_animations[state][action] = {} - local frame_duration = get_animation_frame_duration(action) - for direction, sprite_image in pairs(by_direction) do - character_animations[state][action][direction] = animations.new(sprite_image, 18, frame_duration) - end - end - end - - return character_animations + if invulnerable then + return sprites_invulnerable[character_asset] + else + return sprites[character_asset] + end +end + +local function character_animation( + character_asset: CharacterName, invulnerable: boolean +): {PlayerState: {Action: {Direction: Animation}}} + local resolved_sprites = get_sprite_set(character_asset, invulnerable) + local character_animations: {PlayerState: {Action: {Direction: Animation}}} = {} + for state, by_action in pairs(resolved_sprites) do + character_animations[state] = {} + for action, by_direction in pairs(by_action) do + character_animations[state][action] = {} + local frame_duration = get_animation_frame_duration(action) + for direction, sprite_image in pairs(by_direction) do + character_animations[state][action][direction] = animations.new( + sprite_image, 18, frame_duration + ) + end + end + end + + return character_animations end local SLIDE_WIHT_DINO_DIR_MAP: {Direction: Direction} = { - up = "left", - down = "right", - left = "down", - right = "down" + up = "left", down = "right", left = "down", right = "down" } local DINO_SLIDE_CHARACTER_ADJUST = { - up = vectors.new(7, 0), - down = vectors.new(-7, 0), - right = vectors.new(9, 0), - left = vectors.new(-9, 0) + up = vectors.new(7, 0), + down = vectors.new(-7, 0), + right = vectors.new(9, 0), + left = vectors.new(-9, 0), } local DINO_STUN_CRY_TEXT_POSITION = { - up = vectors.new(8, -4), - down = vectors.new(8, 18), - right = vectors.new(18, 6), - left = vectors.new(-4, 6) + up = vectors.new(8, -4), + down = vectors.new(8, 18), + right = vectors.new(18, 6), + left = vectors.new(-4, 6), } local DINO_SONIC_BLAST_POSITION = { - up = vectors.new(8, -4), - down = vectors.new(8, 12), - right = vectors.new(14, 6), - left = vectors.new(0, 6) + up = vectors.new(8, -4), + down = vectors.new(8, 12), + right = vectors.new(14, 6), + left = vectors.new(0, 6), } local DINO_AFTERIMGE_SHIFT = { - up = vectors.new(0, 1), - down = vectors.new(0, -1), - right = vectors.new(-1, 0), - left = vectors.new(1, 0) + up = vectors.new(0, 1), + down = vectors.new(0, -1), + right = vectors.new(-1, 0), + left = vectors.new(1, 0), } local STUN_CRY_SHAKE_PERIOD_BY_LEVEL = { - [1] = 0.05, - [2] = 0.10, - [3] = 0.20, + [1] = 0.05, + [2] = 0.10, + [3] = 0.20, } local STUN_CRY_SHAKE_AXIS = { - up = vectors.new(0, 1), - down = vectors.new(0, -1), - right = vectors.new(1, 0), - left = vectors.new(-1, 0) -} -local STUN_CRY_REACH_BY_LEVEL = { - [1] = 1.0, - [2] = 1.75, - [3] = 2.5 + up = vectors.new(0, 1), + down = vectors.new(0, -1), + right = vectors.new(1, 0), + left = vectors.new(-1, 0), } +local STUN_CRY_REACH_BY_LEVEL = {[1] = 1.0, [2] = 1.75, [3] = 2.5} local function block_to_anim_draw_pos(animation: Animation, pos: Point): Point - -- y: +1 is because of the sprite, look over when doing animations - return points.new( - pos.x + (grid.TILE_SIZE - animation.width) / 2, - pos.y - animation.height + grid.TILE_SIZE + 1) + -- y: +1 is because of the sprite, look over when doing animations + return points.new( + pos.x + (grid.TILE_SIZE - animation.width) / 2, + pos.y - animation.height + grid.TILE_SIZE + 1 + ) end local function draw_flag(player: Player, draw_position: Point) - if player.team_id then - local flag_pos = draw_position:move(vectors.new(4, -10)) - flag_animations[player.team_id][player.sprite]:draw(flag_pos.x, flag_pos.y) - end + if player.team_id then + local flag_pos = draw_position:move(vectors.new(4, -10)) + flag_animations[player.team_id][player.sprite]:draw(flag_pos.x, flag_pos.y) + end end local function stun_cry_displacement(player: Player): Vector - if not player:is_stun_crying() then - return vectors.new(0, 0) - end + if not player:is_stun_crying() then + return vectors.new(0, 0) + end - local shake_period = STUN_CRY_SHAKE_PERIOD_BY_LEVEL[player.dino.level] - if (player.roar_for % shake_period) > shake_period / 2 then - return STUN_CRY_SHAKE_AXIS[player.direction] - else - return vectors.new(0, 0) - end + local shake_period = STUN_CRY_SHAKE_PERIOD_BY_LEVEL[player.dino.level] + if (player.roar_for % shake_period) > shake_period / 2 then + return STUN_CRY_SHAKE_AXIS[player.direction] + else + return vectors.new(0, 0) + end end -local function get_image_pos_in_front_of_player(player: Player, image: Image, x_scaling: number, y_scaling: number): Point - local pos = player.position - if player.direction == "left" or player.direction == "right" then - pos = pos:move(vectors.new(0, -image:getHeight()*y_scaling / 2)) - else - pos = pos:move(vectors.new(-image:getWidth()*x_scaling / 2, 0)) - end +local function get_image_pos_in_front_of_player( + player: Player, image: Image, x_scaling: number, y_scaling: number +): Point + local pos = player.position + if player.direction == "left" or player.direction == "right" then + pos = pos:move(vectors.new(0, -image:getHeight() * y_scaling / 2)) + else + pos = pos:move(vectors.new(-image:getWidth() * x_scaling / 2, 0)) + end - if player.direction == "left" then - pos = pos:move(vectors.new(-image:getWidth()*x_scaling, 0)) - elseif player.direction == "up" then - pos = pos:move(vectors.new(0, -image:getHeight()*y_scaling)) - end + if player.direction == "left" then + pos = pos:move(vectors.new(-image:getWidth() * x_scaling, 0)) + elseif player.direction == "up" then + pos = pos:move(vectors.new(0, -image:getHeight() * y_scaling)) + end - return pos + return pos end local function draw_sonic_blasts(player: Player) - if not player:is_sonic_blasting() then - return - end - - local function get_text_position(scaling: number, wave_image: Image): Point - local pos = get_image_pos_in_front_of_player(player, wave_image, scaling, scaling) - return pos:move(DINO_SONIC_BLAST_POSITION[player.direction]) - end - - local reach = (player.dino.level + 1) * grid.TILE_SIZE - for _, sonic_wave in ipairs(player.sonic_waves) do - local sonic_wave_speed = reach / sonic_wave.live_for - local sonic_image = sonic_wave_images[player.direction][sonic_wave.color] - local scale = sonic_wave.duration / sonic_wave.live_for - local traveled = sonic_wave.duration * sonic_wave_speed - local pos = get_text_position(scale, sonic_image) - pos = pos:move(vectors.from_direction(player.direction):scale(traveled)) - lutro.graphics.draw(sonic_image, pos.x, pos.y, 0, scale, scale) - end + if not player:is_sonic_blasting() then + return + end + + local function get_text_position(scaling: number, wave_image: Image): Point + local pos = get_image_pos_in_front_of_player( + player, wave_image, scaling, scaling + ) + return pos:move(DINO_SONIC_BLAST_POSITION[player.direction]) + end + + local reach = (player.dino.level + 1) * grid.TILE_SIZE + for _, sonic_wave in ipairs(player.sonic_waves) do + local sonic_wave_speed = reach / sonic_wave.live_for + local sonic_image = sonic_wave_images[player.direction][sonic_wave.color] + local scale = sonic_wave.duration / sonic_wave.live_for + local traveled = sonic_wave.duration * sonic_wave_speed + local pos = get_text_position(scale, sonic_image) + pos = pos:move(vectors.from_direction(player.direction):scale(traveled)) + lutro.graphics.draw(sonic_image, pos.x, pos.y, 0, scale, scale) + end end local function draw_stun_cry_text(player: Player) - if not player:is_stun_crying() then - return - end + if not player:is_stun_crying() then + return + end - local since_roar_buildup = player:have_roared_since_buildup_for() + local since_roar_buildup = player:have_roared_since_buildup_for() - local function get_text_x_y_scaling(): (number, number) - local text_len_max_scale = STUN_CRY_REACH_BY_LEVEL[player.dino.level] - local text_length_scale = math.min(since_roar_buildup*3, text_len_max_scale) - local text_height_scale = math.min(since_roar_buildup*4, 1.0) + local function get_text_x_y_scaling(): (number, number) + local text_len_max_scale = STUN_CRY_REACH_BY_LEVEL[player.dino.level] + local text_length_scale = math.min(since_roar_buildup * 3, text_len_max_scale) + local text_height_scale = math.min(since_roar_buildup * 4, 1.0) - if player.direction == "down" or player.direction == "up" then - return text_height_scale, text_length_scale - else - return text_length_scale, text_height_scale - end - end + if player.direction == "down" or player.direction == "up" then + return text_height_scale, text_length_scale + else + return text_length_scale, text_height_scale + end + end - local function get_text_position(text: Image): Point - local x_scale, y_scale = get_text_x_y_scaling() - local pos = get_image_pos_in_front_of_player(player, text, x_scale, y_scale) - return pos:move(DINO_STUN_CRY_TEXT_POSITION[player.direction]) - end + local function get_text_position(text: Image): Point + local x_scale, y_scale = get_text_x_y_scaling() + local pos = get_image_pos_in_front_of_player(player, text, x_scale, y_scale) + return pos:move(DINO_STUN_CRY_TEXT_POSITION[player.direction]) + end - local text = stun_cry_text_images[player.direction][player.dino.level] - local draw_pos = get_text_position(text) - local x_scale, y_scale = get_text_x_y_scaling() - lutro.graphics.draw(text, draw_pos.x, draw_pos.y, 0, x_scale, y_scale) + local text = stun_cry_text_images[player.direction][player.dino.level] + local draw_pos = get_text_position(text) + local x_scale, y_scale = get_text_x_y_scaling() + lutro.graphics.draw(text, draw_pos.x, draw_pos.y, 0, x_scale, y_scale) end local record PlayerEntity is Entity - player: Player - regular: {PlayerState: {Action: {Direction: Animation}}} - invulnerable: {PlayerState: {Action: {Direction: Animation}}} - dinos: AnimatedDinos + player: Player + regular: {PlayerState: {Action: {Direction: Animation}}} + invulnerable: {PlayerState: {Action: {Direction: Animation}}} + dinos: AnimatedDinos end function PlayerEntity.new(player: Player, animated_dinos: AnimatedDinos): PlayerEntity - local self: PlayerEntity = setmetatable({}, { __index = PlayerEntity }) - self.z_index = z_index.LEVEL - self.player = player - self.regular = character_animation(player.sprite, false) - self.invulnerable = character_animation(player.sprite, true) - self.dinos = animated_dinos - return self + local self: PlayerEntity = setmetatable({}, {__index = PlayerEntity}) + self.z_index = z_index.LEVEL + self.player = player + self.regular = character_animation(player.sprite, false) + self.invulnerable = character_animation(player.sprite, true) + self.dinos = animated_dinos + return self end function PlayerEntity:get_position(): Point - return self.player.position + return self.player.position end function PlayerEntity:hitbox(): Rectangle - return self.player:hitbox() + return self.player:hitbox() end function PlayerEntity:update(dt: number) - self.regular[self.player.state][self.player.action][self.player.direction]:update(dt) - self.invulnerable[self.player.state][self.player.action][self.player.direction]:update(dt) + self.regular[self.player.state][self.player.action][self.player.direction]:update( + dt + ) + self.invulnerable[self.player.state][self.player.action][self.player.direction]:update( + dt + ) end function PlayerEntity:_get_animation(): Animation - local function get_direction(): Direction - if self.player.dino and self.player.dino.action == "slide" and self.player.dino.level == 2 then - -- For level 2 dino sliding the character riding it should turn depending - -- on the direction sliding for it to look like they bracing sliding to a stop: - return SLIDE_WIHT_DINO_DIR_MAP[self.player.direction] - else - return self.player.direction - end - end - - local resolved_dir = get_direction() - if self.player:is_invulnerable() and self.player.invulnerable_for % 0.3 < 0.15 then - return self.invulnerable[self.player.state][self.player.action][resolved_dir] - else - local animation = self.regular[self.player.state][self.player.action][resolved_dir] - if self.player.action == "dying" and animation.loops > 0 then - -- If dying and the animation have played one loop then don't draw anything: - return nil - else - return animation - end - end + local function get_direction(): Direction + if self.player.dino + and self.player.dino.action == "slide" + and self.player.dino.level == 2 then + -- For level 2 dino sliding the character riding it should turn depending + -- on the direction sliding for it to look like they bracing sliding to a stop: + return SLIDE_WIHT_DINO_DIR_MAP[self.player.direction] + else + return self.player.direction + end + end + + local resolved_dir = get_direction() + if self.player:is_invulnerable() and self.player.invulnerable_for % 0.3 < 0.15 then + return self.invulnerable[self.player.state][self.player.action][resolved_dir] + else + local animation = self.regular[self.player.state][self.player.action][resolved_dir] + if self.player.action == "dying" and animation.loops > 0 then + -- If dying and the animation have played one loop then don't draw anything: + return nil + else + return animation + end + end end function PlayerEntity:_draw_foot(animation: Animation) - local draw_position = self.player:get_draw_position() - local draw_pos = block_to_anim_draw_pos(animation, draw_position) - draw_flag(self.player, draw_pos) - animation:draw(draw_pos.x, draw_pos.y) + local draw_position = self.player:get_draw_position() + local draw_pos = block_to_anim_draw_pos(animation, draw_position) + draw_flag(self.player, draw_pos) + animation:draw(draw_pos.x, draw_pos.y) end function PlayerEntity:_draw_mounted() - if not self.player:is_alive() then - return - end - - local player_animation = self:_get_animation() - local dino_animation = self.dinos:get_from(self.player.dino) - local draw_position = self.player:get_draw_position() - - local function draw(animation: Animation, pos: Point, should_draw_flag?: boolean) - local draw_pos = block_to_anim_draw_pos(animation, pos) - if should_draw_flag then - draw_flag(self.player, draw_pos) - end - animation:draw(draw_pos.x, draw_pos.y) - end - - local function dino_position(): Point - if self.player.action == "jump" then - return self.player:get_jump_position() - elseif self.player.action == "roar" then - return self.player.dino.position:move(stun_cry_displacement(self.player)) - else - return self.player.dino.position - end - end - - if self.player.direction == "up" then - -- Stun cry text in upward direction should be behind player and dino. Note that - -- every entity upwards will have lower y-coord so it will be above them - draw_stun_cry_text(self.player) - draw_sonic_blasts(self.player) - end - - if self.player.action == "dash" and self.player.dino and self.player.dino.level == 3 then - local shift_vector = DINO_AFTERIMGE_SHIFT[self.player.direction]:scale(self.player.after_image_shift) - -- Draw after image shifted behind when dashing - draw(dino_animation, dino_position():move(shift_vector)) - end - - if self.player.action == "slide" and self.player.dino and self.player.dino.level == 2 then - local character_pos = draw_position:move(DINO_SLIDE_CHARACTER_ADJUST[self.player.direction]) - if self.player.direction == "right" or self.player.direction == "left" then - -- Need to draw dino in front of player sliding left or right with level 2 dino - draw(player_animation, character_pos, true) - draw(dino_animation, dino_position()) - else - -- For up and down the player is on front - draw(dino_animation, dino_position()) - draw(player_animation, character_pos, true) - end - elseif self.player.direction == "down" then - -- Need to draw dino in front of player walking towards screen - draw(player_animation, draw_position, true) - draw(dino_animation, dino_position()) - else - -- And player in front of dino in all other cases - draw(dino_animation, dino_position()) - draw(player_animation, draw_position, true) - end + if not self.player:is_alive() then + return + end + + local player_animation = self:_get_animation() + local dino_animation = self.dinos:get_from(self.player.dino) + local draw_position = self.player:get_draw_position() + + local function draw(animation: Animation, pos: Point, should_draw_flag?: boolean) + local draw_pos = block_to_anim_draw_pos(animation, pos) + if should_draw_flag then + draw_flag(self.player, draw_pos) + end + animation:draw(draw_pos.x, draw_pos.y) + end + + local function dino_position(): Point + if self.player.action == "jump" then + return self.player:get_jump_position() + elseif self.player.action == "roar" then + return self.player.dino.position:move(stun_cry_displacement(self.player)) + else + return self.player.dino.position + end + end + + if self.player.direction == "up" then + -- Stun cry text in upward direction should be behind player and dino. Note that + -- every entity upwards will have lower y-coord so it will be above them + draw_stun_cry_text(self.player) + draw_sonic_blasts(self.player) + end + + if self.player.action == "dash" + and self.player.dino + and self.player.dino.level == 3 then + local shift_vector = DINO_AFTERIMGE_SHIFT[self.player.direction]:scale( + self.player.after_image_shift + ) + -- Draw after image shifted behind when dashing + draw(dino_animation, dino_position():move(shift_vector)) + end + + if self.player.action == "slide" + and self.player.dino + and self.player.dino.level == 2 then + local character_pos = draw_position:move( + DINO_SLIDE_CHARACTER_ADJUST[self.player.direction] + ) + if self.player.direction == "right" or self.player.direction == "left" then + -- Need to draw dino in front of player sliding left or right with level 2 dino + draw(player_animation, character_pos, true) + draw(dino_animation, dino_position()) + else + -- For up and down the player is on front + draw(dino_animation, dino_position()) + draw(player_animation, character_pos, true) + end + elseif self.player.direction == "down" then + -- Need to draw dino in front of player walking towards screen + draw(player_animation, draw_position, true) + draw(dino_animation, dino_position()) + else + -- And player in front of dino in all other cases + draw(dino_animation, dino_position()) + draw(player_animation, draw_position, true) + end end function PlayerEntity:draw() - if self.player:is_riding_dino() then - self:_draw_mounted() - else - local animation = self:_get_animation() - if animation then - self:_draw_foot(animation) - end - end + if self.player:is_riding_dino() then + self:_draw_mounted() + else + local animation = self:_get_animation() + if animation then + self:_draw_foot(animation) + end + end end local record PlayerAnimations - entities: {PlayerEntity} - dinos: AnimatedDinos + entities: {PlayerEntity} + dinos: AnimatedDinos end function PlayerAnimations:add_new(player: Player) - table.insert(self.entities, PlayerEntity.new(player, self.dinos)) -end - -local function character_sprite_for_action(character_asset: CharacterName, state: PlayerState, action: Action): {Direction: Image} - if state == "mounted" then - if action == "jump_on" or action == "jump_off" then - -- Same sprite for 'jump' when on foot as when mounted - state = "foot" - elseif action == "idle" then - -- Same sprite for 'idle' as 'walk' when mounted - action = "walk" - end - end - - local function action_resolved(): string - if action == "jump_on" or action == "jump_off" then - return "jump" - elseif action == "jump" or action == "dash" or action == "roar" then - -- Same sprite for jump as walk/idle, is always on mount when this happen - return "walk" - else - return action - end - end - - local base_path = "assets/" .. character_asset .. "/" .. state .. "/" .. action_resolved() - return { - up = lutro.graphics.newImage(base_path .. "_up.png"), - down = lutro.graphics.newImage(base_path .. "_down.png"), - right = lutro.graphics.newImage(base_path .. "_right.png"), - left = lutro.graphics.newImage(base_path .. "_left.png"), - } -end - -local function character_sprite_for_dying(character_asset: CharacterName): {Direction: Image} - local asset_file = "assets/" .. character_asset .. "/foot/dying.png" - local image = lutro.graphics.newImage(asset_file) - return { - up = image, - down = image, - right = image, - left = image, - } -end - -local function character_sprite_for_stunned(character_asset: CharacterName, state: PlayerState): {Direction: Image} - if state == "mounted" then - -- Stunned when mounted has same sprite as for walking - return character_sprite_for_action(character_asset, state, "walk") - end - - local asset_file = "assets/" .. character_asset .. "/" .. state .. "/stunned.png" - local image = lutro.graphics.newImage(asset_file) - return { - up = image, - down = image, - right = image, - left = image, - } -end - -local function character_sprites_for_state(character_asset: CharacterName, state: PlayerState): {Action: {Direction: Image}} - local sprite_images : {Action: {Direction: Image}} = { - walk = character_sprite_for_action(character_asset, state, "walk"), - idle = character_sprite_for_action(character_asset, state, "idle"), - jump_on = character_sprite_for_action(character_asset, state, "jump_on"), - jump_off = character_sprite_for_action(character_asset, state, "jump_off"), - jump = character_sprite_for_action(character_asset, state, "jump"), - dash = character_sprite_for_action(character_asset, state, "dash"), - dying = character_sprite_for_dying(character_asset), - stunned = character_sprite_for_stunned(character_asset, state), - crashed = character_sprite_for_stunned(character_asset, state), - slide = character_sprite_for_stunned(character_asset, state), - roar = character_sprite_for_stunned(character_asset, state) - } - return sprite_images + table.insert(self.entities, PlayerEntity.new(player, self.dinos)) +end + +local function character_sprite_for_action( + character_asset: CharacterName, state: PlayerState, action: Action +): {Direction: Image} + if state == "mounted" then + if action == "jump_on" or action == "jump_off" then + -- Same sprite for 'jump' when on foot as when mounted + state = "foot" + elseif action == "idle" then + -- Same sprite for 'idle' as 'walk' when mounted + action = "walk" + end + end + + local function action_resolved(): string + if action == "jump_on" or action == "jump_off" then + return "jump" + elseif action == "jump" or action == "dash" or action == "roar" then + -- Same sprite for jump as walk/idle, is always on mount when this happen + return "walk" + else + return action + end + end + + local base_path = "assets/" .. character_asset .. "/" .. state .. "/" .. action_resolved() + return { + up = lutro.graphics.newImage(base_path .. "_up.png"), + down = lutro.graphics.newImage(base_path .. "_down.png"), + right = lutro.graphics.newImage(base_path .. "_right.png"), + left = lutro.graphics.newImage(base_path .. "_left.png"), + } +end + +local function character_sprite_for_dying( + character_asset: CharacterName +): {Direction: Image} + local asset_file = "assets/" .. character_asset .. "/foot/dying.png" + local image = lutro.graphics.newImage(asset_file) + return { + up = image, + down = image, + right = image, + left = image, + } +end + +local function character_sprite_for_stunned( + character_asset: CharacterName, state: PlayerState +): {Direction: Image} + if state == "mounted" then + -- Stunned when mounted has same sprite as for walking + return character_sprite_for_action(character_asset, state, "walk") + end + + local asset_file = "assets/" .. character_asset .. "/" .. state .. "/stunned.png" + local image = lutro.graphics.newImage(asset_file) + return { + up = image, + down = image, + right = image, + left = image, + } +end + +local function character_sprites_for_state( + character_asset: CharacterName, state: PlayerState +): {Action: {Direction: Image}} + local sprite_images : {Action: {Direction: Image}} = { + walk = character_sprite_for_action(character_asset, state, "walk"), + idle = character_sprite_for_action(character_asset, state, "idle"), + jump_on = character_sprite_for_action(character_asset, state, "jump_on"), + jump_off = character_sprite_for_action(character_asset, state, "jump_off"), + jump = character_sprite_for_action(character_asset, state, "jump"), + dash = character_sprite_for_action(character_asset, state, "dash"), + dying = character_sprite_for_dying(character_asset), + stunned = character_sprite_for_stunned(character_asset, state), + crashed = character_sprite_for_stunned(character_asset, state), + slide = character_sprite_for_stunned(character_asset, state), + roar = character_sprite_for_stunned(character_asset, state), + } + return sprite_images end local function character_sprites(character_asset: CharacterName): SpriteImages - local sprites_images : SpriteImages = { - foot = character_sprites_for_state(character_asset, "foot"), - mounted = character_sprites_for_state(character_asset, "mounted"), - } - return sprites_images + local sprites_images : SpriteImages = { + foot = character_sprites_for_state(character_asset, "foot"), + mounted = character_sprites_for_state(character_asset, "mounted"), + } + return sprites_images end local player_animations = {PlayerAnimations = PlayerAnimations} function player_animations.load_assets() - jump_shadow_image = lutro.graphics.newImage("assets/items/shadow.png") - for _, sprite_name in ipairs(characters.get_names()) do - sprites[sprite_name] = character_sprites(sprite_name) - sprites_invulnerable[sprite_name] = character_sprites(sprite_name) - end - - for _, by_state in pairs(sprites_invulnerable) do - for _, by_action in pairs(by_state) do - for _, by_direction in pairs(by_action) do - for _, sprite_image in pairs(by_direction) do - images.make_lighter(sprite_image, 0.6) - end - end - end - end - - local function get_stun_cry_text_for_dir(dir: string): {integer: Image} - local result: {integer: Image} = {} - for level=1, 3 do - local asset = "assets/tirras/level_" .. level .. "/stun_cry_text_" .. dir .. ".png" - result[level] = lutro.graphics.newImage(asset) - end - - return result - end - - local vertical_image = get_stun_cry_text_for_dir("vertical") - local horizontal_image = get_stun_cry_text_for_dir("horizontal") - -- Right and up images are - stun_cry_text_images = { - up = vertical_image, - down = vertical_image, - right = horizontal_image, - left = horizontal_image - } - - local function get_sonic_blast_for_dir(dir: string): {SonicWaveColor: Image} - local sonic_images: {SonicWaveColor: Image} = { - purple = lutro.graphics.newImage("assets/tirras/sonic-" .. dir .. "-purple.png"), - white = lutro.graphics.newImage("assets/tirras/sonic-" .. dir .. "-white.png"), - yellow = lutro.graphics.newImage("assets/tirras/sonic-" .. dir .. "-yellow.png") - } - return sonic_images - end - - local vertical_sonic_image = get_sonic_blast_for_dir("vertical") - local horizontal_sonic_image = get_sonic_blast_for_dir("horizontal") - sonic_wave_images = { - up = vertical_sonic_image, - down = vertical_sonic_image, - right = horizontal_sonic_image, - left = horizontal_sonic_image - } - - for i=1, 4 do - local flag_image = lutro.graphics.newImage("assets/team_flag.png") - images.change_colors(flag_image, flag_pixel_maps[i]) - flag_animations[i] = {} - for _, sprite in ipairs(characters.get_names()) do - flag_animations[i][sprite] = animations.new(flag_image, 30, 0.1) - end - end + jump_shadow_image = lutro.graphics.newImage("assets/items/shadow.png") + for _, sprite_name in ipairs(characters.get_names()) do + sprites[sprite_name] = character_sprites(sprite_name) + sprites_invulnerable[sprite_name] = character_sprites(sprite_name) + end + + for _, by_state in pairs(sprites_invulnerable) do + for _, by_action in pairs(by_state) do + for _, by_direction in pairs(by_action) do + for _, sprite_image in pairs(by_direction) do + images.make_lighter(sprite_image, 0.6) + end + end + end + end + + local function get_stun_cry_text_for_dir(dir: string): {integer: Image} + local result: {integer: Image} = {} + for level = 1, 3 do + local asset = "assets/tirras/level_" .. level .. "/stun_cry_text_" .. dir .. ".png" + result[level] = lutro.graphics.newImage(asset) + end + + return result + end + + local vertical_image = get_stun_cry_text_for_dir("vertical") + local horizontal_image = get_stun_cry_text_for_dir("horizontal") + -- Right and up images are + stun_cry_text_images = { + up = vertical_image, + down = vertical_image, + right = horizontal_image, + left = horizontal_image, + } + + local function get_sonic_blast_for_dir(dir: string): {SonicWaveColor: Image} + local sonic_images : {SonicWaveColor: Image} = { + purple = lutro.graphics.newImage( + "assets/tirras/sonic-" .. dir .. "-purple.png" + ), + white = lutro.graphics.newImage( + "assets/tirras/sonic-" .. dir .. "-white.png" + ), + yellow = lutro.graphics.newImage( + "assets/tirras/sonic-" .. dir .. "-yellow.png" + ), + } + return sonic_images + end + + local vertical_sonic_image = get_sonic_blast_for_dir("vertical") + local horizontal_sonic_image = get_sonic_blast_for_dir("horizontal") + sonic_wave_images = { + up = vertical_sonic_image, + down = vertical_sonic_image, + right = horizontal_sonic_image, + left = horizontal_sonic_image, + } + + for i = 1, 4 do + local flag_image = lutro.graphics.newImage("assets/team_flag.png") + images.change_colors(flag_image, flag_pixel_maps[i]) + flag_animations[i] = {} + for _, sprite in ipairs(characters.get_names()) do + flag_animations[i][sprite] = animations.new(flag_image, 30, 0.1) + end + end end function player_animations.new(initial_players: {Player}): PlayerAnimations - local self: PlayerAnimations = setmetatable({}, { __index = PlayerAnimations }) + local self: PlayerAnimations = setmetatable({}, {__index = PlayerAnimations}) self.entities = {} - self.dinos = dino_animations.new() + self.dinos = dino_animations.new() for _, player in ipairs(initial_players) do - self:add_new(player) - end + self:add_new(player) + end return self end function PlayerAnimations:update(dt: number) - self.dinos:update(dt) - for _, entity in ipairs(self.entities) do - -- Update animations regardless if alive or not - entity:update(dt) - end - for i=1, 4 do - for _, sprite in ipairs(characters.get_names()) do - flag_animations[i][sprite]:update(dt) - end - end + self.dinos:update(dt) + for _, entity in ipairs(self.entities) do + -- Update animations regardless if alive or not + entity:update(dt) + end + for i = 1, 4 do + for _, sprite in ipairs(characters.get_names()) do + flag_animations[i][sprite]:update(dt) + end + end end function PlayerAnimations:get_all(): {Entity: Entity} - local dinos_drawn_with_player: {Dino} = {} - local entity_set_to_draw: {Entity: Entity} = {} - for _, entity in ipairs(self.entities) do - if entity.player:is_riding_dino() then - table.insert(dinos_drawn_with_player, entity.player.dino) - entity_set_to_draw[entity] = entity - elseif entity:_get_animation() then - entity_set_to_draw[entity] = entity - end - end - for _, mount in pairs(self.dinos:get_mount_entities_except(dinos_drawn_with_player)) do - entity_set_to_draw[mount] = mount - end - return entity_set_to_draw + local dinos_drawn_with_player: {Dino} = {} + local entity_set_to_draw: {Entity: Entity} = {} + for _, entity in ipairs(self.entities) do + if entity.player:is_riding_dino() then + table.insert(dinos_drawn_with_player, entity.player.dino) + entity_set_to_draw[entity] = entity + elseif entity:_get_animation() then + entity_set_to_draw[entity] = entity + end + end + for _, mount in pairs( + self.dinos:get_mount_entities_except(dinos_drawn_with_player) + ) do + entity_set_to_draw[mount] = mount + end + return entity_set_to_draw end function PlayerAnimations:_draw_players_shadow(filter: function(Player): boolean) - for _, entity in ipairs(self.entities) do - if filter(entity.player) then - lutro.graphics.draw(jump_shadow_image, entity.player.position.x - 1, entity.player.position.y - 10) - end - end + for _, entity in ipairs(self.entities) do + if filter(entity.player) then + lutro.graphics.draw( + jump_shadow_image, + entity.player.position.x - 1, + entity.player.position.y - 10 + ) + end + end end function PlayerAnimations:draw_lateral_jump_shadows() - local function jumps_lateral(player: Player): boolean - return player.action == "jump" and player.jump_trajectory.lateral_velocity:length() > 0 - end + local function jumps_lateral(player: Player): boolean + return player.action == "jump" + and player.jump_trajectory.lateral_velocity:length() > 0 + end - self:_draw_players_shadow(jumps_lateral) + self:_draw_players_shadow(jumps_lateral) end function PlayerAnimations:draw_vertical_jump_shadows() - local function jumps_vertically(player: Player): boolean - return player.action == "jump" and player.jump_trajectory.lateral_velocity:length() == 0 - end + local function jumps_vertically(player: Player): boolean + return player.action == "jump" + and player.jump_trajectory.lateral_velocity:length() == 0 + end - self:_draw_players_shadow(jumps_vertically) + self:_draw_players_shadow(jumps_vertically) end function PlayerAnimations:draw_stun_cry_texts_not_up() - for _, entity in ipairs(self.entities) do - if entity.player.direction ~= "up" then - draw_stun_cry_text(entity.player) - draw_sonic_blasts(entity.player) - end - end + for _, entity in ipairs(self.entities) do + if entity.player.direction ~= "up" then + draw_stun_cry_text(entity.player) + draw_sonic_blasts(entity.player) + end + end end return player_animations diff --git a/src/player_selection.tl b/src/player_selection.tl index 880c92a..64bf8bf 100644 --- a/src/player_selection.tl +++ b/src/player_selection.tl @@ -1,17 +1,17 @@ +local characters = require("characters") local animations = require("engine.animations") local audio = require("engine.audio") -local battle = require("game.battle") -local characters = require("characters") -local fonts = require("fonts") +local points = require("engine.geometry.points") +local vectors = require("engine.geometry.vectors") local joysticks = require("engine.joysticks") -local items = require("menus.items") local scenes = require("engine.scenes") -local settings = require("settings") -local vectors = require("engine.geometry.vectors") -local points = require("engine.geometry.points") -local team_selection = require("team_selection") +local fonts = require("fonts") +local battle = require("game.battle") local score_board = require("game.score_board") local win_condition = require("game.win_condition") +local items = require("menus.items") +local settings = require("settings") +local team_selection = require("team_selection") local type Animation = animations.Animation local type Battle = battle.Battle @@ -23,13 +23,13 @@ local type Scene = scenes.Scene local type Point = points.Point local record Character - sprite: CharacterName - number: integer - y_shift: number + sprite: CharacterName + number: integer + y_shift: number end function Character.new(sprite: CharacterName, number: integer): Character - local self: Character = setmetatable({}, { __index = Character }) + local self: Character = setmetatable({}, {__index = Character}) self.sprite = sprite self.number = number self.y_shift = 0.0 @@ -95,7 +95,7 @@ local record PlayerSelection is Scene end function PlayerSelection.new(): PlayerSelection - local self: PlayerSelection = setmetatable({}, { __index = PlayerSelection }) + local self: PlayerSelection = setmetatable({}, {__index = PlayerSelection}) self.score_board = score_board.new_for_singles() self.game = battle.new( self, self.score_board, win_condition.new_for_singles(self.score_board) @@ -157,9 +157,10 @@ end function PlayerSelection:check_for_new_player_selecting() -- No player is selecting, check if anyone is pressing right or left -- indicating that they want to select a character - for player_id=1, joysticks.maxplayers() do - local trying_to_select = joysticks.is_pressed(player_id, joysticks.BUTTONS.RIGHT) or - joysticks.is_pressed(player_id, joysticks.BUTTONS.LEFT) + for player_id = 1, joysticks.maxplayers() do + local trying_to_select = joysticks.is_pressed( + player_id, joysticks.BUTTONS.RIGHT + ) or joysticks.is_pressed(player_id, joysticks.BUTTONS.LEFT) if trying_to_select and characters.have_not_selected(player_id) then self.player_selecting = player_id @@ -175,7 +176,7 @@ function PlayerSelection:handle_player_input() else music:stop() self.score_board:update_avatars(characters.get_selections()) - + if self.going_back_to_game then -- The game might have paused audio, make sure to unpause: audio.unpause() @@ -192,17 +193,18 @@ function PlayerSelection:handle_player_input() self:check_for_new_player_selecting() return end - + local ok_action = false if joysticks.is_pressed(self.player_selecting, joysticks.BUTTONS.RIGHT) then ok_action = self.selector:increment() elseif joysticks.is_pressed(self.player_selecting, joysticks.BUTTONS.LEFT) then ok_action = self.selector:decrement() - elseif (joysticks.is_pressed(self.player_selecting, joysticks.BUTTONS.B) or - joysticks.is_pressed(self.player_selecting, joysticks.BUTTONS.R)) then - - local selected = table.remove(self.selectable_characters, self.selector:get_value()) + elseif (joysticks.is_pressed(self.player_selecting, joysticks.BUTTONS.B) + or joysticks.is_pressed(self.player_selecting, joysticks.BUTTONS.R)) then + local selected = table.remove( + self.selectable_characters, self.selector:get_value() + ) table.insert(self.walking_characters, selected) characters.new_selection(self.player_selecting, selected.sprite) self.selector:reduce_max_val() @@ -240,7 +242,7 @@ function PlayerSelection:draw_slow_walking(character: Character) end function PlayerSelection:draw() - lutro.graphics.draw(background) + lutro.graphics.draw(background) for _, char in ipairs(self.selectable_characters) do self:draw_idle(char) @@ -272,7 +274,7 @@ function PlayerSelection:draw() end function player_selection.setup(): Scene - return PlayerSelection.new() + return PlayerSelection.new() end return player_selection diff --git a/src/players.tl b/src/players.tl index 8d7aec7..05f5a9c 100644 --- a/src/players.tl +++ b/src/players.tl @@ -1,12 +1,12 @@ -local audio = require("engine.audio") local characters = require("characters") local dinos = require("dinos") -local grid = require("level_layout.grid") -local hitboxes = require("engine.hitboxes") -local physics = require("engine.physics") +local audio = require("engine.audio") local points = require("engine.geometry.points") local rectangles = require("engine.geometry.rectangles") local vectors = require("engine.geometry.vectors") +local hitboxes = require("engine.hitboxes") +local physics = require("engine.physics") +local grid = require("level_layout.grid") local settings = require("settings") local z_index = require("z_index") @@ -28,561 +28,596 @@ local SONIC_BLAST_ROAR_FOR = 1.4 local SLIDE_FOR_TIME = 0.4 local CHARACTER_HEIGHT_ON_DINO = 8 -- How high up from ground player is when on dino local DASH_WITH_DINO_SPEED_BONUS = { - [1] = 50, - [2] = 70, - [3] = 90, + [1] = 50, + [2] = 70, + [3] = 90, } local type Action = enum - "walk" - "jump_on" - "jump_off" - "jump" - "idle" - "dying" - "stunned" - "dash" - "crashed" - "slide" - "roar" + "walk" + "jump_on" + "jump_off" + "jump" + "idle" + "dying" + "stunned" + "dash" + "crashed" + "slide" + "roar" end local type PlayerState = enum - "foot" - "mounted" + "foot" + "mounted" end local type SonicWaveColor = enum - "white" - "purple" - "yellow" + "white" + "purple" + "yellow" end local SONIC_WAVE_COLOR_ORDERS: {SonicWaveColor} = { - "white", - "purple", - "white", - "yellow", - "white", - "yellow", - "purple", - "yellow", - "purple", - "yellow", - "purple", - "white", - "purple", - "white", - "yellow", - "white", - "yellow", + "white", + "purple", + "white", + "yellow", + "white", + "yellow", + "purple", + "yellow", + "purple", + "yellow", + "purple", + "white", + "purple", + "white", + "yellow", + "white", + "yellow", } local record SonicWave - duration: number - live_for: number - color: SonicWaveColor + duration: number + live_for: number + color: SonicWaveColor end function SonicWave.new(color: SonicWaveColor, live_for: number): SonicWave - local self: SonicWave = setmetatable({}, { __index = SonicWave }) - self.duration = 0 - self.live_for = live_for - self.color = color - return self + local self: SonicWave = setmetatable({}, {__index = SonicWave}) + self.duration = 0 + self.live_for = live_for + self.color = color + return self end local record Player is Explodable, Kinematic, Entity - id: integer - team_id: integer - sprite: CharacterName - position: Point - direction: Direction - action: Action - state: PlayerState - is_removed: boolean - speed: integer - bombs: integer - lightning_bomb: integer - fire: integer - has_kick: boolean - invulnerable_for: number - stunned_for: number - slide_for: number - roar_for: number - total_roar_time: number - after_image_shift: number - sonic_waves: {SonicWave} - last_time_adding_sonic_wave: number - sonic_wave_color_index: integer - play_dino_roar: boolean - dino: Dino - - jump_trajectory: ProjectileTrajectory - jump_target: Point - ice_velocity: Vector -end - -local players = {Player = Player, Action = Action, PlayerState = PlayerState, SonicWaveColor = SonicWaveColor} + id: integer + team_id: integer + sprite: CharacterName + position: Point + direction: Direction + action: Action + state: PlayerState + is_removed: boolean + speed: integer + bombs: integer + lightning_bomb: integer + fire: integer + has_kick: boolean + invulnerable_for: number + stunned_for: number + slide_for: number + roar_for: number + total_roar_time: number + after_image_shift: number + sonic_waves: {SonicWave} + last_time_adding_sonic_wave: number + sonic_wave_color_index: integer + play_dino_roar: boolean + dino: Dino + + jump_trajectory: ProjectileTrajectory + jump_target: Point + ice_velocity: Vector +end + +local players = { + Player = Player, + Action = Action, + PlayerState = PlayerState, + SonicWaveColor = SonicWaveColor, +} function players.new(id: integer, sprite: CharacterName, position: Point): Player - local self: Player = setmetatable({}, { __index = Player }) - self.id = id - self.team_id = nil - self.sprite = sprite - self.position = position - self.z_index = z_index.LEVEL - self.direction = "down" - self.action = "idle" - self.state = "foot" - self.is_removed = false - local player_settings = settings.game_rules.players - self.speed = player_settings.start_speed - self.has_kick = player_settings.start_with_kick - self.bombs = player_settings.start_with_bombs - self.lightning_bomb = player_settings.start_with_lightning_bombs - self.fire = player_settings.start_with_fire - self.invulnerable_for = 0 - self.stunned_for = 0 - self.after_image_shift = 0 - self.slide_for = 0 - self.roar_for = 0 - self.total_roar_time = 0 - self.dino = nil - self.play_dino_roar = false - self.sonic_waves = {} - self.last_time_adding_sonic_wave = 0 - self.sonic_wave_color_index = 1 - self.ice_velocity = vectors.new(0, 0) - return self + local self: Player = setmetatable({}, {__index = Player}) + self.id = id + self.team_id = nil + self.sprite = sprite + self.position = position + self.z_index = z_index.LEVEL + self.direction = "down" + self.action = "idle" + self.state = "foot" + self.is_removed = false + local player_settings = settings.game_rules.players + self.speed = player_settings.start_speed + self.has_kick = player_settings.start_with_kick + self.bombs = player_settings.start_with_bombs + self.lightning_bomb = player_settings.start_with_lightning_bombs + self.fire = player_settings.start_with_fire + self.invulnerable_for = 0 + self.stunned_for = 0 + self.after_image_shift = 0 + self.slide_for = 0 + self.roar_for = 0 + self.total_roar_time = 0 + self.dino = nil + self.play_dino_roar = false + self.sonic_waves = {} + self.last_time_adding_sonic_wave = 0 + self.sonic_wave_color_index = 1 + self.ice_velocity = vectors.new(0, 0) + return self end function Player:get_position(): Point - return self.position + return self.position end function Player:draw() - assert(false, "Not implemented") + assert(false, "Not implemented") end function Player:is_jumping_on_off_dino(): boolean - return self.action == "jump_on" or self.action == "jump_off" + return self.action == "jump_on" or self.action == "jump_off" end function Player:is_jumping(): boolean - return self:is_jumping_on_off_dino() or self.action == "jump" + return self:is_jumping_on_off_dino() or self.action == "jump" end function Player:is_stunned(): boolean - return self.stunned_for > 0 + return self.stunned_for > 0 end function Player:can_slide(): boolean - return self.action == "dash" and self.dino and self.dino.level >= 2 + return self.action == "dash" and self.dino and self.dino.level >= 2 end function Player:is_sliding(): boolean - return self.slide_for > 0 + return self.slide_for > 0 end function Player:have_roared_since_buildup_for(): number - return self.total_roar_time - self.roar_for - dinos.ROAR_BUILDUP_TIME + return self.total_roar_time - self.roar_for - dinos.ROAR_BUILDUP_TIME end function Player:is_roaring(): boolean - return self.roar_for > 0 and self.dino ~= nil + return self.roar_for > 0 and self.dino ~= nil end function Player:is_stun_crying(): boolean - return self:is_roaring() and self.dino.color == "yellow" and self:have_roared_since_buildup_for() > 0 + return self:is_roaring() + and self.dino.color == "yellow" + and self:have_roared_since_buildup_for() > 0 end function Player:is_sonic_blasting(): boolean - return self:is_roaring() and self.dino.color == "purple" and self:have_roared_since_buildup_for() > 0 + return self:is_roaring() + and self.dino.color == "purple" + and self:have_roared_since_buildup_for() > 0 end function Player:set_action(action: Action) - if self.action == "dying" or self:is_stunned() or self:is_sliding() then - -- Not able to perform new action due to conditions - return - end - - if (self.action == "dash" or action == "roar") and (action == "walk" or action == "idle") then - -- Not possible to exit dash action into walk or idle - return - end - - self.action = action - if self.dino then - if action == "walk" then - self.dino.action = "walk" - elseif action == "idle" then - self.dino.action = "idle" - elseif action == "stunned" then - self.dino.action = "stunned" - elseif action == "jump" then - self.dino.action = "jump" - elseif action == "dash" then - self.dino.action = "dash" - elseif action == "crashed" then - self.dino.action = "crashed" - elseif action == "slide" then - self.dino.action = "slide" - elseif action == "roar" then - self.dino.action = "roar" - end - end + if self.action == "dying" or self:is_stunned() or self:is_sliding() then + -- Not able to perform new action due to conditions + return + end + + if (self.action == "dash" or action == "roar") + and (action == "walk" or action == "idle") then + -- Not possible to exit dash action into walk or idle + return + end + + self.action = action + if self.dino then + if action == "walk" then + self.dino.action = "walk" + elseif action == "idle" then + self.dino.action = "idle" + elseif action == "stunned" then + self.dino.action = "stunned" + elseif action == "jump" then + self.dino.action = "jump" + elseif action == "dash" then + self.dino.action = "dash" + elseif action == "crashed" then + self.dino.action = "crashed" + elseif action == "slide" then + self.dino.action = "slide" + elseif action == "roar" then + self.dino.action = "roar" + end + end end function Player:set_movement(position: Point, direction: Direction) - if self.action == "dash" and self.position:equal(position) then + if self.action == "dash" and self.position:equal(position) then + self:set_action("crashed") + if self.dino and self.dino.level == 1 then + self.stunned_for = 1.2 + end - self:set_action("crashed") - if self.dino and self.dino.level == 1 then - self.stunned_for = 1.2 - end + return + end - return - end - - self.position = position - self.direction = direction - if self.dino then - self.dino.position = position - self.dino.direction = direction - end + self.position = position + self.direction = direction + if self.dino then + self.dino.position = position + self.dino.direction = direction + end end function Player:slide_with_dino() - self:set_action("slide") - self.slide_for = SLIDE_FOR_TIME + self:set_action("slide") + self.slide_for = SLIDE_FOR_TIME end function Player:stun_for(duration: number) - self:set_action("stunned") - self.stunned_for = duration - self.direction = "down" + self:set_action("stunned") + self.stunned_for = duration + self.direction = "down" end function Player:hit_with_bomb() - self:stun_for(1) + self:stun_for(1) end function Player:hitbox(): Rectangle - -- This hitbox is not for movement - return hitboxes.new_block(self.position):shrink(4) + -- This hitbox is not for movement + return hitboxes.new_block(self.position):shrink(4) end function Player:movementbox(): Rectangle - return hitboxes.new_block(self.position) + return hitboxes.new_block(self.position) end -local function get_character_on_dino_ground_position(original_pos: Point, direction: Direction): Point - local horizonal_displacements: {Direction: integer} = { - right = -5, left = 5, up = 0, down = 0, - } - local horizonal_displacement = horizonal_displacements[direction] - return original_pos:move( - vectors.new(horizonal_displacement, 0) - ) +local function get_character_on_dino_ground_position( + original_pos: Point, direction: Direction +): Point + local horizonal_displacements: {Direction: integer} = { + right = -5, + left = 5, + up = 0, + down = 0, + } + local horizonal_displacement = horizonal_displacements[direction] + return original_pos:move(vectors.new(horizonal_displacement, 0)) end -local function get_character_position_on_top_of_dino(original_pos: Point, direction: Direction): Point - local to_the_side_pos = get_character_on_dino_ground_position(original_pos, direction) - return to_the_side_pos:move(vectors.new(0, -CHARACTER_HEIGHT_ON_DINO)) +local function get_character_position_on_top_of_dino( + original_pos: Point, direction: Direction +): Point + local to_the_side_pos = get_character_on_dino_ground_position( + original_pos, direction + ) + return to_the_side_pos:move(vectors.new(0, -CHARACTER_HEIGHT_ON_DINO)) end function Player:is_riding_dino(): boolean - return self.dino and not self.dino.hatching + return self.dino and not self.dino.hatching end function Player:is_invulnerable(): boolean - return self.invulnerable_for > 0 + return self.invulnerable_for > 0 end function Player:accepts_input(): boolean - return not self:is_jumping() and not self:is_stunned() and not self:is_roaring() + return not self:is_jumping() and not self:is_stunned() and not self:is_roaring() end --- Player jump constants -local JUMP_ON_TIME = 1.2 -- Unit: seconds -local JUMP_OFF_TIME = 0.9 -- Unit: seconds -local JUMP_ON_HEIGHT = 2 * grid.TILE_SIZE -- Unit: pixels (not SI unit!) +-- Player jump constants +local JUMP_ON_TIME = 1.2 -- Unit: seconds +local JUMP_OFF_TIME = 0.9 -- Unit: seconds +local JUMP_ON_HEIGHT = 2 * grid.TILE_SIZE -- Unit: pixels (not SI unit!) local JUMP_OFF_HEIGHT = 3 * grid.TILE_SIZE -- Unit: pixels (not SI unit!) function Player:jumps_onto(dino: Dino) - if not self:is_jumping() then - self:set_action("jump_on") - self.state = "mounted" - local character_distance_over_ground = 0.0 - - if self.dino then - -- Jump in place when upgrading existing dino - self.dino.action = "idle" - self.dino:level_up() - dino.level = 0 -- Dino is consumed into existing dino - character_distance_over_ground = CHARACTER_HEIGHT_ON_DINO - else - self.dino = dino - end - - local character_ground_pos = get_character_on_dino_ground_position(self.dino.position, self.direction) - self.jump_trajectory = physics.calc_trajectory( - character_distance_over_ground, - CHARACTER_HEIGHT_ON_DINO, - JUMP_ON_HEIGHT, - JUMP_ON_TIME, - self.position, - character_ground_pos - ) - self.jump_target = self.dino.position - end + if not self:is_jumping() then + self:set_action("jump_on") + self.state = "mounted" + local character_distance_over_ground = 0.0 + + if self.dino then + -- Jump in place when upgrading existing dino + self.dino.action = "idle" + self.dino:level_up() + dino.level = 0 -- Dino is consumed into existing dino + character_distance_over_ground = CHARACTER_HEIGHT_ON_DINO + else + self.dino = dino + end + + local character_ground_pos = get_character_on_dino_ground_position( + self.dino.position, self.direction + ) + self.jump_trajectory = physics.calc_trajectory( + character_distance_over_ground, + CHARACTER_HEIGHT_ON_DINO, + JUMP_ON_HEIGHT, + JUMP_ON_TIME, + self.position, + character_ground_pos + ) + self.jump_target = self.dino.position + end end function Player:jump_off_dino() - if not self:is_jumping() then - self:set_action("jump_off") - self.state = "foot" - - self.jump_target = self.position - -- Set current position as shifted by riding dino to jump from that position - self.position = get_character_on_dino_ground_position(self.position, self.direction) - self.jump_trajectory = physics.calc_trajectory( - CHARACTER_HEIGHT_ON_DINO, 0, JUMP_OFF_HEIGHT, JUMP_OFF_TIME, self.position, self.jump_target - ) - end + if not self:is_jumping() then + self:set_action("jump_off") + self.state = "foot" + + self.jump_target = self.position + -- Set current position as shifted by riding dino to jump from that position + self.position = get_character_on_dino_ground_position( + self.position, self.direction + ) + self.jump_trajectory = physics.calc_trajectory( + CHARACTER_HEIGHT_ON_DINO, + 0, + JUMP_OFF_HEIGHT, + JUMP_OFF_TIME, + self.position, + self.jump_target + ) + end end local DINO_POWERUP_TIME_INTERVAL = 0.2 function Player:can_jump(): boolean - return self.dino and self.dino.color == "pink" and not self:is_jumping() + return self.dino and self.dino.color == "pink" and not self:is_jumping() end function Player:get_max_jump_dist(): integer - local jump_dist = 2 * (math.floor(self.dino.jump_powerup_for / DINO_POWERUP_TIME_INTERVAL) + 1) - -- Clamp max jump dist - return math.min(jump_dist, self.dino.level * 2) + local jump_dist = 2 * (math.floor( + self.dino.jump_powerup_for / DINO_POWERUP_TIME_INTERVAL + ) + 1) + -- Clamp max jump dist + return math.min(jump_dist, self.dino.level * 2) end function Player:_get_max_jump_powerup_time(): number - return self.dino.level * DINO_POWERUP_TIME_INTERVAL + return self.dino.level * DINO_POWERUP_TIME_INTERVAL end function Player:must_jump(): boolean - return self.dino and self.dino.jump_powerup_for > self:_get_max_jump_powerup_time() + return self.dino and self.dino.jump_powerup_for > self:_get_max_jump_powerup_time() end function Player:jump_with_dino(jump_to: Point | nil) - if self.dino and self.dino.color == "pink" and not self:is_jumping() then - self:set_action("jump") - - self.jump_target = jump_to or self.position - local dino_jump_time = { - [1] = 0.9, - [2] = 1.0, - [3] = 1.1, - [4] = 1.2, - [5] = 1.3, - [6] = 1.4, - } - local dino_jump_height = { - [1] = 1.2 * grid.TILE_SIZE, - [2] = 1.4 * grid.TILE_SIZE, - [3] = 1.7 * grid.TILE_SIZE, - [4] = 2.0 * grid.TILE_SIZE, - [5] = 2.3 * grid.TILE_SIZE, - [6] = 2.6 * grid.TILE_SIZE, - } - local jump_power = self:get_max_jump_dist() - assert(jump_power <= 6 and jump_power >= 1) - self.jump_trajectory = physics.calc_trajectory( - 0, - 0, - dino_jump_height[jump_power], - dino_jump_time[jump_power], - self.position, - self.jump_target - ) - self.dino.jump_powerup_for = 0 - end + if self.dino and self.dino.color == "pink" and not self:is_jumping() then + self:set_action("jump") + + self.jump_target = jump_to or self.position + local dino_jump_time = { + [1] = 0.9, + [2] = 1.0, + [3] = 1.1, + [4] = 1.2, + [5] = 1.3, + [6] = 1.4, + } + local dino_jump_height = { + [1] = 1.2 * grid.TILE_SIZE, + [2] = 1.4 * grid.TILE_SIZE, + [3] = 1.7 * grid.TILE_SIZE, + [4] = 2.0 * grid.TILE_SIZE, + [5] = 2.3 * grid.TILE_SIZE, + [6] = 2.6 * grid.TILE_SIZE, + } + local jump_power = self:get_max_jump_dist() + assert(jump_power <= 6 and jump_power >= 1) + self.jump_trajectory = physics.calc_trajectory( + 0, + 0, + dino_jump_height[jump_power], + dino_jump_time[jump_power], + self.position, + self.jump_target + ) + self.dino.jump_powerup_for = 0 + end end function Player:dash_with_dino() - self:set_action("dash") + self:set_action("dash") end function Player:stun_cry_with_dino() - self:set_action("roar") - self.roar_for = STUN_CRY_ROAR_FOR - self.total_roar_time = STUN_CRY_ROAR_FOR - self.play_dino_roar = true + self:set_action("roar") + self.roar_for = STUN_CRY_ROAR_FOR + self.total_roar_time = STUN_CRY_ROAR_FOR + self.play_dino_roar = true end function Player:sonic_blast_with_dino() - self:set_action("roar") - self.roar_for = SONIC_BLAST_ROAR_FOR - self.total_roar_time = SONIC_BLAST_ROAR_FOR - self.play_dino_roar = true - self.sonic_waves = {} + self:set_action("roar") + self.roar_for = SONIC_BLAST_ROAR_FOR + self.total_roar_time = SONIC_BLAST_ROAR_FOR + self.play_dino_roar = true + self.sonic_waves = {} end function Player:_get_hitbox_in_dir(from_position: Point, grid_reach: number): Rectangle - local x = from_position.x - local y = from_position.y - if self.direction == "up" then - return rectangles.new(x, y - grid_reach, grid.TILE_SIZE, grid_reach) - elseif self.direction == "down" then - return rectangles.new(x, y + grid.TILE_SIZE, grid.TILE_SIZE, grid_reach) - elseif self.direction == "right" then - return rectangles.new(x + grid.TILE_SIZE, y, grid_reach, grid.TILE_SIZE) - elseif self.direction == "left" then - return rectangles.new(x - grid_reach, y, grid_reach, grid.TILE_SIZE) - else - assert(false, "Bad direction") - end + local x = from_position.x + local y = from_position.y + if self.direction == "up" then + return rectangles.new(x, y - grid_reach, grid.TILE_SIZE, grid_reach) + elseif self.direction == "down" then + return rectangles.new(x, y + grid.TILE_SIZE, grid.TILE_SIZE, grid_reach) + elseif self.direction == "right" then + return rectangles.new(x + grid.TILE_SIZE, y, grid_reach, grid.TILE_SIZE) + elseif self.direction == "left" then + return rectangles.new(x - grid_reach, y, grid_reach, grid.TILE_SIZE) + else + assert(false, "Bad direction") + end end function Player:get_stun_cry_hitbox(): Rectangle - return self:_get_hitbox_in_dir(self.position, self.dino.level * 3 * grid.TILE_SIZE) + return self:_get_hitbox_in_dir(self.position, self.dino.level * 3 * grid.TILE_SIZE) end function Player:get_sonic_blast_hitbox(): Rectangle - local max_reach = (self.dino.level + 1) * grid.TILE_SIZE - local sound_traveled = self:have_roared_since_buildup_for() * 200 - local reach = math.min(sound_traveled, max_reach) - return self:_get_hitbox_in_dir(grid.snap_to(self.position), reach) + local max_reach = (self.dino.level + 1) * grid.TILE_SIZE + local sound_traveled = self:have_roared_since_buildup_for() * 200 + local reach = math.min(sound_traveled, max_reach) + return self:_get_hitbox_in_dir(grid.snap_to(self.position), reach) end function Player:get_speed(): number - if self.action == "dash" and self.dino then - return self.speed + DASH_WITH_DINO_SPEED_BONUS[self.dino.level] - elseif self.action == "slide" then - return self.speed * (self.slide_for / SLIDE_FOR_TIME) - else - return self.speed - end + if self.action == "dash" and self.dino then + return self.speed + DASH_WITH_DINO_SPEED_BONUS[self.dino.level] + elseif self.action == "slide" then + return self.speed * (self.slide_for / SLIDE_FOR_TIME) + else + return self.speed + end end function Player:kill() - if self:is_invulnerable() or self:is_jumping() then - return - end + if self:is_invulnerable() or self:is_jumping() then + return + end - if self.dino then - self.dino.action = "dying" - self.dino = nil - self.state = "foot" + if self.dino then + self.dino.action = "dying" + self.dino = nil + self.state = "foot" - self.invulnerable_for = 2 - self:jump_off_dino() - return - end + self.invulnerable_for = 2 + self:jump_off_dino() + return + end - self.action = "dying" + self.action = "dying" end function Player:remove() - self.is_removed = true - self.action = "dying" + self.is_removed = true + self.action = "dying" end function Player:blow_up() - self:kill() + self:kill() end function Player:is_alive(): boolean - return self.action ~= "dying" + return self.action ~= "dying" end function Player:get_jump_position(): Point - return self.jump_trajectory:in_air_position(self.position) + return self.jump_trajectory:in_air_position(self.position) end function Player:_new_sonic_wave(): SonicWave - local color = SONIC_WAVE_COLOR_ORDERS[self.sonic_wave_color_index] - local time_to_live = 0.4 - self.sonic_wave_color_index = self.sonic_wave_color_index % #SONIC_WAVE_COLOR_ORDERS - self.sonic_wave_color_index = self.sonic_wave_color_index + 1 - return SonicWave.new(color, time_to_live) + local color = SONIC_WAVE_COLOR_ORDERS[self.sonic_wave_color_index] + local time_to_live = 0.4 + self.sonic_wave_color_index = self.sonic_wave_color_index % #SONIC_WAVE_COLOR_ORDERS + self.sonic_wave_color_index = self.sonic_wave_color_index + 1 + return SonicWave.new(color, time_to_live) end function Player:update(dt: number) - self.invulnerable_for = self.invulnerable_for - dt - self.stunned_for = self.stunned_for - dt - self.slide_for = self.slide_for - dt - self.roar_for = self.roar_for - dt - self.last_time_adding_sonic_wave = self.last_time_adding_sonic_wave + dt - - self.after_image_shift = self.after_image_shift + self:get_speed() * dt + lutro.math.random(4) - self.after_image_shift = math.fmod(self.after_image_shift, 37) - - if self:is_sonic_blasting() then - local time_between_waves = 0.1 - if self.last_time_adding_sonic_wave > time_between_waves then - self.last_time_adding_sonic_wave = 0 - table.insert(self.sonic_waves, self:_new_sonic_wave()) - end - - for i = #self.sonic_waves, 1, -1 do - local sonic_wave = self.sonic_waves[i] - sonic_wave.duration = sonic_wave.duration + dt - if sonic_wave.duration > sonic_wave.live_for then - table.remove(self.sonic_waves, i) - end - end - end - - if self.play_dino_roar and self:is_stun_crying() then - stun_cry_sound[self.dino.level]:play() - self.play_dino_roar = false - end - if self.play_dino_roar and self:is_sonic_blasting() then - sonic_blast_sound:play() - self.play_dino_roar = false - end - - if self.action == "stunned" or self.action == "slide" then - self:set_action("idle") -- Try to transition from stunned/slide - end - - if self:is_jumping() then - self.position = self.jump_trajectory:update(dt, self.position) - if self.jump_trajectory:finished() then - -- When distance from target not decreasing while falling we have landed - self:set_action("idle") - self.position = self.jump_target - if self.dino then - self.dino.hatching = false - end - end - end + self.invulnerable_for = self.invulnerable_for - dt + self.stunned_for = self.stunned_for - dt + self.slide_for = self.slide_for - dt + self.roar_for = self.roar_for - dt + self.last_time_adding_sonic_wave = self.last_time_adding_sonic_wave + dt + + self.after_image_shift = self.after_image_shift + self:get_speed() * dt + lutro.math.random( + 4 + ) + self.after_image_shift = math.fmod(self.after_image_shift, 37) + + if self:is_sonic_blasting() then + local time_between_waves = 0.1 + if self.last_time_adding_sonic_wave > time_between_waves then + self.last_time_adding_sonic_wave = 0 + table.insert(self.sonic_waves, self:_new_sonic_wave()) + end + + for i = #self.sonic_waves, 1, -1 do + local sonic_wave = self.sonic_waves[i] + sonic_wave.duration = sonic_wave.duration + dt + if sonic_wave.duration > sonic_wave.live_for then + table.remove(self.sonic_waves, i) + end + end + end + + if self.play_dino_roar and self:is_stun_crying() then + stun_cry_sound[self.dino.level]:play() + self.play_dino_roar = false + end + if self.play_dino_roar and self:is_sonic_blasting() then + sonic_blast_sound:play() + self.play_dino_roar = false + end + + if self.action == "stunned" or self.action == "slide" then + self:set_action("idle") -- Try to transition from stunned/slide + end + + if self:is_jumping() then + self.position = self.jump_trajectory:update(dt, self.position) + if self.jump_trajectory:finished() then + -- When distance from target not decreasing while falling we have landed + self:set_action("idle") + self.position = self.jump_target + if self.dino then + self.dino.hatching = false + end + end + end end function Player:get_draw_position(): Point - if self:is_jumping_on_off_dino() then - return self:get_jump_position() - elseif self.action == "jump" then - -- Adjust player sprite for jump height and being on top of its mount - return get_character_position_on_top_of_dino(self:get_jump_position(), self.direction) - elseif self.state == "mounted" then - -- Adjust player sprite for being on top of its mount - return get_character_position_on_top_of_dino(self.position, self.direction) - end + if self:is_jumping_on_off_dino() then + return self:get_jump_position() + elseif self.action == "jump" then + -- Adjust player sprite for jump height and being on top of its mount + return get_character_position_on_top_of_dino( + self:get_jump_position(), self.direction + ) + elseif self.state == "mounted" then + -- Adjust player sprite for being on top of its mount + return get_character_position_on_top_of_dino(self.position, self.direction) + end - return self.position + return self.position end function players.load_assets() - for level=1, 3 do - local sound = audio.new_source_pool("assets/tirras/level_" .. level .. "/roar.ogg", "static", 2) - sound:setPitch(0.1) - sound:setVolume(1.25) - stun_cry_sound[level] = sound - end - sonic_blast_sound = audio.new_source_pool("assets/tirras/sonic-blast.ogg", "static", 2) - sonic_blast_sound:setPitch(0.005) - sonic_blast_sound:setVolume(0.13) + for level = 1, 3 do + local sound = audio.new_source_pool( + "assets/tirras/level_" .. level .. "/roar.ogg", "static", 2 + ) + sound:setPitch(0.1) + sound:setVolume(1.25) + stun_cry_sound[level] = sound + end + sonic_blast_sound = audio.new_source_pool( + "assets/tirras/sonic-blast.ogg", "static", 2 + ) + sonic_blast_sound:setPitch(0.005) + sonic_blast_sound:setVolume(0.13) end return players diff --git a/src/pressure_block.tl b/src/pressure_block.tl index 9751a43..7195090 100644 --- a/src/pressure_block.tl +++ b/src/pressure_block.tl @@ -1,9 +1,9 @@ local collision = require("engine.collision") -local grid = require("level_layout.grid") -local hitboxes = require("engine.hitboxes") -local map_entities = require("level_layout.map_entities") local points = require("engine.geometry.points") local rectangles = require("engine.geometry.rectangles") +local hitboxes = require("engine.hitboxes") +local grid = require("level_layout.grid") +local map_entities = require("level_layout.map_entities") local z_index = require("z_index") local type Explodable = require("bombs.explodable") @@ -14,7 +14,7 @@ local type Point = points.Point local type Rectangle = rectangles.Rectangle local record PressureBlock is Entity, Explodable, Kinematic - falling_position: Point + falling_position: Point position: Point in_explosion: boolean end @@ -23,12 +23,12 @@ local pressure_block_image: Image local block_fall_shadow: Image function PressureBlock.new(target: Point): PressureBlock - local self: PressureBlock = setmetatable({}, { __index = PressureBlock }) - self.falling_position = points.new(target.x, -grid.TILE_SIZE) + local self: PressureBlock = setmetatable({}, {__index = PressureBlock}) + self.falling_position = points.new(target.x, -grid.TILE_SIZE) self.position = target self.z_index = z_index.LEVEL self.in_explosion = false - return self + return self end function PressureBlock:get_position(): Point @@ -36,7 +36,9 @@ function PressureBlock:get_position(): Point end function PressureBlock:draw() - lutro.graphics.draw(pressure_block_image, self.falling_position.x, self.falling_position.y) + lutro.graphics.draw( + pressure_block_image, self.falling_position.x, self.falling_position.y + ) end function PressureBlock:draw_shadow() @@ -50,16 +52,16 @@ function PressureBlock:blow_up() end function PressureBlock:hit_with_bomb() - -- Does nothing + -- Does nothing end function PressureBlock:hitbox(): Rectangle - -- Not for movement blocking but hitting entities - return hitboxes.new_block(self.position):shrink(4) + -- Not for movement blocking but hitting entities + return hitboxes.new_block(self.position):shrink(4) end function PressureBlock:movementbox(): Rectangle - return hitboxes.new_block(self.position) + return hitboxes.new_block(self.position) end function PressureBlock:is_on_ground(): boolean @@ -68,14 +70,16 @@ end function PressureBlock:update(dt: number) local block_fall_speed = 285 - self.falling_position.y = math.min(self.position.y, self.falling_position.y + dt * block_fall_speed) + self.falling_position.y = math.min( + self.position.y, self.falling_position.y + dt * block_fall_speed + ) if self:is_on_ground() then self.z_index = z_index.BELOW end end local record PressureBlocks - next_left_index: integer + next_left_index: integer next_right_index: integer left_positions: {Point} right_positions: {Point} @@ -91,7 +95,7 @@ end local pressure_blocks = {PressureBlocks = PressureBlocks, PressureBlock = PressureBlock} function pressure_blocks.new(): PressureBlocks - local self: PressureBlocks = setmetatable({}, { __index = PressureBlocks }) + local self: PressureBlocks = setmetatable({}, {__index = PressureBlocks}) self.left_positions = map_entities.get_left_side_pressure_blocks() self.right_positions = map_entities.get_right_side_pressure_blocks() self.on_ground = collision.new_group() @@ -103,7 +107,7 @@ function pressure_blocks.new(): PressureBlocks self.left_fall_finished = false self.no_more_falling_blocks = false self.sudden_death = false - return self + return self end function PressureBlocks:no_updates(): boolean @@ -113,7 +117,9 @@ end function PressureBlocks:set_next_right_falling_block() self.right_fall_finished = false if self.next_right_index <= #self.right_positions then - self.right_falling = PressureBlock.new(self.right_positions[self.next_right_index]) + self.right_falling = PressureBlock.new( + self.right_positions[self.next_right_index] + ) self.next_right_index = self.next_right_index + 1 else self.no_more_falling_blocks = true @@ -136,7 +142,7 @@ function PressureBlocks:falling(): {PressureBlock: PressureBlock} end return { [self.left_falling] = self.left_falling, - [self.right_falling] = self.right_falling + [self.right_falling] = self.right_falling, } end diff --git a/src/settings.tl b/src/settings.tl index f898bfd..3339cc6 100644 --- a/src/settings.tl +++ b/src/settings.tl @@ -1,11 +1,11 @@ local engine_settings = require("engine.settings") local enum ItemType - "speed" - "bomb" - "kick" - "fire" - "lightning_bomb" + "speed" + "bomb" + "kick" + "fire" + "lightning_bomb" end local record TimingRules @@ -15,7 +15,7 @@ local record TimingRules end function TimingRules.new(): TimingRules - local self: TimingRules = setmetatable({}, { __index = TimingRules }) + local self: TimingRules = setmetatable({}, {__index = TimingRules}) self.nbr_battles = 3 self.battle = 180 self.sudden_death = 60 @@ -29,7 +29,7 @@ local record ItemRules end function ItemRules.new(): ItemRules - local self: ItemRules = setmetatable({}, { __index = ItemRules }) + local self: ItemRules = setmetatable({}, {__index = ItemRules}) self.nbr_soft_blocks = 105 self.nbr_of_items = { bomb = 8, @@ -54,7 +54,7 @@ local record MadBombers end function MadBombers.new(): MadBombers - local self: MadBombers = setmetatable({}, { __index = MadBombers }) + local self: MadBombers = setmetatable({}, {__index = MadBombers}) self.enabled = true self.after_sudden_death = false return self @@ -71,7 +71,7 @@ local record PlayerRules end function PlayerRules.new(): PlayerRules - local self: PlayerRules = setmetatable({}, { __index = PlayerRules }) + local self: PlayerRules = setmetatable({}, {__index = PlayerRules}) self.start_position_shuffle = true self.mad_bombers = MadBombers.new() self.start_speed = 60 @@ -96,13 +96,13 @@ local record DinoRules end function DinoRules.new(): DinoRules - local self: DinoRules = setmetatable({}, { __index = DinoRules }) + local self: DinoRules = setmetatable({}, {__index = DinoRules}) local enabled : {DinoColor: boolean} = { - blue=true, - pink=true, - purple=true, - green=true, - yellow=true, + blue = true, + pink = true, + purple = true, + green = true, + yellow = true, } self.nbr_of_eggs = 4 @@ -116,7 +116,7 @@ local record IceRules end function IceRules.new(): IceRules - local self: IceRules = setmetatable({}, { __index = IceRules }) + local self: IceRules = setmetatable({}, {__index = IceRules}) self.acceleration_factor = 4 self.friction_factor = 1 return self @@ -131,7 +131,7 @@ local record GameRules end function GameRules.new(): GameRules - local self: GameRules = setmetatable({}, { __index = GameRules }) + local self: GameRules = setmetatable({}, {__index = GameRules}) self.players = PlayerRules.new() self.items = ItemRules.new() self.timings = TimingRules.new() @@ -141,15 +141,15 @@ function GameRules.new(): GameRules end local type PlayerSelectionMethod = enum - "on" - "off" - "keep" + "on" + "off" + "keep" end local type Map = enum - "football" - "factory" - "ice_rink" + "football" + "factory" + "ice_rink" end local record Settings @@ -166,7 +166,7 @@ local record Settings end function Settings.new(): Settings - local self: Settings = setmetatable({}, { __index = Settings }) + local self: Settings = setmetatable({}, {__index = Settings}) self.number_of_players = 4 self.player_selection = "on" self.teams = false @@ -256,8 +256,8 @@ end function Settings.clear_mad_bombers(is_suddent_death: boolean): boolean local mad_bombers = settings.game_rules.players.mad_bombers - local clear_bombers = is_suddent_death and not mad_bombers.after_sudden_death - return not mad_bombers.enabled or clear_bombers + local clear_bombers = is_suddent_death and not mad_bombers.after_sudden_death + return not mad_bombers.enabled or clear_bombers end function Settings.set_start_speed(value: integer) @@ -305,9 +305,7 @@ function Settings.get_enabled_dinos(): {DinoColor} end function Settings.set_dino_enabled(color: DinoColor): function(value: boolean) - return function(value: boolean) - settings.game_rules.dinos.enabled[color] = value - end + return function(value: boolean) settings.game_rules.dinos.enabled[color] = value end end function Settings.set_teams(value: boolean) diff --git a/src/soft_blocks.tl b/src/soft_blocks.tl index a63a095..de78c75 100644 --- a/src/soft_blocks.tl +++ b/src/soft_blocks.tl @@ -1,12 +1,12 @@ local animations = require("engine.animations") local collision = require("engine.collision") -local grid = require("level_layout.grid") -local hitboxes = require("engine.hitboxes") -local map_entities = require("level_layout.map_entities") local points = require("engine.geometry.points") local rectangles = require("engine.geometry.rectangles") -local settings = require("settings") +local hitboxes = require("engine.hitboxes") local soft_block_animations = require("level_graphics.soft_block_animations") +local grid = require("level_layout.grid") +local map_entities = require("level_layout.map_entities") +local settings = require("settings") local z_index = require("z_index") local type Animation = animations.Animation @@ -20,7 +20,7 @@ local type SoftBlockConfig = map_entities.SoftBlockConfig local type ItemType = settings.ItemType local record SoftBlock is Entity, Explodable, Kinematic - position: Point + position: Point is_exploding: boolean in_explosion: boolean base_animation: Animation @@ -31,8 +31,14 @@ local record SoftBlock is Entity, Explodable, Kinematic is_revealed: boolean end -function SoftBlock.new(position: Point, base_animation: Animation, revealed_animation: Animation, item: ItemType, egg: boolean): SoftBlock - local self: SoftBlock = setmetatable({}, { __index = SoftBlock }) +function SoftBlock.new( + position: Point, + base_animation: Animation, + revealed_animation: Animation, + item: ItemType, + egg: boolean +): SoftBlock + local self: SoftBlock = setmetatable({}, {__index = SoftBlock}) self.position = position self.z_index = z_index.LEVEL self.base_animation = base_animation @@ -51,21 +57,21 @@ function SoftBlock:get_position(): Point end function SoftBlock:hit_with_bomb() - -- Does nothing + -- Does nothing end function SoftBlock:reveal() if self.egg or self.item then - self.is_revealed = true + self.is_revealed = true end end function SoftBlock:hitbox(): Rectangle - return hitboxes.new_block(self.position):shrink(4) + return hitboxes.new_block(self.position):shrink(4) end function SoftBlock:movementbox(): Rectangle - return hitboxes.new_block(self.position) + return hitboxes.new_block(self.position) end function SoftBlock:update(dt: number) @@ -75,9 +81,9 @@ function SoftBlock:update(dt: number) end local function center_animation_on_block(animation: Animation, position: Point): Point - return points.new( - position.x - (animation.width - grid.TILE_SIZE) / 2, - position.y - (animation.height - grid.TILE_SIZE) / 2 + return points.new( + position.x - (animation.width - grid.TILE_SIZE) / 2, + position.y - (animation.height - grid.TILE_SIZE) / 2 ) end @@ -105,7 +111,7 @@ function SoftBlock:is_destroyed(): boolean end local record SoftBlocks - on_ground: Group + on_ground: Group rigid_animation: Animation revealed_animation: Animation end @@ -113,7 +119,7 @@ end local soft_blocks = {SoftBlocks = SoftBlocks, SoftBlock = SoftBlock} function soft_blocks.new(configs: {SoftBlockConfig}): SoftBlocks - local self: SoftBlocks = setmetatable({}, { __index = SoftBlocks }) + local self: SoftBlocks = setmetatable({}, {__index = SoftBlocks}) self.on_ground = collision.new_group() self.rigid_animation = soft_block_animations.get_rigid() self.revealed_animation = soft_block_animations.get_revealed() diff --git a/src/team_selection.tl b/src/team_selection.tl index e1a0e31..a47a366 100644 --- a/src/team_selection.tl +++ b/src/team_selection.tl @@ -1,19 +1,19 @@ +local characters = require("characters") local animations = require("engine.animations") local audio = require("engine.audio") -local battle = require("game.battle") -local characters = require("characters") -local fonts = require("fonts") -local grid = require("level_layout.grid") -local joysticks = require("engine.joysticks") -local hitboxes = require("engine.hitboxes") local points = require("engine.geometry.points") local rectangles = require("engine.geometry.rectangles") +local vectors = require("engine.geometry.vectors") +local hitboxes = require("engine.hitboxes") +local joysticks = require("engine.joysticks") local scenes = require("engine.scenes") +local fonts = require("fonts") +local battle = require("game.battle") local score_board = require("game.score_board") +local win_condition = require("game.win_condition") +local grid = require("level_layout.grid") local screen = require("screen") -local vectors = require("engine.geometry.vectors") local teams = require("teams") -local win_condition = require("game.win_condition") local type Animation = animations.Animation local type Battle = battle.Battle @@ -33,284 +33,289 @@ local COUNTDOWN_DURATION = 3.0 local background: Image local font: MenuFont -local player_animations: {boolean: {CharacterName: {Direction: Animation}}} = {} +local player_animations: {boolean: {CharacterName: {Direction: Animation}}} = {} local record TeamAssignment - player_id: integer - team: integer - sprite: CharacterName + player_id: integer + team: integer + sprite: CharacterName end local record TeamZone - team: Team - bounds: Rectangle + team: Team + bounds: Rectangle end local function new_zone(team: Team, area: Rectangle): TeamZone - local self: TeamZone = setmetatable({}, { __index = TeamZone }) - self.team = team - self.bounds = area - return self + local self: TeamZone = setmetatable({}, {__index = TeamZone}) + self.team = team + self.bounds = area + return self end local ZONES: {TeamZone} = { - new_zone(teams.get(1), rectangles.new(24, 32, 112, 64)), - new_zone(teams.get(2), rectangles.new(184, 32, 112, 64)), - new_zone(teams.get(3), rectangles.new(24, 144, 112, 64)), - new_zone(teams.get(4), rectangles.new(184, 144, 112, 64)), + new_zone(teams.get(1), rectangles.new(24, 32, 112, 64)), + new_zone(teams.get(2), rectangles.new(184, 32, 112, 64)), + new_zone(teams.get(3), rectangles.new(24, 144, 112, 64)), + new_zone(teams.get(4), rectangles.new(184, 144, 112, 64)), } local record TeamPlayer - player_id: integer - sprite: CharacterName - position: Point - direction: Direction - walking: boolean + player_id: integer + sprite: CharacterName + position: Point + direction: Direction + walking: boolean end -local function new_team_player(player_id: integer, sprite: CharacterName, position: Point): TeamPlayer - local self: TeamPlayer = setmetatable({}, { __index = TeamPlayer }) - self.player_id = player_id - self.sprite = sprite - self.position = position - self.direction = "down" - self.walking = false - return self +local function new_team_player( + player_id: integer, sprite: CharacterName, position: Point +): TeamPlayer + local self: TeamPlayer = setmetatable({}, {__index = TeamPlayer}) + self.player_id = player_id + self.sprite = sprite + self.position = position + self.direction = "down" + self.walking = false + return self end local START_POSITIONS: {Point} = { - grid.get_tile_position(8, 4), - grid.get_tile_position(8, 6), - grid.get_tile_position(7, 5), - grid.get_tile_position(9, 5), - grid.get_tile_position(7, 4), - grid.get_tile_position(9, 4), - grid.get_tile_position(7, 6), - grid.get_tile_position(9, 6), - grid.get_tile_position(8, 5), - grid.get_tile_position(8, 3), + grid.get_tile_position(8, 4), + grid.get_tile_position(8, 6), + grid.get_tile_position(7, 5), + grid.get_tile_position(9, 5), + grid.get_tile_position(7, 4), + grid.get_tile_position(9, 4), + grid.get_tile_position(7, 6), + grid.get_tile_position(9, 6), + grid.get_tile_position(8, 5), + grid.get_tile_position(8, 3), } function TeamPlayer:get_moves_from_input(): {Direction} - local result: {Direction} = {} - - if lutro.joystick.isDown(self.player_id, joysticks.BUTTONS.DOWN) then - table.insert(result, "down") - end - if lutro.joystick.isDown(self.player_id, joysticks.BUTTONS.UP) then - table.insert(result, "up") - end - if lutro.joystick.isDown(self.player_id, joysticks.BUTTONS.RIGHT) then - table.insert(result, "right") - end - if lutro.joystick.isDown(self.player_id, joysticks.BUTTONS.LEFT) then - table.insert(result, "left") - end - - return result + local result: {Direction} = {} + + if lutro.joystick.isDown(self.player_id, joysticks.BUTTONS.DOWN) then + table.insert(result, "down") + end + if lutro.joystick.isDown(self.player_id, joysticks.BUTTONS.UP) then + table.insert(result, "up") + end + if lutro.joystick.isDown(self.player_id, joysticks.BUTTONS.RIGHT) then + table.insert(result, "right") + end + if lutro.joystick.isDown(self.player_id, joysticks.BUTTONS.LEFT) then + table.insert(result, "left") + end + + return result end function TeamPlayer:movementbox(): Rectangle - return hitboxes.new_block(self.position) + return hitboxes.new_block(self.position) end function TeamPlayer:_get_animation(): Animation - return player_animations[self.walking][self.sprite][self.direction] + return player_animations[self.walking][self.sprite][self.direction] end function TeamPlayer:update(dt: number) - self.walking = false -- Assume no direction - for _, direction in ipairs(self:get_moves_from_input()) do - self.walking = true -- Input to walk some direction => is walking + self.walking = false -- Assume no direction + for _, direction in ipairs(self:get_moves_from_input()) do + self.walking = true -- Input to walk some direction => is walking - -- Move player - local move = vectors.from_direction(direction):scale(WALK_SPEED * dt) - self.position = self.position:move(move) - self.direction = direction + -- Move player + local move = vectors.from_direction(direction):scale(WALK_SPEED * dt) + self.position = self.position:move(move) + self.direction = direction - -- Can't move outside of play area: - self.position = grid.area():clamp(self:movementbox()):top_left() - end + -- Can't move outside of play area: + self.position = grid.area():clamp(self:movementbox()):top_left() + end - self:_get_animation():update(dt) + self:_get_animation():update(dt) end local function block_to_anim_draw_pos(animation: Animation, pos: Point): Point - -- y: +1 is because of the sprite, look over when doing animations - return points.new( - pos.x + (grid.TILE_SIZE - animation.width) / 2, - pos.y - animation.height + grid.TILE_SIZE + 1) + -- y: +1 is because of the sprite, look over when doing animations + return points.new( + pos.x + (grid.TILE_SIZE - animation.width) / 2, + pos.y - animation.height + grid.TILE_SIZE + 1 + ) end function TeamPlayer:draw() - local anim = self:_get_animation() - local draw_pos = block_to_anim_draw_pos(anim, self.position) - anim:draw(draw_pos.x, draw_pos.y) + local anim = self:_get_animation() + local draw_pos = block_to_anim_draw_pos(anim, self.position) + anim:draw(draw_pos.x, draw_pos.y) end function TeamPlayer:get_team_zone(): TeamZone - for _, zone in ipairs(ZONES) do - if zone.bounds:contains(self:movementbox()) then - return zone - end - end + for _, zone in ipairs(ZONES) do + if zone.bounds:contains(self:movementbox()) then + return zone + end + end - return nil + return nil end local record TeamSelectionScene is Scene - team_players: {integer: TeamPlayer} - countdown_time: number - music: Source - score_board: TeamsScoreBoard - win_condition: TeamsWinCondition - game: Battle - going_back_to_game: boolean + team_players: {integer: TeamPlayer} + countdown_time: number + music: Source + score_board: TeamsScoreBoard + win_condition: TeamsWinCondition + game: Battle + going_back_to_game: boolean end function TeamSelectionScene:load() - self.countdown_time = COUNTDOWN_DURATION - local old_states = self.team_players - self.team_players = {} - - local selections = characters.get_selections() - for i, selection in ipairs(selections) do - local old_state = old_states[selection.player_id] - if old_state and old_state.sprite == selection.sprite then - self.team_players[selection.player_id] = old_states[selection.player_id] - else - local new_player = new_team_player( - selection.player_id, selection.sprite, START_POSITIONS[i] - ) - self.team_players[selection.player_id] = new_player - end - end + self.countdown_time = COUNTDOWN_DURATION + local old_states = self.team_players + self.team_players = {} + + local selections = characters.get_selections() + for i, selection in ipairs(selections) do + local old_state = old_states[selection.player_id] + if old_state and old_state.sprite == selection.sprite then + self.team_players[selection.player_id] = old_states[selection.player_id] + else + local new_player = new_team_player( + selection.player_id, selection.sprite, START_POSITIONS[i] + ) + self.team_players[selection.player_id] = new_player + end + end end function TeamSelectionScene:all_ready(): boolean - for _, player in pairs(self.team_players) do - if player:get_team_zone() == nil then - return false - end - end + for _, player in pairs(self.team_players) do + if player:get_team_zone() == nil then + return false + end + end - return true + return true end function TeamSelectionScene:update(dt: number) - for _, player in pairs(self.team_players) do - player:update(dt) - end - - if self:all_ready() then - self.countdown_time = math.max(self.countdown_time - dt, 0) - else - self.countdown_time = COUNTDOWN_DURATION - end - - -- Transition - if self.countdown_time == 0 then - local selections: {TeamSelection} = {} - self.game.team_by_player_id = {} - for _, player in pairs(self.team_players) do - local team = player:get_team_zone().team - local selection = teams.new_selection( - team, player.player_id, player.sprite - ) - table.insert(selections, selection) - self.game.team_by_player_id[player.player_id] = team.id - end - - self.music:stop() - self.score_board:update_avatars(selections) - self.win_condition:update_team_mapping(selections) - - if self.going_back_to_game then - -- The game might have paused audio, make sure to unpause: - audio.unpause() - end - scenes.set(self.game, self.going_back_to_game) - self.going_back_to_game = true - end + for _, player in pairs(self.team_players) do + player:update(dt) + end + + if self:all_ready() then + self.countdown_time = math.max(self.countdown_time - dt, 0) + else + self.countdown_time = COUNTDOWN_DURATION + end + + -- Transition + if self.countdown_time == 0 then + local selections: {TeamSelection} = {} + self.game.team_by_player_id = {} + for _, player in pairs(self.team_players) do + local team = player:get_team_zone().team + local selection = teams.new_selection(team, player.player_id, player.sprite) + table.insert(selections, selection) + self.game.team_by_player_id[player.player_id] = team.id + end + + self.music:stop() + self.score_board:update_avatars(selections) + self.win_condition:update_team_mapping(selections) + + if self.going_back_to_game then + -- The game might have paused audio, make sure to unpause: + audio.unpause() + end + scenes.set(self.game, self.going_back_to_game) + self.going_back_to_game = true + end end function TeamSelectionScene:get_players_to_draw(): {TeamPlayer} - local players_to_draw: {TeamPlayer} = {} - for _, p in pairs(self.team_players) do - table.insert(players_to_draw, p) - end - table.sort(players_to_draw, function(a: TeamPlayer, b: TeamPlayer): boolean - return a.position.y < b.position.y - end) - - return players_to_draw + local players_to_draw: {TeamPlayer} = {} + for _, p in pairs(self.team_players) do + table.insert(players_to_draw, p) + end + table.sort( + players_to_draw, + function(a: TeamPlayer, b: TeamPlayer): boolean + return a.position.y < b.position.y + end + ) + + return players_to_draw end function TeamSelectionScene:draw() - lutro.graphics.draw(background, 0, 0) - - for _, zone in ipairs(ZONES) do - local zone_opacity = 100 - teams.draw_square(zone.team, zone.bounds, zone_opacity) - - local label_y = zone.bounds.y + zone.bounds.height / 2 - 4 - local label_pos = points.new(zone.bounds.x, label_y) - font:print_centered(zone.team.name, zone.bounds.width, label_pos) - end - - for _, player in ipairs(self:get_players_to_draw()) do - player:draw() - end - - if self:all_ready() then - local seconds_left = math.ceil(self.countdown_time) - local text = "start in " .. seconds_left - local text_pos = points.new(0, 105) - font:print_centered(text, screen.WIDTH, text_pos) - end + lutro.graphics.draw(background, 0, 0) + + for _, zone in ipairs(ZONES) do + local zone_opacity = 100 + teams.draw_square(zone.team, zone.bounds, zone_opacity) + + local label_y = zone.bounds.y + zone.bounds.height / 2 - 4 + local label_pos = points.new(zone.bounds.x, label_y) + font:print_centered(zone.team.name, zone.bounds.width, label_pos) + end + + for _, player in ipairs(self:get_players_to_draw()) do + player:draw() + end + + if self:all_ready() then + local seconds_left = math.ceil(self.countdown_time) + local text = "start in " .. seconds_left + local text_pos = points.new(0, 105) + font:print_centered(text, screen.WIDTH, text_pos) + end end local team_selection = {TeamAssignment = TeamAssignment, TeamSelection = TeamSelection} -local function get_image(is_walking: boolean, sprite: CharacterName, direction: Direction): Image - local walk_name = { - [true] = "walk", - [false] = "idle" - } - local path = "assets/" .. sprite .. "/foot/" .. walk_name[is_walking] .. "_" .. direction .. ".png" - return lutro.graphics.newImage(path) +local function get_image( + is_walking: boolean, sprite: CharacterName, direction: Direction +): Image + local walk_name = {[true] = "walk", [false] = "idle"} + local path = "assets/" .. sprite .. "/foot/" .. walk_name[is_walking] .. "_" .. direction .. ".png" + return lutro.graphics.newImage(path) end function team_selection.load_assets() - local all_directions: {Direction} = {"up", "down", "left", "right"} - - background = lutro.graphics.newImage("assets/grass.png") - font = fonts.new_menu_white_outlined() - - for _, is_walking in ipairs({true, false}) do - player_animations[is_walking] = {} - for _, sprite_name in ipairs(characters.get_names()) do - player_animations[is_walking][sprite_name] = {} - for _, direction in ipairs(all_directions) do - local char_image = get_image(is_walking, sprite_name, direction) - player_animations[is_walking][sprite_name][direction] = animations.new(char_image, 18, 1/6) - end - end - end + local all_directions: {Direction} = {"up", "down", "left", "right"} + + background = lutro.graphics.newImage("assets/grass.png") + font = fonts.new_menu_white_outlined() + + for _, is_walking in ipairs({true, false}) do + player_animations[is_walking] = {} + for _, sprite_name in ipairs(characters.get_names()) do + player_animations[is_walking][sprite_name] = {} + for _, direction in ipairs(all_directions) do + local char_image = get_image(is_walking, sprite_name, direction) + player_animations[is_walking][sprite_name][direction] = animations.new( + char_image, 18, 1 / 6 + ) + end + end + end end -function team_selection.new(player_selection_scene: Scene, music: Source): TeamSelectionScene - local self: TeamSelectionScene = setmetatable({}, { __index = TeamSelectionScene }) - self.team_players = {} - self.countdown_time = COUNTDOWN_DURATION - self.music = music - self.score_board = score_board.new_for_teams() - self.win_condition = win_condition.new_for_teams(self.score_board) - self.game = battle.new( - player_selection_scene, self.score_board, self.win_condition - ) +function team_selection.new( + player_selection_scene: Scene, music: Source +): TeamSelectionScene + local self: TeamSelectionScene = setmetatable({}, {__index = TeamSelectionScene}) + self.team_players = {} + self.countdown_time = COUNTDOWN_DURATION + self.music = music + self.score_board = score_board.new_for_teams() + self.win_condition = win_condition.new_for_teams(self.score_board) + self.game = battle.new(player_selection_scene, self.score_board, self.win_condition) self.going_back_to_game = false - return self + return self end return team_selection diff --git a/src/teams.tl b/src/teams.tl index 2290db3..c5ac196 100644 --- a/src/teams.tl +++ b/src/teams.tl @@ -1,6 +1,6 @@ local characters = require("characters") -local images = require("engine.images") local rectangles = require("engine.geometry.rectangles") +local images = require("engine.images") local type CharacterName = characters.CharacterName local type CharacterSelection = characters.CharacterSelection @@ -27,7 +27,7 @@ end local teams = {Team = Team, TeamSelection = TeamSelection} function Team.new(id: integer, name: string, color: Color): Team - local self: Team = setmetatable({}, { __index = Team }) + local self: Team = setmetatable({}, {__index = Team}) self.id = id self.name = name self.color = color @@ -45,14 +45,18 @@ function Team:color_darker_shade(): Color end function TeamSelectionGrouped.new(team: Team): TeamSelectionGrouped - local self: TeamSelectionGrouped = setmetatable({}, { __index = TeamSelectionGrouped }) + local self: TeamSelectionGrouped = setmetatable( + {}, {__index = TeamSelectionGrouped} + ) self.team = team self.selections = {} return self end -function teams.new_selection(team: Team, player_id: integer, sprite: CharacterName): TeamSelection - local self: TeamSelection = setmetatable({}, { __index = TeamSelection }) +function teams.new_selection( + team: Team, player_id: integer, sprite: CharacterName +): TeamSelection + local self: TeamSelection = setmetatable({}, {__index = TeamSelection}) self.team = team self.player_id = player_id self.sprite = sprite diff --git a/src/z_index.tl b/src/z_index.tl index c296591..89baa23 100644 --- a/src/z_index.tl +++ b/src/z_index.tl @@ -1,8 +1,7 @@ local z_index = { BELOW = -3, -- On ground and you walk over it LEVEL = 0, -- Ground level, walk/moving - ABOVE = 3, -- Above/Flying above + ABOVE = 3, -- Above/Flying above } - -return z_index \ No newline at end of file +return z_index