120 lines
3.5 KiB
Lua
120 lines
3.5 KiB
Lua
local World = require("game.world")
|
||
|
||
local Player = {}
|
||
|
||
-- Tank-feel constants tuned for the authentic 1980 arcade rhythm: deliberate
|
||
-- movement, slow rotation, one-track drive is a partial speed boost.
|
||
local MAX_SPEED = 60
|
||
local ROT_SPEED = 0.5 -- ~28°/s — deliberate but playable
|
||
local ONE_TRACK_BONUS = 1.5 -- one-track-only drives at 1.5× the average
|
||
local COLLISION_RADIUS = 12
|
||
local MOVE_EPSILON = 0.02
|
||
|
||
local BLOCKED_MSG_DURATION = 0.8
|
||
|
||
local player = {
|
||
x = 0, z = 0,
|
||
angle = 0,
|
||
alive = true,
|
||
invulnTimer = 0,
|
||
deathTimer = 0,
|
||
blockedMessageTimer = 0,
|
||
blockedJustTriggered = false,
|
||
_wasBlocked = false,
|
||
}
|
||
|
||
function Player.init()
|
||
player.x = World.FIELD_SIZE / 2
|
||
player.z = World.FIELD_SIZE / 2
|
||
player.angle = 0
|
||
player.alive = true
|
||
player.invulnTimer = 0 -- no respawn-invuln; the scene resets around the player
|
||
player.deathTimer = 0
|
||
player.blockedMessageTimer = 0
|
||
player.blockedJustTriggered = false
|
||
player._wasBlocked = false
|
||
end
|
||
|
||
function Player.get()
|
||
return player
|
||
end
|
||
|
||
function Player.update(dt, obstacles, leftTrack, rightTrack)
|
||
player.blockedMessageTimer = math.max(0, player.blockedMessageTimer - dt)
|
||
player._moving = false
|
||
|
||
if not player.alive then
|
||
player.deathTimer = player.deathTimer + dt
|
||
return
|
||
end
|
||
|
||
player.invulnTimer = math.max(0, player.invulnTimer - dt)
|
||
|
||
leftTrack = leftTrack or 0
|
||
rightTrack = rightTrack or 0
|
||
|
||
-- Rotation (opposing tracks): left track forward + right track back => turn right
|
||
local turn = (leftTrack - rightTrack) * 0.5
|
||
player.angle = player.angle + turn * ROT_SPEED * dt
|
||
|
||
-- Forward/reverse (average of tracks), with one-track-only speed bonus
|
||
local drive = (leftTrack + rightTrack) * 0.5
|
||
local leftActive = math.abs(leftTrack) > MOVE_EPSILON
|
||
local rightActive = math.abs(rightTrack) > MOVE_EPSILON
|
||
if (leftActive and not rightActive) or (rightActive and not leftActive) then
|
||
drive = drive * ONE_TRACK_BONUS
|
||
end
|
||
|
||
local blocked = false
|
||
if math.abs(drive) > MOVE_EPSILON then
|
||
player._moving = true
|
||
local speed = drive * MAX_SPEED
|
||
-- Forward vector: (sin(angle), cos(angle)); angle=0 → +z
|
||
local nx = player.x + math.sin(player.angle) * speed * dt
|
||
local nz = player.z + math.cos(player.angle) * speed * dt
|
||
nx, nz = World.wrapField(nx, nz)
|
||
|
||
if obstacles then
|
||
for _, o in ipairs(obstacles) do
|
||
local dx = World.wrappedDelta(nx, o.x, World.FIELD_SIZE)
|
||
local dz = World.wrappedDelta(nz, o.z, World.FIELD_SIZE)
|
||
local dist = math.sqrt(dx*dx + dz*dz)
|
||
if dist < COLLISION_RADIUS + o.radius then
|
||
blocked = true
|
||
break
|
||
end
|
||
end
|
||
end
|
||
|
||
if not blocked then
|
||
player.x = nx
|
||
player.z = nz
|
||
end
|
||
end
|
||
|
||
-- Fire the "MOTION BLOCKED" message once per sustained block, not every frame
|
||
if blocked and not player._wasBlocked then
|
||
player.blockedMessageTimer = BLOCKED_MSG_DURATION
|
||
player.blockedJustTriggered = true
|
||
end
|
||
player._wasBlocked = blocked
|
||
end
|
||
|
||
function Player.die()
|
||
player.alive = false
|
||
player.deathTimer = 0
|
||
end
|
||
|
||
function Player.respawn()
|
||
player.x = World.FIELD_SIZE / 2
|
||
player.z = World.FIELD_SIZE / 2
|
||
player.alive = true
|
||
player.invulnTimer = 0
|
||
player.deathTimer = 0
|
||
end
|
||
|
||
function Player.isMoving()
|
||
return player._moving == true
|
||
end
|
||
|
||
return Player
|