Fix enemy facing direction and add player damage feedback
This commit is contained in:
@@ -1,7 +1,10 @@
|
|||||||
# ── Wackelpeter container build/deploy ──────────────────────────────────────
|
# ── Wackelpeter build/deploy (podman) ───────────────────────────────────────
|
||||||
# Uses Podman (docker-compose optional). Targets:
|
# Targets:
|
||||||
# make build – Build image
|
# make – Build web frontend locally (npm run build)
|
||||||
# make push – Push image to Gitea registry
|
# make web – Same as `make`
|
||||||
|
# make dev – Start Vite dev server
|
||||||
|
# make build – Build container image (podman)
|
||||||
|
# make push – Build & push image to registry (podman)
|
||||||
# make up/down – Start/stop via docker-compose
|
# make up/down – Start/stop via docker-compose
|
||||||
# make run – Start via podman directly
|
# make run – Start via podman directly
|
||||||
# make clean – Remove local image
|
# make clean – Remove local image
|
||||||
@@ -11,7 +14,14 @@ IMAGE ?= $(REGISTRY)/nico/wackelpeter
|
|||||||
TAG ?= latest
|
TAG ?= latest
|
||||||
COMPOSE ?= docker-compose
|
COMPOSE ?= docker-compose
|
||||||
|
|
||||||
.PHONY: build push up down run clean login
|
.PHONY: web dev build push up down run clean login
|
||||||
|
|
||||||
|
web:
|
||||||
|
npm --prefix web install
|
||||||
|
npm --prefix web run build
|
||||||
|
|
||||||
|
dev:
|
||||||
|
npm --prefix web run dev
|
||||||
|
|
||||||
build:
|
build:
|
||||||
podman build -t $(IMAGE):$(TAG) .
|
podman build -t $(IMAGE):$(TAG) .
|
||||||
|
|||||||
@@ -15,6 +15,28 @@
|
|||||||
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
||||||
pointer-events: none; font-family: monospace; z-index: 10;
|
pointer-events: none; font-family: monospace; z-index: 10;
|
||||||
}
|
}
|
||||||
|
#damage-vignette {
|
||||||
|
position: absolute; inset: 0; opacity: 0; pointer-events: none;
|
||||||
|
background: radial-gradient(ellipse at center,
|
||||||
|
rgba(255,0,0,0) 40%, rgba(255,0,0,0.35) 100%);
|
||||||
|
box-shadow: inset 0 0 120px 40px rgba(255,0,0,0.5);
|
||||||
|
}
|
||||||
|
#damage-vignette.hurt { animation: hurt-flash 0.45s ease-out; }
|
||||||
|
@keyframes hurt-flash {
|
||||||
|
0% { opacity: 1; }
|
||||||
|
100% { opacity: 0; }
|
||||||
|
}
|
||||||
|
#low-hp-vignette {
|
||||||
|
position: absolute; inset: 0; opacity: 0; pointer-events: none;
|
||||||
|
background: radial-gradient(ellipse at center,
|
||||||
|
rgba(255,0,0,0.05) 0%, rgba(255,0,0,0.15) 55%, rgba(220,0,0,0.7) 100%);
|
||||||
|
box-shadow: inset 0 0 160px 60px rgba(220,0,0,0.55);
|
||||||
|
}
|
||||||
|
#low-hp-vignette.active { animation: low-hp-pulse 1.1s ease-in-out infinite; }
|
||||||
|
@keyframes low-hp-pulse {
|
||||||
|
0%, 100% { opacity: 0.25; }
|
||||||
|
50% { opacity: 0.6; }
|
||||||
|
}
|
||||||
#health-bar-bg {
|
#health-bar-bg {
|
||||||
position: absolute; top: 16px; left: 16px; width: 250px; height: 20px;
|
position: absolute; top: 16px; left: 16px; width: 250px; height: 20px;
|
||||||
background: rgba(0,0,0,0.6); border: 2px solid #555;
|
background: rgba(0,0,0,0.6); border: 2px solid #555;
|
||||||
@@ -199,6 +221,8 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="ui-overlay">
|
<div id="ui-overlay">
|
||||||
|
<div id="damage-vignette"></div>
|
||||||
|
<div id="low-hp-vignette"></div>
|
||||||
<div id="health-bar-bg"></div>
|
<div id="health-bar-bg"></div>
|
||||||
<div id="health-bar"></div>
|
<div id="health-bar"></div>
|
||||||
<div id="health-text">100</div>
|
<div id="health-text">100</div>
|
||||||
|
|||||||
+4
-4
@@ -28,15 +28,15 @@ export class Bat extends Enemy {
|
|||||||
this.registerFadeMesh(bodyMesh);
|
this.registerFadeMesh(bodyMesh);
|
||||||
this.model.add(bodyMesh);
|
this.model.add(bodyMesh);
|
||||||
|
|
||||||
// Head with ears (face toward the player: -Z)
|
// Head with ears (face toward the player: +Z)
|
||||||
const head = new THREE.Mesh(new THREE.SphereGeometry(0.18, 12, 10), furMat);
|
const head = new THREE.Mesh(new THREE.SphereGeometry(0.18, 12, 10), furMat);
|
||||||
head.position.set(0, 0.15, -0.3);
|
head.position.set(0, 0.15, 0.3);
|
||||||
this.registerFadeMesh(head);
|
this.registerFadeMesh(head);
|
||||||
this.model.add(head);
|
this.model.add(head);
|
||||||
|
|
||||||
for (const side of [-1, 1]) {
|
for (const side of [-1, 1]) {
|
||||||
const ear = new THREE.Mesh(new THREE.ConeGeometry(0.07, 0.18, 6), furMat);
|
const ear = new THREE.Mesh(new THREE.ConeGeometry(0.07, 0.18, 6), furMat);
|
||||||
ear.position.set(side * 0.12, 0.32, -0.25);
|
ear.position.set(side * 0.12, 0.32, 0.25);
|
||||||
this.registerFadeMesh(ear);
|
this.registerFadeMesh(ear);
|
||||||
this.model.add(ear);
|
this.model.add(ear);
|
||||||
}
|
}
|
||||||
@@ -45,7 +45,7 @@ export class Bat extends Enemy {
|
|||||||
const eyeMat = new THREE.MeshToonMaterial({ color: 0xff2222 });
|
const eyeMat = new THREE.MeshToonMaterial({ color: 0xff2222 });
|
||||||
for (const side of [-1, 1]) {
|
for (const side of [-1, 1]) {
|
||||||
const eye = new THREE.Mesh(new THREE.SphereGeometry(0.035, 8, 8), eyeMat);
|
const eye = new THREE.Mesh(new THREE.SphereGeometry(0.035, 8, 8), eyeMat);
|
||||||
eye.position.set(side * 0.07, 0.16, -0.42);
|
eye.position.set(side * 0.07, 0.16, 0.42);
|
||||||
this.registerFadeMesh(eye);
|
this.registerFadeMesh(eye);
|
||||||
this.model.add(eye);
|
this.model.add(eye);
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -53,11 +53,11 @@ export class Brute extends Enemy {
|
|||||||
this.registerFadeMesh(head);
|
this.registerFadeMesh(head);
|
||||||
this.model.add(head);
|
this.model.add(head);
|
||||||
|
|
||||||
// Glowing eyes (face toward the player: -Z)
|
// Glowing eyes (face toward the player: +Z)
|
||||||
const eyeMat = new THREE.MeshBasicMaterial({ color: 0xff6622 });
|
const eyeMat = new THREE.MeshBasicMaterial({ color: 0xff6622 });
|
||||||
for (const side of [-1, 1]) {
|
for (const side of [-1, 1]) {
|
||||||
const eye = new THREE.Mesh(new THREE.BoxGeometry(0.14, 0.1, 0.05), eyeMat);
|
const eye = new THREE.Mesh(new THREE.BoxGeometry(0.14, 0.1, 0.05), eyeMat);
|
||||||
eye.position.set(side * 0.16, 1.9, -0.32);
|
eye.position.set(side * 0.16, 1.9, 0.32);
|
||||||
this.registerFadeMesh(eye);
|
this.registerFadeMesh(eye);
|
||||||
this.model.add(eye);
|
this.model.add(eye);
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-6
@@ -40,16 +40,16 @@ export class Crab extends Enemy {
|
|||||||
this.model.add(spot);
|
this.model.add(spot);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Eye stalks (on the armored front side, -Z)
|
// Eye stalks (on the armored front side, +Z)
|
||||||
const stalkMat = new THREE.MeshToonMaterial({ color: 0xdd5533 });
|
const stalkMat = new THREE.MeshToonMaterial({ color: 0xdd5533 });
|
||||||
const eyeMat = new THREE.MeshToonMaterial({ color: 0x111122 });
|
const eyeMat = new THREE.MeshToonMaterial({ color: 0x111122 });
|
||||||
for (const side of [-1, 1]) {
|
for (const side of [-1, 1]) {
|
||||||
const stalk = new THREE.Mesh(new THREE.CylinderGeometry(0.035, 0.035, 0.22, 6), stalkMat);
|
const stalk = new THREE.Mesh(new THREE.CylinderGeometry(0.035, 0.035, 0.22, 6), stalkMat);
|
||||||
stalk.position.set(side * 0.22, 0.55, -0.42);
|
stalk.position.set(side * 0.22, 0.55, 0.42);
|
||||||
this.registerFadeMesh(stalk);
|
this.registerFadeMesh(stalk);
|
||||||
this.model.add(stalk);
|
this.model.add(stalk);
|
||||||
const eye = new THREE.Mesh(new THREE.SphereGeometry(0.09, 10, 8), eyeMat);
|
const eye = new THREE.Mesh(new THREE.SphereGeometry(0.09, 10, 8), eyeMat);
|
||||||
eye.position.set(side * 0.22, 0.68, -0.42);
|
eye.position.set(side * 0.22, 0.68, 0.42);
|
||||||
this.registerFadeMesh(eye);
|
this.registerFadeMesh(eye);
|
||||||
this.model.add(eye);
|
this.model.add(eye);
|
||||||
}
|
}
|
||||||
@@ -70,8 +70,8 @@ export class Crab extends Enemy {
|
|||||||
const clawGeom = new THREE.SphereGeometry(0.22, 12, 10);
|
const clawGeom = new THREE.SphereGeometry(0.22, 12, 10);
|
||||||
this.clawL = new THREE.Mesh(clawGeom, shellMat);
|
this.clawL = new THREE.Mesh(clawGeom, shellMat);
|
||||||
this.clawR = new THREE.Mesh(clawGeom, shellMat);
|
this.clawR = new THREE.Mesh(clawGeom, shellMat);
|
||||||
this.clawL.position.set(-0.45, 0.45, -0.5);
|
this.clawL.position.set(-0.45, 0.45, 0.5);
|
||||||
this.clawR.position.set(0.45, 0.45, -0.5);
|
this.clawR.position.set(0.45, 0.45, 0.5);
|
||||||
this.clawL.scale.set(1.2, 0.7, 1.4);
|
this.clawL.scale.set(1.2, 0.7, 1.4);
|
||||||
this.clawR.scale.set(1.2, 0.7, 1.4);
|
this.clawR.scale.set(1.2, 0.7, 1.4);
|
||||||
this.clawL.castShadow = true;
|
this.clawL.castShadow = true;
|
||||||
@@ -144,7 +144,7 @@ export class Crab extends Enemy {
|
|||||||
) {
|
) {
|
||||||
// Armored from the front: reduce damage coming from the facing side
|
// Armored from the front: reduce damage coming from the facing side
|
||||||
if (sourcePos) {
|
if (sourcePos) {
|
||||||
const front = new THREE.Vector3(0, 0, -1)
|
const front = new THREE.Vector3(0, 0, 1)
|
||||||
.applyQuaternion(this.body.quaternion);
|
.applyQuaternion(this.body.quaternion);
|
||||||
front.y = 0;
|
front.y = 0;
|
||||||
front.normalize();
|
front.normalize();
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export class EnemyProjectile {
|
|||||||
if (game.player.health > 0) {
|
if (game.player.health > 0) {
|
||||||
const dist = this.body.position.distanceTo(game.player.body.position);
|
const dist = this.body.position.distanceTo(game.player.body.position);
|
||||||
if (dist < HIT_RADIUS) {
|
if (dist < HIT_RADIUS) {
|
||||||
game.player.damage(this.damage);
|
game.player.damage(this.damage, this.body.position);
|
||||||
this.dead = true;
|
this.dead = true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-6
@@ -52,18 +52,18 @@ export class Frog extends Enemy {
|
|||||||
// Belly
|
// Belly
|
||||||
const belly = new THREE.Mesh(new THREE.SphereGeometry(0.4, 16, 12), this.whiteMat);
|
const belly = new THREE.Mesh(new THREE.SphereGeometry(0.4, 16, 12), this.whiteMat);
|
||||||
belly.scale.set(0.85, 0.5, 0.85);
|
belly.scale.set(0.85, 0.5, 0.85);
|
||||||
belly.position.set(0, 0.3, -0.42);
|
belly.position.set(0, 0.3, 0.42);
|
||||||
this.registerFadeMesh(belly);
|
this.registerFadeMesh(belly);
|
||||||
this.model.add(belly);
|
this.model.add(belly);
|
||||||
|
|
||||||
// Bulging eyes (face toward the player: -Z)
|
// Bulging eyes (face toward the player: +Z)
|
||||||
const eyeGeom = new THREE.SphereGeometry(0.17, 14, 12);
|
const eyeGeom = new THREE.SphereGeometry(0.17, 14, 12);
|
||||||
const pupilGeom = new THREE.SphereGeometry(0.08, 10, 8);
|
const pupilGeom = new THREE.SphereGeometry(0.08, 10, 8);
|
||||||
const pupilMat = new THREE.MeshToonMaterial({ color: 0x111122 });
|
const pupilMat = new THREE.MeshToonMaterial({ color: 0x111122 });
|
||||||
this.eyeL = new THREE.Mesh(eyeGeom, this.whiteMat);
|
this.eyeL = new THREE.Mesh(eyeGeom, this.whiteMat);
|
||||||
this.eyeR = new THREE.Mesh(eyeGeom, this.whiteMat);
|
this.eyeR = new THREE.Mesh(eyeGeom, this.whiteMat);
|
||||||
this.eyeL.position.set(-0.24, 0.72, -0.28);
|
this.eyeL.position.set(-0.24, 0.72, 0.28);
|
||||||
this.eyeR.position.set(0.24, 0.72, -0.28);
|
this.eyeR.position.set(0.24, 0.72, 0.28);
|
||||||
this.eyeL.castShadow = true;
|
this.eyeL.castShadow = true;
|
||||||
this.eyeR.castShadow = true;
|
this.eyeR.castShadow = true;
|
||||||
this.registerFadeMesh(this.eyeL);
|
this.registerFadeMesh(this.eyeL);
|
||||||
@@ -72,7 +72,7 @@ export class Frog extends Enemy {
|
|||||||
|
|
||||||
for (const eye of [this.eyeL, this.eyeR]) {
|
for (const eye of [this.eyeL, this.eyeR]) {
|
||||||
const pupil = new THREE.Mesh(pupilGeom, pupilMat);
|
const pupil = new THREE.Mesh(pupilGeom, pupilMat);
|
||||||
pupil.position.set(0, 0.02, -0.12);
|
pupil.position.set(0, 0.02, 0.12);
|
||||||
this.registerFadeMesh(pupil);
|
this.registerFadeMesh(pupil);
|
||||||
eye.add(pupil);
|
eye.add(pupil);
|
||||||
}
|
}
|
||||||
@@ -82,7 +82,7 @@ export class Frog extends Enemy {
|
|||||||
for (const side of [-1, 1]) {
|
for (const side of [-1, 1]) {
|
||||||
const thigh = new THREE.Mesh(legGeom, greenMat);
|
const thigh = new THREE.Mesh(legGeom, greenMat);
|
||||||
thigh.scale.set(0.8, 1, 1.2);
|
thigh.scale.set(0.8, 1, 1.2);
|
||||||
thigh.position.set(side * 0.42, 0.25, 0.35);
|
thigh.position.set(side * 0.42, 0.25, -0.35);
|
||||||
thigh.rotation.y = side * 0.5;
|
thigh.rotation.y = side * 0.5;
|
||||||
thigh.castShadow = true;
|
thigh.castShadow = true;
|
||||||
this.registerFadeMesh(thigh);
|
this.registerFadeMesh(thigh);
|
||||||
|
|||||||
+79
-1
@@ -96,8 +96,17 @@ export class Game {
|
|||||||
private uiOfferIcons: HTMLElement[] = [];
|
private uiOfferIcons: HTMLElement[] = [];
|
||||||
private uiOfferTitles: HTMLElement[] = [];
|
private uiOfferTitles: HTMLElement[] = [];
|
||||||
private uiOfferDescs: HTMLElement[] = [];
|
private uiOfferDescs: HTMLElement[] = [];
|
||||||
|
private uiDamageVignette!: HTMLElement;
|
||||||
|
private uiLowHpVignette!: HTMLElement;
|
||||||
private mouseOnCanvas = false;
|
private mouseOnCanvas = false;
|
||||||
|
|
||||||
|
private bloodParticles: {
|
||||||
|
mesh: THREE.Mesh;
|
||||||
|
vel: THREE.Vector3;
|
||||||
|
life: number;
|
||||||
|
mat: THREE.MeshBasicMaterial;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.scene = new THREE.Scene();
|
this.scene = new THREE.Scene();
|
||||||
this.scene.background = new THREE.Color(0x0d1117);
|
this.scene.background = new THREE.Color(0x0d1117);
|
||||||
@@ -178,6 +187,8 @@ export class Game {
|
|||||||
this.uiLevelUp = document.getElementById('level-up')!;
|
this.uiLevelUp = document.getElementById('level-up')!;
|
||||||
this.uiConfirmBtn = document.getElementById('btn-confirm') as HTMLButtonElement;
|
this.uiConfirmBtn = document.getElementById('btn-confirm') as HTMLButtonElement;
|
||||||
this.uiConfirmBtn.addEventListener('click', () => this.confirmOffer());
|
this.uiConfirmBtn.addEventListener('click', () => this.confirmOffer());
|
||||||
|
this.uiDamageVignette = document.getElementById('damage-vignette')!;
|
||||||
|
this.uiLowHpVignette = document.getElementById('low-hp-vignette')!;
|
||||||
|
|
||||||
for (let i = 0; i < 3; i++) {
|
for (let i = 0; i < 3; i++) {
|
||||||
const card = document.getElementById(`offer-${i}`)!;
|
const card = document.getElementById(`offer-${i}`)!;
|
||||||
@@ -869,6 +880,71 @@ export class Game {
|
|||||||
if (this.player.health <= 0 && this.state === 'playing') {
|
if (this.player.health <= 0 && this.state === 'playing') {
|
||||||
this.gameOver();
|
this.gameOver();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.uiLowHpVignette.classList.toggle(
|
||||||
|
'active', healthPct < 0.3 && healthPct > 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onPlayerHurt(sourcePos: THREE.Vector3) {
|
||||||
|
const el = this.uiDamageVignette;
|
||||||
|
el.classList.remove('hurt');
|
||||||
|
void el.offsetWidth;
|
||||||
|
el.classList.add('hurt');
|
||||||
|
|
||||||
|
const away = new THREE.Vector3()
|
||||||
|
.subVectors(this.player.body.position, sourcePos);
|
||||||
|
away.y = 0;
|
||||||
|
if (away.lengthSq() < 0.01) {
|
||||||
|
away.set(Math.random() - 0.5, 0, Math.random() - 0.5);
|
||||||
|
}
|
||||||
|
away.normalize();
|
||||||
|
|
||||||
|
const geom = new THREE.SphereGeometry(0.07, 6, 6);
|
||||||
|
const count = 12;
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const mat = new THREE.MeshBasicMaterial({
|
||||||
|
color: 0xcc2222,
|
||||||
|
transparent: true,
|
||||||
|
});
|
||||||
|
const mesh = new THREE.Mesh(geom, mat);
|
||||||
|
mesh.position.copy(this.player.body.position);
|
||||||
|
mesh.position.y += 0.5;
|
||||||
|
this.scene.add(mesh);
|
||||||
|
this.bloodParticles.push({
|
||||||
|
mesh,
|
||||||
|
mat,
|
||||||
|
vel: away.clone()
|
||||||
|
.multiplyScalar(2 + Math.random() * 3)
|
||||||
|
.add(new THREE.Vector3(
|
||||||
|
(Math.random() - 0.5) * 1.5,
|
||||||
|
3 + Math.random() * 3,
|
||||||
|
(Math.random() - 0.5) * 1.5
|
||||||
|
)),
|
||||||
|
life: 0.5 + Math.random() * 0.3,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateBloodParticles(dt: number) {
|
||||||
|
for (let i = this.bloodParticles.length - 1; i >= 0; i--) {
|
||||||
|
const p = this.bloodParticles[i];
|
||||||
|
p.life -= dt;
|
||||||
|
if (p.life <= 0) {
|
||||||
|
this.scene.remove(p.mesh);
|
||||||
|
p.mesh.geometry.dispose();
|
||||||
|
p.mat.dispose();
|
||||||
|
this.bloodParticles.splice(i, 1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
p.vel.y -= 14 * dt;
|
||||||
|
p.mesh.position.addScaledVector(p.vel, dt);
|
||||||
|
if (p.mesh.position.y < 0.06) {
|
||||||
|
p.mesh.position.y = 0.06;
|
||||||
|
p.vel.set(0, 0, 0);
|
||||||
|
}
|
||||||
|
p.mat.opacity = Math.max(0, p.life / 0.6);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private animate() {
|
private animate() {
|
||||||
@@ -889,6 +965,8 @@ export class Game {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.updateBloodParticles(dt);
|
||||||
|
|
||||||
// Update animations
|
// Update animations
|
||||||
this.player.updateAnimations(dt);
|
this.player.updateAnimations(dt);
|
||||||
this.player.rightHandWeapon.updateAnimation(dt);
|
this.player.rightHandWeapon.updateAnimation(dt);
|
||||||
@@ -919,7 +997,7 @@ export class Game {
|
|||||||
if (enemy.dead) continue;
|
if (enemy.dead) continue;
|
||||||
const dist = enemy.body.position.distanceTo(this.player.body.position);
|
const dist = enemy.body.position.distanceTo(this.player.body.position);
|
||||||
if (dist < enemy.contactRadius) {
|
if (dist < enemy.contactRadius) {
|
||||||
this.player.damage(enemy.contactDps * dt);
|
this.player.damage(enemy.contactDps * dt, enemy.body.position);
|
||||||
if (thorns > 0) {
|
if (thorns > 0) {
|
||||||
enemy.takeDamage(thorns * dt, this, false);
|
enemy.takeDamage(thorns * dt, this, false);
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-3
@@ -45,6 +45,12 @@ export class Player {
|
|||||||
private clips: Map<string, THREE.AnimationAction> = new Map();
|
private clips: Map<string, THREE.AnimationAction> = new Map();
|
||||||
private currentAnim = '';
|
private currentAnim = '';
|
||||||
private backShield: THREE.Group | null = null;
|
private backShield: THREE.Group | null = null;
|
||||||
|
private bodyMaterials: THREE.MeshToonMaterial[] = [];
|
||||||
|
private hurtCooldown = 0;
|
||||||
|
private flashTime = 0;
|
||||||
|
private static readonly BASE_COLOR = 0x44aa44;
|
||||||
|
private static readonly HURT_COLOR = 0xff5555;
|
||||||
|
private static readonly HURT_FLASH_TIME = 0.3;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.body = new THREE.Group();
|
this.body = new THREE.Group();
|
||||||
@@ -151,9 +157,10 @@ export class Player {
|
|||||||
child.castShadow = true;
|
child.castShadow = true;
|
||||||
child.receiveShadow = true;
|
child.receiveShadow = true;
|
||||||
const mat = new THREE.MeshToonMaterial({
|
const mat = new THREE.MeshToonMaterial({
|
||||||
color: 0x44aa44,
|
color: Player.BASE_COLOR,
|
||||||
});
|
});
|
||||||
child.material = mat;
|
child.material = mat;
|
||||||
|
this.bodyMaterials.push(mat);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.body.add(model);
|
this.body.add(model);
|
||||||
@@ -170,11 +177,12 @@ export class Player {
|
|||||||
console.warn('Failed to load blob model, using fallback', e);
|
console.warn('Failed to load blob model, using fallback', e);
|
||||||
// Fallback capsule
|
// Fallback capsule
|
||||||
const bodyGeom = new THREE.CapsuleGeometry(0.5, 0.5, 8, 16);
|
const bodyGeom = new THREE.CapsuleGeometry(0.5, 0.5, 8, 16);
|
||||||
const bodyMat = new THREE.MeshToonMaterial({ color: 0x44aa44 });
|
const bodyMat = new THREE.MeshToonMaterial({ color: Player.BASE_COLOR });
|
||||||
const bodyMesh = new THREE.Mesh(bodyGeom, bodyMat);
|
const bodyMesh = new THREE.Mesh(bodyGeom, bodyMat);
|
||||||
bodyMesh.position.y = 0.75;
|
bodyMesh.position.y = 0.75;
|
||||||
bodyMesh.castShadow = true;
|
bodyMesh.castShadow = true;
|
||||||
this.body.add(bodyMesh);
|
this.body.add(bodyMesh);
|
||||||
|
this.bodyMaterials.push(bodyMat);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,6 +216,15 @@ export class Player {
|
|||||||
|
|
||||||
updateAnimations(dt: number) {
|
updateAnimations(dt: number) {
|
||||||
if (this.mixer) this.mixer.update(dt);
|
if (this.mixer) this.mixer.update(dt);
|
||||||
|
if (this.hurtCooldown > 0) this.hurtCooldown -= dt;
|
||||||
|
if (this.flashTime > 0) {
|
||||||
|
this.flashTime -= dt;
|
||||||
|
const t = Math.max(0, this.flashTime) / Player.HURT_FLASH_TIME;
|
||||||
|
const color = new THREE.Color(Player.BASE_COLOR).lerp(
|
||||||
|
new THREE.Color(Player.HURT_COLOR), t
|
||||||
|
);
|
||||||
|
for (const mat of this.bodyMaterials) mat.color.copy(color);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setGame(game: Game) {
|
setGame(game: Game) {
|
||||||
@@ -226,7 +243,7 @@ export class Player {
|
|||||||
this.health = Math.min(this.health + amount, this.stats.maxHealth);
|
this.health = Math.min(this.health + amount, this.stats.maxHealth);
|
||||||
}
|
}
|
||||||
|
|
||||||
damage(amount: number) {
|
damage(amount: number, sourcePos?: THREE.Vector3) {
|
||||||
if (this.shieldHp > 0) {
|
if (this.shieldHp > 0) {
|
||||||
const absorbed = Math.min(this.shieldHp, amount);
|
const absorbed = Math.min(this.shieldHp, amount);
|
||||||
this.shieldHp -= absorbed;
|
this.shieldHp -= absorbed;
|
||||||
@@ -234,6 +251,13 @@ export class Player {
|
|||||||
this.shieldTimer = this.stats.shieldRechargeDelay;
|
this.shieldTimer = this.stats.shieldRechargeDelay;
|
||||||
}
|
}
|
||||||
this.health -= amount;
|
this.health -= amount;
|
||||||
|
if (amount > 0 && this.hurtCooldown <= 0) {
|
||||||
|
this.hurtCooldown = 0.25;
|
||||||
|
this.flashTime = Player.HURT_FLASH_TIME;
|
||||||
|
if (this.game) {
|
||||||
|
this.game.onPlayerHurt(sourcePos ?? this.body.position);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePassives(dt: number) {
|
updatePassives(dt: number) {
|
||||||
|
|||||||
+2
-2
@@ -25,11 +25,11 @@ export class Slime extends Enemy {
|
|||||||
this.registerFadeMesh(this.bodyMesh);
|
this.registerFadeMesh(this.bodyMesh);
|
||||||
this.model.add(this.bodyMesh);
|
this.model.add(this.bodyMesh);
|
||||||
|
|
||||||
// Cute eyes (face toward the player: -Z)
|
// Cute eyes (face toward the player: +Z)
|
||||||
const eyeMat = new THREE.MeshToonMaterial({ color: 0x111122 });
|
const eyeMat = new THREE.MeshToonMaterial({ color: 0x111122 });
|
||||||
for (const side of [-1, 1]) {
|
for (const side of [-1, 1]) {
|
||||||
const eye = new THREE.Mesh(new THREE.SphereGeometry(0.09, 10, 8), eyeMat);
|
const eye = new THREE.Mesh(new THREE.SphereGeometry(0.09, 10, 8), eyeMat);
|
||||||
eye.position.set(side * 0.18, 0.72, -0.42);
|
eye.position.set(side * 0.18, 0.72, 0.42);
|
||||||
this.registerFadeMesh(eye);
|
this.registerFadeMesh(eye);
|
||||||
this.model.add(eye);
|
this.model.add(eye);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user