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
@@ -14,3 +14,11 @@ HANDOFF.md
|
||||
# promo footage is a local build product (tools/capture.py), never served
|
||||
promo/clips
|
||||
tools
|
||||
|
||||
# the Capacitor shell — the server serves the repo root directly, and none of this is ever fetched
|
||||
node_modules
|
||||
www
|
||||
ios
|
||||
package.json
|
||||
package-lock.json
|
||||
capacitor.config.json
|
||||
@@ -5,3 +5,8 @@ assets/_gen/
|
||||
assets/_refcheck/
|
||||
assets/__pycache__/
|
||||
promo/clips/
|
||||
|
||||
# the Capacitor shell. ios/ IS committed (it is the project, and Xcode settings live in it) —
|
||||
# ios/.gitignore already keeps the copied web payload, Pods and Xcode's per-user junk out of it.
|
||||
node_modules/
|
||||
www/
|
||||
@@ -9,4 +9,8 @@
|
||||
4. **Every push deploys** (Coolify → splatter-tennis.wuwi.one). Bump `main.js?v=N` in index.html on
|
||||
every push or phones keep the old build. New top-level files must be added to the Dockerfile COPY.
|
||||
5. **Zero dependencies, no build step, no framework.** This is a settled decision — solve it with
|
||||
what is already in the repo.
|
||||
what is already in the repo. The Capacitor shell (`package.json`, `ios/`) is the ONE exception
|
||||
and it is outside `src/`: **no package JS ships.** Game code calls native plugins through the
|
||||
injected bridge — `Capacitor.nativePromise(name, method, opts)`, see `src/haptics.js` — never an
|
||||
`import` of a package (a bare specifier 404s on the web) and never `Capacitor.Plugins.*` (empty
|
||||
without @capacitor/core, so it fails SILENTLY). `npm run ios` builds the app.
|
||||
+75
-1
@@ -20,7 +20,9 @@ anything, so if you add a `return` for an awkward deal, check the body still run
|
||||
the state-machine contracts that have historically produced the show-stoppers. The Dockerfile runs
|
||||
it as a build stage, so a red suite cannot deploy — but finding out from Coolify is the slow way.
|
||||
|
||||
Last updated 2026-08-20 (the friend across the net gets his OWN sheets — the player's kit, drawn
|
||||
Last updated 2026-08-20 (THE SHELL: a Capacitor iOS project, haptics on the existing sfx call
|
||||
sites, portrait enforced in Info.plist, and swipe-left-to-go-back in the menus — the web build
|
||||
unchanged and still primary. See "THE SHELL". Before that, same day: the friend across the net gets his OWN sheets — the player's kit, drawn
|
||||
from the front, so his pick dyes the same garment yours does; the setup screen shows that front too.
|
||||
See "THE FRIEND ACROSS THE NET WEARS YOUR KIT". Before that 2026-08-18: the store model in LATER revised: free download + €2.99 unlock-everything
|
||||
IAP with the free-tier gates, superseding paid-upfront; before that 2026-08-15, two sessions: the
|
||||
@@ -2536,6 +2538,74 @@ The gates, and the numbers they were calibrated on:
|
||||
**SUPERSEDED 2026-08-12 — see the next section.** Those 31 frames were not art: `--verify` found
|
||||
27 of them dead. The brute is now live (`ZFAMS` has `'br'`, the KEEPER wears it) off ONE sheet.
|
||||
|
||||
## THE SHELL — CAPACITOR, HAPTICS, PORTRAIT, SWIPE-BACK (2026-08-20)
|
||||
|
||||
The first three bullets of LATER item 2 ("What the shell buys GAMEPLAY-wise") are built. **The web
|
||||
build is unchanged and stays the primary surface** — every line below degrades to a no-op in a
|
||||
browser, and `splatter-tennis.wuwi.one` keeps deploying off the same repo on every push.
|
||||
|
||||
**The zero-dependency rule bends exactly once and exactly here.** `package.json`, `node_modules/` and
|
||||
`ios/` exist for the SHELL. **No JS from any package ships.** `src/haptics.js` calls the bridge the
|
||||
shell injects — `Capacitor.nativePromise('Haptics', 'impact', { style })` — never
|
||||
`import ... from '@capacitor/haptics'`. There is no bundler in this project, so a bare specifier
|
||||
inside a `<script type="module">` is a hard 404 the moment the same file is served from the web.
|
||||
`@capacitor/haptics` stays an npm dependency for one reason only: `cap sync` installs its Swift pod.
|
||||
**Anything added to the shell later has to reach the game the same way.**
|
||||
|
||||
> **THE TRAP, and it cost a rebuild to find.** `Capacitor.Plugins.Haptics` reads like the obvious
|
||||
> zero-dependency answer and it is **silently wrong**. The injected `native-bridge.js` defines
|
||||
> `Capacitor`, `isNativePlatform` and `nativePromise`, but leaves `Plugins` EMPTY — it is
|
||||
> `@capacitor/core`'s `registerPlugin`, i.e. exactly the JS we are not shipping, that fills that
|
||||
> object in. Going through `Plugins` therefore falls straight through to the web branch *inside the
|
||||
> app*, where `navigator.vibrate` does not exist on iOS — which looks identical to a phone with
|
||||
> haptics switched off, and would have been diagnosed as "the palette feels flat". Proven the only
|
||||
> way worth trusting: `simctl launch --console-pty` shows `⚡️ To Native -> Haptics impact` for the
|
||||
> `nativePromise` form. **That is the check to repeat for the next plugin**; `simctl spawn log
|
||||
> stream` will NOT show it, because Capacitor `print()`s those lines to stdout.
|
||||
|
||||
- **`npm run www`** rsyncs `index.html`, the manifests, `src/` and `assets/` into `www/` with the
|
||||
same `assets/_*` exclusion `.dockerignore` uses — 32 MB, not the 463 MB the sprite pipeline sits
|
||||
on. `npm run sync` = tests + that copy + `npx cap sync ios`; `npm run ios` opens Xcode after it.
|
||||
`www/` and `node_modules/` are gitignored; `ios/` IS committed (Capacitor's own `ios/.gitignore`
|
||||
keeps the copied payload, Pods and Xcode's per-user junk out of it).
|
||||
- **Portrait is enforced where it can be**: `Info.plist` lists ONE supported orientation, so the app
|
||||
never rotates. That formally retires landscape as a surface (OPEN 6) rather than fixing it. The web
|
||||
half is best-effort — `orientation: portrait` in both manifests and one guarded
|
||||
`screen.orientation.lock()` in `main.js` — because iOS Safari has no lock at all and the browser
|
||||
build keeps doing whatever landscape does today.
|
||||
- **HAPTICS ride the sound map**, as planned: six call sites, all of them places an sfx already fires.
|
||||
Your racket contact (light, medium on a perfect hit), the serve strike, `killZombie` — the ONE
|
||||
funnel every weapon routes through — `explode`, `hurtPlayer`, `killPlayer`, and the weapon swap.
|
||||
**The restraint rule is enforced in `haptics.js`, not at the call sites**: every kind carries a
|
||||
minimum gap, so a bomb that kills twelve bodies is felt twice (one blast, one kill) and the machine
|
||||
gun is a metronome rather than one long rumble. `test.js` pins exactly that. No mute switch on
|
||||
purpose — iOS already routes these through the system's own vibration setting.
|
||||
**Untested on hardware.** The simulator has no Taptic Engine, so every weight and gap below is a
|
||||
guess on paper and needs its own phone round before any of it is assumed good.
|
||||
- **SWIPE LEFT GOES BACK** in the menus. Every screen but the splash wears exactly one chevron, and
|
||||
that button already knows where back is for its own screen — so the gesture finds the visible
|
||||
`.chev` and clicks it rather than keeping a second map of the wizard that would drift the first
|
||||
time a stage is added. Threshold 60 px, horizontal-dominant so the stats and board screens still
|
||||
scroll. The `touchend` listener is deliberately **not passive**: a swipe that starts and ends
|
||||
inside one of the big poster cards also produces that card's click, and `preventDefault` is what
|
||||
stops the screen answering forward in the same frame the chevron walked it back.
|
||||
**iOS convention is the opposite** (the system pop gesture drags rightward from the left edge).
|
||||
Built to the brief as asked; flipping it is the sign on one comparison in `main.js`.
|
||||
- **The app has no server behind it.** Inside the shell the page is served from the bundle
|
||||
(`capacitor://localhost`), so `fetch('api/scores')` and the netplay socket would have resolved
|
||||
against the app's own files. `config.NATIVE`/`config.API` and one branch in `net.js` point the
|
||||
shell at the live host by name; the web build keeps the page-relative form, which is the only
|
||||
reason localhost, staging and prod all work off one file with no switch. Consequence to remember:
|
||||
**the app always talks to PROD**, never to staging.
|
||||
|
||||
Verified: `node test.js` green (105) across four seeds; the app builds and launches on the iOS 26.5
|
||||
simulator; `isNativePlatform()` reads true inside it (so the board really does point at prod) and
|
||||
both bridge calls reach the native plugin; swipe-back walks the whole wizard (mode -> modesub ->
|
||||
back -> back -> splash) in the browser pane, and vertical drags, short slides and rightward swipes
|
||||
correctly do nothing.
|
||||
**Not verified: anything that needs a real device** — the Taptic Engine, 60 fps under WKWebView at
|
||||
`RES` 4 (OPEN 1, and LATER named this as the second place that risk lands), and WKWebView audio.
|
||||
|
||||
## THE FRIEND ACROSS THE NET WEARS YOUR KIT, FROM THE FRONT (2026-08-20)
|
||||
|
||||
Oliver, on a netplay match: *"the opponent color is displayed correctly, but the shirt is colored not
|
||||
@@ -3551,6 +3621,10 @@ Nothing here is broken; these are known gaps and unverified risks.
|
||||
6. **Landscape is under-maintained.** The readability pass (camZ/camY/camF/horizon), the adaptive
|
||||
height and the safe-area work are all portrait-only; landscape still runs the old camera and a fixed
|
||||
480x270. Portrait is the stated primary target, but landscape has drifted and is untested.
|
||||
**RETIRED IN THE APP, 2026-08-20** — `Info.plist` lists one orientation, so the shell never rotates
|
||||
and landscape is officially not-a-surface there. The WEB build can still be turned sideways (iOS
|
||||
Safari has no orientation lock), so this stays open, at a lower stake: it is now a browser-only
|
||||
cosmetic gap rather than something a store reviewer will see.
|
||||
7. **`drawCard` (the chapter/set cards) still cuts in** with no entrance, while modals swoosh
|
||||
(`TUNE.modalIn`). Offered and not yet requested.
|
||||
8. **A chapter can be skipped.** `G.bossDue` takes the max, so going a long stretch without ever
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"appId": "one.wuwi.splattertennis",
|
||||
"appName": "Splatter Tennis",
|
||||
"webDir": "www",
|
||||
"backgroundColor": "#10141c",
|
||||
"ios": {
|
||||
"contentInset": "never",
|
||||
"scrollEnabled": false
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -20,7 +20,9 @@
|
||||
never has to be remembered. Prod is the one named host; everything else (staging, localhost, an
|
||||
IP) is a test build and says so. Runs in <head>, before the install prompt can read any of it. -->
|
||||
<script>
|
||||
if (location.hostname !== 'splatter-tennis.wuwi.one') {
|
||||
// (`Capacitor`: the SHELL is a prod build whatever its origin says — inside the app the hostname is
|
||||
// `localhost`, which would otherwise put the staging face on the shipped game.)
|
||||
if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNativePlatform?.()) {
|
||||
for (const [sel, href] of [['link[rel=icon]', 'assets/favicon-stg.png'],
|
||||
['link[rel=apple-touch-icon]', 'assets/icon-180-stg.png'],
|
||||
['link[rel=manifest]', 'manifest-stg.webmanifest']])
|
||||
@@ -659,6 +661,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one') {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="src/main.js?v=295"></script>
|
||||
<script type="module" src="src/main.js?v=296"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,13 @@
|
||||
App/build
|
||||
App/Pods
|
||||
App/output
|
||||
App/App/public
|
||||
DerivedData
|
||||
xcuserdata
|
||||
|
||||
# Cordova plugins for Capacitor
|
||||
capacitor-cordova-ios-plugins
|
||||
|
||||
# Generated Config files
|
||||
App/App/capacitor.config.json
|
||||
App/App/config.xml
|
||||
@@ -0,0 +1,408 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 48;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; };
|
||||
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; };
|
||||
504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; };
|
||||
504EC30D1FED79650016851F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30B1FED79650016851F /* Main.storyboard */; };
|
||||
504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; };
|
||||
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; };
|
||||
50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; };
|
||||
A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = "<group>"; };
|
||||
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
|
||||
504EC3041FED79650016851F /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
504EC3071FED79650016851F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
504EC30C1FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
504EC30E1FED79650016851F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
|
||||
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = "<group>"; };
|
||||
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
504EC3011FED79650016851F /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
504EC2FB1FED79650016851F = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
504EC3061FED79650016851F /* App */,
|
||||
504EC3051FED79650016851F /* Products */,
|
||||
7F8756D8B27F46E3366F6CEA /* Pods */,
|
||||
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
504EC3051FED79650016851F /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
504EC3041FED79650016851F /* App.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
504EC3061FED79650016851F /* App */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
50379B222058CBB4000EE86E /* capacitor.config.json */,
|
||||
504EC3071FED79650016851F /* AppDelegate.swift */,
|
||||
504EC30B1FED79650016851F /* Main.storyboard */,
|
||||
504EC30E1FED79650016851F /* Assets.xcassets */,
|
||||
504EC3101FED79650016851F /* LaunchScreen.storyboard */,
|
||||
504EC3131FED79650016851F /* Info.plist */,
|
||||
2FAD9762203C412B000D30F8 /* config.xml */,
|
||||
50B271D01FEDC1A000F3C39B /* public */,
|
||||
);
|
||||
path = App;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7F8756D8B27F46E3366F6CEA /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */,
|
||||
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */,
|
||||
);
|
||||
name = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
504EC3031FED79650016851F /* App */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */;
|
||||
buildPhases = (
|
||||
6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */,
|
||||
504EC3001FED79650016851F /* Sources */,
|
||||
504EC3011FED79650016851F /* Frameworks */,
|
||||
504EC3021FED79650016851F /* Resources */,
|
||||
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = App;
|
||||
productName = App;
|
||||
productReference = 504EC3041FED79650016851F /* App.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
504EC2FC1FED79650016851F /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 0920;
|
||||
LastUpgradeCheck = 0920;
|
||||
TargetAttributes = {
|
||||
504EC3031FED79650016851F = {
|
||||
CreatedOnToolsVersion = 9.2;
|
||||
LastSwiftMigration = 1100;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */;
|
||||
compatibilityVersion = "Xcode 8.0";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 504EC2FB1FED79650016851F;
|
||||
packageReferences = (
|
||||
);
|
||||
productRefGroup = 504EC3051FED79650016851F /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
504EC3031FED79650016851F /* App */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
504EC3021FED79650016851F /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */,
|
||||
50B271D11FEDC1A000F3C39B /* public in Resources */,
|
||||
504EC30F1FED79650016851F /* Assets.xcassets in Resources */,
|
||||
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */,
|
||||
504EC30D1FED79650016851F /* Main.storyboard in Resources */,
|
||||
2FAD9763203C412B000D30F8 /* config.xml in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-App-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-App/Pods-App-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
504EC3001FED79650016851F /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
504EC3081FED79650016851F /* AppDelegate.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
504EC30B1FED79650016851F /* Main.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
504EC30C1FED79650016851F /* Base */,
|
||||
);
|
||||
name = Main.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
504EC3101FED79650016851F /* LaunchScreen.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
504EC3111FED79650016851F /* Base */,
|
||||
);
|
||||
name = LaunchScreen.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
504EC3141FED79650016851F /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
504EC3151FED79650016851F /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
504EC3171FED79650016851F /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 1.0;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = one.wuwi.splattertennis;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
504EC3181FED79650016851F /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = one.wuwi.splattertennis;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
504EC3141FED79650016851F /* Debug */,
|
||||
504EC3151FED79650016851F /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
504EC3171FED79650016851F /* Debug */,
|
||||
504EC3181FED79650016851F /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 504EC2FC1FED79650016851F /* Project object */;
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:App.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,49 @@
|
||||
import UIKit
|
||||
import Capacitor
|
||||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
var window: UIWindow?
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
// Override point for customization after application launch.
|
||||
return true
|
||||
}
|
||||
|
||||
func applicationWillResignActive(_ application: UIApplication) {
|
||||
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
|
||||
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
|
||||
}
|
||||
|
||||
func applicationDidEnterBackground(_ application: UIApplication) {
|
||||
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
|
||||
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
|
||||
}
|
||||
|
||||
func applicationWillEnterForeground(_ application: UIApplication) {
|
||||
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
|
||||
}
|
||||
|
||||
func applicationDidBecomeActive(_ application: UIApplication) {
|
||||
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ application: UIApplication) {
|
||||
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
|
||||
}
|
||||
|
||||
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
|
||||
// Called when the app was launched with a url. Feel free to add additional processing here,
|
||||
// but if you want the App API to support tracking app url opens, make sure to keep this call
|
||||
return ApplicationDelegateProxy.shared.application(app, open: url, options: options)
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
|
||||
// Called when the app was launched with an activity, including Universal Links.
|
||||
// Feel free to add additional processing here, but if you want the App API to support
|
||||
// tracking app url opens, make sure to keep this call
|
||||
return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
|
||||
}
|
||||
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 618 KiB |
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "AppIcon-512@2x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "splash-2732x2732-2.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "splash-2732x2732-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "splash-2732x2732.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17132" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17105"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<imageView key="view" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Splash" id="snD-IY-ifK">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
</imageView>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="Splash" width="1366" height="1366"/>
|
||||
<systemColor name="systemBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14111" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14088"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Bridge View Controller-->
|
||||
<scene sceneID="tne-QT-ifu">
|
||||
<objects>
|
||||
<viewController id="BYZ-38-t0r" customClass="CAPBridgeViewController" customModule="Capacitor" sceneMemberID="viewController"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Splatter Tennis</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,24 @@
|
||||
require_relative '../../node_modules/@capacitor/ios/scripts/pods_helpers'
|
||||
|
||||
platform :ios, '14.0'
|
||||
use_frameworks!
|
||||
|
||||
# workaround to avoid Xcode caching of Pods that requires
|
||||
# Product -> Clean Build Folder after new Cordova plugins installed
|
||||
# Requires CocoaPods 1.6 or newer
|
||||
install! 'cocoapods', :disable_input_output_paths => true
|
||||
|
||||
def capacitor_pods
|
||||
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
|
||||
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
||||
pod 'CapacitorHaptics', :path => '../../node_modules/@capacitor/haptics'
|
||||
end
|
||||
|
||||
target 'App' do
|
||||
capacitor_pods
|
||||
# Add your Pods here
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
assertDeploymentTarget(installer)
|
||||
end
|
||||
@@ -0,0 +1,28 @@
|
||||
PODS:
|
||||
- Capacitor (7.6.8):
|
||||
- CapacitorCordova
|
||||
- CapacitorCordova (7.6.8)
|
||||
- CapacitorHaptics (7.0.5):
|
||||
- Capacitor
|
||||
|
||||
DEPENDENCIES:
|
||||
- "Capacitor (from `../../node_modules/@capacitor/ios`)"
|
||||
- "CapacitorCordova (from `../../node_modules/@capacitor/ios`)"
|
||||
- "CapacitorHaptics (from `../../node_modules/@capacitor/haptics`)"
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
Capacitor:
|
||||
:path: "../../node_modules/@capacitor/ios"
|
||||
CapacitorCordova:
|
||||
:path: "../../node_modules/@capacitor/ios"
|
||||
CapacitorHaptics:
|
||||
:path: "../../node_modules/@capacitor/haptics"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
Capacitor: ff6bf01336ac353098378828aff465095a89e459
|
||||
CapacitorCordova: e61ee8c40101b8cd011d0224261606a290c082cf
|
||||
CapacitorHaptics: 1b145fa83c622e5383a3bac04ea81e2514f0b0f5
|
||||
|
||||
PODFILE CHECKSUM: 77e136fb208c6c2a6494b7b510a7f97b304ff314
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
@@ -5,6 +5,7 @@
|
||||
"start_url": "./",
|
||||
"scope": "./",
|
||||
"display": "standalone",
|
||||
"orientation": "portrait",
|
||||
"background_color": "#10141c",
|
||||
"theme_color": "#14334a",
|
||||
"icons": [
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"start_url": "./",
|
||||
"scope": "./",
|
||||
"display": "standalone",
|
||||
"orientation": "portrait",
|
||||
"background_color": "#10141c",
|
||||
"theme_color": "#2b2735",
|
||||
"icons": [
|
||||
|
||||
Generated
+1175
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "splatter-tennis",
|
||||
"version": "1.0.0",
|
||||
"description": "Fifteen. Thirty. Forty. Dead.",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "node test.js",
|
||||
"www": "rm -rf www && mkdir -p www && rsync -a --exclude '_*' --exclude '.DS_Store' index.html manifest.webmanifest manifest-stg.webmanifest src assets www/",
|
||||
"sync": "npm run test && npm run www && npx cap sync ios",
|
||||
"ios": "npm run sync && npx cap open ios"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@capacitor/cli": "^7.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@capacitor/core": "^7.0.0",
|
||||
"@capacitor/haptics": "^7.0.0",
|
||||
"@capacitor/ios": "^7.0.0"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,17 @@
|
||||
// All tuning and constants. TUNE is live-editable in game via the T panel;
|
||||
// the exported consts below are structural values that code shares but the panel doesn't show.
|
||||
|
||||
// THE APP HAS NO SERVER BEHIND IT. Inside the Capacitor shell the page is served out of the app
|
||||
// bundle (capacitor://localhost), so every page-relative call — the world board, the netplay socket
|
||||
// — would resolve against the app's own files and 404. The shell therefore talks to the live host
|
||||
// BY NAME; the web build keeps the page-relative form, which is the only reason staging, localhost
|
||||
// and prod all work off one file with no switch. Consequence worth remembering: THE APP ALWAYS TALKS
|
||||
// TO PROD, never to staging. Deliberately no `location` here — this module is evaluated by the
|
||||
// headless test suite, which has no DOM; net.js reads the protocol inside its connect function.
|
||||
export const NATIVE = !!globalThis.Capacitor?.isNativePlatform?.();
|
||||
export const HOST = 'splatter-tennis.wuwi.one';
|
||||
export const API = NATIVE ? 'https://' + HOST + '/api/' : 'api/';
|
||||
|
||||
export const TUNE = {
|
||||
camZ: 42, camY: 9, camF: 406, horizon: 40, // camF/horizon are per-orientation, set in fitViewport
|
||||
gravity: 9.2, // slightly floaty vs 9.8 for readability
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { TUNE, UI, bossArt, APPLAUSE_T, EATEN_BURST, EATEN_SUMMARY, COURT, CONTACT, VOLLEY, ZONE_DEPTH, HOME, P_BOUNDS, TOSS, SHOT_DEPTH, POSE_TIME, BOUNCE_IDLE, zombieH, Z_CHEST , WEAPONS, RACK_MAX , BARREL_WEAPONS, racketStage, matchWound, CUP_ROUNDS } from './config.js';
|
||||
import { P, O, ball, score, crowd, G, court, tour, other, deuceSide, setupPoint, sendBoy, resetState } from './state.js';
|
||||
import { audio, pock, swish, whoosh, applause, voice, sfx, aww, zombieMusic, zombieDuck, menuDuck, splat, shriek, boom } from './audio.js';
|
||||
import { haptic } from './haptics.js';
|
||||
import { axisX, axisZ, takeFlickAim, touchPX, resetInput } from './input.js';
|
||||
import { W } from './render.js'; // runtime-only cycle (render imports gameplay), same pattern as input/zombies
|
||||
import { updateDrill } from './drill.js';
|
||||
@@ -273,6 +274,9 @@ function doHit(ch, quality, aim) {
|
||||
// the court stays clean)
|
||||
if (ch.side === 'P') G.aiReactT = 0; // CPU hasn't read the shot yet
|
||||
sfx(ch.side === 'P' ? 'tennis-player' : 'tennis-opponent', 0.9, { reverb: true }); // recorded racket strike, per side
|
||||
// CONTACT IS THE TICK. Only YOUR racket: the far side's strike is something you watch, not
|
||||
// something your hand did, and buzzing for both halves of every rally is the numbing failure mode.
|
||||
if (ch.side === 'P') haptic(quality > 0.999 ? 'medium' : 'light'); // a perfect hit is felt as one
|
||||
if (quality > 0.999 && ch.side === 'P') { // the rally hit: a real 45 ms window (perfectMs)
|
||||
G.hitstop = 0.05; pock(0.5, 1600);
|
||||
const w = praiseWord(); // a rally hit IS the impact — the word pops right on the contact
|
||||
@@ -512,6 +516,7 @@ export function hitServe(sv, quality, aim = null, perfect = quality > 0.999) {
|
||||
}
|
||||
// (spendSlot already hands you back plain balls when the last one goes)
|
||||
G.phase = 'rally'; sfx(sv.side === 'P' ? 'tennis-player' : 'tennis-opponent', 0.9, { reverb: true }); // recorded serve strike, per side
|
||||
if (sv.side === 'P') haptic(perfect ? 'medium' : 'light'); // the strike, same tick as a rally hit
|
||||
sv.pose = 'serve'; sv.poseT = 0;
|
||||
if (perfect && sv.side === 'P') {
|
||||
G.hitstop = 0.05;
|
||||
@@ -807,6 +812,7 @@ export function selectWeapon(i) {
|
||||
}
|
||||
G.weapon = i; // temp has just been cleared, and then it is BALL again)
|
||||
sfx('weapon-click2.mp3', 0.45); // a real mechanical click, quiet — switching is audible, not loud
|
||||
haptic('select'); // ...and it is felt, which is what a physical rack would do
|
||||
}
|
||||
|
||||
// Grant a weapon and equip it. Tops up what you already carry, else takes a free slot, else rides
|
||||
@@ -984,6 +990,7 @@ function transition(dt) {
|
||||
// rather than as a glitch — the eye is given something to blame the change on.
|
||||
once('turn', 3.4, () => {
|
||||
G.hitstop = 0.16; // the world holds its breath
|
||||
haptic('heavy'); // ...and the hand feels the world stop
|
||||
G.boomFlash = 0.16; G.shake = { t: 0.7, mag: 7 };
|
||||
shriek(); splat(0.9); boom();
|
||||
for (let k = 0; k < 40; k++) parts.push({ // he bursts, and what stands up is not him
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// The Taptic Engine, behind one call. iOS Safari exposes no vibration API of any kind, so on the WEB
|
||||
// build this is navigator.vibrate on Android and a silent no-op on iPhone — the feature is really
|
||||
// only alive inside the Capacitor shell, which is the point (PROJECT.md, "What the shell buys").
|
||||
//
|
||||
// Reached through the bridge the shell INJECTS — `Capacitor.nativePromise('Haptics', ...)` — and not
|
||||
// through `import { Haptics } from '@capacitor/haptics'`. Two reasons, and the second is the one
|
||||
// that bites:
|
||||
// 1. This project has no bundler and no build step, so a bare specifier inside a
|
||||
// <script type="module"> is a hard 404 the moment the same file is served on the web.
|
||||
// 2. `Capacitor.Plugins.Haptics` LOOKS like the zero-dependency answer and is not: the injected
|
||||
// native-bridge.js declares `Capacitor` and `nativePromise` but leaves `Plugins` empty — it is
|
||||
// @capacitor/core's registerPlugin, i.e. JS we deliberately do not ship, that fills it in. Going
|
||||
// through Plugins therefore silently falls through to the web path INSIDE the app, which looks
|
||||
// exactly like a phone whose haptics are switched off. `nativePromise` needs no JS package;
|
||||
// @capacitor/haptics stays an npm dependency only so `cap sync` installs its Swift pod.
|
||||
//
|
||||
// THE MAP IS THE SOUND MAP. The sfx taxonomy already decided which moments in this game deserve
|
||||
// punctuation, so the haptics ride the same handful of call sites instead of inventing a second
|
||||
// opinion about what matters. Restraint rule: contact is a tick, kills punctuate, heavy is rare — a
|
||||
// game that buzzes continuously numbs the hand inside a minute and then means nothing at all.
|
||||
//
|
||||
// That rule is enforced HERE rather than at the call sites, by a minimum gap per kind. The machine
|
||||
// gun may reach `haptic('medium')` twenty times a second and a bomb may kill twelve bodies in one
|
||||
// frame; the hand still feels a metronome instead of one long rumble, and no caller has to know it.
|
||||
//
|
||||
// No mute switch on purpose: iOS already routes every one of these through the system's own
|
||||
// vibration setting, so a player who has turned haptics off has turned THESE off.
|
||||
|
||||
const KINDS = {
|
||||
select: { gap: 0.05, impact: 'LIGHT', ms: 8 }, // a menu answer, a weapon swapped
|
||||
light: { gap: 0.06, impact: 'LIGHT', ms: 12 }, // racket contact — the metronome of a rally
|
||||
medium: { gap: 0.08, impact: 'MEDIUM', ms: 20 }, // a body comes apart
|
||||
heavy: { gap: 0.13, impact: 'HEAVY', ms: 35 }, // a blast, a beat, a hit you took
|
||||
dead: { gap: 1.0, notify: 'ERROR', ms: [0, 40, 90, 40, 90, 110] }, // eaten — once a run, if that
|
||||
};
|
||||
const at = Object.create(null); // last fire per kind, ms
|
||||
|
||||
export function haptic(kind) {
|
||||
const k = KINDS[kind];
|
||||
if (!k) return;
|
||||
const now = performance.now();
|
||||
if (now - (at[kind] ?? -1e9) < k.gap * 1000) return;
|
||||
at[kind] = now;
|
||||
const cap = globalThis.Capacitor;
|
||||
try { // a buzz is never worth a thrown frame
|
||||
if (cap?.isNativePlatform?.())
|
||||
cap.nativePromise('Haptics', k.notify ? 'notification' : 'impact',
|
||||
k.notify ? { type: k.notify } : { style: k.impact })?.catch?.(() => {});
|
||||
else globalThis.navigator?.vibrate?.(k.ms);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// test hook: what the throttle would let through, without a platform under it
|
||||
export const hapticTest = { KINDS, at };
|
||||
+39
-1
@@ -12,6 +12,7 @@ import { fitViewport, render, proj, RES, SPRV, LOAD, SPR, drawLook } from './ren
|
||||
import { voice, ambient, menuMusic, menuDuck } from './audio.js';
|
||||
import { zombies, spawnZombie, spawnRacketZombie, spawnMachine, spawnChapterBoss, piles, gore, parts, pellets, extras, shots, pops, explode, hurtPlayer, killPlayer , spawnBarrel } from './zombies.js';
|
||||
import { cv } from './dom.js';
|
||||
import { haptic } from './haptics.js';
|
||||
import * as stats from './stats.js';
|
||||
import './input.js';
|
||||
|
||||
@@ -45,6 +46,14 @@ function frame(now) {
|
||||
update(dt); render();
|
||||
}
|
||||
|
||||
// PORTRAIT ONLY, from 2026-08-20. The app enforces it where it can actually be enforced — the
|
||||
// shell's Info.plist lists one supported orientation, so an iPhone running the store build simply
|
||||
// never rotates. This line is the WEB half of the same decision and is best-effort by nature:
|
||||
// Chrome/Android honours a lock once the page is installed or fullscreen, and iOS Safari has no
|
||||
// Screen Orientation lock at all, so the browser build keeps doing whatever landscape does today
|
||||
// (PROJECT.md OPEN 6 — landscape is untested, not a surface). Nothing downstream may assume it took.
|
||||
try { screen.orientation?.lock?.('portrait')?.catch(() => {}); } catch {}
|
||||
|
||||
fitViewport();
|
||||
// iOS PWA cold start. Launched from the homescreen, WebKit resolves the viewport AND every env()
|
||||
// safe-area inset once, against a provisional geometry, and never revisits them until the geometry
|
||||
@@ -528,6 +537,35 @@ document.getElementById('sBack').addEventListener('click', () => {
|
||||
});
|
||||
document.getElementById('bsBack').addEventListener('click', () => go('secret'));
|
||||
|
||||
// SWIPE TO GO BACK. Every screen but the splash wears exactly one chevron in its header (.hdr in
|
||||
// index.html) and that button ALREADY knows where back is for its own screen — the wizard's shape
|
||||
// lives in the handlers above and nowhere else. So the gesture finds the one visible chevron and
|
||||
// clicks it, rather than keeping a second map that would drift the first time a stage is added.
|
||||
// A screen with no chevron (splash, newbest) has nowhere to go and the swipe is simply ignored.
|
||||
// Touch only: a trackpad two-finger scroll is not a swipe and must not navigate.
|
||||
const SWIPE_MIN = 60; // px of travel before it counts — a tap that slides is not a swipe
|
||||
let swipe = null;
|
||||
intro.addEventListener('touchstart', e => {
|
||||
swipe = e.touches.length === 1 ? { x: e.touches[0].clientX, y: e.touches[0].clientY } : null;
|
||||
}, { passive: true });
|
||||
intro.addEventListener('touchend', e => {
|
||||
const from = swipe; swipe = null;
|
||||
if (!from || e.touches.length) return;
|
||||
const t = e.changedTouches[0];
|
||||
const dx = t.clientX - from.x, dy = t.clientY - from.y;
|
||||
// leftward, and clearly horizontal: the stats and board screens scroll vertically under the finger
|
||||
if (dx > -SWIPE_MIN || Math.abs(dy) > Math.abs(dx) * 0.7) return;
|
||||
const back = [...intro.querySelectorAll('.chev')].find(b => b.offsetParent); // the one on screen
|
||||
if (!back) return;
|
||||
// NOT passive, and this is why: a swipe that starts and ends inside one of the big poster cards
|
||||
// still produces that card's click, which would answer the screen forward in the same frame the
|
||||
// chevron walked it back. preventDefault on touchend cancels the compatibility mouse events, so
|
||||
// the gesture means one thing only.
|
||||
e.preventDefault();
|
||||
haptic('select');
|
||||
back.click();
|
||||
});
|
||||
|
||||
// ---- THE BACK ROOM: five taps in the TOP-LEFT CORNER of the splash (#hot, an invisible box in
|
||||
// index.html), and nothing on the screen says so. It replaces the triple-tap on the in-play
|
||||
// scoreboard, which sat on top of the miss-log tap and which the testteam kept opening by accident.
|
||||
@@ -639,7 +677,7 @@ const tick = setInterval(function watchLoad() {
|
||||
// real buttons now. This is the gesture the autoplay policy is waiting for, which is why the theme
|
||||
// is not started at boot. menuMusic itself is a no-op once the bed exists (loopBed's beds.has
|
||||
// guard), so coming back from a run picks up the track already playing rather than restarting it.
|
||||
intro.addEventListener('pointerdown', () => menuMusic());
|
||||
intro.addEventListener('pointerdown', e => { menuMusic(); if (e.target.closest('button')) haptic('select'); });
|
||||
// which build am I looking at? Push = deploy and the browser cannot see git, so the served file's
|
||||
// own timestamp IS the version marker — it changes on every deploy, with nothing to remember to bump.
|
||||
// Match it against `git log -1 --format=%cd` when a phone looks suspiciously out of date.
|
||||
|
||||
+3
-2
@@ -17,7 +17,7 @@
|
||||
//
|
||||
// ponytail: no reconnect-and-resume. A dropped radio ends the match with a card; the recorded
|
||||
// plan calls reconnection "the bulk of the real work" and a prototype must not start there.
|
||||
import { COURT, UI, KITS, SKINS, TOURS } from './config.js';
|
||||
import { COURT, UI, KITS, SKINS, TOURS, NATIVE, HOST } from './config.js';
|
||||
import { G, P, O, ball, score, setupPoint, selectCourt, court, kit, skin, tour } from './state.js';
|
||||
import { pointTo, fault, tossBall } from './gameplay.js';
|
||||
import { startMatch, matchReady, resolveInterlude, netMatchWon } from './beats.js'; // runtime-only cycle: beats sends verdicts back through this module
|
||||
@@ -239,7 +239,8 @@ export function netConnect(code, death = false, courtId = null) {
|
||||
// joining carries only the code — the host's room already knows the mode and the court
|
||||
const q = code ? ['room=' + encodeURIComponent(code)]
|
||||
: [death ? 'death=1' : '', courtId ? 'court=' + encodeURIComponent(courtId) : ''].filter(Boolean);
|
||||
const url = (location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host
|
||||
const url = (NATIVE ? 'wss://' + HOST // the app: the live host, by name
|
||||
: (location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host)
|
||||
+ '/ws' + (q.length ? '?' + q.join('&') : '');
|
||||
ws = new WebSocket(url);
|
||||
ws.onmessage = e => { let m; try { m = JSON.parse(e.data); } catch { return; } netMsg(m); };
|
||||
|
||||
+3
-3
@@ -10,7 +10,7 @@
|
||||
// knows nothing about stats — fine today, and exactly the kind of coupling that breaks quietly the
|
||||
// first time the reset order changes. endRun() is the only thing that empties this bag.
|
||||
|
||||
import { WEAPONS, ENEMIES } from './config.js';
|
||||
import { WEAPONS, ENEMIES, API } from './config.js';
|
||||
|
||||
const KEY = 'splatterRuns';
|
||||
// ponytail: the array is capped, so "ALL TIME" means "your last CAP runs". At ~170 bytes a record
|
||||
@@ -197,7 +197,7 @@ let last = null;
|
||||
export function lastRun() { const r = last; last = null; return r; }
|
||||
|
||||
export async function board(mode = MODE_GORE) {
|
||||
const r = await fetch('api/scores?m=' + mode, { cache: 'no-store' });
|
||||
const r = await fetch(API + 'scores?m=' + mode, { cache: 'no-store' });
|
||||
if (!r.ok) throw new Error('board ' + r.status);
|
||||
return (await r.json()).scores || [];
|
||||
}
|
||||
@@ -221,7 +221,7 @@ export const saveName = s => {
|
||||
export async function submit(rawName, k, g, mode = MODE_GORE) {
|
||||
const name = saveName(rawName);
|
||||
if (!name) throw new Error('empty name');
|
||||
const r = await fetch('api/scores', { method: 'POST',
|
||||
const r = await fetch(API + 'scores', { method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ n: name, k, g, m: mode, id: myId() }) });
|
||||
if (r.status === 409) { const e = new Error('name taken'); e.taken = true; throw e; }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -71,6 +71,11 @@ globalThis.window = globalThis; // audio.js reads window.AudioContext; render re
|
||||
globalThis.AudioContext = function () { return absorb; };
|
||||
globalThis.webkitAudioContext = globalThis.AudioContext;
|
||||
globalThis.matchMedia = () => ({ matches: false, addEventListener() {} });
|
||||
// haptics.js falls back to navigator.vibrate when there is no Capacitor bridge. Node ships a REAL
|
||||
// navigator and it is getter-only, so the stub has to be defined over it rather than assigned.
|
||||
globalThis.hapticLog = [];
|
||||
Object.defineProperty(globalThis, 'navigator', {
|
||||
configurable: true, value: { vibrate: ms => globalThis.hapticLog.push(ms) } });
|
||||
globalThis.addEventListener = globalThis.addEventListener || (() => {});
|
||||
globalThis.removeEventListener = globalThis.removeEventListener || (() => {});
|
||||
globalThis.Image = class { constructor() { this.complete = false; } addEventListener() {} set src(v) {} };
|
||||
@@ -90,13 +95,14 @@ const { mirror, netMsg, netTest, netTick } = await import('./src/net.js');
|
||||
// the drill's first-sight modal freezes the world (that is what modals do) — dismiss it the way a
|
||||
// player would, so the tests step a live court
|
||||
const startDrill = (...a) => { const r = _startDrill(...a); G.modal = null; G.modalQ.length = 0; return r; };
|
||||
const { zombies, spawnZombie, pellets, parts } = await import('./src/zombies.js');
|
||||
const { zombies, spawnZombie, pellets, parts, explode } = await import('./src/zombies.js');
|
||||
const { killPlayer } = await import('./src/zombies.js');
|
||||
const { drillQueue, drillLegal, arrival, drillMult, DRILL_TYPES } = await import('./src/drill.js');
|
||||
const cfg = await import('./src/config.js');
|
||||
const { TUNE, bossArt, COURT, racketStage, matchWound } = cfg;
|
||||
const statsMod = await import('./src/stats.js');
|
||||
const audio = await import('./src/audio.js');
|
||||
const { hapticTest } = await import('./src/haptics.js');
|
||||
const { read: readStats, check: statsCheck } = statsMod;
|
||||
|
||||
const DT = 1 / 60;
|
||||
@@ -1881,5 +1887,17 @@ test('every kit and skin in the tables is a live, distinct pick', () => {
|
||||
assert.equal(tintPx(...SKINPX, cfg.KITS[0], cfg.SKINS[0]), null, 'defaults change nothing');
|
||||
});
|
||||
|
||||
test('haptics: a blast that kills a dozen bodies is felt twice, not thirteen times', () => {
|
||||
// The restraint rule is the whole feature: a hand that buzzes on every body in a bomb radius stops
|
||||
// reading any of them. The gap lives in haptics.js so no call site has to know about it, and this
|
||||
// is what proves it — the funnel (killZombie) is deliberately called once per kill.
|
||||
const buzz = globalThis.hapticLog; buzz.length = 0; // the web fallback; no Capacitor bridge here
|
||||
for (const k in hapticTest.at) delete hapticTest.at[k]; // a fresh hand
|
||||
for (let i = 0; i < 12; i++) spawnZombie(0.3 * i - 1.8, 12, 'a');
|
||||
explode(0, 12);
|
||||
assert.equal(zombies.length, 0, 'the blast cleared its radius (or this proves nothing)');
|
||||
assert.equal(buzz.length, 2, 'one heavy for the blast, one medium for the kills — not one per body');
|
||||
});
|
||||
|
||||
console.log(`\n${passed} tests passed`);
|
||||
process.exit(0); // render.js schedules viewport timers at load; don't let them outlive the report
|
||||
Reference in new issue
Block a user