9.9 KiB
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/<topic>.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.185via@hitplay/playable_template+@hitplay/ads_common. - Physics:
cannon-es. - Tweens:
@tweenjs/tween.jsvia SDKTweenC. - VFX:
three.quarks, designer.jsonprefabs. - Audio:
howler. - Input:
nipplejs. - Debug UI:
lil-gui. - Analytics:
bytebrew-web-sdk. - Toolchain: Vite 6 + TypeScript 7.
Runtime rules
UpdateController.Instance.onUpdateis the only update event.deltais seconds.- SDK calls
world.fixedStep()every frame viaPhysics_internal.update. Never callstep()/fixedStep()manually. - Physics initialization:
Physics_internal.init(...)inbeforeResourcesLoadedCb.ts. - Call
TweenC.init()exactly once. It is already called inbeforeResourcesLoadedCb.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
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 bothResourceC(wood) andZombiePartC(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 / onRespawnedZombieC.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:
Template.getCategory("<categoryId>")["<parameterId>"]
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 anycamera_*config changes. TestSceneC.CAMERA_OFFSETis 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 withscene.traverse(), not GLB JSON. -
GLB materials are shared between nodes. Clone a material before modifying one instance.
-
MeshBasicMaterial.colorcannot produce a visible white→white hit flash. Usesrc/utils/flashOverlayMaterial.ts. -
Zone checks must use
new Box3().setFromObject(node), not visual/radius estimates. -
Perspective changes apparent object size. Use
perspectiveScale.tswhen 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
onCompletefor 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(), neverwindow.innerWidth/Height. SDK uses a letterboxed 9:16 viewport. -
nipplejs.forcecan exceed the visual joystick radius. Clamp it when storing input, not inside every calculation. -
Math.atan2and CSSconic-gradientuse different zero directions. Account for the 90° offset. -
Do not stack independent CSS animations by changing
animation-nameon the same element. Use nested elements. -
To fade to a base color, animate RGB rather than alpha.
-
Use
--lvh/--lvw; avoid rawvh/vw/vmin.
Export / CSS / fonts
-
Export HTML is gzip+base64, not searchable source. Never grep
export/*.htmlto verify bundled content. Inspect the decompressed payload instead. -
.bannerButtonis purged from the release export. Do not add CSS targeting it..bannerLogoand.bannerButtonTextremain. -
Template.setUpFont()works only when!release. It is for platform preview. Release uses our own@font-facefrommain.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:
Lootablecontains approximately 18Wooden_Box_XXXcrates.- Crates may start at S2/S3.
BreakablePropCmust derive initial HP from actual_SNnodes; never assume three stages. UI_Woodis the resource icon and must be cloned per spawn.UI_Interactive_Zone_02is 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 — seeTestSceneC.createPayZoneNodes.UI_Tool_Zone,UI_Wood001, andUIare 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_Pistolis a skinned mesh (skin:0), not a bone prop. Skeleton animation moves it with the hand.- Bat uses
Tool_1/Tool_2visibility switching. Bulletis 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/.