diff --git a/src/controllers/CombatC.ts b/src/controllers/CombatC.ts index cdc5648..fa4ea62 100644 --- a/src/controllers/CombatC.ts +++ b/src/controllers/CombatC.ts @@ -4,38 +4,25 @@ import { PlayerC } from "./PlayerC"; import { Crate, LootableC } from "./LootableC"; import { Trigger } from "./TriggerC"; -// Health removed from a crate per strike (the bat touches it during a swing). -const ATTACK_DAMAGE = 10; -// Bat-tip → crate horizontal distance under which the bat is "touching" it. -const HIT_DIST = 0.8; -// The swing only reaches crates within this asymmetric arc of the facing dir -// (negative = player's left, positive = right): left cut at 90°, right 130°. -const ARC_LEFT = (90 * Math.PI) / 180; -const ARC_RIGHT = (130 * Math.PI) / 180; +const ATTACK_DAMAGE = 10; // damage per bat-tip touch +const CONTACT_DIST = 0.8; // bat tip → crate distance that counts as a touch const _tmp = new Vector3(); const _center = new Vector3(); -const _fwd = new Vector3(); -const _pos = new Vector3(); const _tip = new Vector3(); /** - * "Stand near crates → auto-attack" loop. - * - * Each crate has a Trigger zone (= which crates are in reach). While the player - * is stopped, the Loot swing plays and the player faces the crates' centre. - * The Loot clip has TWO strikes (left swing, then right). Per STRIKE, every - * crate the bat tip actually reaches takes one hit (front arc only) — so one - * swing damages all the crates it sweeps over, and a crate caught by both - * swings takes two sequential hits, rather than being hit at random times. + * Auto-attack: while the player stands near crates, swing the bat and damage a + * crate when the bat tip touches it. Hits are gated by PlayerC's swing window + * (no damage during the wind-up) and limited to once per crate per swing. */ export class CombatC { - private static inRange = new Set(); - private static hitThisStrike = new Set(); // crates already hit in the current strike - private static lastStrike = -1; + private static inRange = new Set(); // crates currently in reach + private static hitThisSwing = new Set(); // crates already hit this swing + private static lastSwingId = 0; static init() { - // A proximity trigger around every crate. Its size = the attack reach. + // Proximity trigger around every crate → decides which crates are in reach. for (const crate of LootableC.crates) { crate.root.getWorldPosition(_tmp); crate.trigger = new Trigger( @@ -54,16 +41,16 @@ export class CombatC { private static update() { this.pruneBroken(); - // Moving → stop at once. + // Moving → stop attacking immediately. if (PlayerC.isMoving()) { PlayerC.setAttacking(false); - this.resetCycle(); + this.endSwing(); return; } - // Nothing left in reach → let the current swing finish, then idle. + // No crates left → let the current swing finish, then idle. if (this.inRange.size === 0) { PlayerC.finishAttack(); - this.resetCycle(); + this.endSwing(); return; } @@ -76,51 +63,42 @@ export class CombatC { _center.divideScalar(this.inRange.size); PlayerC.setAttacking(true, _center); - this.applyBatContact(); + this.applyContact(); } - // Per strike: damage every in-reach crate the bat tip reaches (front arc). - private static applyBatContact() { - // New strike → all crates can be hit once again. - const strike = PlayerC.getSwingCycle(); - if (strike !== this.lastStrike) { - this.hitThisStrike.clear(); - this.lastStrike = strike; + // Damage each touched crate once per swing. Nothing happens during the wind-up. + private static applyContact() { + const swingId = PlayerC.getActiveSwingId(); + if (swingId === 0) return; // bat not live (wind-up / between swings) + + // New swing → every crate can be hit again. + if (swingId !== this.lastSwingId) { + this.hitThisSwing.clear(); + this.lastSwingId = swingId; } const tip = PlayerC.getBatTip(_tip); if (!tip) return; - PlayerC.getForward(_fwd); - PlayerC.getPosition(_pos); - const rx = _fwd.z, rz = -_fwd.x; // player's right for (const c of [...this.inRange]) { - if (c.broken || this.hitThisStrike.has(c)) continue; + if (c.broken || this.hitThisSwing.has(c)) continue; c.root.getWorldPosition(_tmp); - - // Front-arc gate (the bat can't reach behind the player). - const dx = _tmp.x - _pos.x, dz = _tmp.z - _pos.z; - const len = Math.hypot(dx, dz) || 1; - const angle = Math.atan2((rx * dx + rz * dz) / len, (_fwd.x * dx + _fwd.z * dz) / len); - if (angle < -ARC_LEFT || angle > ARC_RIGHT) continue; - - // Bat tip actually reached this crate → hit it once this strike. - if (Math.hypot(tip.x - _tmp.x, tip.z - _tmp.z) <= HIT_DIST) { - this.hitThisStrike.add(c); + if (Math.hypot(tip.x - _tmp.x, tip.z - _tmp.z) <= CONTACT_DIST) { + this.hitThisSwing.add(c); LootableC.damageCrate(c, ATTACK_DAMAGE); } } this.pruneBroken(); } - private static resetCycle() { - this.hitThisStrike.clear(); - this.lastStrike = -1; + private static endSwing() { + this.hitThisSwing.clear(); + this.lastSwingId = 0; } private static pruneBroken() { for (const c of this.inRange) { - if (c.broken) { this.inRange.delete(c); this.hitThisStrike.delete(c); } + if (c.broken) { this.inRange.delete(c); this.hitThisSwing.delete(c); } } } } diff --git a/src/controllers/LootC.ts b/src/controllers/LootC.ts index 033a444..8c3b842 100644 --- a/src/controllers/LootC.ts +++ b/src/controllers/LootC.ts @@ -24,7 +24,7 @@ const LAND_SQUASH = 0.6; // squash on the final landing const LAND_POP_MS = 160; // duration of the final "pop" // Collect (#8): delay after landing before flying to the corner, UI icon size, etc. -const COLLECT_DELAY_MS = 100; // almost immediately after the bounces +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 @@ -37,10 +37,11 @@ const _topV = new Vector3(); export class LootC { static pieces: Sprite[] = []; + static balance = 0; // collected wood the player can spend private static texture: Texture | null = null; private static uiIcon: HTMLImageElement | null = null; - // Our own tween group. In tween.js v25 `new Tween(obj)` does NOT join the - // default group automatically, so we keep and update our own (else tweens freeze). + private static countEl: HTMLElement | null = null; // the "×N" label next to the icon + // Our tween group, pumped each frame (tween.js v25 needs an explicit group). private static tweens = new TWEEN.Group(); static init() { @@ -58,10 +59,45 @@ export class LootC { 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; + this.renderCount(); + // ⚠️ Key: pump our group every frame, otherwise the tweens don't advance. UpdateController.Instance.onUpdate.addDelegate(() => this.tweens.update()); } + /** Current spendable wood. */ + static getBalance(): number { + 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); + this.balance -= taken; + this.renderCount(); + return taken; + } + + /** Screen-pixel center of the UI wood icon (used as a fly source/target). */ + static uiIconScreenCenter(): { x: number; y: number } { + return this.uiIconCenter(); + } + + private static renderCount() { + 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))); @@ -94,14 +130,11 @@ export class LootC { return piece; } - /** - * Scatter: a tall first arc (stretched along its motion), then a few decaying - * bounces off the ground, and a final "pop" (squash → springs back to normal). - */ + /** A piece flies in an arc, bounces a couple times, then pops on landing. */ private static animatePiece(piece: Sprite, from: Vector3, to: Vector3) { - const restY = to.y; // sprite center at rest (= groundY + PIECE_SIZE/2) + const restY = to.y; // sprite center at rest - // One "hop": parabola fx,fz→tx,tz peaking at peak; stretched by speed. + // 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) => new TWEEN.Tween({ t: 0 }, this.tweens) .to({ t: 1 }, ms) @@ -121,8 +154,8 @@ export class LootC { const totalDist = Math.hypot(dx, dz) || 1e-4; const dirX = dx / totalDist, dirZ = dz / totalDist; - // Split the horizontal distance between the flight and the bounces (geometric - // decay) so the plank also moves forward on bounces, not just up; sum = 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; @@ -140,7 +173,7 @@ export class LootC { step *= BOUNCE_FORWARD; peak *= BOUNCE_HEIGHT; ms *= BOUNCE_TIME; } - // 3) final impact: sharp squash (bottom on the ground) → springs back to normal + // Landing pop: squash on the ground, then spring back to normal. const groundY = restY - PIECE_SIZE / 2; const pop = new TWEEN.Tween({ k: 0 }, this.tweens) .to({ k: 1 }, LAND_POP_MS) @@ -151,8 +184,7 @@ export class LootC { piece.position.y = groundY + (PIECE_SIZE * s) / 2; // bottom stays on the ground }) .onComplete(() => { - // After resting briefly → flies into the UI icon. - setTimeout(() => this.collect(piece), COLLECT_DELAY_MS); + setTimeout(() => this.collect(piece), COLLECT_DELAY_MS); // then fly to the UI }); prev!.chain(pop); @@ -160,9 +192,8 @@ export class LootC { } /** - * Collect (#8): project the piece into screen pixels, swap the 3D sprite for an - * HTML image of the same size, shrink it to the UI icon size and fly it to the - * top-right corner — sizes match there, so the "arrival" is seamless. + * Collect: swap the 3D sprite for an HTML image at the same screen spot, then + * shrink it and fly it into the top-right UI counter. */ private static collect(piece: Sprite) { if (!this.pieces.includes(piece)) return; // already collected/removed @@ -171,12 +202,12 @@ export class LootC { if (!cam || !canvas) return; const rect = canvas.getBoundingClientRect(); - // sprite center and top → screen pixels (for on-screen position and size) + // 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); - // drop the 3D sprite, replace it with an HTML image at the same point/size + // Drop the 3D sprite; the HTML image takes over from the same spot. this.remove(piece); const flier = document.createElement("img"); @@ -186,50 +217,53 @@ export class LootC { `z-index:1000; pointer-events:none; transform:translate(-50%,-50%); will-change:left,top,width,height;`; document.body.appendChild(flier); - const st = { x: center.x, y: center.y, size: sizePx }; - const apply = () => { - flier.style.left = `${st.x}px`; - flier.style.top = `${st.y}px`; - flier.style.width = `${st.size}px`; - flier.style.height = `${st.size}px`; - }; - apply(); + // White "glint" copy that rides on top of the flier and fades out as it moves. + const flash = document.createElement("img"); + flash.src = woodIconUrl; + flash.style.cssText = flier.style.cssText; + flash.style.filter = "brightness(0) invert(1)"; // solid white silhouette + flash.style.zIndex = "1002"; + document.body.appendChild(flash); const target = this.uiIconCenter(); - // 1) shrink to UI size in place → 2) fly to the corner - const shrink = new TWEEN.Tween(st, this.tweens) - .to({ size: UI_ICON_SIZE }, SHRINK_MS) - .easing(TWEEN.Easing.Quadratic.Out) - .onUpdate(apply); + + // 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 }; + 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 apply = () => { place(flier); place(flash); flash.style.opacity = `${st.o}`; }; + 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) .onUpdate(apply) - .onComplete(() => { flier.remove(); this.pulseUiIcon(); }); - shrink.chain(fly); - - // Smooth white blink before the flight: a white copy of the plank fades in - // and out on top ("collected" feedback), then the shrink + flight. - const flash = document.createElement("img"); - flash.src = woodIconUrl; - flash.style.cssText = flier.style.cssText; // same position/size - flash.style.filter = "brightness(0) invert(1)"; // solid white silhouette - flash.style.opacity = "0"; - flash.style.zIndex = "1002"; - document.body.appendChild(flash); - - const fl = { o: 0 }; - const setO = () => { flash.style.opacity = `${fl.o}`; }; - const flashIn = new TWEEN.Tween(fl, this.tweens) + .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) + .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(setO); - const flashOut = new TWEEN.Tween(fl, this.tweens) + .onUpdate(apply); + const flashOut = new TWEEN.Tween(st, this.tweens) .to({ o: 0 }, BLINK_MS * 1.6) .easing(TWEEN.Easing.Quadratic.In) - .onUpdate(setO) - .onComplete(() => { flash.remove(); shrink.start(); }); + .onUpdate(apply); flashIn.chain(flashOut); + + // Kick them all off together → blended, fluid collect. + fly.start(); + shrink.start(); flashIn.start(); } diff --git a/src/controllers/LootableC.ts b/src/controllers/LootableC.ts index 76996a0..43ddaa8 100644 --- a/src/controllers/LootableC.ts +++ b/src/controllers/LootableC.ts @@ -1,37 +1,47 @@ -import { Object3D, Vector3 } from "three"; +import { Mesh, Object3D, Vector3 } from "three"; +import * as TWEEN from "@tweenjs/tween.js"; +import { UpdateController } from "@24tools/playable_template"; import { PhysicsBody, PhysicsLayer } from "./PhysicsC"; import { Trigger } from "./TriggerC"; import { LootC } from "./LootC"; +import { VfxManager } from "../resources/vfx/VfxManager"; -// A full crate's health. There are 3 damage levels (S1/S2/S3) splitting it -// evenly: S1 = 100–67%, S2 = 66–34%, S3 = 33–1%, broken at 0. A crate that -// ships already damaged (no S1 node, etc.) starts at the matching lower health. +// Crate health, split evenly across 3 damage states (S1/S2/S3), broken at 0. const CRATE_MAX_HEALTH = 100; const LEVELS = 3; +// --- Hit/break animation tunables --- +const HIT_PUNCH = 0.30; // how hard a hit squashes the crate +const HIT_PUNCH_MS = 280; // squash → spring-back time +const FLASH_MS = 200; // white blink time +const BREAK_MS = 300; // shrink-to-nothing time on break +const BREAK_SPIN = Math.PI * 0.66; // how far it spins while vanishing +const VFX_CENTER_Y = 0.5; // VFX height (crate middle, not its base) + /** One breakable crate. Damage states are indexed by level: 0=S1, 1=S2, 2=S3. */ export interface Crate { root: Object3D; - statesByLevel: (Object3D | null)[]; // length 3; null where that state isn't authored - startLevel: number; // lowest authored state = how damaged it starts + statesByLevel: (Object3D | null)[]; // the 3 damage meshes; null where not authored + startLevel: number; // how damaged it starts level: number; // currently shown level collider: PhysicsBody; trigger: Trigger | null; health: number; maxHealth: number; broken: boolean; + baseScale: Vector3; // resting scale (tweens multiply this) + baseRotY: number; // resting Y rotation + hitTween: TWEEN.Tween<{ k: number }> | null; // live hit-punch tween (killed before re-firing) } /** - * Sets up the crates baked into the map's "Lootable" group. - * - * Each Wooden_Box ships its damage states (S1/S2/S3) all visible at once, but - * NOT every crate has all three — some start at S2 or S3, i.e. pre-damaged. We - * read the lowest authored state, show only it, set health to the matching - * percentage, and turn the collider proxy into a STATIC cannon box. + * Builds the breakable crates from the map's "Lootable" group: shows one damage + * state per crate, sets its health, and gives it a static collider. */ export class LootableC { static crates: Crate[] = []; + private static tweens = new TWEEN.Group(); // our tween group, pumped each frame + private static flashedStates = new WeakSet(); // states whose materials we've cloned for flashing static init(lootableGroup: Object3D | null) { if (!lootableGroup) { @@ -39,6 +49,9 @@ export class LootableC { return; } + // Drive our juice tweens (hit punch, flash, break) every frame. + UpdateController.Instance.onUpdate.addDelegate(() => this.tweens.update()); + lootableGroup.visible = true; lootableGroup.updateWorldMatrix(true, true); // collider world positions must be current @@ -80,6 +93,7 @@ export class LootableC { this.crates.push({ root: crate, statesByLevel, startLevel, level: startLevel, collider, trigger: null, health, maxHealth: CRATE_MAX_HEALTH, broken: false, + baseScale: crate.scale.clone(), baseRotY: crate.rotation.y, hitTween: null, }); } @@ -96,6 +110,10 @@ export class LootableC { return; } + // Per-hit juice: a quick squash-punch + a white blink. + this.punchCrate(crate); + this.flashCrate(crate); + // Map remaining health to a level, never below where this crate started. let level = Math.floor((1 - crate.health / crate.maxHealth) * LEVELS); level = Math.max(crate.startLevel, Math.min(LEVELS - 1, level)); @@ -107,20 +125,101 @@ export class LootableC { } } - /** Crate destroyed: hide it and remove its physics + trigger from the world. */ + /** Crate destroyed: drop loot, then shrink-and-spin away. */ static breakCrate(crate: Crate) { if (crate.broken) return; crate.broken = true; - crate.statesByLevel.forEach(s => { if (s) s.visible = false; }); + // Stop the hit-punch so it doesn't fight the break animation. + crate.hitTween?.stop(); + crate.hitTween = null; + + // Remove physics + trigger now so the player walks through right away. crate.collider.destroy(); crate.trigger?.destroy(); crate.trigger = null; - // TODO #7/#8: spawn wood loot at crate.root world position, scatter with a - // bounce, then tween it into the resource counter. - console.log("[Lootable] crate broken"); - + // Drop loot at the crate's spot. LootC.spawn(crate.root.getWorldPosition(new Vector3())); + + // Shrink to nothing while spinning, then hide the meshes. + const s = { k: 0 }; + new TWEEN.Tween(s, this.tweens) + .to({ k: 1 }, BREAK_MS) + .easing(TWEEN.Easing.Back.In) + .onUpdate(({ k }) => { + const sc = Math.max(0, 1 - k); + crate.root.scale.set(crate.baseScale.x * sc, crate.baseScale.y * sc, crate.baseScale.z * sc); + crate.root.rotation.y = crate.baseRotY + k * BREAK_SPIN; + }) + .onComplete(() => { + crate.statesByLevel.forEach(st => { if (st) st.visible = false; }); + }) + .start(); + + const breakFxPos = crate.root.getWorldPosition(new Vector3()); + breakFxPos.y += VFX_CENTER_Y; + VfxManager.Play("DestroyEffect", null, breakFxPos); + } + + // Squash the crate, then spring back (with a small overshoot pop). + private static punchCrate(crate: Crate) { + crate.hitTween?.stop(); // drop the previous punch so rapid hits don't stack + const s = { k: 0 }; + crate.hitTween = new TWEEN.Tween(s, this.tweens) + .to({ k: 1 }, HIT_PUNCH_MS) + .easing(TWEEN.Easing.Back.Out) + .onUpdate(({ k }) => { + const q = (1 - k) * HIT_PUNCH; // HIT_PUNCH (squashed) → 0 (rest) + crate.root.scale.set( + crate.baseScale.x * (1 + q), + crate.baseScale.y * (1 - q), + crate.baseScale.z * (1 + q), + ); + }) + .onComplete(() => { + crate.root.scale.copy(crate.baseScale); // snap back exactly + crate.hitTween = null; + }) + .start(); + + // Hit spark at the crate's middle (world position, no parent). + const hitPos = crate.root.getWorldPosition(new Vector3()); + hitPos.y += VFX_CENTER_Y; + VfxManager.Play("HitEffect", null, hitPos); + } + + // Flash the crate white, then fade out. Materials are cloned per state so the + // flash doesn't bleed onto other crates sharing the same material. + private static flashCrate(crate: Crate) { + const state = crate.statesByLevel[crate.level]; + if (!state) return; + + if (!this.flashedStates.has(state)) { + state.traverse(o => { + const mesh = o as Mesh; + if (!mesh.isMesh) return; + mesh.material = Array.isArray(mesh.material) + ? mesh.material.map(m => m.clone()) + : (mesh.material as any).clone(); + }); + this.flashedStates.add(state); + } + + const mats: any[] = []; + state.traverse(o => { + const mesh = o as Mesh; + if (!mesh.isMesh) return; + (Array.isArray(mesh.material) ? mesh.material : [mesh.material]).forEach(m => mats.push(m)); + }); + mats.forEach(m => { if (m.emissive) m.emissive.setRGB(1, 1, 1); }); + + const s = { k: 1 }; + new TWEEN.Tween(s, this.tweens) + .to({ k: 0 }, FLASH_MS) + .easing(TWEEN.Easing.Quadratic.Out) + .onUpdate(({ k }) => { mats.forEach(m => { if (m.emissive) m.emissiveIntensity = k; }); }) + .onComplete(() => { mats.forEach(m => { if (m.emissive) m.emissiveIntensity = 0; }); }) + .start(); } } diff --git a/src/controllers/PayZoneC.ts b/src/controllers/PayZoneC.ts new file mode 100644 index 0000000..734b3f7 --- /dev/null +++ b/src/controllers/PayZoneC.ts @@ -0,0 +1,201 @@ +import { Object3D, Raycaster, Vector3 } from "three"; +import * as TWEEN from "@tweenjs/tween.js"; +import { UpdateController, CameraC_internal } from "@24tools/playable_template"; +import { Trigger } from "./TriggerC"; +import { TestSceneC } from "./TestSceneC"; +import { LootC } from "./LootC"; +import { woodIconUrl } from "../resources/images/woodIcon"; + +// --- Tunables --- +const COST = 15; // wood needed to fully pay the zone +const ZONE_FALLBACK = new Vector3(0, 0, 0); // zone spot if UI_Interactive_Zone_02 is missing +const ZONE_OFFSET = new Vector3(0, 0, -4); // shift the zone toward the player (off the crates) +const ZONE_SCALE = 1.6; // scale the pad up for presence +const ICON_LIFT = 0.04; // wood icon height above the pad (local) +const TRIGGER_HALF = 1.0; // enter-trigger half size (x/z) +const PAY_INTERVAL = 0.09; // seconds between flying planks +const FLY_MS = 420; // UI → zone flight time +const FLY_ARC_PX = 90; // height of the flight arc (px) +const PLANK_PX = 26; // flying plank size (px) +const FILL_GROW = 0.15; // how much the pad grows when full +const PULSE_MS = 160; // per-plank pulse time + +const _v = new Vector3(); +const _ndc = new Vector3(); + +/** + * Pay zone: the player stands on the UI_Tool_Zone pad and the wood collected in + * the top-right counter flies into it, plank by plank. When COST is reached the + * pad pops and disappears. + */ +export class PayZoneC { + private static zone: Object3D | null = null; + private static trigger: Trigger | null = null; + private static tweens = new TWEEN.Group(); // our tween group, pumped each frame + + private static inside = false; // is the player standing on the pad + private static done = false; // already paid in full + private static paid = 0; // wood delivered so far + private static payCd = 0; // cooldown until the next plank + + private static baseScale = new Vector3(1, 1, 1); // pad resting scale (tweens multiply it) + private static pulseTween: TWEEN.Tween<{ k: number }> | null = null; + + static init() { + const zone = TestSceneC.payZone; + if (!zone) { console.warn("[PayZone] UI_Tool_Zone not found"); return; } + this.zone = zone; + + // Where to put the pad: UI_Interactive_Zone_02's spot, shifted toward the + // player, sitting on the real surface (raycast — the road is above the sand). + const target = _v.copy(ZONE_FALLBACK); + if (TestSceneC.interactiveZone) TestSceneC.interactiveZone.getWorldPosition(target); + target.x += ZONE_OFFSET.x; + target.z += ZONE_OFFSET.z; + target.y = this.sampleSurfaceY(target.x, target.z) + 0.02; + + // Make the wood icon a child of the pad so it moves/vanishes with it. + const icon = TestSceneC.payZoneIcon; + if (icon) { + zone.attach(icon); + icon.position.set(0, ICON_LIFT, 0); + icon.visible = true; + } + + // Place, scale, remember the resting scale, show. + zone.position.copy(target); + zone.scale.multiplyScalar(ZONE_SCALE); + this.baseScale.copy(zone.scale); + zone.visible = true; + + // Trigger that tells us when the player is on the pad. + this.trigger = new Trigger( + { x: target.x, y: target.y + 0.5, z: target.z }, + { x: TRIGGER_HALF, y: 1.0, z: TRIGGER_HALF }, + { onEnter: () => { this.inside = true; }, onExit: () => { this.inside = false; } }, + ); + + UpdateController.Instance.onUpdate.addDelegate((d) => this.update(d)); + } + + private static update(delta: number) { + this.tweens.update(); + if (this.done || !this.inside) return; + + // One plank at a time: wait out the cooldown, stop if paid or out of wood. + this.payCd -= delta; + if (this.payCd > 0) return; + if (this.paid >= COST || LootC.getBalance() <= 0) return; + + // Spend one wood and fly it into the zone. + LootC.spend(1); + this.paid++; + this.payCd = PAY_INTERVAL; + this.flyOnePlank(); + + if (this.paid >= COST) this.complete(); + } + + // Fly one HTML plank from the UI counter to the zone along an arc. + private static flyOnePlank() { + const src = LootC.uiIconScreenCenter(); + const tgt = this.zoneScreenCenter(); + if (!tgt) return; + + const img = document.createElement("img"); + img.src = woodIconUrl; + img.style.cssText = + `position:fixed; left:0; top:0; width:${PLANK_PX}px; height:${PLANK_PX}px;` + + `z-index:1000; pointer-events:none; transform:translate(-50%,-50%); will-change:left,top;`; + document.body.appendChild(img); + + const st = { t: 0 }; + new TWEEN.Tween(st, this.tweens) + .to({ t: 1 }, FLY_MS) + .easing(TWEEN.Easing.Quadratic.In) + .onUpdate(({ t }) => { + const x = src.x + (tgt.x - src.x) * t; + const y = src.y + (tgt.y - src.y) * t - Math.sin(Math.PI * t) * FLY_ARC_PX; // arc + img.style.left = `${x}px`; + img.style.top = `${y}px`; + }) + .onComplete(() => { img.remove(); this.onPlankArrived(); }) + .start(); + } + + // A plank reached the zone → pulse the pad (bigger the fuller it is). + private static onPlankArrived() { + const zone = this.zone; + if (!zone || this.done) return; + + const fill = FILL_GROW * (this.paid / COST); + this.pulseTween?.stop(); // drop the previous pulse so they don't fight + const s = { k: 0 }; + this.pulseTween = new TWEEN.Tween(s, this.tweens) + .to({ k: 1 }, PULSE_MS) + .easing(TWEEN.Easing.Back.Out) + .onUpdate(({ k }) => { + const g = 1 + fill + (1 - k) * 0.12; // settle at fill size with a small punch + zone.scale.set(this.baseScale.x * g, this.baseScale.y * g, this.baseScale.z * g); + }) + .onComplete(() => { this.pulseTween = null; }) + .start(); + } + + // Paid in full: grow, then shrink to nothing. + private static complete() { + this.done = true; + this.trigger?.destroy(); + this.trigger = null; + this.pulseTween?.stop(); + this.pulseTween = null; + + const zone = this.zone; + if (!zone) return; + + const setScale = (g: number) => zone.scale.set(this.baseScale.x * g, this.baseScale.y * g, this.baseScale.z * g); + + const grow = new TWEEN.Tween({ k: 0 }, this.tweens) + .to({ k: 1 }, 180) + .easing(TWEEN.Easing.Back.Out) + .onUpdate(({ k }) => setScale(1 + 0.45 * k)); + const vanish = new TWEEN.Tween({ k: 0 }, this.tweens) + .to({ k: 1 }, 260) + .easing(TWEEN.Easing.Back.In) + .onUpdate(({ k }) => setScale(1.45 * Math.max(0, 1 - k))) + .onComplete(() => { + zone.visible = false; + if (TestSceneC.payZoneIcon) TestSceneC.payZoneIcon.visible = false; // hide the icon too + }); + + grow.chain(vanish); + grow.start(); + console.log("[PayZone] paid in full"); + } + + // Ground height (road/sand) at an x/z via a downward ray. + private static sampleSurfaceY(x: number, z: number): number { + const env = TestSceneC.environment; + if (env) { + const ray = new Raycaster(new Vector3(x, 5, z), new Vector3(0, -1, 0)); + const hits = ray.intersectObject(env, true); + if (hits.length) return hits[0].point.y; + } + return TestSceneC.groundY; + } + + // Zone world center → screen pixels (target for the flying planks). + private static zoneScreenCenter(): { x: number; y: number } | null { + const cam = CameraC_internal.camera; + const canvas = document.querySelector("canvas"); + const zone = this.zone; + if (!cam || !canvas || !zone) return null; + const rect = canvas.getBoundingClientRect(); + _v.copy(zone.position); _v.y += 0.3; + _ndc.copy(_v).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, + }; + } +} diff --git a/src/controllers/PlayerC.ts b/src/controllers/PlayerC.ts index d9c1c03..b0fd78e 100644 --- a/src/controllers/PlayerC.ts +++ b/src/controllers/PlayerC.ts @@ -13,15 +13,18 @@ enum MoveState { Idle = "idle", Walk = "walk", Run = "run" } const CHAR_RADIUS = 0.35; -// Extra time the bat keeps swinging after the hit that breaks the last crate, -// so the strike completes visually before returning to idle. +// How long the bat keeps swinging after the killing hit, before going idle. const ATTACK_FOLLOW_THROUGH = 0.25; -// How fast the feet ease toward the sampled ground height (per second). -// Higher = snappier / hugs the surface tighter; lower = smoother but lags more -// on slopes and curbs. This is what kills the jolt when crossing a curb. +// How fast the feet ease toward the ground height (per second). Higher = snappier. const GROUND_SMOOTH = 12; +// The two moments in the Loot clip where each swing connects (0..1 of the clip). +const IMPACT_PHASES = [0.38, 0.68]; +// The bat can hit only within ±this of an impact phase — outside it (wind-up / +// follow-through) it deals no damage. Bigger = longer hit window. +const CONTACT_WINDOW = 0.14; + const _inputTarget = new Vector3(); export class PlayerC { @@ -44,21 +47,17 @@ export class PlayerC { private static hasAttackTarget = false; private static attackAction: AnimationAction | null = null; // the "Loot" clip used as the swing - // Cached local ends of the bat mesh (computed once) → its swinging tip in - // world space. CombatC reads getBatTip() to damage a crate only when the bat - // actually reaches it (geometric contact, no animation-time markers). + // Swing hit-window tracking. _activeWindow = which IMPACT_PHASES window we're in + // (-1 = none). _swingId bumps on entering a window, so CombatC knows it's a new swing. + private static _activeWindow = -1; + private static _swingId = 0; + + // Cached bat-mesh ends → its swinging tip in world space (used by getBatTip). private static _batEndA: Vector3 | null = null; private static _batEndB: Vector3 | null = null; private static _tipA = new Vector3(); private static _tipB = new Vector3(); - // Swing counter: the Loot clip contains TWO strikes (a left swing then a - // right swing), so this bumps TWICE per loop — at the half-way point and at - // the wrap. CombatC uses it to damage every touched crate once per strike, - // so a crate the bat sweeps over in both swings takes two sequential hits. - private static swingCycle = 0; - private static _prevStrike = 0; - // When the killing blow lands we don't cut the swing at the impact frame — // we let the bat follow through for this long, then return to idle. private static finishing = false; @@ -117,8 +116,8 @@ export class PlayerC { this.currentAction?.fadeOut(0.15); this.attackAction.reset().fadeIn(0.15).play(); this.currentAction = this.attackAction; - this._prevStrike = 0; - this.swingCycle++; // new attack = fresh strike + this._activeWindow = -1; // not in a live window yet this attack + this._swingId = 0; } else if (!active) { this.hasAttackTarget = false; this.attackAction?.fadeOut(0.15); @@ -293,15 +292,19 @@ export class PlayerC { // locomotion state machine below is suspended so it can't override it. if (this.attacking) { if (this.hasAttackTarget) this.faceTowards(this.attackTarget.x, this.attackTarget.z, delta); - // Split the Loot clip into its two strikes (first half = left swing, - // second half = right swing). Bump the counter on each → two hits/loop. + // Which hit-window the Loot clip is in now (-1 = none → bat can't hit). const a = this.attackAction; if (a) { const dur = a.getClip().duration; - const phase = dur > 0 ? (a.time % dur) / dur : 0; // 0..1 within the swing - const strike = phase < 0.5 ? 0 : 1; - if (strike !== this._prevStrike) this.swingCycle++; - this._prevStrike = strike; + const phase = dur > 0 ? (a.time % dur) / dur : 0; // 0..1 within the clip + let wi = -1; + for (let i = 0; i < IMPACT_PHASES.length; i++) { + if (Math.abs(phase - IMPACT_PHASES[i]) <= CONTACT_WINDOW) { wi = i; break; } + } + if (wi !== this._activeWindow) { + if (wi !== -1) this._swingId++; // entered a new window → new swing + this._activeWindow = wi; + } } if (this.finishing) { this.finishTimer -= delta; @@ -331,9 +334,12 @@ export class PlayerC { } } - // World position of the bat's swinging tip (the end farther from the body), - // or null when the bat isn't in hand. CombatC uses this to damage a crate the - // instant the bat actually reaches it. + /** Current swing id while the bat is in a hit window, or 0 when it can't hit. */ + static getActiveSwingId(): number { + return this._activeWindow === -1 ? 0 : this._swingId; + } + + /** World position of the bat tip, or null if the bat isn't in hand. */ static getBatTip(out: Vector3): Vector3 | null { const bat = this.batInHand as Mesh | null; if (!bat || !bat.visible || !bat.geometry) return null; @@ -356,11 +362,6 @@ export class PlayerC { return out.copy(farther); } - /** Index of the current strike (bumps twice per Loot loop: left then right swing). */ - static getSwingCycle(): number { - return this.swingCycle; - } - // Smoothly rotate the mesh's Y so it faces the given world x/z point. private static faceTowards(x: number, z: number, delta: number) { const dx = x - this.mesh.position.x; diff --git a/src/controllers/TestSceneC.ts b/src/controllers/TestSceneC.ts index be919a7..9d083e2 100644 --- a/src/controllers/TestSceneC.ts +++ b/src/controllers/TestSceneC.ts @@ -13,6 +13,9 @@ export class TestSceneC { static environment: Object3D | null = null; // Ground_ — visual ground/road/borders, the only thing rendered static colliderGroup: Object3D | null = null; // Colliders — invisible BoxCollider proxies → cannon bodies static lootableGroup: Object3D | null = null; // Lootable — interactive crates, hidden until activated per-crate + static payZone: Object3D | null = null; // UI_Tool_Zone — the pay-zone pad + static payZoneIcon: Object3D | null = null; // UI_Wood — wood icon shown on the pad + static interactiveZone: Object3D | null = null; // UI_Interactive_Zone_02 — marks where the pad goes // World Y of the walkable sand surface (the floor proxy is aligned to it). static groundY = 0; @@ -38,8 +41,10 @@ export class TestSceneC { this.environment = this.mapObject.getObjectByName("Ground_") ?? null; this.colliderGroup = this.mapObject.getObjectByName("Colliders") ?? null; this.lootableGroup = this.mapObject.getObjectByName("Lootable") ?? null; - const uiGroup = this.mapObject.getObjectByName("UI") ?? null; - const uiWood = this.mapObject.getObjectByName("UI_Wood") ?? null; + this.payZone = this.mapObject.getObjectByName("UI_Tool_Zone") ?? null; + this.payZoneIcon = this.mapObject.getObjectByName("UI_Wood") ?? null; + this.interactiveZone = this.mapObject.getObjectByName("UI_Interactive_Zone_02") ?? null; + const uiGroup = this.mapObject.getObjectByName("UI") ?? null; // Add the whole graph so every world transform stays intact (the collider // proxies' world positions depend on the full parent chain), then hide @@ -56,7 +61,14 @@ export class TestSceneC { if (this.colliderGroup) this.colliderGroup.visible = false; // physics-only, never rendered // Lootable crates are set up by LootableC (one state shown + colliders). if (uiGroup) uiGroup.visible = false; // playable UI is HTML/CSS, not in-world - if (uiWood) uiWood.visible = false; + if (this.interactiveZone) this.interactiveZone.visible = false; // only its position is used + // payZone + its icon are placed and shown by PayZoneC. + if (this.payZone) this.payZone.visible = false; + if (this.payZoneIcon) this.payZoneIcon.visible = false; + // Hide the unused duplicate wood icon (three.js drops the dot → "UI_Wood001"). + const strayWoodIcon = this.mapObject.getObjectByName("UI_Wood001") + ?? this.mapObject.getObjectByName("UI_Wood.001"); + if (strayWoodIcon) strayWoodIcon.visible = false; // Only the environment is rendered with shadows. if (this.environment) { diff --git a/src/enums/ResourcesType.ts b/src/enums/ResourcesType.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/enums/VFXType.ts b/src/enums/VFXType.ts new file mode 100644 index 0000000..0bea72c --- /dev/null +++ b/src/enums/VFXType.ts @@ -0,0 +1,4 @@ +export enum VFXType { + HitEffect, + DestroyEffect +} \ No newline at end of file diff --git a/src/resources/images/woodIcon.ts b/src/resources/images/woodIcon.ts index a0c7bbd..2ba0874 100644 --- a/src/resources/images/woodIcon.ts +++ b/src/resources/images/woodIcon.ts @@ -1,5 +1,5 @@ import { ConvertToBase64WhenRelease } from "@24tools/ads_common"; -// URL дерев'яної іконки лута. У релізі інлайниться в base64 (як меші/звуки). -// Шлях відносно цього файлу. +// URL of the wood loot icon. In a release build it's inlined as base64 (like +// meshes/sounds). Path is relative to this file. export const woodIconUrl = ConvertToBase64WhenRelease("./Icon_Wood.webp"); diff --git a/src/resources/resources.ts b/src/resources/resources.ts index 280f359..e60cfcf 100644 --- a/src/resources/resources.ts +++ b/src/resources/resources.ts @@ -1,5 +1,6 @@ import { ConvertResourcesType } from "@24tools/playable_template"; import { meshes } from "./meshes/meshes"; import { sounds } from "./sounds/sounds"; +import { vfx_json } from "./vfx/vfx_json"; -export const resources: ConvertResourcesType = [meshes, sounds]; +export const resources: ConvertResourcesType = [meshes, sounds, vfx_json]; diff --git a/src/resources/vfx/VfxManager.ts b/src/resources/vfx/VfxManager.ts index 5dd7c2d..766c611 100644 --- a/src/resources/vfx/VfxManager.ts +++ b/src/resources/vfx/VfxManager.ts @@ -1,17 +1,16 @@ import { BatchedRenderer, QuarksLoader, QuarksUtil } from "three.quarks"; import { Object3D, Euler, Vector3 } from "three"; import { ResourcesC, UpdateController } from "@24tools/playable_template"; -import { ThreeC } from "../../ThreeC"; -import { TimeC } from "../Timers/TimeC"; -import { VFXType } from "../Enums/VFXType"; -import { ResourcesType } from "../Enums/ResourcesType"; -import { vfxTest } from "./RunTimeTest"; +import { ThreeC } from "../../controllers/ThreeC"; + +// Resource "type" under which Quark VFX JSONs are registered (see resources.ts). +const VFX_RESOURCE_TYPE = "vfx_json"; export class VfxManager { static batchRenderer: BatchedRenderer; static loader: QuarksLoader; - static Init() { + static init() { this.batchRenderer = new BatchedRenderer(); this.loader = new QuarksLoader(); ThreeC.addToScene(this.batchRenderer); @@ -26,24 +25,30 @@ export class VfxManager { } static update(delta: number) { - delta *= TimeC.TimeScale; this.batchRenderer.update(delta); } - static Play(type: VFXType|string, parent: Object3D | null = null, position: Vector3 | null = null, rotation: Euler | null = null, scale: Vector3 | null = null, odred: number | null = null) { - let loaded = (ResourcesC.getResource(ResourcesType.VFX, type.toString()) as { obj: any }).obj; - if (!loaded) return new Object3D(); - // console.error("Type non loaded " + loaded); - const effect = loaded.clone(true) as Object3D; + static Play( + name: string, + parent: Object3D | null = null, + position: Vector3 | null = null, + rotation: Euler | null = null, + scale: Vector3 | null = null, + renderOrder: number | null = null, + ): Object3D { + const resource = ResourcesC.getResource<{ obj: Object3D } | undefined>(VFX_RESOURCE_TYPE, name); + if (!resource?.obj) return new Object3D(); + + const effect = resource.obj.clone(true); QuarksUtil.setAutoDestroy(effect, true); QuarksUtil.addToBatchRenderer(effect, this.batchRenderer); - if (parent) parent.add(effect) + if (parent) parent.add(effect); else ThreeC.addToScene(effect); if (position) effect.position.copy(position); if (rotation) effect.rotation.copy(rotation); if (scale) effect.scale.copy(scale); - if (odred) effect.renderOrder = odred; + if (renderOrder != null) effect.renderOrder = renderOrder; return effect; } diff --git a/src/resources/vfx/files/VFX_Lootable_Destroy.json b/src/resources/vfx/files/VFX_Lootable_Destroy.json new file mode 100644 index 0000000..54ca73d --- /dev/null +++ b/src/resources/vfx/files/VFX_Lootable_Destroy.json @@ -0,0 +1 @@ +{"metadata":{"version":4.6,"type":"Object","generator":"Object3D.toJSON"},"geometries":[{"uuid":"b00f800b-07dc-42e4-983f-ce4384bf8465","type":"PlaneGeometry","name":"_geometry","width":1,"height":1,"widthSegments":1,"heightSegments":1},{"uuid":"52cbc31f-4506-4ae4-82f7-281aee003c89","type":"PlaneGeometry","name":"BreakingDust_TestNormalBlend_geometry","width":1,"height":1,"widthSegments":1,"heightSegments":1}],"materials":[{"uuid":"c2d19b71-bcf8-42fe-a21a-32fe08815584","type":"MeshBasicMaterial","color":16777215,"map":"24199738-679d-433c-8793-47a02a5cb389","envMapRotation":[0,0,0,"XYZ"],"reflectivity":1,"refractionRatio":0.98,"blending":2,"transparent":true,"blendColor":0},{"uuid":"2b57c4e4-cbfa-44fc-9ff9-cac8eb169f42","type":"MeshBasicMaterial","color":16777215,"map":"78fb6e7c-6965-42f5-adda-26becbd923ad","envMapRotation":[0,0,0,"XYZ"],"reflectivity":1,"refractionRatio":0.98,"blending":2,"transparent":true,"blendColor":0},{"uuid":"08ce040f-d44c-4ec3-9f4b-be635e57f30f","type":"MeshBasicMaterial","color":16777215,"map":"d8a227ed-8e47-4c1a-9568-debc9c58a4e8","envMapRotation":[0,0,0,"XYZ"],"reflectivity":1,"refractionRatio":0.98,"transparent":true,"blendColor":0},{"uuid":"11389946-5878-4ac2-b05c-06dc2c89b988","type":"MeshBasicMaterial","color":16777215,"map":"f991f769-e88f-4821-ac06-135eff9a0100","envMapRotation":[0,0,0,"XYZ"],"reflectivity":1,"refractionRatio":0.98,"transparent":true,"blendColor":0}],"textures":[{"uuid":"24199738-679d-433c-8793-47a02a5cb389","name":"ST_T_Dust_Small.webp","image":"0c7091f9-93b8-431f-b684-89dda0df9912","mapping":300,"channel":0,"repeat":[1,1],"offset":[0,0],"center":[0,0],"rotation":0,"wrap":[1001,1001],"format":1023,"internalFormat":null,"type":1009,"colorSpace":"","minFilter":1008,"magFilter":1006,"anisotropy":1,"flipY":true,"generateMipmaps":true,"premultiplyAlpha":false,"unpackAlignment":4},{"uuid":"78fb6e7c-6965-42f5-adda-26becbd923ad","name":"default_texture","image":"94acb485-4fdf-4130-893e-24f09c945b8e","mapping":300,"channel":0,"repeat":[1,1],"offset":[0,0],"center":[0,0],"rotation":0,"wrap":[1001,1001],"format":1023,"internalFormat":null,"type":1009,"colorSpace":"","minFilter":1008,"magFilter":1006,"anisotropy":1,"flipY":true,"generateMipmaps":true,"premultiplyAlpha":false,"unpackAlignment":4},{"uuid":"d8a227ed-8e47-4c1a-9568-debc9c58a4e8","name":"ST_T_Debris.webp","image":"88e709ba-fe36-484b-8aa1-b1541a7e5212","mapping":300,"channel":0,"repeat":[1,1],"offset":[0,0],"center":[0,0],"rotation":0,"wrap":[1001,1001],"format":1023,"internalFormat":null,"type":1009,"colorSpace":"","minFilter":1008,"magFilter":1006,"anisotropy":1,"flipY":true,"generateMipmaps":true,"premultiplyAlpha":false,"unpackAlignment":4},{"uuid":"f991f769-e88f-4821-ac06-135eff9a0100","name":"ST_T_Dust - Copy.webp","image":"58d4ac6a-1ccb-44f6-bb7e-e2823866cf68","mapping":300,"channel":0,"repeat":[1,1],"offset":[0,0],"center":[0,0],"rotation":0,"wrap":[1001,1001],"format":1023,"internalFormat":null,"type":1009,"colorSpace":"","minFilter":1008,"magFilter":1006,"anisotropy":1,"flipY":true,"generateMipmaps":true,"premultiplyAlpha":false,"unpackAlignment":4}],"images":[{"uuid":"0c7091f9-93b8-431f-b684-89dda0df9912","url":"data:image/webp;base64,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"},{"uuid":"94acb485-4fdf-4130-893e-24f09c945b8e","url":"data:image/png;base64,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"},{"uuid":"88e709ba-fe36-484b-8aa1-b1541a7e5212","url":"data:image/webp;base64,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"},{"uuid":"58d4ac6a-1ccb-44f6-bb7e-e2823866cf68","url":"data:image/webp;base64,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"}],"object":{"uuid":"e5ff142e-0406-400f-a06d-893214b17fb7","type":"Group","name":"VFX_Lootable_Destroy","layers":1,"matrix":[1,0,0,0,0,1,0,0,0,0,1.0000000000000022,0,0,0.23,0,1],"up":[0,1,0],"children":[{"uuid":"90bfb6b2-3a79-41ab-b264-64001c0a4ba5","type":"ParticleEmitter","name":"Smoke_2","layers":1,"matrix":[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1],"up":[0,1,0],"ps":{"version":"3.0","autoDestroy":false,"looping":false,"prewarm":false,"duration":1,"shape":{"type":"cone","radius":0.3,"arc":6.283185307179586,"thickness":1,"angle":0.7853981633974483,"mode":0,"spread":0,"speed":{"type":"ConstantValue","value":1}},"startLife":{"type":"ConstantValue","value":0.5},"startSpeed":{"type":"ConstantValue","value":1},"startRotation":{"type":"ConstantValue","value":0},"startSize":{"type":"ConstantValue","value":1},"startColor":{"type":"ConstantColor","color":{"r":1,"g":1,"b":1,"a":0.5}},"emissionOverTime":{"type":"ConstantValue","value":5},"emissionOverDistance":{"type":"ConstantValue","value":0},"emissionBursts":[{"time":0,"count":{"type":"ConstantValue","value":1},"probability":1,"interval":0.01,"cycle":1}],"onlyUsedByOther":false,"instancingGeometry":"b00f800b-07dc-42e4-983f-ce4384bf8465","renderOrder":0,"renderMode":0,"rendererEmitterSettings":{},"material":"c2d19b71-bcf8-42fe-a21a-32fe08815584","layers":1,"startTileIndex":{"type":"ConstantValue","value":0},"uTileCount":1,"vTileCount":1,"blendTiles":false,"softParticles":false,"softFarFade":0,"softNearFade":0,"behaviors":[{"type":"SizeOverLife","size":{"type":"PiecewiseBezier","functions":[{"function":{"p0":0.5374011537017755,"p1":0.5963267188006545,"p2":0.6670373969193091,"p3":0.7023927359786364},"start":0}]}},{"type":"RotationOverLife","angularVelocity":{"type":"ConstantValue","value":0.87266463}},{"type":"ColorOverLife","color":{"type":"Gradient","color":{"type":"CLinearFunction","subType":"Color","keys":[{"value":{"r":0.9803921568627451,"g":0.9803921568627451,"b":0.9803921568627451},"pos":0},{"value":{"r":0.6313725490196078,"g":0.6313725490196078,"b":0.6313725490196078},"pos":1}]},"alpha":{"type":"CLinearFunction","subType":"Number","keys":[{"value":1,"pos":0},{"value":0,"pos":0.99609375}]}}}],"worldSpace":false}},{"uuid":"d602bae1-d346-42ca-9150-c2230e78cdee","type":"ParticleEmitter","name":"Pieces","layers":1,"matrix":[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1],"up":[0,1,0],"ps":{"version":"3.0","autoDestroy":false,"looping":false,"prewarm":false,"duration":0.2,"shape":{"type":"cone","radius":0.03,"arc":6.283185307179586,"thickness":1,"angle":0.7853981633974483,"mode":0,"spread":0,"speed":{"type":"ConstantValue","value":1}},"startLife":{"type":"ConstantValue","value":0.4},"startSpeed":{"type":"ConstantValue","value":5},"startRotation":{"type":"ConstantValue","value":0},"startSize":{"type":"ConstantValue","value":0.15},"startColor":{"type":"ConstantColor","color":{"r":1,"g":1,"b":1,"a":1}},"emissionOverTime":{"type":"ConstantValue","value":30},"emissionOverDistance":{"type":"ConstantValue","value":0},"emissionBursts":[{"time":0,"count":{"type":"ConstantValue","value":10},"probability":1,"interval":0.01,"cycle":1}],"onlyUsedByOther":false,"instancingGeometry":"b00f800b-07dc-42e4-983f-ce4384bf8465","renderOrder":0,"renderMode":0,"rendererEmitterSettings":{},"material":"2b57c4e4-cbfa-44fc-9ff9-cac8eb169f42","layers":1,"startTileIndex":{"type":"ConstantValue","value":0},"uTileCount":1,"vTileCount":1,"blendTiles":false,"softParticles":false,"softFarFade":0,"softNearFade":0,"behaviors":[{"type":"ColorOverLife","color":{"type":"Gradient","color":{"type":"CLinearFunction","subType":"Color","keys":[{"value":{"r":0.27058823529411763,"g":0.27058823529411763,"b":0.27058823529411763},"pos":0},{"value":{"r":0.27058823529411763,"g":0.27058823529411763,"b":0.27058823529411763},"pos":1}]},"alpha":{"type":"CLinearFunction","subType":"Number","keys":[{"value":1,"pos":0},{"value":0,"pos":1}]}}}],"worldSpace":false}},{"uuid":"cd26b80a-8eb6-4d42-8105-e3a9157ad4e0","type":"ParticleEmitter","name":"Ground_Dirt","layers":1,"matrix":[2,0,0,0,0,2,0,0,0,0,2,0,0,0,0,1],"up":[0,1,0],"ps":{"version":"3.0","autoDestroy":false,"looping":false,"prewarm":false,"duration":1,"shape":{"type":"point"},"startLife":{"type":"ConstantValue","value":2},"startSpeed":{"type":"ConstantValue","value":0},"startRotation":{"type":"AxisAngle","axis":{"x":0,"y":1,"z":0},"angle":{"type":"IntervalValue","a":0,"b":6.283185}},"startSize":{"type":"ConstantValue","value":1},"startColor":{"type":"ConstantColor","color":{"r":0.42745098039215684,"g":0.32941176470588235,"b":0.17647058823529413,"a":0.25}},"emissionOverTime":{"type":"ConstantValue","value":0},"emissionOverDistance":{"type":"ConstantValue","value":0},"emissionBursts":[{"time":0,"count":{"type":"ConstantValue","value":1},"probability":1,"interval":0.01,"cycle":1}],"onlyUsedByOther":false,"instancingGeometry":"b00f800b-07dc-42e4-983f-ce4384bf8465","renderOrder":0,"renderMode":2,"rendererEmitterSettings":{},"material":"08ce040f-d44c-4ec3-9f4b-be635e57f30f","layers":1,"startTileIndex":{"type":"ConstantValue","value":0},"uTileCount":1,"vTileCount":1,"blendTiles":false,"softParticles":false,"softFarFade":0,"softNearFade":0,"behaviors":[{"type":"ColorOverLife","color":{"type":"Gradient","color":{"type":"CLinearFunction","subType":"Color","keys":[{"value":{"r":1,"g":1,"b":1},"pos":0},{"value":{"r":1,"g":1,"b":1},"pos":1}]},"alpha":{"type":"CLinearFunction","subType":"Number","keys":[{"value":1,"pos":0.68359375},{"value":0,"pos":1}]}}}],"worldSpace":false}},{"uuid":"d44a8e3e-2e7d-4a21-b0ef-f2a7a5b8c6d5","type":"ParticleEmitter","name":"BreakingDust_TestNormalBlend","layers":1,"matrix":[1,0,0,0,0,2.220446049250313e-16,-1,0,0,1,2.220446049250313e-16,0,0,0,0,1],"up":[0,1,0],"ps":{"version":"3.0","autoDestroy":false,"looping":false,"prewarm":false,"duration":1,"shape":{"type":"cone","radius":0.75,"arc":6.283185307179586,"thickness":0.5,"angle":0.7853981633974483,"mode":0,"spread":0,"speed":{"type":"ConstantValue","value":1}},"startLife":{"type":"ConstantValue","value":1},"startSpeed":{"type":"ConstantValue","value":1},"startRotation":{"type":"ConstantValue","value":0.087},"startSize":{"type":"ConstantValue","value":2},"startColor":{"type":"ConstantColor","color":{"r":1,"g":1,"b":1,"a":0.25}},"emissionOverTime":{"type":"ConstantValue","value":0},"emissionOverDistance":{"type":"ConstantValue","value":0},"emissionBursts":[{"time":0,"count":{"type":"ConstantValue","value":20},"probability":1,"interval":0.01,"cycle":1}],"onlyUsedByOther":false,"instancingGeometry":"52cbc31f-4506-4ae4-82f7-281aee003c89","renderOrder":0,"renderMode":4,"rendererEmitterSettings":{},"material":"11389946-5878-4ac2-b05c-06dc2c89b988","layers":1,"startTileIndex":{"type":"ConstantValue","value":0},"uTileCount":1,"vTileCount":1,"blendTiles":false,"softParticles":false,"softFarFade":0,"softNearFade":0,"behaviors":[{"type":"SizeOverLife","size":{"type":"PiecewiseBezier","functions":[{"function":{"p0":0.46166666666666667,"p1":1.035,"p2":0.24,"p3":0.3516666666666667},"start":0}]}},{"type":"ColorOverLife","color":{"type":"Gradient","color":{"type":"CLinearFunction","subType":"Color","keys":[{"value":{"r":1,"g":1,"b":1},"pos":0},{"value":{"r":1,"g":1,"b":1},"pos":1}]},"alpha":{"type":"CLinearFunction","subType":"Number","keys":[{"value":1,"pos":0},{"value":0,"pos":1}]}}}],"worldSpace":true}}]}} \ No newline at end of file diff --git a/src/resources/vfx/files/VFX_Lootable_Hit.json b/src/resources/vfx/files/VFX_Lootable_Hit.json new file mode 100644 index 0000000..07de0b7 --- /dev/null +++ b/src/resources/vfx/files/VFX_Lootable_Hit.json @@ -0,0 +1 @@ +{"metadata":{"version":4.6,"type":"Object","generator":"Object3D.toJSON"},"geometries":[{"uuid":"adb67b77-5c80-425f-b4b1-be92549279e7","type":"PlaneGeometry","width":1,"height":1,"widthSegments":1,"heightSegments":1},{"uuid":"b00f800b-07dc-42e4-983f-ce4384bf8465","type":"PlaneGeometry","name":"_geometry","width":1,"height":1,"widthSegments":1,"heightSegments":1}],"materials":[{"uuid":"81e3dbea-20ec-4181-bada-b356311f8d4f","type":"MeshBasicMaterial","color":16777215,"map":"8a53669e-d554-4f26-a1e8-75c980bb1f2f","envMapRotation":[0,0,0,"XYZ"],"reflectivity":1,"refractionRatio":0.98,"blending":2,"transparent":true,"blendColor":0},{"uuid":"549c213d-5733-491f-84d9-7ec0a775940c","type":"MeshBasicMaterial","color":16777215,"map":"5b08c971-a8cc-41f4-8777-7d4622c93f23","envMapRotation":[0,0,0,"XYZ"],"reflectivity":1,"refractionRatio":0.98,"blending":2,"transparent":true,"blendColor":0}],"textures":[{"uuid":"8a53669e-d554-4f26-a1e8-75c980bb1f2f","name":"cfxr stretch smoke arc dissolve.webp","image":"9c434f28-8daf-4ac0-89b4-17edfe811ad2","mapping":300,"channel":0,"repeat":[1,1],"offset":[0,0],"center":[0,0],"rotation":0,"wrap":[1001,1001],"format":1023,"internalFormat":null,"type":1009,"colorSpace":"","minFilter":1008,"magFilter":1006,"anisotropy":1,"flipY":true,"generateMipmaps":true,"premultiplyAlpha":false,"unpackAlignment":4},{"uuid":"5b08c971-a8cc-41f4-8777-7d4622c93f23","name":"novalines.webp","image":"be6c6b3a-3781-4d85-ba07-b2620f712f60","mapping":300,"channel":0,"repeat":[1,1],"offset":[0,0],"center":[0,0],"rotation":0,"wrap":[1001,1001],"format":1023,"internalFormat":null,"type":1009,"colorSpace":"","minFilter":1008,"magFilter":1006,"anisotropy":1,"flipY":true,"generateMipmaps":true,"premultiplyAlpha":false,"unpackAlignment":4}],"images":[{"uuid":"9c434f28-8daf-4ac0-89b4-17edfe811ad2","url":"data:image/webp;base64,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"},{"uuid":"be6c6b3a-3781-4d85-ba07-b2620f712f60","url":"data:image/webp;base64,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"}],"object":{"uuid":"69898012-621b-4dff-9409-a127223d9549","type":"Group","name":"VFX_Lootable_Hit","layers":1,"matrix":[1,0,0,0,0,1,0,0,0,0,1,0,0,0.75,0,1],"up":[0,1,0],"children":[{"uuid":"17793595-4102-40fb-b7b0-0b755cb75020","type":"ParticleEmitter","name":"StretchSmoke","layers":1,"matrix":[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1],"up":[0,1,0],"ps":{"version":"3.0","autoDestroy":false,"looping":false,"prewarm":false,"duration":1.1,"shape":{"type":"sphere","radius":0.0001,"arc":6.283185307179586,"thickness":1,"mode":0,"spread":0,"speed":{"type":"ConstantValue","value":1}},"startLife":{"type":"IntervalValue","a":0.2,"b":0.3},"startSpeed":{"type":"ConstantValue","value":14},"startRotation":{"type":"ConstantValue","value":0},"startSize":{"type":"ConstantValue","value":0.04},"startColor":{"type":"ConstantColor","color":{"r":1,"g":1,"b":1,"a":0.5}},"emissionOverTime":{"type":"ConstantValue","value":0},"emissionOverDistance":{"type":"ConstantValue","value":0},"emissionBursts":[{"time":0.05,"count":{"type":"ConstantValue","value":20},"probability":1,"interval":0.01,"cycle":1}],"onlyUsedByOther":false,"instancingGeometry":"adb67b77-5c80-425f-b4b1-be92549279e7","renderOrder":0,"renderMode":1,"rendererEmitterSettings":{"speedFactor":1,"lengthFactor":0},"material":"81e3dbea-20ec-4181-bada-b356311f8d4f","layers":1,"startTileIndex":{"type":"ConstantValue","value":0},"uTileCount":1,"vTileCount":1,"blendTiles":false,"softParticles":false,"softFarFade":0,"softNearFade":0,"behaviors":[{"type":"ColorOverLife","color":{"type":"Gradient","color":{"type":"CLinearFunction","subType":"Color","keys":[{"value":{"r":1,"g":1,"b":1},"pos":0},{"value":{"r":1,"g":1,"b":1},"pos":1}]},"alpha":{"type":"CLinearFunction","subType":"Number","keys":[{"value":0,"pos":0.1640625},{"value":1,"pos":0.49609375},{"value":0,"pos":1}]}}},{"type":"LimitSpeedOverLife","speed":{"type":"ConstantValue","value":0.1},"dampen":0.2}],"worldSpace":false}},{"uuid":"7fd6d08c-601c-47b0-b5ad-7aa7860992d4","type":"ParticleEmitter","name":"SharpImpact","layers":1,"matrix":[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1],"up":[0,1,0],"ps":{"version":"3.0","autoDestroy":false,"looping":false,"prewarm":false,"duration":1,"shape":{"type":"sphere","radius":0.3,"arc":6.283185307179586,"thickness":1,"mode":0,"spread":0,"speed":{"type":"ConstantValue","value":1}},"startLife":{"type":"IntervalValue","a":0.2,"b":0.3},"startSpeed":{"type":"IntervalValue","a":2,"b":6},"startRotation":{"type":"IntervalValue","a":0,"b":6.283185},"startSize":{"type":"IntervalValue","a":1,"b":1},"startColor":{"type":"ConstantColor","color":{"r":1,"g":1,"b":1,"a":0.25}},"emissionOverTime":{"type":"ConstantValue","value":0},"emissionOverDistance":{"type":"ConstantValue","value":0},"emissionBursts":[{"time":0,"count":{"type":"ConstantValue","value":20},"probability":1,"interval":0.01,"cycle":1}],"onlyUsedByOther":false,"instancingGeometry":"b00f800b-07dc-42e4-983f-ce4384bf8465","renderOrder":5,"renderMode":0,"rendererEmitterSettings":{},"material":"549c213d-5733-491f-84d9-7ec0a775940c","layers":1,"startTileIndex":{"type":"ConstantValue","value":0},"uTileCount":1,"vTileCount":1,"blendTiles":false,"softParticles":false,"softFarFade":0,"softNearFade":0,"behaviors":[{"type":"SizeOverLife","size":{"type":"PiecewiseBezier","functions":[{"function":{"p0":0,"p1":0.7094638037905021,"p2":0.9993775840769863,"p3":1},"start":0}]}},{"type":"ColorOverLife","color":{"type":"Gradient","color":{"type":"CLinearFunction","subType":"Color","keys":[{"value":{"r":0.9529411764705882,"g":0.9529411764705882,"b":0.8392156862745098},"pos":0},{"value":{"r":0.9529411764705882,"g":0.8274509803921568,"b":0.6235294117647059},"pos":1}]},"alpha":{"type":"CLinearFunction","subType":"Number","keys":[{"value":1,"pos":0},{"value":0,"pos":1}]}}},{"type":"LimitSpeedOverLife","speed":{"type":"ConstantValue","value":1},"dampen":0.25},{"type":"RotationOverLife","angularVelocity":{"type":"IntervalValue","a":-3.4906585,"b":3.4906585}}],"worldSpace":false}}]}} \ No newline at end of file diff --git a/src/resources/vfx/test.json b/src/resources/vfx/files/test.json similarity index 100% rename from src/resources/vfx/test.json rename to src/resources/vfx/files/test.json diff --git a/src/resources/vfx/vfx_json.ts b/src/resources/vfx/vfx_json.ts new file mode 100644 index 0000000..37e20cd --- /dev/null +++ b/src/resources/vfx/vfx_json.ts @@ -0,0 +1,37 @@ +import { ConvertToBase64WhenRelease } from "@24tools/ads_common"; +import { ConvertResourceType, Template3d } from "@24tools/playable_template"; +import { QuarksLoader } from "three.quarks"; + +export const vfx_json: ConvertResourceType = { + type: "vfx_json", + resources: [ + { + name: "HitEffect", + value: ConvertToBase64WhenRelease("resources/vfx/files/VFX_Lootable_Hit.json"), + }, + { + name: "DestroyEffect", + value: ConvertToBase64WhenRelease("resources/vfx/files/VFX_Lootable_Destroy.json"), + }, + { + name: "Test", + value: ConvertToBase64WhenRelease("resources/vfx/files/test.json"), + }, + ], + loader: quarksLoader, +} + +export function quarksLoader(base64String: string) { + return new Promise((resolve, reject) => { + try { + new QuarksLoader(Template3d.manager).parse( + JSON.parse(atob(base64String.split(",")[1])), + (obj) => { + resolve({ obj }); + } + ); + } catch (error) { + reject("Error loading vfx: " + error); + } + }); +} \ No newline at end of file diff --git a/src/templateConfig/afterResourcesLoadedCb.ts b/src/templateConfig/afterResourcesLoadedCb.ts index 810f75e..e5a75f4 100644 --- a/src/templateConfig/afterResourcesLoadedCb.ts +++ b/src/templateConfig/afterResourcesLoadedCb.ts @@ -6,6 +6,8 @@ import { TriggerC } from "../controllers/TriggerC"; import { CombatC } from "../controllers/CombatC"; import { JoystickC, SoundC, Template } from "@24tools/playable_template"; import { LootC } from "../controllers/LootC"; +import { PayZoneC } from "../controllers/PayZoneC"; +import { VfxManager } from "../resources/vfx/VfxManager"; export const afterResourcesLoadedCb: (() => void) | undefined = async () => { TestSceneC.init(); @@ -49,6 +51,12 @@ export const afterResourcesLoadedCb: (() => void) | undefined = async () => { // Loot: spawn loot pieces when a crate breaks. LootC.init(); + // Pay zone: walk onto UI_Tool_Zone → spend collected wood into it, then it vanishes. + PayZoneC.init(); + + // VFX: set up the quark particle renderer. + VfxManager.init(); + // if (import.meta.env.DEV) { // const { CameraDebugUI } = await import("../controllers/CameraDebugUI"); // CameraDebugUI.init();