diff --git a/.vimrc.lua b/.vimrc.lua index 3c6ce83..ba1b09c 100644 --- a/.vimrc.lua +++ b/.vimrc.lua @@ -1,6 +1,6 @@ local map = vim.api.nvim_set_keymap -map('n', 'l', ":execute 'silent !kitty -d src love . &' | redraw!", { +map('n', 'l', ":execute 'silent !kitty -d src love . &' | redraw!", { silent = true, noremap = true }) diff --git a/src/bolt-mods/bouncy.lua b/src/bolt-mods/bouncy.lua new file mode 100644 index 0000000..e1ca20b --- /dev/null +++ b/src/bolt-mods/bouncy.lua @@ -0,0 +1,9 @@ +local BouncyMod = {} + +BouncyMod.dont_die_on_touch_obstacle = true + +function BouncyMod.on_shoot(bolt) + bolt.collider:setRestitution(1) +end + +return BouncyMod diff --git a/src/bolt-mods/init.lua b/src/bolt-mods/init.lua new file mode 100644 index 0000000..ebfdc9b --- /dev/null +++ b/src/bolt-mods/init.lua @@ -0,0 +1,19 @@ + +local mods = { + bouncy = require(... .. ".bouncy"), + speed_bounce = require(... .. ".speed-bounce"), + retain_speed = require(... .. ".retain-speed") +} + +local order = { + mods.bouncy, + mods.speed_bounce, + mods.retain_speed +} + +local priority = {} +for i, mod in ipairs(order) do + priority[mod] = i +end + +return { mods = mods, priority = priority } diff --git a/src/bolt-mods/retain-speed.lua b/src/bolt-mods/retain-speed.lua new file mode 100644 index 0000000..4df45ce --- /dev/null +++ b/src/bolt-mods/retain-speed.lua @@ -0,0 +1,26 @@ +local RetainSpeedMod = {} + +local MIN_LIFETIME = 0.1 + +function RetainSpeedMod.on_shoot(bolt, player) + local attr = bolt.attributes + if not attr.max_vel then return end + + local velx, vely = bolt.collider:getLinearVelocity() + local old_velocity = (velx^2 + vely^2)^0.5 + local new_velocity = attr.max_vel + bolt.max_lifetime = math.max(bolt.max_lifetime / (new_velocity / old_velocity)^2, MIN_LIFETIME) + + local aim_dir = player:get_aim_dir() + bolt.collider:setLinearVelocity(aim_dir.x * new_velocity, aim_dir.y * new_velocity) +end + +function RetainSpeedMod.on_update(bolt) + if bolt.dead then return end + local attr = bolt.attributes + + local velx, vely = bolt.collider:getLinearVelocity() + attr.max_vel = math.max(attr.max_vel or 0, (velx^2 + vely^2)^0.5) +end + +return RetainSpeedMod diff --git a/src/bolt-mods/speed-bounce.lua b/src/bolt-mods/speed-bounce.lua new file mode 100644 index 0000000..6e58f9e --- /dev/null +++ b/src/bolt-mods/speed-bounce.lua @@ -0,0 +1,10 @@ +local SpeedBounceMod = {} + +local SPEEDUP_AMOUNT = 0.2 + +function SpeedBounceMod.on_shoot(bolt) + bolt.max_lifetime = bolt.max_lifetime / 2 + bolt.collider:setRestitution(1 + SPEEDUP_AMOUNT) +end + +return SpeedBounceMod diff --git a/src/player.lua b/src/player.lua index e399b7c..c9aefd8 100644 --- a/src/player.lua +++ b/src/player.lua @@ -3,7 +3,7 @@ local Vec = require("lib.brinevector") local Player = {} Player.__index = Player -local SHOOT_COOLDOWN = 1 +Player.MAX_HEALTH = 3 function Player:new(controls, collision_world, pos) local player = setmetatable({}, Player) @@ -11,13 +11,17 @@ function Player:new(controls, collision_world, pos) player.vel = Vec() player.controls = controls player.collider = collision_world:newCollider("Circle", { pos.x, pos.y, PLAYER_SIZE }) - player.pickedup_bolt_speeds = {} + player.pickedup_bolt_speed = nil player.shoot_cooldown = nil - player.health = 3 + player.health = Player.MAX_HEALTH + player.has_bolt = true + player.bolt_mods = {} if controls == "joystick" then local joysticks = love.joystick.getJoysticks() - assert(joysticks[1], "no joystick connected") + if not joysticks[1] then + print("no joystick connected") + end player.joystick = joysticks[1] end return player @@ -36,7 +40,7 @@ function Player:get_move_dir() local dirx = getDirectionKey("d", "a") local diry = getDirectionKey("s", "w") return Vec(dirx, diry).normalized - elseif self.controls == "joystick" then + elseif self.controls == "joystick" and self.joystick then local dirx = self.joystick:getGamepadAxis("leftx") local diry = self.joystick:getGamepadAxis("lefty") local size = dirx^2 + diry^2 @@ -53,7 +57,7 @@ end function Player:get_aim_dir() if self.controls == "keyboard" then return (Vec(love.mouse.getPosition()) - self.pos).normalized - elseif self.controls == "joystick" then + elseif self.controls == "joystick" and self.joystick then local dirx = self.joystick:getGamepadAxis("rightx") local diry = self.joystick:getGamepadAxis("righty") local dir = Vec(dirx, diry).normalized @@ -68,23 +72,11 @@ function Player:get_aim_dir() end end -function Player:process_shoot() - if not self.on_shoot then return end - local now = love.timer.getTime() - if self.last_shot_time then - if now - self.last_shot_time < SHOOT_COOLDOWN then return end - end - - local shoot = false +function Player:pressed_shoot() if self.controls == "keyboard" then - shoot = love.keyboard.isDown("space") - elseif self.controls == "joystick" then - shoot = self.joystick:isGamepadDown("leftshoulder") - end - - if shoot then - self:on_shoot() - self.last_shot_time = now + return love.keyboard.isDown("space") + elseif self.controls == "joystick" and self.joystick then + return self.joystick:isGamepadDown("leftshoulder") end end diff --git a/src/states/main.lua b/src/states/main.lua index fb29cd1..e83719a 100644 --- a/src/states/main.lua +++ b/src/states/main.lua @@ -2,36 +2,33 @@ local MainState = {} local Player = require("player") local Vec = require("lib.brinevector") local bf = require("lib.breezefield") +local rgb = require("helpers.rgb") +local lerp = require("lib.lume").lerp +local BoltMods = require("bolt-mods") local BOLT_DIST_FROM_PLAYER = 40 -local BOLT_SIZE = 8 +local BOLT_SIZE = 4 local DRAW_COLLIDERS = false local BOLT_DURATION = 2.5 -local PLAYER1_MASK = 2 -local PLAYER2_MASK = 3 +local SHOOT_COOLDOWN = 1 + +local FULL_HEALTH_COLOR = rgb(20, 200, 20) +local EMPTY_HEALTH_COLOR = rgb(180, 20, 20) + +local ACTIVE_BOLT_COLOR = rgb(240, 20, 20) +local INACTIVE_BOLT_COLOR = rgb(200, 100, 100) function MainState:enter() + love.graphics.setNewFont(72) love.mouse.setVisible(false) self.world = bf.newWorld(0, 0, true) self.world:setCallbacks(function(...) self:on_begin_contact(...) end) - local winw, winh = love.graphics.getDimensions() - self.player_1 = Player:new("keyboard", self.world, Vec(50, 200)) - self.player_2 = Player:new("joystick", self.world, Vec(winw-50, 200)) - - self.player_1.collider.fixture:setCategory(PLAYER1_MASK) - self.player_2.collider.fixture:setCategory(PLAYER2_MASK) - - self.players = { self.player_1, self.player_2 } - - local on_shoot = function(...) self:on_player_shoot(...) end - for _, player in ipairs(self.players) do - player.on_shoot = on_shoot - player.collider.fixture:setUserData("player") - end + self.players = {} + self.bolts = {} self.obstacles = {} table.insert(self.obstacles, {{90, 90}, {100, 200}, {300, 300}, {200, 100}}) @@ -52,23 +49,76 @@ function MainState:enter() end local collider = self.world:newCollider("Polygon", points) collider:setType("static") + collider.fixture:setUserData("obstacle") table.insert(self.obstacle_colliders, collider) end do - self.world:newCollider("Edge", { 0, 0, winw, 0 }):setType("static") -- top - self.world:newCollider("Edge", { 0, 0, 0, winh }):setType("static") -- left - self.world:newCollider("Edge", { winw, 0, winw, winh }):setType("static") -- right - self.world:newCollider("Edge", { 0, winh, winw, winh }):setType("static") -- bottom + local winw, winh = love.graphics.getDimensions() + local edges = { + { 0, 0, winw, 0 }, -- top + { 0, 0, 0, winh }, -- left + { winw, 0, winw, winh }, -- right + { 0, winh, winw, winh } -- bottom + } + for _, edge in ipairs(edges) do + local collider = self.world:newCollider("Edge", edge) + collider.fixture:setUserData("obstacle") + collider:setType("static") + end end - self.bolts = {} - self.bolt_colliders = {} + self:start_match() +end + +function MainState:stop_match() + for _, bolt in ipairs(self.bolts) do + self:destroy_bolt(bolt) + end + for _, player in ipairs(self.players) do + self:destroy_player(player) + end +end + +function MainState:start_match() + self:stop_match() + + local winw, winh = love.graphics.getDimensions() + self:create_player("keyboard", Vec(50, 200)) + -- self:create_player("joystick", Vec(winw-50, 200)) + self:create_player("joystick", Vec(50, 300)) end function MainState:update(dt) + local now = love.timer.getTime() + for _, bolt in ipairs(self.bolts) do + for _, mod in ipairs(bolt.shot_by.bolt_mods) do + if mod.on_update then + mod.on_update(bolt, dt) + end + end + + local lifetime = now - bolt.created_at + if lifetime > bolt.max_lifetime then + bolt.dead = true + end + + if bolt.dead then + local dampening = 0.75 + local velx, vely = bolt.collider:getLinearVelocity() + bolt.collider:setLinearVelocity(velx * dampening, vely * dampening) + end + end + for _, player in ipairs(self.players) do - player:process_shoot() + if player.has_bolt and player:pressed_shoot() then + player.last_shot_time = player.last_shot_time or 0 + if now - player.last_shot_time > SHOOT_COOLDOWN then + self:on_player_shoot(player) + player.last_shot_time = now + player.has_bolt = false + end + end local move_dir = player:get_move_dir() local acc = move_dir * 300 @@ -76,41 +126,47 @@ function MainState:update(dt) player.vel = player.vel * 0.98 player.collider:setLinearVelocity(player.vel.x, player.vel.y) - local now = love.timer.getTime() - for i, bolt in ipairs(self.bolts) do - local collider = self.bolt_colliders[i] - local lifetime = now - bolt.created_at - local velx, vely = collider:getLinearVelocity() - if not bolt.dead then - bolt.max_vel = math.max(bolt.max_vel, (velx^2 + vely^2)^0.5) - end - if lifetime > BOLT_DURATION then - bolt.dead = true - local dampening = 0.9 - collider:setLinearVelocity(velx * dampening, vely * dampening) - end - - if bolt.dead then - local distance = (bolt.pos - player.pos).length - if distance < BOLT_DIST_FROM_PLAYER then - table.insert(player.pickedup_bolt_speeds, bolt.max_vel) - self:destroy_bolt(i) + if not player.has_bolt then + for _, bolt in ipairs(self.bolts) do + if bolt.dead then + local distance = (bolt.pos - player.pos).length + if distance < BOLT_DIST_FROM_PLAYER then + player.pickup_bolt_attributes = bolt.attributes + player.has_bolt = true + self:destroy_bolt(bolt) + break + end end end end + + local was_dead = player.dead + if player.health <= 0 then + player.dead = true + end + if not was_dead and player.dead then + self:destroy_player(player) + end end - self.world:update(dt) + if #self.players > 1 then + self.world:update(dt) - for i, bolt in ipairs(self.bolts) do - local collider = self.bolt_colliders[i] - bolt.pos.x = collider:getX() - bolt.pos.y = collider:getY() - end + for _, bolt in ipairs(self.bolts) do + bolt.pos.x = bolt.collider:getX() + bolt.pos.y = bolt.collider:getY() + end - for _, player in ipairs(self.players) do - player.pos.x = player.collider:getX() - player.pos.y = player.collider:getY() + for _, player in ipairs(self.players) do + player.pos.x = player.collider:getX() + player.pos.y = player.collider:getY() + end + else + self.victory_timer = (self.victory_timer or 0) + dt + if self.victory_timer > 1 then + self.victory_timer = nil + self:start_match() + end end if love.keyboard.isDown("escape") then @@ -118,10 +174,41 @@ function MainState:update(dt) end end -function MainState:destroy_bolt(idx) - local collider = table.remove(self.bolt_colliders, idx) - table.remove(self.bolts, idx) - self.world:removeCollider(collider) +local function sort_bolt_mods(bolt_mods) + table.sort(bolt_mods, function(a, b) + return BoltMods.priority[a] < BoltMods.priority[b] + end) +end + +function MainState:create_player(controls, pos) + local player = Player:new(controls, self.world, pos) + player.bolt_mods = { + BoltMods.mods.speed_bounce, + BoltMods.mods.bouncy, + BoltMods.mods.retain_speed, + } + sort_bolt_mods(player.bolt_mods) + player.collider.fixture:setUserData("player") + table.insert(self.players, player) +end + +local function remove_by_value(array, value) + for i, v in ipairs(array) do + if v == value then + table.remove(array, i) + return + end + end +end + +function MainState:destroy_player(player) + remove_by_value(self.players, player) + self.world:removeCollider(player.collider) +end + +function MainState:destroy_bolt(bolt) + remove_by_value(self.bolts, bolt) + self.world:removeCollider(bolt.collider) end function MainState:on_player_shoot(player) @@ -129,111 +216,112 @@ function MainState:on_player_shoot(player) local bolt = { pos = player.pos + aim_dir * BOLT_DIST_FROM_PLAYER, created_at = love.timer.getTime(), - max_vel = 0 + shot_by = player, + max_lifetime = BOLT_DURATION, + attributes = player.pickup_bolt_attributes or {} } - local velocity = 500 - local pickup_idx = -1 - for i, vel in ipairs(player.pickedup_bolt_speeds) do - if vel > velocity then - velocity = vel - pickup_idx = i + local DEFAULT_VELOCITY = 500 + bolt.collider = self.world:newCollider("Circle", { bolt.pos.x, bolt.pos.y, BOLT_SIZE }) + bolt.collider:setLinearVelocity(aim_dir.x * DEFAULT_VELOCITY, aim_dir.y * DEFAULT_VELOCITY) + bolt.collider:setRestitution(0.1) + bolt.collider.fixture:setUserData("bolt") + + for _, mod in ipairs(player.bolt_mods) do + if mod.on_shoot then + mod.on_shoot(bolt, player) end end - if pickup_idx ~= -1 then - table.remove(player.pickedup_bolt_speeds, pickup_idx) - end - - local collider = self.world:newCollider("Circle", { bolt.pos.x, bolt.pos.y, BOLT_SIZE }) - collider:setLinearVelocity(aim_dir.x * velocity, aim_dir.y * velocity) - -- collider:applyLinearImpulse(aim_dir.x * launch_force, aim_dir.y * launch_force) - collider.fixture:setUserData("bolt") - collider:setRestitution(1.2) - - if player == self.player_1 then - collider:setMask(PLAYER1_MASK) - else - collider:setMask(PLAYER2_MASK) - end table.insert(self.bolts, bolt) - table.insert(self.bolt_colliders, collider) end function MainState:get_bolt_by_collider(collider) - for i, other_collider in ipairs(self.bolt_colliders) do - if other_collider == collider then - return i + for _, bolt in ipairs(self.bolts) do + if bolt.collider.fixture == collider then + return bolt end end end function MainState:get_player_by_collider(collider) - if self.player_1.collider.fixture == collider then - return self.player_1 - elseif self.player_2.collider.fixture == collider then - return self.player_2 + for _, player in ipairs(self.players) do + if player.collider.fixture == collider then + return player + end end end -function MainState:on_player_touch_bolt(player, bolt_idx) +function MainState:on_player_touch_bolt(player, bolt) + bolt.shot_by.has_bolt = true + player.health = player.health - 1 - self:destroy_bolt(bolt_idx) + self:destroy_bolt(bolt) +end + +function MainState:on_bolt_touch_obstacle(bolt) + local should_die = true + local player = bolt.shot_by + for _, mod in ipairs(player.bolt_mods) do + if mod.on_touch_obstacle then + mod.on_touch_obstacle(bolt, player) + end + if mod.dont_die_on_touch_obstacle then + should_die = false + end + end + + if should_die then + bolt.dead = true + end end function MainState:on_begin_contact(a, b) local a_data = a:getUserData() local b_data = b:getUserData() + if a_data == "player" and b_data == "bolt" then self:on_player_touch_bolt(self:get_player_by_collider(a), self:get_bolt_by_collider(b)) elseif a_data == "bolt" and b_data == "player" then self:on_player_touch_bolt(self:get_bolt_by_collider(a), self:get_player_by_collider(b)) + + elseif a_data == "bolt" and b_data == "obstacle" then + self:on_bolt_touch_obstacle(self:get_bolt_by_collider(a)) + elseif a_data == "obstacle" and b_data == "bolt" then + self:on_bolt_touch_obstacle(self:get_bolt_by_collider(b)) + end end -function MainState:draw_health(x, y, player, align) - local max_health = 3 - local rect_width = 20 - local rect_height = 25 - local gap = 10 - local padding = 10 - - local health_bar_width = max_health * (rect_width + gap) - gap + 2*padding - if align == "right" then - x = x - health_bar_width - end - - love.graphics.setColor(0.1, 0.1, 0.1) - love.graphics.rectangle("fill", - x, y, - health_bar_width, - rect_height + 2*padding - ) - - for i=1, max_health do - local rect_x = x + (rect_width + gap) * (i-1) + padding - if player.health >= i then - love.graphics.setColor(0.7, 0.2, 0.2) - else - love.graphics.setColor(0.2, 0.2, 0.2) - end - love.graphics.rectangle("fill", rect_x, y+padding, rect_width, rect_height) - end +local function lerp_color(a, b, t) + return lerp(a[1], b[1], t), lerp(a[2], b[2], t), lerp(a[3], b[3], t) end function MainState:draw() for _, player in ipairs(self.players) do love.graphics.setLineWidth(4) + local health_percent = player.health / Player.MAX_HEALTH + love.graphics.setColor(lerp_color(EMPTY_HEALTH_COLOR, FULL_HEALTH_COLOR, health_percent)) + love.graphics.circle("fill", player.pos.x, player.pos.y, PLAYER_SIZE) + love.graphics.setColor(1, 1, 1) love.graphics.circle("line", player.pos.x, player.pos.y, PLAYER_SIZE) - love.graphics.setLineWidth(3) - love.graphics.setColor(0.8, 0.1, 0.1) - local bolt_pos = player.pos + player:get_aim_dir() * BOLT_DIST_FROM_PLAYER - love.graphics.circle("line", bolt_pos.x, bolt_pos.y, BOLT_SIZE) + if player.has_bolt then + love.graphics.setLineWidth(3) + love.graphics.setColor(ACTIVE_BOLT_COLOR) + local bolt_pos = player.pos + player:get_aim_dir() * BOLT_DIST_FROM_PLAYER + love.graphics.circle("line", bolt_pos.x, bolt_pos.y, BOLT_SIZE) + end end + love.graphics.setLineWidth(3) for _, bolt in ipairs(self.bolts) do + if bolt.dead then + love.graphics.setColor(INACTIVE_BOLT_COLOR) + else + love.graphics.setColor(ACTIVE_BOLT_COLOR) + end love.graphics.circle("line", bolt.pos.x, bolt.pos.y, BOLT_SIZE) end @@ -260,9 +348,10 @@ function MainState:draw() self.world:draw() end - local winw, winh = love.graphics.getDimensions() - self:draw_health(10, 10, self.player_1, "left") - self:draw_health(winw-10, 10, self.player_2, "right") + if #self.players == 1 then + love.graphics.setColor(0.2, 1, 0.2) + love.graphics.print("Victory!", 300, 200) + end end return MainState