import { CameraC_internal, CameraType, JoystickC, Physics_internal, Template, Template3d, ThreeC_internal, TweenC, FilterScene, InstallBanner, } from "@hitplay/playable_template"; import { CameraC } from "../controllers/CameraC"; import { CameraFollowC } from "../controllers/CameraFollowC"; import { ThreeC } from "../controllers/ThreeC"; import { Color } from "three"; import { Vec3 } from "cannon-es"; export const beforeResourcesLoadedCb = () => { FilterScene.init(); InstallBanner.init(); Template3d.init(); CameraC_internal.init(CameraType.perspective); CameraC.setCamera(window.screenSize.portrait); ThreeC_internal.init(); ThreeC.createBaseLights(); ThreeC.setupDirectionalLight(); Physics_internal.init(new Vec3(0, -9.81, 0)); TweenC.init(); // zone is omitted on purpose: JoystickC auto-creates a full-screen zone // when none is given, so the joystick can appear wherever the player taps JoystickC.init({ mode: "dynamic", fadeTime: 100, restJoystick: true, restOpacity: 0, follow: false, // nipplejs's own default is 100; the inner thumb is always exactly // half of this by nipplejs's own rule — irrelevant here since .front's // size is overridden independently in main.css. size: 105, // "front" (the thumb/finger position) is a single flat color. Its // actual on-screen SIZE is overridden separately in main.css, since // nipplejs ties it to a hardcoded half of "size" above with no // independent option. "back" (the ring) is a single flat, darkened // color around most of its circumference, except a wide arc right // where the thumb sits, which brightens toward the thumb's own color // as if lit by it. --joy-angle (WHERE) and --joy-force (HOW STRONG) // are live CSS vars driven from onJoysticMove below. The ring's // transparent middle and the thumb's forced-opaque override // (nipplejs hardcodes the thumb at 50% opacity) also live in // main.css, since `color` can't express either. // // Alpha is fixed (not interpolated to 0) at every stop, only the RGB // channels shift with --joy-force — interpolating alpha down to 0 // at rest made the highlight's position read as a see-through gap // instead of matching the base color. Alpha itself still ramps up // slightly with force (0.75 -> 0.95) so the highlight reads as the // thumb's actual saturated color rather than diluted by the sandy // background showing through it. color: { back: "conic-gradient(from var(--joy-angle, 0deg), rgba(calc(70 + 185 * var(--joy-force, 0)), calc(55 + 85 * var(--joy-force, 0)), calc(45 - 5 * var(--joy-force, 0)), calc(0.75 + 0.2 * var(--joy-force, 0))) 0deg, rgba(70, 55, 45, 0.75) 85deg, rgba(70, 55, 45, 0.75) 275deg, rgba(calc(70 + 185 * var(--joy-force, 0)), calc(55 + 85 * var(--joy-force, 0)), calc(45 - 5 * var(--joy-force, 0)), calc(0.75 + 0.2 * var(--joy-force, 0))) 360deg)", front: "#ff8c28", }, }); // Keeps --joy-angle/--joy-force (read by the conic-gradient above) // tracking the thumb in real time. // // Angle: uses the thumb's own pixel offset from center (frontPosition — // what nipplejs itself uses to `translate()` the thumb) through // Math.atan2, not nipplejs's own `angle` field (flipped into a // "human-readable" convention internally and rotated again for // orientation-lock logical space — neither matches plain CSS/screen // angle math). atan2 and CSS conic-gradient both sweep clockwise as the // angle increases, but their zero points differ by 90° (atan2's 0° is // 3 o'clock/east; conic-gradient's `from 0deg` is 12 o'clock/north) — the // fix is a flat +90 added once, not a sign flip. // // Force: nipplejs documents `force` as normalized 0..1, but it's // actually computed from the RAW pre-clamp touch distance, while // frontPosition (and so the angle above) uses the CLAMPED distance — so // force keeps climbing past 1 the further you drag beyond the ring's // edge even though the thumb itself is visibly pinned there. Every // calc() above assumes force is 0..1, so it's clamped once here rather // than in every calc() in the gradient string. // // `data.instance` isn't in the SDK's own (stale) type declaration for // this event, though it's really there on every nipplejs 1.x payload — // the cast below bridges that gap. JoystickC.onJoysticMove.addDelegate(({ data }) => { const { instance, force } = data as unknown as { instance?: { frontPosition: { x: number; y: number }; ui: { el: HTMLElement } }; force?: number; }; if (!instance) return; const { x, y } = instance.frontPosition; const angleDeg = Math.atan2(y, x) * (180 / Math.PI) + 90; const clampedForce = Math.min(Math.max(force ?? 0, 0), 1); instance.ui.el.style.setProperty("--joy-angle", `${angleDeg}deg`); instance.ui.el.style.setProperty("--joy-force", `${clampedForce}`); }); Template.updateVariableConfig.addDelegate(([category, variable, value]) => { if (category === "global") { switch (variable) { case "light_intensity": ThreeC_internal.defaultAmbientLight.intensity = Number(value); break; case "light_color": ThreeC_internal.defaultAmbientLight.color = new Color(value as any); break; default: // The camera's config is split between two owners on purpose: // FOV lives on the camera itself (CameraC), while the rig — // offset and angle — belongs to CameraFollowC, which is the only // thing allowed to write camera.rotation. Routing each field to // its own owner is what keeps them from stomping each other. if (["camera_fov_p", "camera_fov_l"].includes(variable)) { CameraC.setCamera(window.screenSize.portrait); } else if ( ["camera_position_p", "camera_position_l", "camera_rotation_p", "camera_rotation_l"].includes(variable) ) { // No-ops safely before the scene exists: CameraFollowC reads // the same config again in its own init(). CameraFollowC.applyRigFromConfig(); } break; } return; } }); };