5 changed files with 1025 additions and 628 deletions
@ -0,0 +1,176 @@ |
|||||||
|
export class StickFigureRenderer { |
||||||
|
draw(player, ctx, time) { |
||||||
|
const headRadius = 10; |
||||||
|
const x = player.x; |
||||||
|
let y = player.y; |
||||||
|
|
||||||
|
const crouch = !!player.crouching; |
||||||
|
const bodyLength = crouch ? 20 : 30; |
||||||
|
const legLength = crouch ? 20 : 25; |
||||||
|
if (crouch) y += 15; // shift down a touch so crouch looks grounded
|
||||||
|
|
||||||
|
// Set drawing styles
|
||||||
|
ctx.strokeStyle = "black"; |
||||||
|
ctx.fillStyle = "black"; |
||||||
|
ctx.lineWidth = 2; |
||||||
|
|
||||||
|
// head
|
||||||
|
ctx.beginPath(); |
||||||
|
ctx.arc(x, y, headRadius, 0, Math.PI * 2); |
||||||
|
ctx.stroke(); |
||||||
|
|
||||||
|
// eye
|
||||||
|
ctx.beginPath(); |
||||||
|
ctx.arc(x + player.facing * 5, y - 3, 2, 0, Math.PI * 2); |
||||||
|
ctx.fill(); |
||||||
|
|
||||||
|
// body
|
||||||
|
const chestY = y + headRadius; |
||||||
|
const hipY = y + headRadius + bodyLength; |
||||||
|
ctx.beginPath(); |
||||||
|
ctx.moveTo(x, chestY); |
||||||
|
ctx.lineTo(x, hipY); |
||||||
|
ctx.stroke(); |
||||||
|
|
||||||
|
// arms
|
||||||
|
ctx.beginPath(); |
||||||
|
ctx.moveTo(x, y + 20); |
||||||
|
if (player.action === "punch" && player.hitFrame < 10) { |
||||||
|
ctx.lineTo(x + 30 * player.facing, y + 10); |
||||||
|
} else { |
||||||
|
ctx.lineTo(x + 20 * player.facing, y + 20); |
||||||
|
} |
||||||
|
ctx.moveTo(x, y + 20); |
||||||
|
ctx.lineTo(x - 20 * player.facing, y + 20); |
||||||
|
ctx.stroke(); |
||||||
|
|
||||||
|
// legs
|
||||||
|
ctx.beginPath(); |
||||||
|
if (player.vx !== 0) { |
||||||
|
const step = Math.sin(time * 0.75); |
||||||
|
const stride = 14; |
||||||
|
const lift = 0; |
||||||
|
|
||||||
|
// right leg
|
||||||
|
ctx.moveTo(x, hipY); |
||||||
|
ctx.lineTo(x + stride * step, hipY + legLength + (-lift * Math.abs(step))); |
||||||
|
|
||||||
|
// left leg
|
||||||
|
ctx.moveTo(x, hipY); |
||||||
|
ctx.lineTo(x - stride * step, hipY + legLength + (-lift * Math.abs(step))); |
||||||
|
} else { |
||||||
|
const idleSpread = 10; |
||||||
|
ctx.moveTo(x, hipY); |
||||||
|
ctx.lineTo(x + idleSpread, hipY + legLength); |
||||||
|
ctx.moveTo(x, hipY); |
||||||
|
ctx.lineTo(x - idleSpread, hipY + legLength); |
||||||
|
} |
||||||
|
ctx.stroke(); |
||||||
|
ctx.strokeStyle = "black"; |
||||||
|
|
||||||
|
// speech bubble
|
||||||
|
if (player.talking) { |
||||||
|
const padding = 12; |
||||||
|
const maxMessageWidth = 100; |
||||||
|
|
||||||
|
// set font BEFORE measuring
|
||||||
|
ctx.font = "12px sans-serif"; |
||||||
|
|
||||||
|
const lines = this._wrapText(ctx, player.message, maxMessageWidth); |
||||||
|
const textWidth = ctx.measureText(player.message).width; |
||||||
|
const widths = lines.map(line => ctx.measureText(line).width); |
||||||
|
const contentWidth = Math.max(...widths, textWidth); |
||||||
|
const bubbleWidth = Math.min(contentWidth + padding + 10, maxMessageWidth + padding + 10); |
||||||
|
const lineHeight = 14; |
||||||
|
const bubbleHeight = lines.length * lineHeight + padding; |
||||||
|
const bubbleX = x - bubbleWidth - 15; |
||||||
|
const bubbleY = y - 20; |
||||||
|
|
||||||
|
ctx.beginPath(); |
||||||
|
ctx.fillStyle = "white"; |
||||||
|
ctx.strokeStyle = "black"; |
||||||
|
if (typeof ctx.roundRect === "function") { |
||||||
|
ctx.roundRect(bubbleX, bubbleY, bubbleWidth, bubbleHeight, 4); |
||||||
|
} else { |
||||||
|
ctx.rect(bubbleX, bubbleY, bubbleWidth, bubbleHeight); |
||||||
|
} |
||||||
|
ctx.fill(); |
||||||
|
ctx.stroke(); |
||||||
|
|
||||||
|
ctx.fillStyle = "black"; |
||||||
|
for (let i = 0; i < lines.length; i++) { |
||||||
|
ctx.fillText(lines[i], bubbleX + padding, bubbleY + padding + i * lineHeight); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
let nameY = y - 80; |
||||||
|
if (player.isAlive) { |
||||||
|
nameY = this._drawHealthBar(ctx, player) |
||||||
|
} |
||||||
|
|
||||||
|
// name tag
|
||||||
|
ctx.save(); |
||||||
|
ctx.font = "10px sans-serif"; |
||||||
|
ctx.textAlign = "center"; |
||||||
|
ctx.textBaseline = "bottom"; |
||||||
|
ctx.fillStyle = "black"; |
||||||
|
ctx.fillText(player.id, x, nameY); |
||||||
|
ctx.restore(); |
||||||
|
} |
||||||
|
|
||||||
|
_drawHealthBar(ctx, player) { |
||||||
|
const x = player.x; |
||||||
|
const headCenterY = player.y; |
||||||
|
const topOfHead = headCenterY - 10; |
||||||
|
const barWidth = 40; |
||||||
|
const barHeight = 5; |
||||||
|
|
||||||
|
const barX = x - barWidth / 2; |
||||||
|
const barY = topOfHead - 10; |
||||||
|
|
||||||
|
ctx.fillStyle = "#ddd"; |
||||||
|
ctx.fillRect(barX, barY, barWidth, barHeight); |
||||||
|
|
||||||
|
const pct = Math.max(0, Math.min(1, player.hp / player.maxHp)); |
||||||
|
let color = '#27ae60'; |
||||||
|
|
||||||
|
if (pct <= 0.33) { |
||||||
|
color = '#e74c3c'; |
||||||
|
} else if (pct <= 0.66) { |
||||||
|
color = '#f1c40f'; |
||||||
|
} |
||||||
|
|
||||||
|
ctx.fillStyle = color; |
||||||
|
ctx.fillRect(barX, barY, barWidth * pct, barHeight); |
||||||
|
|
||||||
|
|
||||||
|
ctx.strokeStyle = '#000'; |
||||||
|
ctx.lineWidth = 1; |
||||||
|
ctx.strokeRect(barX, barY, barWidth, barHeight); |
||||||
|
|
||||||
|
return barY; |
||||||
|
} |
||||||
|
|
||||||
|
_wrapText(ctx, text, maxWidth) { |
||||||
|
const words = text.split(" "); |
||||||
|
const lines = []; |
||||||
|
let line = ""; |
||||||
|
|
||||||
|
for (let i = 0; i < words.length; i++) { |
||||||
|
const testLine = line + words[i] + " "; |
||||||
|
const testWidth = ctx.measureText(testLine).width; |
||||||
|
|
||||||
|
if (testWidth > maxWidth && i > 0) { |
||||||
|
lines.push(line.trimEnd()); |
||||||
|
line = words[i] + " "; |
||||||
|
} else { |
||||||
|
line = testLine; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
lines.push(line.trim()); |
||||||
|
return lines; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
@ -0,0 +1,259 @@ |
|||||||
|
class Command { |
||||||
|
execute() { } |
||||||
|
toNetwork() { |
||||||
|
return null; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class PunchCmd extends Command { |
||||||
|
execute(p) { |
||||||
|
p.punch(); |
||||||
|
} |
||||||
|
toNetwork(p) { |
||||||
|
return { |
||||||
|
Id: p.id, |
||||||
|
Action: ' ', |
||||||
|
ActionType: "keyDown" |
||||||
|
}; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class JumpCmd extends Command { |
||||||
|
execute(p) { |
||||||
|
p.jump(); |
||||||
|
} |
||||||
|
toNetwork(p) { |
||||||
|
return { |
||||||
|
Id: p.id, |
||||||
|
Action: 'w', |
||||||
|
ActionType: 'keyDown' |
||||||
|
}; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class CrouchPressCmd extends Command { |
||||||
|
execute(p) { |
||||||
|
p.crouching = true; |
||||||
|
} |
||||||
|
toNetwork(p) { |
||||||
|
return { |
||||||
|
Id: p.id, |
||||||
|
Action: 's', |
||||||
|
ActionType: 'keyDown' |
||||||
|
}; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class CrouchUnpressCmd extends Command { |
||||||
|
execute(p) { |
||||||
|
p.crouching = false; |
||||||
|
} |
||||||
|
toNetwork(p) { |
||||||
|
return { |
||||||
|
Id: p.id, |
||||||
|
Action: 's', |
||||||
|
ActionType: 'keyUp' |
||||||
|
}; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class MoveLeftPressCmd extends Command { |
||||||
|
execute(p) { |
||||||
|
p.keys.a = true; |
||||||
|
} |
||||||
|
toNetwork(p) { |
||||||
|
return { |
||||||
|
Id: p.id, |
||||||
|
Action: 'a', |
||||||
|
ActionType: 'keyDown' |
||||||
|
}; |
||||||
|
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class MoveLeftUnpressCmd extends Command { |
||||||
|
execute(p) { |
||||||
|
p.keys.a = false; |
||||||
|
} |
||||||
|
toNetwork(p) { |
||||||
|
return { |
||||||
|
Id: p.id, |
||||||
|
Action: 'a', |
||||||
|
ActionType: 'keyUp' |
||||||
|
}; |
||||||
|
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class MoveRightPressCmd extends Command { |
||||||
|
execute(p) { |
||||||
|
p.keys.d = true; |
||||||
|
} |
||||||
|
toNetwork(p) { |
||||||
|
return { |
||||||
|
Id: p.id, |
||||||
|
Action: 'd', |
||||||
|
ActionType: 'keyDown' |
||||||
|
}; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class MoveRightUnpressCmd extends Command { |
||||||
|
execute(p) { |
||||||
|
p.keys.d = false; |
||||||
|
} |
||||||
|
toNetwork(p) { |
||||||
|
return { |
||||||
|
Id: p.id, |
||||||
|
Action: 'd', |
||||||
|
ActionType: 'keyUp' |
||||||
|
}; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class TalkCmd extends Command { |
||||||
|
constructor(message) { |
||||||
|
super(); |
||||||
|
this.message = message; |
||||||
|
} |
||||||
|
execute(p) { |
||||||
|
p.talk(this.message); |
||||||
|
} |
||||||
|
toNetwork(p) { |
||||||
|
return { |
||||||
|
Id: p.id, |
||||||
|
Action: 't', |
||||||
|
ActionType: '', |
||||||
|
Message: this.message |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class DisconnectedCmd extends Command { |
||||||
|
execute() { } |
||||||
|
toNetwork(p) { |
||||||
|
return { |
||||||
|
Id: p.id, |
||||||
|
Action: "disconnected", |
||||||
|
ActionType: "" |
||||||
|
}; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export class InputHandler { |
||||||
|
constructor(game) { |
||||||
|
this.game = game; |
||||||
|
|
||||||
|
this.downMap = new Map([ |
||||||
|
[' ', () => new PunchCmd()], |
||||||
|
['w', () => new JumpCmd()], |
||||||
|
['s', () => new CrouchPressCmd()], |
||||||
|
['a', () => new MoveLeftPressCmd()], |
||||||
|
['d', () => new MoveRightPressCmd()], |
||||||
|
]); |
||||||
|
|
||||||
|
this.upMap = new Map([ |
||||||
|
['s', () => new CrouchUnpressCmd()], |
||||||
|
['a', () => new MoveLeftUnpressCmd()], |
||||||
|
['d', () => new MoveRightUnpressCmd()], |
||||||
|
]); |
||||||
|
} |
||||||
|
|
||||||
|
handleKeyDown(e, player) { |
||||||
|
const key = e.code === 'Space' ? ' ' : (e.key || '').toLowerCase(); |
||||||
|
const k = this.downMap.get(key); |
||||||
|
if (!k) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
e.preventDefault(); |
||||||
|
e.stopPropagation(); |
||||||
|
this.dispatch(k(), player) |
||||||
|
} |
||||||
|
|
||||||
|
handleKeyUp(e, player) { |
||||||
|
const key = e.code === "Space" ? ' ' : e.key; |
||||||
|
const k = this.upMap.get(key); |
||||||
|
if (!k) { |
||||||
|
return; |
||||||
|
} |
||||||
|
e.preventDefault(); |
||||||
|
e.stopPropagation(); |
||||||
|
this.dispatch(k(), player) |
||||||
|
} |
||||||
|
|
||||||
|
dispatch(cmd, player) { |
||||||
|
if (!player) return; |
||||||
|
cmd.execute(player, this.game); |
||||||
|
if (player.isLocal) { |
||||||
|
const msg = cmd.toNetwork(player); |
||||||
|
if (msg) { |
||||||
|
this.game._send({ |
||||||
|
...msg, |
||||||
|
PosX: Math.floor(player.x), |
||||||
|
PosY: Math.floor(player.y), |
||||||
|
Facing: player.facing, |
||||||
|
Ts: Date.now(), |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
handleNetworkMessage(data) { |
||||||
|
this.game._ensurePlayer(data.Id); |
||||||
|
const p = this.game.players?.[data.Id]; |
||||||
|
if (!p) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (typeof data.PosX === "number") { |
||||||
|
p._lastTx = p._tx ?? data.PosX; |
||||||
|
p._tx = data.PosX; |
||||||
|
} |
||||||
|
if (typeof data.PosY === "number") { |
||||||
|
p._lastTy = p._ty ?? data.PosY; |
||||||
|
p._ty = data.PosY; |
||||||
|
} |
||||||
|
if (typeof data.Facing === "number") { |
||||||
|
p._tFacing = data.Facing; |
||||||
|
} |
||||||
|
|
||||||
|
p._lastUpdateTime = Date.now(); |
||||||
|
|
||||||
|
const action = data.Action; |
||||||
|
const actionType = data.ActionType; |
||||||
|
|
||||||
|
const apply = (cmd) => cmd && cmd.execute(p, this.game); |
||||||
|
|
||||||
|
switch (action) { |
||||||
|
case 'disconnected': |
||||||
|
delete this.game.players[data.Id]; |
||||||
|
break; |
||||||
|
case 't': |
||||||
|
apply(new TalkCmd(data.Message ?? "")); |
||||||
|
break; |
||||||
|
case ' ': |
||||||
|
apply(new PunchCmd()); |
||||||
|
break; |
||||||
|
case 'w': |
||||||
|
apply(new JumpCmd()); |
||||||
|
break; |
||||||
|
case 's': |
||||||
|
apply(actionType === 'keyDown' ? new CrouchPressCmd() : new CrouchUnpressCmd()); |
||||||
|
break; |
||||||
|
case 'a': |
||||||
|
apply(actionType === 'keyDown' ? new MoveLeftPressCmd() : new MoveLeftUnpressCmd()); |
||||||
|
break; |
||||||
|
case 'd': |
||||||
|
apply(actionType === 'keyDown' ? new MoveRightPressCmd() : new MoveRightUnpressCmd()); |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
releaseAll(player) { |
||||||
|
const cmds = []; |
||||||
|
if (player.keys?.a) cmds.push(new MoveLeftUnpressCmd()); |
||||||
|
if (player.keys?.d) cmds.push(new MoveRightUnpressCmd()); |
||||||
|
if (player.crouching) cmds.push(new CrouchUnpressCmd()); |
||||||
|
for (const c of cmds) this.dispatch(c, player); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,44 @@ |
|||||||
|
export class PhysicsSystems { |
||||||
|
resolveCollisions(player, platforms, groundY) { |
||||||
|
const totalHeight = 60; |
||||||
|
const halfFootW = 12; |
||||||
|
const EPS = 0.5; |
||||||
|
|
||||||
|
if (player._prevY === undefined) { |
||||||
|
player._prevY = player.y; |
||||||
|
} |
||||||
|
|
||||||
|
let landedOnPlatform = false; |
||||||
|
|
||||||
|
// one-way platforms (land only when falling and crossing from above)
|
||||||
|
if (player.vy >= 0) { |
||||||
|
for (const pf of platforms) { |
||||||
|
const feetLeft = player.x - halfFootW; |
||||||
|
const feetRight = player.x + halfFootW; |
||||||
|
const pfLeft = pf.x, pfRight = pf.x + pf.w; |
||||||
|
|
||||||
|
const horizOverlap = feetRight > pfLeft && feetLeft < pfRight; |
||||||
|
const wasAbove = (player._prevY + totalHeight) <= pf.y + EPS; |
||||||
|
const nowBelowTop = (player.y + totalHeight) >= pf.y - EPS; |
||||||
|
const dropping = player.crouching === true; // hold 's' to drop through
|
||||||
|
|
||||||
|
if (horizOverlap && wasAbove && nowBelowTop && !dropping) { |
||||||
|
player.y = pf.y - totalHeight; |
||||||
|
player.vy = 0; |
||||||
|
landedOnPlatform = true; |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
// --- ground after platforms ---
|
||||||
|
if (!landedOnPlatform && player.y + totalHeight >= groundY) { |
||||||
|
player.y = groundY - totalHeight; |
||||||
|
player.vy = 0; |
||||||
|
player.onGround = true; |
||||||
|
} else if (landedOnPlatform) { |
||||||
|
player.onGround = true; |
||||||
|
} else { |
||||||
|
player.onGround = false; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,193 @@ |
|||||||
|
export class StickFigure { |
||||||
|
constructor(x, facing, id, canvas) { |
||||||
|
this.canvas = canvas; |
||||||
|
this.id = id; |
||||||
|
this.x = x; |
||||||
|
this.y = canvas.height - 60; |
||||||
|
this.facing = facing; |
||||||
|
this._lastTx = x; |
||||||
|
this._lastTy = this.y; |
||||||
|
this._lastUpdateTime = 0; |
||||||
|
this._walkTTL = 0; |
||||||
|
this.action = "idle"; |
||||||
|
this.hitFrame = 0; |
||||||
|
this.speed = 2; |
||||||
|
this.vx = 0; |
||||||
|
this.vy = 0; |
||||||
|
this.gravity = 0.5; |
||||||
|
this.jumpStrength = -10; |
||||||
|
this.onGround = true; |
||||||
|
this.talking = false; |
||||||
|
this.talkTimer = 0; |
||||||
|
this.message = ""; |
||||||
|
this.keys = { |
||||||
|
a: false, |
||||||
|
d: false, |
||||||
|
} |
||||||
|
this.crouching = false; |
||||||
|
this.isPunching = false; |
||||||
|
this.punchHasHit = false; |
||||||
|
this.sentJoin = false; |
||||||
|
this.hp = 100; |
||||||
|
this.maxHp = 100; |
||||||
|
this.isAlive = true; |
||||||
|
this._tx = x; |
||||||
|
this._ty = canvas.height - 60; |
||||||
|
this._lastTs = 0; |
||||||
|
this._tFacing = undefined; |
||||||
|
this._lastUpdateTime = Date.now(); |
||||||
|
this._updateInterval = 10; |
||||||
|
this.baseAttackPower = 14; |
||||||
|
this.baseDefense = 14; |
||||||
|
} |
||||||
|
|
||||||
|
update() { |
||||||
|
const prevX = this.x; |
||||||
|
this._prevY = this.y; |
||||||
|
this.vx = 0; |
||||||
|
|
||||||
|
if (this.keys.a) { |
||||||
|
this.vx = -this.speed; |
||||||
|
this.facing = -1; |
||||||
|
} |
||||||
|
|
||||||
|
if (this.crouching && this.keys.d) { |
||||||
|
this.speed = 0.5; |
||||||
|
this.vx = this.speed; |
||||||
|
} |
||||||
|
if (this.crouching && this.keys.a) { |
||||||
|
this.vx = -this.speed; |
||||||
|
} |
||||||
|
|
||||||
|
if (this.keys.d) { |
||||||
|
this.vx = this.speed; |
||||||
|
this.facing = 1; |
||||||
|
} |
||||||
|
|
||||||
|
this.x += this.vx; |
||||||
|
|
||||||
|
if (this.isLocal) { |
||||||
|
this.vy += this.gravity; |
||||||
|
this.y += this.vy; |
||||||
|
} |
||||||
|
|
||||||
|
this.x = Math.max(20, Math.min(this.canvas.width - 20, this.x)); |
||||||
|
|
||||||
|
if (this.action === "punch") { |
||||||
|
this.hitFrame++; |
||||||
|
if (this.hitFrame > 15) { |
||||||
|
this.action = 'idle'; |
||||||
|
this.hitFrame = 0; |
||||||
|
this.isPunching = false; |
||||||
|
this.punchHasHit = false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (this.talking) { |
||||||
|
this.talkTimer--; |
||||||
|
if (this.talkTimer <= 0) { |
||||||
|
this.talking = false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (!this.isLocal |
||||||
|
&& typeof this._tx === "number" |
||||||
|
&& typeof this._ty === "number") { |
||||||
|
const now = Date.now(); |
||||||
|
const timeSinceUpdate = now - this._lastUpdateTime; |
||||||
|
|
||||||
|
const targetLerpTime = 200; |
||||||
|
const deltaTime = 16.67; |
||||||
|
const lerpFactor = Math.min(1, deltaTime / targetLerpTime); |
||||||
|
const stalenessFactor = Math.min(2, timeSinceUpdate / this._updateInterval); |
||||||
|
const adjustedLerpFactor = lerpFactor * stalenessFactor; |
||||||
|
|
||||||
|
const deltaY = Math.abs(this.y - this._ty); |
||||||
|
|
||||||
|
if (deltaY > 8 || !this.onGround) { |
||||||
|
this.x = this._tx; |
||||||
|
this.y = this._ty; |
||||||
|
} else { |
||||||
|
this.x = this.lerp(this.x, this._tx, adjustedLerpFactor); |
||||||
|
this.y = this.lerp(this.y, this._ty, adjustedLerpFactor); |
||||||
|
} |
||||||
|
this.facing = this._tFacing ?? this.facing; |
||||||
|
|
||||||
|
this.vx = this.x - prevX; |
||||||
|
if (Math.abs(this.vx) < 0.5) { |
||||||
|
this.vx = 0; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
lerp(a, b, t) { |
||||||
|
return a + (b - a) * Math.min(1, t); |
||||||
|
} |
||||||
|
|
||||||
|
applyBuff(type, amount) { |
||||||
|
if (type === 'atk') { |
||||||
|
this.baseAttackPower += amount; |
||||||
|
} |
||||||
|
|
||||||
|
if (type === 'def') { |
||||||
|
this.baseDefense += amount; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
punch() { |
||||||
|
this.isPunching = true; |
||||||
|
this.punchHasHit = false; |
||||||
|
|
||||||
|
if (this.action === 'idle') { |
||||||
|
this.action = 'punch'; |
||||||
|
this.hitFrame = 0; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
talk(message) { |
||||||
|
this.talking = true; |
||||||
|
this.talkTimer = 120; |
||||||
|
this.message = message; |
||||||
|
} |
||||||
|
|
||||||
|
jump() { |
||||||
|
if (this.onGround) { |
||||||
|
this.vy = this.jumpStrength; |
||||||
|
this.onGround = false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
getBodyHitbox() { |
||||||
|
const w = 24; |
||||||
|
const h = 60 - (this.crouching ? 10 : 0); |
||||||
|
return { |
||||||
|
x: this.x - w / 2, |
||||||
|
y: this.y - 10, |
||||||
|
w, |
||||||
|
h |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
getPunchHitbox() { |
||||||
|
if (this.action === 'punch' && this.hitFrame < 10) { |
||||||
|
const w = 18; |
||||||
|
const h = 14; |
||||||
|
const frontX = this.x + this.facing * 22; |
||||||
|
const x = this.facing === 1 ? frontX : frontX - w; |
||||||
|
const y = this.y + (this.crouching ? 18 : 10); |
||||||
|
return { |
||||||
|
x, |
||||||
|
y, |
||||||
|
w, |
||||||
|
h |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
Loading…
Reference in new issue