129 lines
4.2 KiB
Lua
129 lines
4.2 KiB
Lua
local World = require("game.world")
|
|
local Palette = require("rendering.palette")
|
|
local Projection = require("rendering.projection")
|
|
local Models = require("data.models")
|
|
|
|
local Projectiles = {}
|
|
|
|
local active = {}
|
|
local SPEED = 540 -- world units/sec — shell travels fast
|
|
local LIFETIME = 2.0
|
|
|
|
function Projectiles.fire(x, z, angle, owner)
|
|
-- Forward = (sin(angle), cos(angle))
|
|
table.insert(active, {
|
|
x = x, z = z, y = 10,
|
|
angle = angle,
|
|
vx = math.sin(angle) * SPEED,
|
|
vz = math.cos(angle) * SPEED,
|
|
owner = owner or "player",
|
|
life = LIFETIME,
|
|
})
|
|
end
|
|
|
|
-- Closest distance from a point (px, pz) to a line segment ((ax, az)-(bx, bz)).
|
|
-- Used for swept-sphere hit tests so fast-moving shells can't tunnel through a target.
|
|
local function segmentDist(px, pz, ax, az, bx, bz)
|
|
local lx, lz = bx - ax, bz - az
|
|
local len2 = lx * lx + lz * lz
|
|
if len2 < 1e-6 then
|
|
local dx, dz = px - ax, pz - az
|
|
return math.sqrt(dx * dx + dz * dz)
|
|
end
|
|
local t = ((px - ax) * lx + (pz - az) * lz) / len2
|
|
if t < 0 then t = 0 elseif t > 1 then t = 1 end
|
|
local cx, cz = ax + t * lx, az + t * lz
|
|
local dx, dz = px - cx, pz - cz
|
|
return math.sqrt(dx * dx + dz * dz)
|
|
end
|
|
|
|
function Projectiles.update(dt, player, enemy, obstacles, ufo)
|
|
local obstacleList = obstacles and obstacles.getAll() or nil
|
|
for i = #active, 1, -1 do
|
|
local b = active[i]
|
|
local oldX, oldZ = b.x, b.z
|
|
b.x = b.x + b.vx * dt
|
|
b.z = b.z + b.vz * dt
|
|
b.x, b.z = World.wrapField(b.x, b.z)
|
|
b.life = b.life - dt
|
|
|
|
local remove = false
|
|
|
|
if b.life <= 0 then
|
|
remove = true
|
|
end
|
|
|
|
-- Obstacle hit — swept test against every obstacle (prevents tunnelling at 540 u/s).
|
|
-- Shells that are flying above the obstacle's top pass over cleanly so
|
|
-- the player can shoot over low slabs like the arcade.
|
|
if not remove and obstacleList then
|
|
for _, o in ipairs(obstacleList) do
|
|
if b.y <= (o.height or 999) then
|
|
local d = segmentDist(o.x, o.z, oldX, oldZ, b.x, b.z)
|
|
if d < (o.radius or 20) + 3 then
|
|
remove = true
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Player bullet vs enemy — swept test: check the segment (oldX,oldZ)→(b.x,b.z)
|
|
if not remove and b.owner == "player" and enemy and enemy.alive then
|
|
local dist = segmentDist(enemy.x, enemy.z, oldX, oldZ, b.x, b.z)
|
|
if dist < enemy.radius then
|
|
remove = true
|
|
enemy.hit = true
|
|
end
|
|
end
|
|
|
|
-- Player bullet vs saucer/UFO — bonus target; uses its full radius
|
|
if not remove and b.owner == "player" and ufo and ufo.alive then
|
|
local dist = segmentDist(ufo.x, ufo.z, oldX, oldZ, b.x, b.z)
|
|
if dist < ufo.radius then
|
|
remove = true
|
|
ufo.hit = true
|
|
end
|
|
end
|
|
|
|
-- Enemy bullet vs player — same swept test
|
|
if not remove and b.owner == "enemy" and player and player.alive and player.invulnTimer <= 0 then
|
|
local dist = segmentDist(player.x, player.z, oldX, oldZ, b.x, b.z)
|
|
if dist < 15 then
|
|
remove = true
|
|
player.wasHit = true
|
|
end
|
|
end
|
|
|
|
if remove then
|
|
table.remove(active, i)
|
|
end
|
|
end
|
|
end
|
|
|
|
function Projectiles.clear()
|
|
active = {}
|
|
end
|
|
|
|
function Projectiles.hasPlayerShot()
|
|
for _, b in ipairs(active) do
|
|
if b.owner == "player" then return true end
|
|
end
|
|
return false
|
|
end
|
|
|
|
function Projectiles.draw(playerX, playerZ)
|
|
local p = Palette.get()
|
|
love.graphics.setColor(p.projectile)
|
|
love.graphics.setLineWidth(2)
|
|
|
|
for _, b in ipairs(active) do
|
|
local dx, dz = World.wrappedDist(playerX, playerZ, b.x, b.z)
|
|
local wx = playerX + dx
|
|
local wz = playerZ + dz
|
|
-- drawModel rotY = -angle so the shell's local +z nose aligns with its direction
|
|
Projection.drawModel(Models.shell, wx, b.y, wz, -b.angle)
|
|
end
|
|
end
|
|
|
|
return Projectiles
|