feat:
-Add UI (Health bars, Resources UI, HUD Interface, Weapon U, Zoombie Invasion progress Indicator) -Refactor code structure for improved readability and maintainability
This commit is contained in:
+136
-129
@@ -4,6 +4,7 @@ import { UpdateController, CameraC_internal } from "@24tools/playable_template";
|
||||
import { ThreeC } from "./ThreeC";
|
||||
import { TestSceneC } from "./TestSceneC"; // for groundY (ground level)
|
||||
import { woodIconUrl } from "../resources/images/woodIcon";
|
||||
import { worldToScreen } from "../utils/screen";
|
||||
|
||||
// Tunables — tweak here
|
||||
const PIECES_MIN = 3; // min pieces per drop
|
||||
@@ -19,21 +20,21 @@ const BOUNCES = 2; // how many bounces after landing
|
||||
const BOUNCE_HEIGHT = 0.4; // each bounce = this fraction of the previous height
|
||||
const BOUNCE_TIME = 0.6; // each bounce is shorter in time
|
||||
const BOUNCE_FORWARD = 0.5; // each bounce covers this fraction of the previous horizontal step
|
||||
const FLIGHT_STRETCH = 0.35; // vertical stretch in flight (scaled by hop height)
|
||||
const LAND_SQUASH = 0.6; // squash on the final landing
|
||||
const LAND_POP_MS = 160; // duration of the final "pop"
|
||||
const FLIGHT_STRETCH = 0.22; // vertical stretch in flight (softer = smoother, less rubbery)
|
||||
const LAND_SQUASH = 0.72; // squash on the final landing (gentler)
|
||||
const LAND_POP_MS = 180; // duration of the final "pop"
|
||||
|
||||
// Collect (#8): delay after landing before flying to the corner, UI icon size, etc.
|
||||
const COLLECT_DELAY_MS = 40; // almost immediately after the bounces (flows into the collect)
|
||||
const UI_ICON_SIZE = 28; // wood UI icon size (px) — smaller than loot on the ground, but not tiny
|
||||
const UI_RIGHT = 16; // offset from the right edge (px)
|
||||
const UI_TOP = 110; // offset from the top (px) — lower, like in the REF
|
||||
const SHRINK_MS = 250; // shrink to UI size before the flight
|
||||
const FLY_MS = 500; // duration of the flight to the corner
|
||||
const BLINK_MS = 120; // ramp-up duration of the white flash (fade-out is longer)
|
||||
// Collect (#8): when/where the wood flies to the UI.
|
||||
const COLLECT_LEAD_MS = 170; // start the collect this long BEFORE the bounces finish,
|
||||
// so the shrink+flight flow out of the last bounce
|
||||
const COLLECT_STAGGER_MS = 70; // extra per-piece delay so they stream in, not all at once
|
||||
const UI_ICON_SIZE = 28; // wood arrival size (px) — about the wood plank on the panel
|
||||
const UI_WOOD_X_FRAC = 0.82; // where the wood art sits across the panel (right-side plank)
|
||||
const FLY_MS = 950; // flight duration — slower, calmer travel to the UI (size tracks it)
|
||||
const FLY_ARC_PX = 90; // how high the flight bows upward (curved path, not a straight line)
|
||||
const BLINK_MS = 120; // ramp-up of the white glint
|
||||
|
||||
const _ndc = new Vector3();
|
||||
const _topV = new Vector3();
|
||||
const _spriteTop = new Vector3(); // scratch: a sprite's top point, for measuring its screen size
|
||||
|
||||
export class LootC {
|
||||
static pieces: Sprite[] = [];
|
||||
@@ -48,28 +49,10 @@ export class LootC {
|
||||
this.texture = new TextureLoader().load(woodIconUrl);
|
||||
this.texture.colorSpace = SRGBColorSpace; // correct color
|
||||
|
||||
// Wood UI icon in the top-right corner (HTML overlay). Loot flies into it.
|
||||
const icon = document.createElement("img");
|
||||
icon.id = "wood-ui"; // stable id → UI/counter hooks onto it, LootC reads its position
|
||||
icon.src = woodIconUrl;
|
||||
icon.style.cssText =
|
||||
`position:fixed; top:${UI_TOP}px; right:${UI_RIGHT}px;` +
|
||||
`width:${UI_ICON_SIZE}px; height:${UI_ICON_SIZE}px;` +
|
||||
`z-index:1001; pointer-events:none; transition:transform 0.12s ease-out;`;
|
||||
document.body.appendChild(icon);
|
||||
this.uiIcon = icon;
|
||||
|
||||
// The numeric balance, just left of the icon.
|
||||
const count = document.createElement("div");
|
||||
count.id = "wood-count";
|
||||
count.style.cssText =
|
||||
`position:fixed; top:${UI_TOP}px; right:${UI_RIGHT + UI_ICON_SIZE + 6}px;` +
|
||||
`height:${UI_ICON_SIZE}px; line-height:${UI_ICON_SIZE}px;` +
|
||||
`font-family:sans-serif; font-weight:700; font-size:18px; color:#fff;` +
|
||||
`text-shadow:0 1px 2px rgba(0,0,0,0.6); z-index:1001; pointer-events:none;` +
|
||||
`transition:transform 0.12s ease-out;`;
|
||||
document.body.appendChild(count);
|
||||
this.countEl = count;
|
||||
// The wood icon/count live in the HUD (built by HudC). We just reference
|
||||
// them: read the icon's screen position as the fly target, write the count.
|
||||
this.uiIcon = document.getElementById("wood-ui") as HTMLImageElement | null;
|
||||
this.countEl = document.getElementById("wood-count");
|
||||
this.renderCount();
|
||||
|
||||
// ⚠️ Key: pump our group every frame, otherwise the tweens don't advance.
|
||||
@@ -81,9 +64,9 @@ export class LootC {
|
||||
return this.balance;
|
||||
}
|
||||
|
||||
/** Spend up to `n` wood; returns how much was actually taken (clamped to balance). */
|
||||
static spend(n: number): number {
|
||||
const taken = Math.min(n, this.balance);
|
||||
/** Spend up to `amount` wood; returns how much was actually taken (clamped to balance). */
|
||||
static spend(amount: number): number {
|
||||
const taken = Math.min(amount, this.balance);
|
||||
this.balance -= taken;
|
||||
this.renderCount();
|
||||
return taken;
|
||||
@@ -95,100 +78,115 @@ export class LootC {
|
||||
}
|
||||
|
||||
private static renderCount() {
|
||||
if (this.countEl) this.countEl.textContent = `×${this.balance}`;
|
||||
if (this.countEl) this.countEl.textContent = `${this.balance}`;
|
||||
}
|
||||
|
||||
/** Spawn loot at a point. If count is omitted → random PIECES_MIN..PIECES_MAX. */
|
||||
static spawn(origin: Vector3, count?: number) {
|
||||
const n = count ?? (PIECES_MIN + Math.floor(Math.random() * (PIECES_MAX - PIECES_MIN + 1)));
|
||||
const slice = (Math.PI * 2) / n; // each piece gets its own sector of the circle
|
||||
const pieceCount = count ?? (PIECES_MIN + Math.floor(Math.random() * (PIECES_MAX - PIECES_MIN + 1)));
|
||||
const sectorAngle = (Math.PI * 2) / pieceCount; // each piece gets its own slice of the circle
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
for (let i = 0; i < pieceCount; i++) {
|
||||
const piece = this.createPiece();
|
||||
piece.position.copy(origin);
|
||||
|
||||
// Even sector + a little jitter → pieces spread out and don't clump.
|
||||
const angle = i * slice + (Math.random() - 0.5) * slice * ANGLE_JITTER;
|
||||
const dist = SCATTER_MIN + Math.random() * (SCATTER_MAX - SCATTER_MIN);
|
||||
const angle = i * sectorAngle + (Math.random() - 0.5) * sectorAngle * ANGLE_JITTER;
|
||||
const radius = SCATTER_MIN + Math.random() * (SCATTER_MAX - SCATTER_MIN);
|
||||
const landing = new Vector3(
|
||||
origin.x + Math.cos(angle) * dist,
|
||||
TestSceneC.groundY + PIECE_SIZE / 2, // sprite center above ground → its bottom touches the ground
|
||||
origin.z + Math.sin(angle) * dist,
|
||||
origin.x + Math.cos(angle) * radius,
|
||||
TestSceneC.groundY + PIECE_SIZE / 2, // sprite center above ground → its bottom touches the ground
|
||||
origin.z + Math.sin(angle) * radius,
|
||||
);
|
||||
|
||||
this.animatePiece(piece, origin.clone(), landing);
|
||||
this.animatePiece(piece, origin.clone(), landing, i);
|
||||
this.pieces.push(piece);
|
||||
}
|
||||
}
|
||||
|
||||
/** Flat piece: a Sprite (billboard — always faces the camera) with the wood texture. */
|
||||
private static createPiece(): Sprite {
|
||||
const mat = new SpriteMaterial({ map: this.texture, transparent: true });
|
||||
const piece = new Sprite(mat);
|
||||
const material = new SpriteMaterial({ map: this.texture, transparent: true });
|
||||
const piece = new Sprite(material);
|
||||
piece.scale.set(PIECE_SIZE, PIECE_SIZE, 1);
|
||||
ThreeC.addToScene(piece);
|
||||
return piece;
|
||||
}
|
||||
|
||||
/** A piece flies in an arc, bounces a couple times, then pops on landing. */
|
||||
private static animatePiece(piece: Sprite, from: Vector3, to: Vector3) {
|
||||
private static animatePiece(piece: Sprite, from: Vector3, to: Vector3, index = 0) {
|
||||
const restY = to.y; // sprite center at rest
|
||||
|
||||
// One hop: an arc from (fx,fz) to (tx,tz), stretched while moving fast.
|
||||
const hop = (fx: number, fz: number, tx: number, tz: number, peak: number, ms: number) =>
|
||||
// One hop: a parabolic arc from (fromX,fromZ) to (toX,toZ), stretched while
|
||||
// moving fast (the "squash & stretch" that sells the speed).
|
||||
const hop = (fromX: number, fromZ: number, toX: number, toZ: number, peakHeight: number, durationMs: number) =>
|
||||
new TWEEN.Tween({ t: 0 }, this.tweens)
|
||||
.to({ t: 1 }, ms)
|
||||
.to({ t: 1 }, durationMs)
|
||||
.easing(TWEEN.Easing.Linear.None)
|
||||
.onUpdate(({ t }) => {
|
||||
piece.position.x = fx + (tx - fx) * t;
|
||||
piece.position.z = fz + (tz - fz) * t;
|
||||
piece.position.y = restY + peak * 4 * t * (1 - t); // parabolic arc
|
||||
piece.position.x = fromX + (toX - fromX) * t;
|
||||
piece.position.z = fromZ + (toZ - fromZ) * t;
|
||||
piece.position.y = restY + peakHeight * 4 * t * (1 - t); // parabolic arc
|
||||
// |1-2t|: fast on the way up/down → stretch; at the peak → normal.
|
||||
// Scale the stretch by hop height (small bounces stretch less).
|
||||
const s = 1 + FLIGHT_STRETCH * (peak / ARC_HEIGHT) * Math.abs(1 - 2 * t);
|
||||
piece.scale.set(PIECE_SIZE / s, PIECE_SIZE * s, 1);
|
||||
const stretch = 1 + FLIGHT_STRETCH * (peakHeight / ARC_HEIGHT) * Math.abs(1 - 2 * t);
|
||||
piece.scale.set(PIECE_SIZE / stretch, PIECE_SIZE * stretch, 1);
|
||||
});
|
||||
|
||||
// Horizontal throw direction (target = the final resting spot).
|
||||
const dx = to.x - from.x, dz = to.z - from.z;
|
||||
const totalDist = Math.hypot(dx, dz) || 1e-4;
|
||||
const dirX = dx / totalDist, dirZ = dz / totalDist;
|
||||
const deltaX = to.x - from.x, deltaZ = to.z - from.z;
|
||||
const totalDist = Math.hypot(deltaX, deltaZ) || 1e-4;
|
||||
const dirX = deltaX / totalDist, dirZ = deltaZ / totalDist;
|
||||
|
||||
// Share the horizontal distance across the flight + bounces (so it also
|
||||
// moves forward on each bounce, not just up).
|
||||
const hops = BOUNCES + 1;
|
||||
const series = (1 - Math.pow(BOUNCE_FORWARD, hops)) / (1 - BOUNCE_FORWARD);
|
||||
let step = totalDist / series;
|
||||
let cx = from.x, cz = from.z;
|
||||
let peak = ARC_HEIGHT, ms = FLIGHT_MS;
|
||||
let first: TWEEN.Tween<{ t: number }> | null = null;
|
||||
let prev: TWEEN.Tween<{ t: number }> | null = null;
|
||||
// Share the horizontal distance across the flight + bounces (so the piece
|
||||
// also moves forward on each bounce, not just up). Each hop covers
|
||||
// BOUNCE_FORWARD× the previous one, so the steps form a geometric series
|
||||
// whose sum we divide the total distance by to get the first step.
|
||||
const hopCount = BOUNCES + 1;
|
||||
const forwardSum = (1 - Math.pow(BOUNCE_FORWARD, hopCount)) / (1 - BOUNCE_FORWARD);
|
||||
let stepDist = totalDist / forwardSum;
|
||||
let curX = from.x, curZ = from.z;
|
||||
let peakHeight = ARC_HEIGHT, durationMs = FLIGHT_MS;
|
||||
let firstHop: TWEEN.Tween<{ t: number }> | null = null;
|
||||
let prevHop: TWEEN.Tween<{ t: number }> | null = null;
|
||||
const allTweens: TWEEN.Tween<any>[] = []; // every hop/pop tween (so we can stop them early)
|
||||
let bouncesMs = 0; // total duration of all the hop arcs
|
||||
|
||||
for (let k = 0; k < hops; k++) {
|
||||
const nx = cx + dirX * step, nz = cz + dirZ * step;
|
||||
const h = hop(cx, cz, nx, nz, peak, ms);
|
||||
if (!first) first = h; else prev!.chain(h);
|
||||
prev = h;
|
||||
cx = nx; cz = nz;
|
||||
step *= BOUNCE_FORWARD; peak *= BOUNCE_HEIGHT; ms *= BOUNCE_TIME;
|
||||
for (let i = 0; i < hopCount; i++) {
|
||||
const nextX = curX + dirX * stepDist, nextZ = curZ + dirZ * stepDist;
|
||||
const hopTween = hop(curX, curZ, nextX, nextZ, peakHeight, durationMs);
|
||||
allTweens.push(hopTween);
|
||||
if (!firstHop) firstHop = hopTween; else prevHop!.chain(hopTween);
|
||||
prevHop = hopTween;
|
||||
bouncesMs += durationMs;
|
||||
curX = nextX; curZ = nextZ;
|
||||
stepDist *= BOUNCE_FORWARD; peakHeight *= BOUNCE_HEIGHT; durationMs *= BOUNCE_TIME;
|
||||
}
|
||||
|
||||
// Landing pop: squash on the ground, then spring back to normal.
|
||||
// Landing pop: squash on the ground, then spring back. Only seen if the piece
|
||||
// somehow isn't collected first (the collect normally lifts off before this).
|
||||
const groundY = restY - PIECE_SIZE / 2;
|
||||
const pop = new TWEEN.Tween({ k: 0 }, this.tweens)
|
||||
.to({ k: 1 }, LAND_POP_MS)
|
||||
const pop = new TWEEN.Tween({ t: 0 }, this.tweens)
|
||||
.to({ t: 1 }, LAND_POP_MS)
|
||||
.easing(TWEEN.Easing.Back.Out)
|
||||
.onUpdate(({ k }) => {
|
||||
const s = LAND_SQUASH + (1 - LAND_SQUASH) * k; // 0.6 → 1 (with a slight overshoot)
|
||||
piece.scale.set(PIECE_SIZE / s, PIECE_SIZE * s, 1);
|
||||
piece.position.y = groundY + (PIECE_SIZE * s) / 2; // bottom stays on the ground
|
||||
})
|
||||
.onComplete(() => {
|
||||
setTimeout(() => this.collect(piece), COLLECT_DELAY_MS); // then fly to the UI
|
||||
.onUpdate(({ t }) => {
|
||||
const squash = LAND_SQUASH + (1 - LAND_SQUASH) * t; // 0.72 → 1 (with a slight overshoot)
|
||||
piece.scale.set(PIECE_SIZE / squash, PIECE_SIZE * squash, 1);
|
||||
piece.position.y = groundY + (PIECE_SIZE * squash) / 2; // bottom stays on the ground
|
||||
});
|
||||
prev!.chain(pop);
|
||||
allTweens.push(pop);
|
||||
prevHop!.chain(pop);
|
||||
|
||||
first!.start();
|
||||
firstHop!.start();
|
||||
|
||||
// Lift off into the UI a bit BEFORE the bounces finish, so the shrink + flight
|
||||
// flow straight out of the last bounce (no "settle, pause, then fly"). Stop the
|
||||
// remaining bounce/pop on this piece and hand straight over to the collect.
|
||||
const liftOffDelayMs = Math.max(FLIGHT_MS * 0.7, bouncesMs - COLLECT_LEAD_MS) + index * COLLECT_STAGGER_MS;
|
||||
setTimeout(() => {
|
||||
allTweens.forEach(tween => tween.stop());
|
||||
this.collect(piece);
|
||||
}, liftOffDelayMs);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -203,9 +201,9 @@ export class LootC {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
|
||||
// Sprite center + size in screen pixels.
|
||||
const center = this.toScreen(piece.position, cam, rect);
|
||||
_topV.copy(piece.position); _topV.y += piece.scale.y / 2;
|
||||
const sizePx = Math.max(8, Math.abs(center.y - this.toScreen(_topV, cam, rect).y) * 2);
|
||||
const center = worldToScreen(piece.position, cam, rect);
|
||||
_spriteTop.copy(piece.position); _spriteTop.y += piece.scale.y / 2;
|
||||
const sizePx = Math.max(8, Math.abs(center.y - worldToScreen(_spriteTop, cam, rect).y) * 2);
|
||||
|
||||
// Drop the 3D sprite; the HTML image takes over from the same spot.
|
||||
this.remove(piece);
|
||||
@@ -214,7 +212,8 @@ export class LootC {
|
||||
flier.src = woodIconUrl;
|
||||
flier.style.cssText =
|
||||
`position:fixed; left:0; top:0; width:${sizePx}px; height:${sizePx}px;` +
|
||||
`z-index:1000; pointer-events:none; transform:translate(-50%,-50%); will-change:left,top,width,height;`;
|
||||
// above #hud (z-index 9999) so the wood clearly flies on top of, and into, the icon
|
||||
`z-index:10000; pointer-events:none; transform:translate(-50%,-50%); will-change:left,top,width,height;`;
|
||||
document.body.appendChild(flier);
|
||||
|
||||
// White "glint" copy that rides on top of the flier and fades out as it moves.
|
||||
@@ -222,63 +221,69 @@ export class LootC {
|
||||
flash.src = woodIconUrl;
|
||||
flash.style.cssText = flier.style.cssText;
|
||||
flash.style.filter = "brightness(0) invert(1)"; // solid white silhouette
|
||||
flash.style.zIndex = "1002";
|
||||
flash.style.zIndex = "10001";
|
||||
document.body.appendChild(flash);
|
||||
|
||||
const target = this.uiIconCenter();
|
||||
|
||||
// Shared state. The flight, shrink and blink below all run at once, so they
|
||||
// blend into one smooth motion instead of separate steps.
|
||||
const st = { x: center.x, y: center.y, size: sizePx, o: 0 };
|
||||
// Curved flight path (quadratic Bézier): start → a lifted control point → UI.
|
||||
// The upward bow makes the wood swoop in an arc instead of a flat diagonal,
|
||||
// which reads much smoother.
|
||||
const startX = center.x, startY = center.y;
|
||||
const endX = target.x, endY = target.y;
|
||||
const ctrlX = (startX + endX) / 2;
|
||||
const ctrlY = Math.min(startY, endY) - FLY_ARC_PX;
|
||||
|
||||
// Shared animation state: progress along the path (0→1), current size, and
|
||||
// the white glint's opacity. All three tweens below mutate this one object.
|
||||
const anim = { progress: 0, size: sizePx, glow: 0 };
|
||||
const place = (el: HTMLElement) => {
|
||||
el.style.left = `${st.x}px`;
|
||||
el.style.top = `${st.y}px`;
|
||||
el.style.width = `${st.size}px`;
|
||||
el.style.height = `${st.size}px`;
|
||||
const inv = 1 - anim.progress; // (1−t) term of the Bézier
|
||||
const x = inv * inv * startX + 2 * inv * anim.progress * ctrlX + anim.progress * anim.progress * endX;
|
||||
const y = inv * inv * startY + 2 * inv * anim.progress * ctrlY + anim.progress * anim.progress * endY;
|
||||
el.style.left = `${x}px`;
|
||||
el.style.top = `${y}px`;
|
||||
el.style.width = `${anim.size}px`;
|
||||
el.style.height = `${anim.size}px`;
|
||||
};
|
||||
const apply = () => { place(flier); place(flash); flash.style.opacity = `${st.o}`; };
|
||||
const apply = () => { place(flier); place(flash); flash.style.opacity = `${anim.glow}`; };
|
||||
apply();
|
||||
|
||||
// Flight (position) — the longest tween, so it owns the cleanup.
|
||||
const fly = new TWEEN.Tween(st, this.tweens)
|
||||
.to({ x: target.x, y: target.y }, FLY_MS)
|
||||
.easing(TWEEN.Easing.Quadratic.In)
|
||||
// Flight along the curve — owns the cleanup. Ease in AND out so it starts and
|
||||
// arrives gently.
|
||||
const fly = new TWEEN.Tween(anim, this.tweens)
|
||||
.to({ progress: 1 }, FLY_MS)
|
||||
.easing(TWEEN.Easing.Quadratic.InOut)
|
||||
.onUpdate(apply)
|
||||
.onComplete(() => { flier.remove(); flash.remove(); this.balance++; this.renderCount(); this.pulseUiIcon(); });
|
||||
// Shrink (size) — runs alongside the flight, eases out so it shrinks early.
|
||||
const shrink = new TWEEN.Tween(st, this.tweens)
|
||||
.to({ size: UI_ICON_SIZE }, SHRINK_MS)
|
||||
// Shrink — SAME duration as the flight, so the two start AND finish together
|
||||
// (no "shrink first"); Sinusoidal makes the size change extra smooth.
|
||||
const shrink = new TWEEN.Tween(anim, this.tweens)
|
||||
.to({ size: UI_ICON_SIZE }, FLY_MS)
|
||||
.easing(TWEEN.Easing.Sinusoidal.InOut)
|
||||
.onUpdate(apply);
|
||||
// Blink — a quick glint that overlaps the start of the motion.
|
||||
const flashIn = new TWEEN.Tween(anim, this.tweens)
|
||||
.to({ glow: 1 }, BLINK_MS)
|
||||
.easing(TWEEN.Easing.Quadratic.Out)
|
||||
.onUpdate(apply);
|
||||
// Blink (opacity) — a quick glint that overlaps the start of the motion.
|
||||
const flashIn = new TWEEN.Tween(st, this.tweens)
|
||||
.to({ o: 1 }, BLINK_MS)
|
||||
.easing(TWEEN.Easing.Quadratic.Out)
|
||||
.onUpdate(apply);
|
||||
const flashOut = new TWEEN.Tween(st, this.tweens)
|
||||
.to({ o: 0 }, BLINK_MS * 1.6)
|
||||
const flashOut = new TWEEN.Tween(anim, this.tweens)
|
||||
.to({ glow: 0 }, BLINK_MS * 1.6)
|
||||
.easing(TWEEN.Easing.Quadratic.In)
|
||||
.onUpdate(apply);
|
||||
flashIn.chain(flashOut);
|
||||
|
||||
// Kick them all off together → blended, fluid collect.
|
||||
// Kick them off together → one blended, fluid collect.
|
||||
fly.start();
|
||||
shrink.start();
|
||||
flashIn.start();
|
||||
}
|
||||
|
||||
/** World point → screen pixels (accounting for the canvas position on the page). */
|
||||
private static toScreen(world: Vector3, cam: any, rect: DOMRect) {
|
||||
_ndc.copy(world).project(cam);
|
||||
return {
|
||||
x: rect.left + (_ndc.x * 0.5 + 0.5) * rect.width,
|
||||
y: rect.top + (-_ndc.y * 0.5 + 0.5) * rect.height,
|
||||
};
|
||||
}
|
||||
|
||||
private static uiIconCenter() {
|
||||
const r = this.uiIcon?.getBoundingClientRect();
|
||||
return r ? { x: r.left + r.width / 2, y: r.top + r.height / 2 } : { x: 0, y: 0 };
|
||||
// Aim at the wood plank on the right of the panel, not the panel's center,
|
||||
// so the loot lands on the actual wood art.
|
||||
return r ? { x: r.left + r.width * UI_WOOD_X_FRAC, y: r.top + r.height / 2 } : { x: 0, y: 0 };
|
||||
}
|
||||
|
||||
/** A small "pulse" of the UI icon when a piece arrives. */
|
||||
@@ -289,9 +294,11 @@ export class LootC {
|
||||
setTimeout(() => { if (this.uiIcon) this.uiIcon.style.transform = "scale(1)"; }, 120);
|
||||
}
|
||||
|
||||
/** Remove a piece from the scene (used by #8 — after collecting). */
|
||||
/** Remove a piece from the scene and free its material (the texture is shared,
|
||||
* so it's loaded once in init() and never disposed per-piece). */
|
||||
static remove(piece: Sprite) {
|
||||
ThreeC.removeFromScene(piece);
|
||||
(piece.material as SpriteMaterial).dispose();
|
||||
const i = this.pieces.indexOf(piece);
|
||||
if (i >= 0) this.pieces.splice(i, 1);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user