The game gets a shell: it buzzes in your hand, never turns sideways, and a swipe walks you back
A Capacitor iOS project (`npm run ios`), and the first three bullets of LATER item 2 built. The web
build is untouched and stays primary: every line here degrades to a no-op in a browser.
- HAPTICS ride the sound map, as the plan said they would: six call sites, all of them places an
sfx already fires — your racket contact, the serve strike, `killZombie` (the one funnel every
weapon routes through), `explode`, `hurtPlayer`, `killPlayer`, the weapon swap. The restraint
rule lives in `haptics.js`, not at the call sites: a minimum gap per kind, so a bomb that kills
twelve bodies is felt twice and the machine gun is a metronome rather than one long rumble.
Pinned by a test. Untested on hardware — the simulator has no Taptic Engine.
- The plugin is reached through the INJECTED bridge, `Capacitor.nativePromise('Haptics', ...)`.
`Capacitor.Plugins.Haptics` reads like the zero-dependency answer and is silently empty without
@capacitor/core's registerPlugin — it would have fallen through to the web path inside the app
and looked exactly like a phone with haptics switched off. No package JS ships.
- PORTRAIT is enforced where it can be: one orientation in Info.plist, so the app never rotates.
That retires landscape as a surface rather than fixing it (OPEN 6). The web half is best-effort —
`orientation` in both manifests and one guarded `screen.orientation.lock()`.
- SWIPE LEFT GOES BACK. The gesture finds the screen's one visible chevron and clicks it, so the
shape of the wizard stays in the handlers that already own it. Not passive on purpose: a swipe
inside a poster card also fires that card's click, and preventDefault is what stops the screen
answering forward in the frame the chevron walked it back.
- The app has no server behind it, so `config.API` and one branch in `net.js` point the shell at the
live host by name. Consequence: the app always talks to PROD, never staging.
`node test.js` green (105) over four seeds; builds, launches and reaches the native plugin on the
iOS 26.5 simulator; swipe-back walks the whole wizard in the browser pane.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c5eb6846e3
commit
d71a26831a
35 files changed
+2124
-11
No files matched your search
@@ -6,6 +6,7 @@
|
||||
import { TUNE, UI, COURT, CONTACT, BOSS_ART, bossArt, WEAPONS, BARREL_WEAPONS, woundStage, zombieH, EATEN_BURST, EATEN_SUMMARY, EATEN_FEED_R } from './config.js';
|
||||
import { G, P, O, ball, score, sendBoy, setupPoint } from './state.js';
|
||||
import { splat, moan, pock, thud, zap, voice, boom, swish, shriek, aww, sfx , whoosh, flame } from './audio.js';
|
||||
import { haptic } from './haptics.js';
|
||||
import { drillKill, drillCaught } from './drill.js';
|
||||
import { splatPoint, cheer, spentBall, aside, grantWeapon, endBurst, sliceCarom } from './gameplay.js';
|
||||
import { resolveInterlude, endBossFight } from './beats.js'; // runtime-only cycle, like the gameplay one
|
||||
@@ -266,6 +267,7 @@ export function hurtPlayer(n) {
|
||||
if (G.mode !== 'splatter' || G.hurtT > 0 || G.dodgeI > 0 || G.mode === 'eaten') return;
|
||||
G.hp -= n;
|
||||
G.hurtT = TUNE.hurtGrace; G.hurtFlash = 0.3;
|
||||
haptic('heavy'); // taking one is rare (hurtGrace gates it) and is the loudest thing the hand gets
|
||||
G.shake = { t: 0.22, mag: 4 };
|
||||
splat(0.7); aww(0.5);
|
||||
for (let k = 0; k < 14; k++) parts.push({ // he takes it in the chest
|
||||
@@ -292,6 +294,7 @@ export function killPlayer() {
|
||||
splat(1);
|
||||
voice('game_set_massacre', 0.8); aww(1); // the run is over — the crowd groans
|
||||
// the death animation runs off G.eatenT: thrash here, burst at EATEN_BURST (playerGore), summary after
|
||||
haptic('dead'); // the notification pattern, once a run — the only place it is ever used
|
||||
G.eatenT = 0; G.eatenAt = { x: P.x, z: P.z };
|
||||
G.mode = 'eaten'; ball.active = false; G.flash = null; G.modal = null; // zombies keep shambling for ambience
|
||||
G.horde = null; pellets.length = 0; extras.length = 0; shots.length = 0; G.mgT = 0; G.gunId = null; // everything stops
|
||||
@@ -485,6 +488,7 @@ function bossDeath(zb) {
|
||||
|
||||
function killZombie(i, quiet) {
|
||||
const zb = zombies[i];
|
||||
haptic('medium'); // every weapon routes through here, so the kill is felt exactly once per kill
|
||||
// The drill's ONE rule, read at the single point every weapon routes through — hooking the ball
|
||||
// path instead would have the swish, the pellets and the blast silently exempt from the order.
|
||||
const mul = GORE_MUL[zb.variant] || 1;
|
||||
@@ -856,6 +860,7 @@ function tickHorde(dt) {
|
||||
// the Phase-2 fanfare escalates), plus a crater — big stain, size-3 pile, extra particles, boom.
|
||||
export function explode(x, z, why = 'bomb') {
|
||||
CAUSE = why; // the rocket detonates through here too, and it is its own row
|
||||
haptic('heavy'); // the blast, before the kills it causes — heavy and medium throttle separately
|
||||
for (let i = zombies.length - 1; i >= 0; i--) { // backwards: splatter() splices (and can sweep) the array
|
||||
const zb = zombies[i];
|
||||
if (!zb || Math.hypot(zb.x - x, zb.z - z) >= TUNE.bombRadius) continue;
|
||||
|
||||
Reference in new issue
Block a user