Add game source, converter pipeline, and web port
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
import * as THREE from 'three';
|
||||
import { Sword } from './Sword';
|
||||
import { Staff } from './Staff';
|
||||
import { loadGLB } from './MeshLoader';
|
||||
import type { Game } from './Game';
|
||||
|
||||
export class Player {
|
||||
body: THREE.Group;
|
||||
leftHandWeapon: Staff;
|
||||
rightHandWeapon: Sword;
|
||||
velocity = new THREE.Vector3();
|
||||
health = 100;
|
||||
MAX_HEALTH = 100;
|
||||
game!: Game;
|
||||
jumpVelocity = 0;
|
||||
onGround = true;
|
||||
mixer!: THREE.AnimationMixer;
|
||||
private clips: Map<string, THREE.AnimationAction> = new Map();
|
||||
private currentAnim = '';
|
||||
|
||||
constructor() {
|
||||
this.body = new THREE.Group();
|
||||
|
||||
// Helmet
|
||||
const helmetGeom = new THREE.SphereGeometry(0.45, 8, 8, 0, Math.PI * 2, 0, Math.PI / 2);
|
||||
const helmetMat = new THREE.MeshToonMaterial({ color: 0x888888 });
|
||||
const helmet = new THREE.Mesh(helmetGeom, helmetMat);
|
||||
helmet.position.y = 0.9;
|
||||
helmet.castShadow = true;
|
||||
this.body.add(helmet);
|
||||
|
||||
// Try loading the real helmet model (replaces the placeholder sphere)
|
||||
loadGLB('helmet').then((gltf) => {
|
||||
const realHelmet = gltf.scene;
|
||||
const helmetTexture = new THREE.TextureLoader().load('./textures/helmet.png');
|
||||
helmetTexture.flipY = false;
|
||||
helmetTexture.colorSpace = THREE.SRGBColorSpace;
|
||||
realHelmet.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
child.castShadow = true;
|
||||
child.material = new THREE.MeshToonMaterial({ map: helmetTexture });
|
||||
}
|
||||
});
|
||||
realHelmet.scale.set(0.35, 0.35, 0.35);
|
||||
realHelmet.position.set(0, 0.9, -0.2);
|
||||
realHelmet.rotation.set(-0.4, 0, 0);
|
||||
this.body.remove(helmet);
|
||||
this.body.add(realHelmet);
|
||||
}).catch(() => {
|
||||
// Keep placeholder helmet
|
||||
});
|
||||
|
||||
// Shadow disc
|
||||
const shadowGeom = new THREE.CircleGeometry(0.4, 16);
|
||||
const shadowMat = new THREE.MeshBasicMaterial({
|
||||
color: 0x000000, transparent: true, opacity: 0.3,
|
||||
});
|
||||
const shadow = new THREE.Mesh(shadowGeom, shadowMat);
|
||||
shadow.rotation.x = -Math.PI / 2;
|
||||
shadow.position.y = -0.48;
|
||||
shadow.renderOrder = 999;
|
||||
this.body.add(shadow);
|
||||
|
||||
// Weapons
|
||||
this.leftHandWeapon = new Staff();
|
||||
this.rightHandWeapon = new Sword();
|
||||
this.body.add(this.leftHandWeapon.mesh);
|
||||
this.body.add(this.rightHandWeapon.mesh);
|
||||
}
|
||||
|
||||
async init() {
|
||||
try {
|
||||
const gltf = await loadGLB('blob');
|
||||
const model = gltf.scene;
|
||||
model.scale.set(0.5, 0.5, 0.5);
|
||||
model.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
child.castShadow = true;
|
||||
child.receiveShadow = true;
|
||||
const mat = new THREE.MeshToonMaterial({
|
||||
color: 0x44aa44,
|
||||
});
|
||||
child.material = mat;
|
||||
}
|
||||
});
|
||||
this.body.add(model);
|
||||
|
||||
if (gltf.animations.length > 0) {
|
||||
this.mixer = new THREE.AnimationMixer(model);
|
||||
for (const clip of gltf.animations) {
|
||||
const action = this.mixer.clipAction(clip);
|
||||
this.clips.set(clip.name, action);
|
||||
}
|
||||
this.playAnimation('stand');
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to load blob model, using fallback', e);
|
||||
// Fallback capsule
|
||||
const bodyGeom = new THREE.CapsuleGeometry(0.5, 0.5, 8, 16);
|
||||
const bodyMat = new THREE.MeshToonMaterial({ color: 0x44aa44 });
|
||||
const bodyMesh = new THREE.Mesh(bodyGeom, bodyMat);
|
||||
bodyMesh.position.y = 0.75;
|
||||
bodyMesh.castShadow = true;
|
||||
this.body.add(bodyMesh);
|
||||
}
|
||||
}
|
||||
|
||||
playAnimation(name: string, loop = true, speed = 1) {
|
||||
if (!this.clips.has(name)) return;
|
||||
if (this.currentAnim === name) return;
|
||||
// Stop the previous animation so it doesn't blend with the new one
|
||||
if (this.currentAnim) {
|
||||
const prev = this.clips.get(this.currentAnim);
|
||||
if (prev) prev.stop();
|
||||
}
|
||||
this.currentAnim = name;
|
||||
const action = this.clips.get(name)!;
|
||||
action.reset();
|
||||
action.setLoop(loop ? THREE.LoopRepeat : THREE.LoopOnce, loop ? Infinity : 1);
|
||||
action.clampWhenFinished = !loop;
|
||||
action.setEffectiveTimeScale(speed);
|
||||
action.weight = 1;
|
||||
action.play();
|
||||
return action;
|
||||
}
|
||||
|
||||
stopAnimation(name: string) {
|
||||
const action = this.clips.get(name);
|
||||
if (action) action.stop();
|
||||
}
|
||||
|
||||
getCurrentAnimation(): string {
|
||||
return this.currentAnim;
|
||||
}
|
||||
|
||||
updateAnimations(dt: number) {
|
||||
if (this.mixer) this.mixer.update(dt);
|
||||
}
|
||||
|
||||
setGame(game: Game) {
|
||||
this.game = game;
|
||||
}
|
||||
|
||||
jump() {
|
||||
if (this.onGround) {
|
||||
this.jumpVelocity = 8;
|
||||
this.onGround = false;
|
||||
this.playAnimation('jump', false);
|
||||
}
|
||||
}
|
||||
|
||||
switchHands() {
|
||||
const temp = this.leftHandWeapon;
|
||||
this.leftHandWeapon = this.rightHandWeapon as unknown as Staff;
|
||||
this.rightHandWeapon = temp as unknown as Sword;
|
||||
|
||||
const leftPos = this.leftHandWeapon.mesh.position.clone();
|
||||
const rightPos = this.rightHandWeapon.mesh.position.clone();
|
||||
this.leftHandWeapon.mesh.position.copy(rightPos);
|
||||
this.rightHandWeapon.mesh.position.copy(leftPos);
|
||||
|
||||
const leftRot = this.leftHandWeapon.mesh.rotation.clone();
|
||||
const rightRot = this.rightHandWeapon.mesh.rotation.clone();
|
||||
this.leftHandWeapon.mesh.rotation.copy(rightRot);
|
||||
this.rightHandWeapon.mesh.rotation.copy(leftRot);
|
||||
}
|
||||
|
||||
heal(amount: number) {
|
||||
this.health = Math.min(this.health + amount, this.MAX_HEALTH);
|
||||
}
|
||||
|
||||
die() {
|
||||
this.playAnimation('die', false);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user