1
0

decompose bouncing into "mods"

This commit is contained in:
Rokas Puzonas 2022-12-26 19:02:37 +02:00
parent e2134d76bd
commit a709e4f1c4
7 changed files with 291 additions and 146 deletions

View File

@ -1,6 +1,6 @@
local map = vim.api.nvim_set_keymap
map('n', '<leader>l', ":execute 'silent !kitty -d src love . &' | redraw!<cr>", {
map('n', '<leader><leader>l', ":execute 'silent !kitty -d src love . &' | redraw!<cr>", {
silent = true,
noremap = true
})

9
src/bolt-mods/bouncy.lua Normal file
View File

@ -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

19
src/bolt-mods/init.lua Normal file
View File

@ -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 }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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