oma-tank/game/projectiles.lua
2026-04-18 11:35:06 +01:00

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