# MyGamePlayable — Project Instructions ## Context rules This file contains persistent project rules and non-obvious constraints, not history. **Keep this file under 200 lines.** Add only: * rules that affect future implementation decisions; * non-obvious SDK/library/toolchain pitfalls; * structural conventions; * public SDK APIs that are not obvious from code. Do not add: * development history, dates, or completed tasks; * test/build results; * current tuning values; * information obvious from nearby code; * temporary project state. If knowledge is specific to one subsystem, prefer `docs/.md`. If it explains why a decision was made, put the detailed history in `docs/worklog/`. When a pitfall is fixed by a utility, replace the explanation with one short rule pointing to that utility. Worklog files: `docs/worklog/NN-short-english-name.md` Update `docs/worklog/README.md` when adding one. --- ## Stack * Three.js `0.185` via `@hitplay/playable_template` + `@hitplay/ads_common`. * Physics: `cannon-es`. * Tweens: `@tweenjs/tween.js` via SDK `TweenC`. * VFX: `three.quarks`, designer `.json` prefabs. * Audio: `howler`. * Input: `nipplejs`. * Debug UI: `lil-gui`. * Analytics: `bytebrew-web-sdk`. * Toolchain: Vite 6 + TypeScript 7. ### Runtime rules * `UpdateController.Instance.onUpdate` is the only update event. `delta` is seconds. * SDK calls `world.fixedStep()` every frame via `Physics_internal.update`. Never call `step()` / `fixedStep()` manually. * Physics initialization: `Physics_internal.init(...)` in `beforeResourcesLoadedCb.ts`. * Call `TweenC.init()` exactly once. It is already called in `beforeResourcesLoadedCb.ts`. * Use `ThreeC.removeFromScene(obj)` to remove spawned meshes. --- ## Commands | Command | Purpose | | ---------------- | --------------------------------------------------------- | | `npm start` | Vite dev server | | `npm run export` | Single-file HTML → `export/output_export_browser_en.html` | | `npm run build` | 24ADS platform zip → `zip/` | | `npm run update` | Update HitPlay SDK packages | Verify changes with `npx vite build`. Use `stats.html` when investigating bundle-size growth. --- ## Structure ```text src/index.ts entry point / Template.initConfig() src/controllers/*C.ts one system per file, flat structure src/configUIParams/ 24ADS config categories src/templateConfig/*Cb.ts SDK lifecycle callbacks src/utils/ shared utilities src/resources/ meshes/images/fonts/VFX/sounds src/css/ main.css, ui.css, health-bar.css docs/worklog/ development history ``` Do not create controller subdirectories. The SDK uses a flat structure and there is no benefit in mirroring artificial subfolders. --- ## Utilities — reuse, don't reimplement * `perspectiveScale.ts` — perspective compensation for flight toward/away from camera. * `screenWorldProjection.ts` — world↔screen projection. * `tweenChain.ts` — safe tween-chain registration. * `flashOverlayMaterial.ts` — additive hit-flash overlay. * `pickupFlight.ts` — shared drop → scatter → bounce → fly-to-target animation, driven by both `ResourceC` (wood) and `ZombiePartC` (zombie-part chunks). Never write a second one. `HealthBarC` is shared by crates, zombies, and player. Do not create another health-bar implementation. Two weapon progressions (bat level from wood, pistol level from zombie-part chunks), each with its own fixed Pay Zone (`PayZoneC`, instance-based — no relocation), and crate respawn: see `docs/progression.md`. `HealthBarOptions.variant` controls colors via `health-bar.css`. `follow` re-samples the anchor every frame for moving owners. Health bars appear on first hit and hide after `HIDE_DELAY_S`; full-HP entities do not show a bar. --- ## Dependency injection Controllers must not directly import each other when that would create a cycle. Inject getters/callbacks instead and wire them from `TestSceneC`. Existing DI examples: * `ResourceC.setFlyTarget()` * `HudC.setBalanceGetter()` * `CameraFollowC.setFacingTargetGetter()` * `BreakablePropC.onHit / onDestroyed / onRespawned` * `ZombieC.init(bounds, getPlayerPosition, damagePlayer, isPlayerAlive)` * `ZombieInvasionHudC.setOnComplete()` * `ZombiePartC.setFlyTarget()` / `PistolHudC.setLevelGetter()` `ZombieInvasionHudC.setOnComplete()` must be called **before** `init()`; `init()` immediately starts its countdown. --- ## Platform config 24ADS renders config categories from `src/configUIParams/` plus SDK categories. | Platform tab | Source | | --------------------------------------- | ------------------------------ | | Analytics params | `analytics.ts` | | Install Banner Settings | `installBanner.ts` | | Scene settings | `globalSettings.ts` (`global`) | | Gameplay tuning | `gameplay.ts` | | Audio Settings | `sounds.ts` | | Screen Orientation | `orientation.ts` | | Filter scene / Redirect / Loader / Font | SDK | Read runtime values with: ```ts Template.getCategory("")[""] ``` ### Config format `configUIParams` array parameters use `[min, max, default]` per axis. Runtime `Template.getCategory()` returns the actual `[x, y, z]` value. Never interpret `[min, max, default]` as runtime coordinates. ### Camera ownership There must be exactly one writer of `camera.rotation`: `CameraFollowC`. * `CameraC.setCamera()` owns FOV only. * `CameraFollowC.applyRigFromConfig()` owns camera position/rotation. * `camera_position_*` is a player-relative offset, not an absolute position. * `camera_rotation_*` is a fixed angle in degrees. * `camera_fov_*` is FOV. * Do not write camera position/rotation from `CameraC.setCamera()`. `setCamera()` can run again when any `camera_*` config changes. * `TestSceneC.CAMERA_OFFSET` is fallback only. ### Gameplay tuning `gameplay.ts` is the source for platform tuning defaults. `DebugConfigC` mirrors these values locally in lil-gui (`C` key, dev-only). The debug panel copies defaults once at startup; it does not live-sync. --- ## Critical SDK / runtime pitfalls * GLTF loader removes dots from node names: `UI_Wood.001` → `UI_Wood001`. Inspect runtime names with `scene.traverse()`, not GLB JSON. * GLB materials are shared between nodes. Clone a material before modifying one instance. * `MeshBasicMaterial.color` cannot produce a visible white→white hit flash. Use `src/utils/flashOverlayMaterial.ts`. * Zone checks must use `new Box3().setFromObject(node)`, not visual/radius estimates. * Perspective changes apparent object size. Use `perspectiveScale.ts` when compensating for flight toward/away from the camera. * Flight targets must be re-sampled every frame because the camera follows the player. * Do not rely solely on tween `onComplete` for critical cleanup. Use a timeout fallback with a small safety margin. `removeFromScene()` is safe to call twice. * screen↔world projection must use `renderer.domElement.getBoundingClientRect()`, never `window.innerWidth/Height`. SDK uses a letterboxed 9:16 viewport. * `nipplejs.force` can exceed the visual joystick radius. Clamp it when storing input, not inside every calculation. * `Math.atan2` and CSS `conic-gradient` use different zero directions. Account for the 90° offset. * Do not stack independent CSS animations by changing `animation-name` on the same element. Use nested elements. * To fade to a base color, animate RGB rather than alpha. * Use `--lvh` / `--lvw`; avoid raw `vh` / `vw` / `vmin`. --- ## Export / CSS / fonts * Export HTML is gzip+base64, not searchable source. Never grep `export/*.html` to verify bundled content. Inspect the decompressed payload instead. * `.bannerButton` is purged from the release export. Do not add CSS targeting it. `.bannerLogo` and `.bannerButtonText` remain. * `Template.setUpFont()` works only when `!release`. It is for platform preview. Release uses our own `@font-face` from `main.css`. * Release images must use: `ConvertToBase64WhenRelease("./file.webp")` from the same directory as the asset. * CSS fonts are inlined automatically by the configured `assetsInlineLimit`. --- ## Asset rules `ZombiePunk_Map.glb`: * `Lootable` contains approximately 18 `Wooden_Box_XXX` crates. * Crates may start at S2/S3. `BreakablePropC` must derive initial HP from actual `_SN` nodes; never assume three stages. * `UI_Wood` is the resource icon and must be cloned per spawn. * `UI_Interactive_Zone_02` is the pistol's Pay Zone marker (halved in scale); the bat's own Pay Zone is a runtime clone of it, not a second authored node — see `TestSceneC.createPayZoneNodes`. * `UI_Tool_Zone`, `UI_Wood001`, and `UI` are hidden decorative nodes. `ZombiePunk_Character.glb`: * Existing clips include `Idle`, `SlowWalk`, `Loot`, `WeaponTakeOut`, `PistolShoot`, `WeaponPutDown`, `Death`, `PistolBack/Forward/Left/Right`. * Do not invent character states before checking existing clips. * `Character_Pistol` is a skinned mesh (`skin:0`), not a bone prop. Skeleton animation moves it with the hand. * Bat uses `Tool_1` / `Tool_2` visibility switching. * `Bullet` is a separate hidden rig. --- ## Change discipline * Prefer minimal, focused changes. * Do not refactor unrelated code. * Reuse existing utilities and controllers before creating new ones. * Preserve the existing DI pattern. * Before adding a new SDK workaround, check `src/utils/` and this file. * When a new persistent rule is discovered, update this file only if it affects future decisions; otherwise document it in the appropriate source file or `docs/worklog/`.