diff --git a/web/public/textures/shield.png b/web/public/textures/shield.png index fbee839..2209cfa 100644 Binary files a/web/public/textures/shield.png and b/web/public/textures/shield.png differ diff --git a/web/src/Player.ts b/web/src/Player.ts index 6766a48..cf5d89b 100644 --- a/web/src/Player.ts +++ b/web/src/Player.ts @@ -44,6 +44,7 @@ export class Player { mixer!: THREE.AnimationMixer; private clips: Map = new Map(); private currentAnim = ''; + private backShield: THREE.Group | null = null; constructor() { this.body = new THREE.Group(); @@ -88,6 +89,49 @@ export class Player { shadow.renderOrder = 999; this.body.add(shadow); + // Shield on the back while the shield skill is charged + loadGLB('shield').then((gltf) => { + const shieldModel = gltf.scene; + const shieldTexture = new THREE.TextureLoader().load('./textures/shield.png'); + shieldTexture.flipY = false; + shieldTexture.colorSpace = THREE.SRGBColorSpace; + shieldModel.traverse((child) => { + if (child instanceof THREE.Mesh) { + const geo = child.geometry; + // The converted model has no UVs; project them from the + // front plane so the shield texture maps onto the face + if (!geo.attributes.uv) { + geo.computeBoundingBox(); + const bb = geo.boundingBox!; + const pos = geo.attributes.position; + const uv = new Float32Array(pos.count * 2); + for (let i = 0; i < pos.count; i++) { + uv[i * 2] = (pos.getX(i) - bb.min.x) / (bb.max.x - bb.min.x); + uv[i * 2 + 1] = (pos.getY(i) - bb.min.y) / (bb.max.y - bb.min.y); + } + geo.setAttribute('uv', new THREE.BufferAttribute(uv, 2)); + } + child.castShadow = true; + child.material = new THREE.MeshToonMaterial({ map: shieldTexture }); + } + }); + // The geometry is offset from the model origin; recenter it so + // rotation and scale act on the shield's center + const box = new THREE.Box3().setFromObject(shieldModel); + const center = box.getCenter(new THREE.Vector3()); + shieldModel.position.sub(center); + const shield = new THREE.Group(); + shield.add(shieldModel); + shield.scale.set(0.25, 0.25, 0.25); + shield.position.set(0, 0.85, -0.5); + shield.rotation.set(-0.15, Math.PI, 0); + shield.visible = this.stats.shield > 0 && this.shieldHp > 0; + this.backShield = shield; + this.body.add(shield); + }).catch(() => { + // No shield model available + }); + // Weapons this.leftHandWeapon = new Staff(); this.rightHandWeapon = new Sword(); @@ -206,6 +250,9 @@ export class Player { this.shieldTimer = 0; } } + if (this.backShield) { + this.backShield.visible = this.stats.shield > 0 && this.shieldHp > 0; + } } applySkillSystem(skills: SkillSystem) { @@ -232,6 +279,9 @@ export class Player { if (this.stats.shield > 0 && this.shieldHp <= 0) { this.shieldHp = this.stats.shield; } + if (this.backShield) { + this.backShield.visible = this.stats.shield > 0 && this.shieldHp > 0; + } } die() {