diff --git a/docs/runtime/p9_1/visual_slice/CANDIDATE.md b/docs/runtime/p9_1/visual_slice/CANDIDATE.md new file mode 100644 index 00000000..371c225d --- /dev/null +++ b/docs/runtime/p9_1/visual_slice/CANDIDATE.md @@ -0,0 +1,54 @@ +# ABG-P9.1-VISUAL-SLICE Candidate Evidence + +> Legal terminal: `ABG-P9.1-VISUAL-SLICE candidate ready` +> Source baseline after dispatch recovery: `efffb7b715d981eeb499628fea77e05ff8b5695e` +> PMO activation ancestor observed before candidate commit: `3a02d51c48b206b040090111c3fa8d8743ac731e` +> Candidate closure: populated by the Git commit containing this file + +## Produced + +- One play-size `844x390` graybox with fixed tracks and first/second/third + focal priorities. +- One editable HTML/CSS/JS slice covering formation review, approach/contact, + damage, heal, ranged response and first causal diagnosis. +- Seven `844x390` previews: one graybox plus six source-bound frames. +- Four-Recruit identity inventory, accepted action/enemy/VFX sources, font + license, source paths and SHA-256 verification. +- Explicit environment, actor, connector, feedback, UI and caption layers. + +## Verification + +Command: + +```text +npm ci && npm run capture && npm run verify +``` + +Result: `PASS`. `evidence/verification.json` records: + +- viewport: `844x390`; +- frame count: `6`; +- copied asset count: `14`; +- viewport overflow: `0` errors; +- required pair identity copy: present in every frame; +- accepted source hash mismatch: `0`; +- paid calls: `0 / 4`; +- paid cost: `$0 / $5`. + +Visual inspection covered every preview at original `844x390`. Actor order, +target lines, signed damage/heal values, captions and cause-first result copy +remain readable without incoherent overlap. + +## Scope And Cleanup + +- Changed paths are confined to `docs/runtime/p9_1/visual_slice/`. +- Runtime source, tests, accepted runtime assets and PMO files were not edited. +- No image, audio or video generation process was started. +- Local Playwright dependencies were removed after evidence capture. +- Owned processes: none. + +## Residual Risk + +This is a static visual/layer reference, not runtime integration. Owner visual +judgment and runtime comprehension, motion, sound, device, packaging and release +gates remain open exactly as required by the frozen contract. diff --git a/docs/runtime/p9_1/visual_slice/GRAYBOX.md b/docs/runtime/p9_1/visual_slice/GRAYBOX.md new file mode 100644 index 00000000..bee55124 --- /dev/null +++ b/docs/runtime/p9_1/visual_slice/GRAYBOX.md @@ -0,0 +1,50 @@ +# P9.1 Representative Visual Slice Graybox + +> Viewport: `844x390` +> Fixture: `zh_CN`, 100%, standard motion/flashes, captions on +> Pair: `Farstring + Kindhand` + +## Composition + +The play-size authority is [`graybox.html`](graybox.html). Its tracks match +`UX_VISUAL_FLOW_SPEC.md` exactly: + +| Track | Bounds | Purpose | +|---|---:|---| +| Top status rail | `12,10,820,54` | Context, progress and non-command tools | +| Main world/event | `18,72,544,242` | Party line, enemy, connectors and event proof | +| Context rail | `570,72,256,242` | Current evidence, exact delta and causal explanation | +| Bottom action dock | `18,322,808,56` | One next action; event ribbon replaces it in battle | + +The battle variant expands the painted world behind the edge UI. The event +ribbon uses the dock footprint so it cannot cover actors or feedback numbers. + +## Focal Priorities + +1. **Current event and affected actor.** The party line, target mark and exact + number must resolve before any result summary. +2. **Cause and changed value.** The right rail or event ribbon names source, + action, target, before/after and result. +3. **Next legal action.** A single gold enamel control occupies a stable + `248x56` visual plate inside the `56`-high dock. + +Formation review changes priority 1 to the committed automatic order. Result 1 +changes priority 1 to the earliest causal divergence, while preserving the same +actor identities and left-to-right order. + +## Readability Decisions + +- Kindhand remains protected-left and Farstring exposed-right in every frame. +- The enemy-facing action gap stays clear of text and controls. +- Damage uses coral plus a split-triangle family; healing uses jade plus leaf + chevrons. Signed values and before/after text remain authoritative. +- No player combat controls are introduced. Pause, Inspect and Captions remain + symbol tools in the top rail. +- Functional text is engine-rendered and no generated asset contains text. + +## Media Decision + +Paid generation was not needed. Accepted P9.0 assets already provide the two +identities, action poses, battlefield, enemy and semantic VFX required by this +bounded proof. Reusing those accepted sources reduces identity drift and keeps +all four authorized calls in reserve; the final call count is still zero. diff --git a/docs/runtime/p9_1/visual_slice/HANDOFF.md b/docs/runtime/p9_1/visual_slice/HANDOFF.md new file mode 100644 index 00000000..d7e21858 --- /dev/null +++ b/docs/runtime/p9_1/visual_slice/HANDOFF.md @@ -0,0 +1,81 @@ +# P9.1 Formation-To-Diagnosis Visual Handoff + +## Scope + +This handoff covers one representative chain only: + +```text +formation review -> approach/contact -> damage -> heal response +-> ranged finish -> first causal diagnosis +``` + +It is a design and layer reference, not integrated runtime source and not Owner +visual acceptance. Open `index.html?frame=` at exactly `844x390`. + +## Representative Truth + +- All four accepted Recruit identity anchors are included in + `assets/actors/`; this representative pair renders only Farstring and + Kindhand and does not imply that either is a fixed protagonist. +- Starting pair: `Farstring + Kindhand` (`FS-KH`). +- Automatic order: `Farstring` is order `0`, exposed-right lead; `Kindhand` is + order `1`, protected-left second member. +- Battle 1 starts both at `100 HP`; exposed Farstring takes `-42` to `58`; + Kindhand restores `+12` to `70`. +- The final remote response is a representative ordered event showing the + enemy at `34/100`; it is not presented as a new combat formula. +- Result 1 names the system order, both profession actions, current HP, the + once-only `+20 Coin` receipt and the next adaptation comparison. + +## Frames + +| ID | Contract proof | First read | +|---|---|---| +| `formation-review` | `P91-E04` | Automatic order and both responsibilities | +| `approach-contact` | `P91-E05` contact | Actual target geometry before impact | +| `damage-feedback` | `P91-E05` damage | One `-42`, attached to Farstring, `100 -> 58` | +| `heal-response` | profession response | Kindhand-to-Farstring connector and one `+12` | +| `ranged-finish` | ranged response | Farstring-to-enemy direction and one `-30` | +| `first-diagnosis` | Result 1 | Earliest divergence, response and cause-first copy | + +## Editable Layers + +The HTML keeps layers semantic and independently replaceable: + +1. `.layer-environment`: accepted hand-painted battlefield and scene wash. +2. `.layer-actors`: stable Recruit/enemy art and action poses. +3. `.layer-connectors`: party cord, target line, projectile and heal path. +4. `.layer-feedback`: signed number and semantic VFX. +5. `.layer-ui`: status, objective, context rail and controls. +6. `.layer-captions`: ordered event ribbon or cause-first result text. + +Runtime implementation should bind those layers to immutable party snapshots +and ordered events. It must not reproduce branch logic from these static frames. + +## Accessibility And Variants + +- Every mechanic has text and geometry in addition to color. +- The current target remains visible before impact. +- Captions use `来源 -> 动作 -> 目标 -> 结果`. +- Touch controls are at least `48x48`; primary actions are `56` high. +- Reduced motion can use the same six stable poses and connectors with no + camera travel, parallax, bobbing or continuous motion. +- Reduced flashes can remove any transient emphasis without removing contours, + signed numbers or before/after values. +- Mute changes no geometry, timing or caption. + +## Integration Notes + +- Preserve accepted actor sources and their stable IDs; do not redraw names or + profession labels into raster art. +- Do not copy the static result values into UI logic. Bind resolved result + clauses from `P91_RESULT`. +- Do not expose manual order, front/rear toggles or combat commands. +- The result context rail is deliberately dense at 100% and is not a 130% + acceptance frame. Runtime 130% reflow follows the frozen spec. + +## Residual Gates + +- Owner visual judgment remains open. +- Runtime comprehension, motion feel, sound, devices, packaging and release + remain outside this visual-slice handoff. diff --git a/docs/runtime/p9_1/visual_slice/README.md b/docs/runtime/p9_1/visual_slice/README.md new file mode 100644 index 00000000..f3d50b10 --- /dev/null +++ b/docs/runtime/p9_1/visual_slice/README.md @@ -0,0 +1,35 @@ +# P9.1 Representative First-Battle Visual Slice + +Open [`index.html`](index.html) with one of the frame query values in +[`frame-manifest.json`](frame-manifest.json), for example: + +```text +index.html?frame=formation-review +index.html?frame=approach-contact +index.html?frame=damage-feedback +index.html?frame=heal-response +index.html?frame=ranged-finish +index.html?frame=first-diagnosis +``` + +The source is an editable HTML/CSS/JS layer reference at fixed `844x390`. +`GRAYBOX.md` records composition and focal priorities; `HANDOFF.md` records +bindings, layers and runtime cautions; the two JSON manifests record frame and +asset provenance. `generation-record.json` records `0` paid calls and `$0`. + +## Evidence Commands + +From this directory: + +```bash +npm ci +npm run capture +npm run verify +``` + +`capture` writes the seven PNGs under `previews/`. `verify` checks every frame +at `844x390`, rejects viewport overflow or missing pair identity, confirms the +paid-call ceiling, and compares each copied asset byte-for-byte with its +accepted repository source. It writes `evidence/verification.json`. + +Delete `node_modules/` after verification; it is not part of the handoff. diff --git a/docs/runtime/p9_1/visual_slice/asset-manifest.json b/docs/runtime/p9_1/visual_slice/asset-manifest.json new file mode 100644 index 00000000..d62991b4 --- /dev/null +++ b/docs/runtime/p9_1/visual_slice/asset-manifest.json @@ -0,0 +1,20 @@ +{ + "revision": "p91-visual-slice-r1", + "policy": "Repository-accepted sources copied into the Goal-owned handoff for a self-contained editable reference; no third-party commercial assets and no new paid generation.", + "assets": [ + {"path": "assets/backgrounds/first_expedition_field.png", "source": "runtime/assets/backgrounds/first_expedition_field.png", "use": "environment"}, + {"path": "assets/actors/hearthguard.png", "source": "runtime/assets/actors/hearthguard.png", "use": "four-Recruit identity inventory; not rendered in this pair"}, + {"path": "assets/actors/wayrunner.png", "source": "runtime/assets/actors/wayrunner.png", "use": "four-Recruit identity inventory; not rendered in this pair"}, + {"path": "assets/actors/farstring.png", "source": "runtime/assets/actors/farstring.png", "use": "formation/result identity"}, + {"path": "assets/actors/kindhand.png", "source": "runtime/assets/actors/kindhand.png", "use": "formation/result identity"}, + {"path": "assets/actions/farstring/idle.png", "source": "runtime/assets/animations/farstring/idle/frame-0.png", "use": "battle idle"}, + {"path": "assets/actions/farstring/draw-release.png", "source": "runtime/assets/animations/farstring/draw_release/frame-2.png", "use": "ranged response"}, + {"path": "assets/actions/kindhand/idle.png", "source": "runtime/assets/animations/kindhand/idle/frame-0.png", "use": "battle idle"}, + {"path": "assets/actions/kindhand/lantern-heal.png", "source": "runtime/assets/animations/kindhand/lantern_heal/frame-2.png", "use": "heal response"}, + {"path": "assets/enemies/riftblade-idle.png", "source": "docs/prototype/generation/phase6_motion_proof_03/runs/enemy-riftblade/anchors/idle.png", "use": "accepted enemy identity"}, + {"path": "assets/vfx/damage-heavy.png", "source": "runtime/assets/vfx/damage_heavy.png", "use": "damage event accent"}, + {"path": "assets/vfx/heal-safe.png", "source": "runtime/assets/vfx/protect_gain.png", "use": "heal event accent"}, + {"path": "assets/fonts/NotoSansSC.ttf", "source": "runtime/assets/fonts/NotoSansSC.ttf", "use": "engine-rendered Chinese UI"}, + {"path": "assets/fonts/OFL.txt", "source": "runtime/assets/fonts/OFL.txt", "use": "font license"} + ] +} diff --git a/docs/runtime/p9_1/visual_slice/assets/actions/farstring/draw-release.png b/docs/runtime/p9_1/visual_slice/assets/actions/farstring/draw-release.png new file mode 100644 index 00000000..2eebd572 Binary files /dev/null and b/docs/runtime/p9_1/visual_slice/assets/actions/farstring/draw-release.png differ diff --git a/docs/runtime/p9_1/visual_slice/assets/actions/farstring/idle.png b/docs/runtime/p9_1/visual_slice/assets/actions/farstring/idle.png new file mode 100644 index 00000000..dabe7851 Binary files /dev/null and b/docs/runtime/p9_1/visual_slice/assets/actions/farstring/idle.png differ diff --git a/docs/runtime/p9_1/visual_slice/assets/actions/kindhand/idle.png b/docs/runtime/p9_1/visual_slice/assets/actions/kindhand/idle.png new file mode 100644 index 00000000..a354e963 Binary files /dev/null and b/docs/runtime/p9_1/visual_slice/assets/actions/kindhand/idle.png differ diff --git a/docs/runtime/p9_1/visual_slice/assets/actions/kindhand/lantern-heal.png b/docs/runtime/p9_1/visual_slice/assets/actions/kindhand/lantern-heal.png new file mode 100644 index 00000000..a376c47e Binary files /dev/null and b/docs/runtime/p9_1/visual_slice/assets/actions/kindhand/lantern-heal.png differ diff --git a/docs/runtime/p9_1/visual_slice/assets/actors/farstring.png b/docs/runtime/p9_1/visual_slice/assets/actors/farstring.png new file mode 100644 index 00000000..46a36ed8 Binary files /dev/null and b/docs/runtime/p9_1/visual_slice/assets/actors/farstring.png differ diff --git a/docs/runtime/p9_1/visual_slice/assets/actors/hearthguard.png b/docs/runtime/p9_1/visual_slice/assets/actors/hearthguard.png new file mode 100644 index 00000000..d17656b5 Binary files /dev/null and b/docs/runtime/p9_1/visual_slice/assets/actors/hearthguard.png differ diff --git a/docs/runtime/p9_1/visual_slice/assets/actors/kindhand.png b/docs/runtime/p9_1/visual_slice/assets/actors/kindhand.png new file mode 100644 index 00000000..01c890c6 Binary files /dev/null and b/docs/runtime/p9_1/visual_slice/assets/actors/kindhand.png differ diff --git a/docs/runtime/p9_1/visual_slice/assets/actors/wayrunner.png b/docs/runtime/p9_1/visual_slice/assets/actors/wayrunner.png new file mode 100644 index 00000000..91f8b666 Binary files /dev/null and b/docs/runtime/p9_1/visual_slice/assets/actors/wayrunner.png differ diff --git a/docs/runtime/p9_1/visual_slice/assets/backgrounds/first_expedition_field.png b/docs/runtime/p9_1/visual_slice/assets/backgrounds/first_expedition_field.png new file mode 100644 index 00000000..e59c8fca Binary files /dev/null and b/docs/runtime/p9_1/visual_slice/assets/backgrounds/first_expedition_field.png differ diff --git a/docs/runtime/p9_1/visual_slice/assets/enemies/riftblade-idle.png b/docs/runtime/p9_1/visual_slice/assets/enemies/riftblade-idle.png new file mode 100644 index 00000000..c3d0c783 Binary files /dev/null and b/docs/runtime/p9_1/visual_slice/assets/enemies/riftblade-idle.png differ diff --git a/docs/runtime/p9_1/visual_slice/assets/fonts/NotoSansSC.ttf b/docs/runtime/p9_1/visual_slice/assets/fonts/NotoSansSC.ttf new file mode 100644 index 00000000..4c4da763 Binary files /dev/null and b/docs/runtime/p9_1/visual_slice/assets/fonts/NotoSansSC.ttf differ diff --git a/docs/runtime/p9_1/visual_slice/assets/fonts/OFL.txt b/docs/runtime/p9_1/visual_slice/assets/fonts/OFL.txt new file mode 100644 index 00000000..31ea83be --- /dev/null +++ b/docs/runtime/p9_1/visual_slice/assets/fonts/OFL.txt @@ -0,0 +1,92 @@ +Copyright 2014, 2015 Adobe Systems Incorporated (http://www.adobe.com/). + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply to +any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/docs/runtime/p9_1/visual_slice/assets/vfx/damage-heavy.png b/docs/runtime/p9_1/visual_slice/assets/vfx/damage-heavy.png new file mode 100644 index 00000000..b1a682a7 Binary files /dev/null and b/docs/runtime/p9_1/visual_slice/assets/vfx/damage-heavy.png differ diff --git a/docs/runtime/p9_1/visual_slice/assets/vfx/heal-safe.png b/docs/runtime/p9_1/visual_slice/assets/vfx/heal-safe.png new file mode 100644 index 00000000..6e9f33ec Binary files /dev/null and b/docs/runtime/p9_1/visual_slice/assets/vfx/heal-safe.png differ diff --git a/docs/runtime/p9_1/visual_slice/capture.mjs b/docs/runtime/p9_1/visual_slice/capture.mjs new file mode 100644 index 00000000..ce3a29eb --- /dev/null +++ b/docs/runtime/p9_1/visual_slice/capture.mjs @@ -0,0 +1,38 @@ +import { chromium } from "playwright"; +import { createServer } from "node:http"; +import { readFile } from "node:fs/promises"; +import { extname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = fileURLToPath(new URL(".", import.meta.url)); +const frames = ["graybox", "formation-review", "approach-contact", "damage-feedback", "heal-response", "ranged-finish", "first-diagnosis"]; +const mime = { ".html": "text/html", ".css": "text/css", ".js": "text/javascript", ".png": "image/png", ".ttf": "font/ttf", ".json": "application/json" }; + +const server = createServer(async (request, response) => { + const rawPath = decodeURIComponent((request.url || "/").split("?")[0]); + const relativePath = rawPath === "/" ? "index.html" : rawPath.replace(/^\//, ""); + try { + const body = await readFile(join(root, relativePath)); + response.writeHead(200, { "content-type": mime[extname(relativePath)] || "application/octet-stream" }); + response.end(body); + } catch { + response.writeHead(404); + response.end("Not found"); + } +}); + +await new Promise(resolve => server.listen(0, "127.0.0.1", resolve)); +const { port } = server.address(); +const browser = await chromium.launch({ headless: true }); +const page = await browser.newPage({ viewport: { width: 844, height: 390 }, deviceScaleFactor: 1 }); + +for (const id of frames) { + const url = id === "graybox" + ? `http://127.0.0.1:${port}/graybox.html` + : `http://127.0.0.1:${port}/index.html?frame=${id}`; + await page.goto(url, { waitUntil: "networkidle" }); + await page.screenshot({ path: join(root, "previews", `${id}.png`) }); +} + +await browser.close(); +server.close(); diff --git a/docs/runtime/p9_1/visual_slice/evidence/verification.json b/docs/runtime/p9_1/visual_slice/evidence/verification.json new file mode 100644 index 00000000..ad88d459 --- /dev/null +++ b/docs/runtime/p9_1/visual_slice/evidence/verification.json @@ -0,0 +1,70 @@ +{ + "ok": true, + "viewport": [ + 844, + 390 + ], + "frame_count": 6, + "asset_count": 14, + "paid_calls_used": 0, + "paid_cost_usd": 0, + "errors": [], + "hashes": { + "assets/backgrounds/first_expedition_field.png": { + "source": "runtime/assets/backgrounds/first_expedition_field.png", + "sha256": "d8c1d2fb7fb466ef1abf8f9809ed9db276359d4905d260771ebd70be39fc3512" + }, + "assets/actors/hearthguard.png": { + "source": "runtime/assets/actors/hearthguard.png", + "sha256": "fac57d23584b713e39817ea36dc432743b63acd18c74dbb5dfabb1f819685a18" + }, + "assets/actors/wayrunner.png": { + "source": "runtime/assets/actors/wayrunner.png", + "sha256": "b1c06349cd91c84d86912579aa1137f634e52abeb983373fdea2a6857fc3f239" + }, + "assets/actors/farstring.png": { + "source": "runtime/assets/actors/farstring.png", + "sha256": "0f7eda3b8ac4431fb5a9401e02d37cd4addea4d08d94568331654b8d4c405a26" + }, + "assets/actors/kindhand.png": { + "source": "runtime/assets/actors/kindhand.png", + "sha256": "899eb7eb81af5acc2b85d344a87f3d0d15f2299d585dbb8fcb1031f19cc242e9" + }, + "assets/actions/farstring/idle.png": { + "source": "runtime/assets/animations/farstring/idle/frame-0.png", + "sha256": "5a4e91eebd04d3ac70e86c576ca76d7be958329a825bdec316b46343cddb512c" + }, + "assets/actions/farstring/draw-release.png": { + "source": "runtime/assets/animations/farstring/draw_release/frame-2.png", + "sha256": "8c339527f41cef9f6c194ac517aa1071ca6ddec7b90546998b6b1cf56167bf3d" + }, + "assets/actions/kindhand/idle.png": { + "source": "runtime/assets/animations/kindhand/idle/frame-0.png", + "sha256": "7c82a307e1cadc2879e45c61e514748477db576a8f7c6e30753fdcbb9459230f" + }, + "assets/actions/kindhand/lantern-heal.png": { + "source": "runtime/assets/animations/kindhand/lantern_heal/frame-2.png", + "sha256": "f883bacc2b9a0445ec0848f66364a69b7a632212411af39e5ba26668a61393b1" + }, + "assets/enemies/riftblade-idle.png": { + "source": "docs/prototype/generation/phase6_motion_proof_03/runs/enemy-riftblade/anchors/idle.png", + "sha256": "e1c706cda49d5a969d8b3b79cf0d249e68eda62317cd10482573b4e2b4694b9a" + }, + "assets/vfx/damage-heavy.png": { + "source": "runtime/assets/vfx/damage_heavy.png", + "sha256": "6204571743ed7914929f4fef1ab835b0c521023e5e93e9db5e65f6aaa875478f" + }, + "assets/vfx/heal-safe.png": { + "source": "runtime/assets/vfx/protect_gain.png", + "sha256": "4c3d9310eb3a420beb0e4919f27df131c7393a3f06e1af59c3e0c12a5472b8fd" + }, + "assets/fonts/NotoSansSC.ttf": { + "source": "runtime/assets/fonts/NotoSansSC.ttf", + "sha256": "a0af66489baf1f306732ff16da58d86c342e0b3b4b3674079ee2a9b0aee23258" + }, + "assets/fonts/OFL.txt": { + "source": "runtime/assets/fonts/OFL.txt", + "sha256": "e2b6ccef8ef613a8fad6571020ad77c9143273a752001b42293a089da46940d0" + } + } +} diff --git a/docs/runtime/p9_1/visual_slice/frame-manifest.json b/docs/runtime/p9_1/visual_slice/frame-manifest.json new file mode 100644 index 00000000..6749ac26 --- /dev/null +++ b/docs/runtime/p9_1/visual_slice/frame-manifest.json @@ -0,0 +1,25 @@ +{ + "revision": "p91-visual-slice-r1", + "viewport": [844, 390], + "locale": "zh_CN", + "text_scale": "100%", + "motion": "standard", + "flashes": "standard", + "audio": "mute-safe; captions on", + "contrast": "standard", + "power": "standard", + "fixture": { + "starting_pair": ["Farstring", "Kindhand"], + "field_order": ["Farstring", "Kindhand"], + "lead_id": "Farstring", + "initial_second_id": "Kindhand" + }, + "frames": [ + {"id": "formation-review", "screen": "P91-FORMATION-REVIEW", "proof": "P91-E04", "focus": "commit_formation", "interactive": false}, + {"id": "approach-contact", "screen": "P91-BATTLE-1", "proof": "P91-E05/contact", "focus": "captions", "interactive": false}, + {"id": "damage-feedback", "screen": "P91-BATTLE-1", "proof": "P91-E05/damage", "focus": "captions", "interactive": false}, + {"id": "heal-response", "screen": "P91-BATTLE-1", "proof": "profession-response/heal", "focus": "captions", "interactive": false}, + {"id": "ranged-finish", "screen": "P91-BATTLE-1", "proof": "profession-response/ranged", "focus": "captions", "interactive": false}, + {"id": "first-diagnosis", "screen": "P91-RESULT-1", "proof": "cause-first-result", "focus": "next_threat", "interactive": false} + ] +} diff --git a/docs/runtime/p9_1/visual_slice/generation-record.json b/docs/runtime/p9_1/visual_slice/generation-record.json new file mode 100644 index 00000000..660f88b9 --- /dev/null +++ b/docs/runtime/p9_1/visual_slice/generation-record.json @@ -0,0 +1,13 @@ +{ + "scope": "P9.1 representative first-battle/diagnosis visual slice", + "authorized_call_ceiling": 4, + "authorized_cost_ceiling_usd": 5.0, + "paid_calls_used": 0, + "returned_cost_usd": 0.0, + "task_ids": [], + "prompts": [], + "provider_expansion": false, + "decision": "No paid generation. Accepted P9.0 assets satisfy the bounded slice and preserve identity better than a new render.", + "accepted_outputs": [], + "rejected_outputs": [] +} diff --git a/docs/runtime/p9_1/visual_slice/graybox.html b/docs/runtime/p9_1/visual_slice/graybox.html new file mode 100644 index 00000000..0152b202 --- /dev/null +++ b/docs/runtime/p9_1/visual_slice/graybox.html @@ -0,0 +1,46 @@ + + + + + + P9.1 首战视觉切片灰盒 + + + +
+
TOP STATUS RAIL · 820 x 54
+
+ MAIN WORLD / CAUSAL EVENT · 544 x 242 +
1
第二位
0
先行位
+
敌人
+
+ +
BOTTOM ACTION DOCK · 808 x 56PRIMARY ACTION · 248 x 56
+ 1 · 当前事件 / 受影响角色 + 2 · 原因 / 精确变化 + 3 · 下一步 ↓ +
+ + diff --git a/docs/runtime/p9_1/visual_slice/index.html b/docs/runtime/p9_1/visual_slice/index.html new file mode 100644 index 00000000..80f1bfa5 --- /dev/null +++ b/docs/runtime/p9_1/visual_slice/index.html @@ -0,0 +1,13 @@ + + + + + + P9.1 首战视觉切片 + + + +
+ + + diff --git a/docs/runtime/p9_1/visual_slice/package-lock.json b/docs/runtime/p9_1/visual_slice/package-lock.json new file mode 100644 index 00000000..f3b3df26 --- /dev/null +++ b/docs/runtime/p9_1/visual_slice/package-lock.json @@ -0,0 +1,62 @@ +{ + "name": "aetherbound-p91-visual-slice", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "aetherbound-p91-visual-slice", + "version": "1.0.0", + "devDependencies": { + "playwright": "1.62.1" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz", + "integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.62.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz", + "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + } + } +} diff --git a/docs/runtime/p9_1/visual_slice/package.json b/docs/runtime/p9_1/visual_slice/package.json new file mode 100644 index 00000000..7a15efe4 --- /dev/null +++ b/docs/runtime/p9_1/visual_slice/package.json @@ -0,0 +1,13 @@ +{ + "name": "aetherbound-p91-visual-slice", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "capture": "node capture.mjs", + "verify": "node verify.mjs" + }, + "devDependencies": { + "playwright": "1.62.1" + } +} diff --git a/docs/runtime/p9_1/visual_slice/previews/approach-contact.png b/docs/runtime/p9_1/visual_slice/previews/approach-contact.png new file mode 100644 index 00000000..7162a3da Binary files /dev/null and b/docs/runtime/p9_1/visual_slice/previews/approach-contact.png differ diff --git a/docs/runtime/p9_1/visual_slice/previews/damage-feedback.png b/docs/runtime/p9_1/visual_slice/previews/damage-feedback.png new file mode 100644 index 00000000..c4e01ebc Binary files /dev/null and b/docs/runtime/p9_1/visual_slice/previews/damage-feedback.png differ diff --git a/docs/runtime/p9_1/visual_slice/previews/first-diagnosis.png b/docs/runtime/p9_1/visual_slice/previews/first-diagnosis.png new file mode 100644 index 00000000..1c4094c1 Binary files /dev/null and b/docs/runtime/p9_1/visual_slice/previews/first-diagnosis.png differ diff --git a/docs/runtime/p9_1/visual_slice/previews/formation-review.png b/docs/runtime/p9_1/visual_slice/previews/formation-review.png new file mode 100644 index 00000000..2c70851b Binary files /dev/null and b/docs/runtime/p9_1/visual_slice/previews/formation-review.png differ diff --git a/docs/runtime/p9_1/visual_slice/previews/graybox.png b/docs/runtime/p9_1/visual_slice/previews/graybox.png new file mode 100644 index 00000000..8889ff81 Binary files /dev/null and b/docs/runtime/p9_1/visual_slice/previews/graybox.png differ diff --git a/docs/runtime/p9_1/visual_slice/previews/heal-response.png b/docs/runtime/p9_1/visual_slice/previews/heal-response.png new file mode 100644 index 00000000..9798e8ab Binary files /dev/null and b/docs/runtime/p9_1/visual_slice/previews/heal-response.png differ diff --git a/docs/runtime/p9_1/visual_slice/previews/ranged-finish.png b/docs/runtime/p9_1/visual_slice/previews/ranged-finish.png new file mode 100644 index 00000000..d3b0eb06 Binary files /dev/null and b/docs/runtime/p9_1/visual_slice/previews/ranged-finish.png differ diff --git a/docs/runtime/p9_1/visual_slice/slice.css b/docs/runtime/p9_1/visual_slice/slice.css new file mode 100644 index 00000000..f23516b3 --- /dev/null +++ b/docs/runtime/p9_1/visual_slice/slice.css @@ -0,0 +1,176 @@ +@font-face { + font-family: "Aetherbound UI"; + src: url("assets/fonts/NotoSansSC.ttf") format("truetype"); + font-display: swap; +} + +:root { + --ink: #182127; + --ink-2: #34424a; + --mist: #f3f6f2; + --mist-2: #d7e0dd; + --teal: #13818a; + --gold: #d6a72e; + --jade: #27845e; + --coral: #c94e52; + --cobalt: #4169b1; + --plum: #855579; + --silver: #88969b; + --paper: rgba(246, 249, 244, 0.94); +} + +* { box-sizing: border-box; } +html, body { width: 844px; height: 390px; margin: 0; overflow: hidden; } +body { + background: #10181d; + color: var(--ink); + font-family: "Aetherbound UI", sans-serif; + letter-spacing: 0; +} +button { font: inherit; color: inherit; } +.slice { position: relative; width: 844px; height: 390px; overflow: hidden; background: #c7d8cf; } + +.status-rail { + position: absolute; z-index: 80; left: 12px; top: 10px; width: 820px; height: 54px; + display: flex; align-items: center; gap: 8px; padding: 2px 8px; + color: var(--mist); background: rgba(24, 33, 39, 0.94); + border: 1px solid rgba(243, 246, 242, 0.34); border-radius: 4px; + box-shadow: 0 4px 14px rgba(15, 24, 29, 0.25); +} +.tool { + width: 48px; height: 48px; flex: 0 0 48px; display: grid; place-items: center; + border: 1px solid rgba(243,246,242,.3); border-radius: 4px; background: #34424a; color: white; +} +.tool.active { border-color: #8fcfd1; box-shadow: inset 0 -3px 0 var(--teal); } +.icon { width: 21px; height: 21px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; } +.status-copy { min-width: 250px; display: flex; flex-direction: column; line-height: 1.05; } +.status-copy .eyebrow { color: #b9cac6; font-size: 9px; margin-bottom: 4px; } +.status-copy strong { color: white; font-size: 15px; font-weight: 700; } +.status-spacer { flex: 1; } +.resource, .battle-index { min-width: 70px; height: 42px; display: flex; flex-direction: column; justify-content: center; padding: 0 9px; border-left: 2px solid var(--gold); } +.resource span, .battle-index span { color: #bdcbc7; font-size: 9px; } +.resource b, .battle-index b { font-size: 12px; color: white; } + +.field-art { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; } +.formation-world { position: absolute; left: 18px; top: 72px; width: 544px; height: 242px; overflow: hidden; border: 1px solid rgba(24,33,39,.4); border-radius: 4px; background: #6f9b86; } +.formation-world .field-art { object-position: center 66%; filter: saturate(.72) brightness(.76); } +.formation-wash { position: absolute; inset: 0; background: linear-gradient(90deg, rgba(20,44,46,.42), rgba(39,56,50,.03) 58%, rgba(21,32,36,.32)); } +.formation-cord { position: absolute; left: 90px; right: 82px; bottom: 46px; height: 18px; border-bottom: 3px solid rgba(243,246,242,.75); } +.formation-cord:before, .formation-cord:after { content: ""; position: absolute; bottom: -7px; width: 10px; height: 10px; background: var(--gold); border: 2px solid var(--ink); transform: rotate(45deg); } +.formation-cord:before { left: 42px; }.formation-cord:after { right: 50px; } +.threat-gap { position: absolute; right: 12px; bottom: 56px; width: 90px; height: 35px; color: white; font-size: 9px; text-align: right; } +.threat-gap i { position: absolute; right: 0; bottom: 0; width: 75px; border-top: 2px dashed var(--coral); } +.threat-gap i:after { content:""; position:absolute; right:-1px; top:-5px; border-left:8px solid var(--coral); border-top:4px solid transparent; border-bottom:4px solid transparent; } + +.actor { position: absolute; z-index: 24; left: var(--x); top: var(--y); width: 132px; height: 185px; } +.actor > img { position: absolute; left: 4px; top: 2px; width: 124px; height: 126px; object-fit: contain; object-position: 50% 100%; filter: drop-shadow(0 5px 3px rgba(15,23,28,.45)); } +.formation-world .actor { width: 142px; height: 175px; } +.formation-world .actor > img { width: 138px; height: 148px; left: 2px; top: 0; } +.actor-badge { position: absolute; z-index: 4; left: 0; top: 3px; display: flex; align-items: center; height: 30px; color: white; background: rgba(24,33,39,.93); border: 1px solid rgba(255,255,255,.46); } +.actor-badge b { width: 30px; height: 28px; display: grid; place-items: center; background: var(--gold); color: var(--ink); } +.actor-badge span { padding: 0 7px; font-size: 10px; } +.actor-meta { position: absolute; z-index: 4; left: 0; bottom: 0; width: 132px; min-height: 51px; padding: 5px 7px; color: white; background: rgba(24,33,39,.94); border-left: 3px solid var(--silver); border-radius: 2px; } +.actor.urgent .actor-meta { border-left-color: var(--coral); box-shadow: 0 0 0 1px rgba(201,78,82,.55); } +.actor-meta strong { display: block; font-size: 11px; line-height: 14px; } +.actor-meta small { display: block; margin-top: 2px; color: #d7e0dd; font-size: 8px; } +.hp { display: block; width: 100%; height: 6px; margin-top: 3px; background: rgba(255,255,255,.18); border: 1px solid rgba(255,255,255,.33); } +.hp i { display: block; width: var(--hp); height: 100%; background: var(--jade); } +.formation-world .actor-meta { display: none; } +.formation-world .actor-badge { top: 112px; left: 14px; } + +.formation-note { position: absolute; bottom: 7px; z-index: 32; width: 185px; min-height: 42px; padding: 5px 7px; background: rgba(243,246,242,.94); border-left: 3px solid var(--teal); box-shadow: 0 2px 8px rgba(10,20,24,.22); } +.formation-note b { display: block; font-size: 10px; } +.formation-note span { display: block; font-size: 9px; line-height: 13px; color: var(--ink-2); } +.note-second { left: 18px; }.note-lead { left: 260px; border-left-color: var(--coral); } + +.context-rail { position: absolute; z-index: 60; left: 570px; top: 72px; width: 256px; height: 242px; padding: 13px 14px; overflow: hidden; background: var(--paper); border: 1px solid #b7c3bf; border-radius: 4px; box-shadow: 0 3px 12px rgba(20,31,36,.16); } +.rail-kicker { display: block; color: var(--teal); font-size: 9px; font-weight: 800; } +.context-rail h2 { margin: 3px 0 7px; font-size: 17px; line-height: 21px; } +.context-rail p { margin: 0 0 8px; font-size: 10px; line-height: 14px; color: var(--ink-2); } +.evidence-row { display: grid; grid-template-columns: 31px 1fr; gap: 7px; align-items: start; margin: 7px 0; padding-top: 6px; border-top: 1px solid #cbd4d1; } +.evidence-row span { font-size: 9px; line-height: 13px; } +.evidence-row b { display: block; font-size: 10px; } +.symbol { width: 27px; height: 27px; display: grid; place-items: center; font-style: normal; font-weight: 900; color: white; background: var(--coral); clip-path: polygon(50% 0,100% 20%,84% 100%,16% 100%,0 20%); } +.symbol.heal { background: var(--jade); clip-path: polygon(50% 0,70% 28%,100% 50%,70% 72%,50% 100%,30% 72%,0 50%,30% 28%); } +.system-note { padding-left: 8px; border-left: 2px solid var(--gold); font-weight: 700; } + +.action-dock { position: absolute; z-index: 80; left: 18px; top: 322px; width: 808px; height: 56px; display: flex; align-items: center; gap: 10px; padding: 0 8px; color: var(--mist); background: rgba(24,33,39,.96); border-radius: 4px; } +.action-dock > span { flex: 1; text-align: center; color: #bfcac7; font-size: 9px; } +.action-dock button { height: 48px; border-radius: 4px; } +.secondary { min-width: 146px; padding: 0 14px; color: white; border: 1px solid #77888e; background: #34424a; } +.primary { min-width: 248px; height: 56px !important; display: flex; align-items: center; justify-content: center; gap: 8px; padding: 0 16px; color: var(--ink); border: 2px solid #f0c854; border-bottom: 5px solid #936c12; background: var(--gold); font-weight: 800; } +.primary .icon { width: 18px; height: 18px; } + +.battlefield { position: absolute; z-index: 1; inset: 0; overflow: hidden; background: #588771; } +.battlefield .field-art { object-position: 50% 68%; filter: saturate(.78) brightness(.73) contrast(1.04); } +.field-wash { position: absolute; inset: 64px 0 0; background: linear-gradient(180deg, rgba(9,32,40,.16), rgba(17,31,33,.05) 47%, rgba(12,25,29,.72)); } +.objective { position: absolute; z-index: 8; left: 18px; top: 72px; width: 274px; min-height: 44px; padding: 7px 10px; color: white; background: rgba(24,33,39,.86); border-left: 3px solid var(--gold); } +.objective span { display: block; color: #bdd1cb; font-size: 8px; }.objective strong { display: block; margin-top: 2px; font-size: 12px; } +.party-ground { position: absolute; z-index: 4; left: 92px; top: 275px; width: 368px; height: 18px; border-bottom: 2px solid rgba(243,246,242,.55); transform: skewX(-10deg); } +.party-ground i { position: absolute; bottom: -6px; width: 11px; height: 11px; background: var(--gold); border: 2px solid var(--ink); transform: rotate(45deg); } +.party-ground i:first-child { left: 92px; }.party-ground i:last-child { left: 257px; } + +.enemy { position: absolute; z-index: 24; left: 558px; top: 126px; width: 170px; height: 175px; } +.enemy-sprite { position: absolute; left: 12px; top: 17px; width: 144px; height: 132px; object-fit: contain; filter: drop-shadow(0 6px 4px rgba(12,16,21,.5)); transform: scaleX(-1); } +.enemy-name { position: absolute; z-index: 3; top: 0; right: 0; min-width: 120px; padding: 4px 7px; text-align: right; color: white; background: rgba(24,33,39,.93); border-right: 3px solid var(--plum); } +.enemy-name strong { display:block; font-size: 11px; }.enemy-name span { display:block; font-size:8px; color:#d4c6d1; } +.enemy-hp { position: absolute; z-index: 4; left: 18px; bottom: 10px; width: 136px; height: 7px; } +.enemy small { position: absolute; z-index: 4; right: 17px; bottom: -2px; color: white; font-size: 8px; text-shadow: 0 1px 2px black; } +.enemy.urgent .enemy-name { border-right-color: var(--coral); } + +.event-ribbon { position: absolute; z-index: 70; left: 18px; bottom: 12px; width: 808px; height: 62px; display: grid; grid-template-columns: 84px 244px 1fr; align-items: center; gap: 10px; padding: 8px 11px; color: white; background: rgba(24,33,39,.96); border: 1px solid rgba(243,246,242,.35); border-radius: 4px; box-shadow: 0 -3px 14px rgba(12,20,25,.25); } +.event-step { align-self: stretch; display: grid; place-items: center; color: #d3e1dd; font-size: 9px; border-right: 1px solid #617178; } +.event-ribbon > strong { font-size: 12px; line-height: 16px; } +.event-chain { height: 38px; display: flex; align-items: center; justify-content: space-between; gap: 5px; padding: 0 8px; color: #d6e0dd; background: #2a373e; border-left: 3px solid var(--teal); font-size: 9px; } +.event-chain i { color: #819295; font-style: normal; }.event-chain b { color: white; font-size: 10px; } + +.target-line { position: absolute; z-index: 20; left: 392px; top: 195px; width: 184px; border-top: 3px dashed var(--coral); transform: rotate(-4deg); transform-origin: left; } +.target-line:before, .target-line:after { content:""; position:absolute; top:-7px; width:11px; height:11px; border:2px solid var(--coral); transform:rotate(45deg); background:rgba(201,78,82,.18); } +.target-line:before { left:-4px; }.target-line:after { right:-4px; } +.target-line span { position:absolute; left:44px; top:-24px; padding:2px 7px; color:white; background:rgba(90,31,35,.86); font-size:9px; transform:rotate(4deg); } +.threat-mark { position:absolute; z-index:38; left:372px; top:137px; width:42px; height:40px; } +.threat-mark i { position:absolute; bottom:0; width:0; height:0; border-left:18px solid transparent; border-right:18px solid transparent; border-bottom:31px solid var(--coral); } +.threat-mark i:first-child { left:-8px; transform:rotate(-16deg); }.threat-mark i:nth-child(2) { right:-8px; transform:rotate(16deg); } +.threat-mark span { position:absolute; z-index:2; left:15px; top:13px; color:white; font-weight:900; } + +.impact-line { position:absolute; z-index:20; left:390px; top:207px; width:181px; border-top:5px solid var(--coral); transform:rotate(-2deg); box-shadow:0 0 0 2px rgba(255,255,255,.42); } +.impact-line:after { content:""; position:absolute; left:-10px; top:-10px; width:19px; height:19px; background:var(--coral); clip-path:polygon(50% 0,100% 100%,0 100%); transform:rotate(-90deg); } +.damage-burst { position:absolute; z-index:35; left:350px; top:155px; width:86px; height:86px; }.vfx { width:100%; height:100%; object-fit:contain; } +.number { position:absolute; z-index:45; min-width:76px; padding:4px 7px; color:white; text-align:center; border:2px solid white; box-shadow:0 3px 0 rgba(24,33,39,.8); } +.number b { display:block; font-size:21px; line-height:22px; }.number span { display:block; font-size:8px; } +.damage-number { left:392px; top:122px; background:var(--coral); clip-path:polygon(0 0,100% 0,88% 100%,12% 100%); } + +.heal-path { position:absolute; z-index:20; left:232px; top:214px; width:137px; border-top:4px solid var(--jade); transform:rotate(-11deg); box-shadow:0 0 0 1px rgba(255,255,255,.45); } +.heal-path i { position:absolute; top:-8px; width:12px; height:12px; background:var(--jade); clip-path:polygon(50% 0,100% 50%,50% 100%,0 50%); } +.heal-path i:nth-child(1) { left:12px; }.heal-path i:nth-child(2) { left:61px; }.heal-path i:nth-child(3) { right:-2px; } +.heal-burst { position:absolute; z-index:35; left:345px; top:155px; width:82px; height:82px; } +.heal-number { left:388px; top:122px; background:var(--jade); clip-path:polygon(50% 0,100% 25%,92% 100%,8% 100%,0 25%); } + +.projectile-line { position:absolute; z-index:26; left:389px; top:195px; width:190px; height:4px; background:var(--gold); box-shadow:0 0 0 1px white, 12px 0 8px var(--gold); transform:rotate(-2deg); } +.projectile-line:after { content:""; position:absolute; right:-10px; top:-6px; border-left:12px solid var(--gold); border-top:7px solid transparent; border-bottom:7px solid transparent; } +.projectile-line span { position:absolute; left:70px; top:-5px; width:9px; height:9px; border-radius:50%; background:white; box-shadow:0 0 9px var(--gold); } +.enemy-number { left:590px; top:119px; background:var(--coral); clip-path:polygon(0 0,100% 0,88% 100%,12% 100%); } + +.result-world { position:absolute; left:18px; top:72px; width:544px; height:242px; overflow:hidden; border:1px solid rgba(24,33,39,.4); border-radius:4px; background:#547d6d; } +.result-world .field-art { object-position:48% 70%; filter:saturate(.55) brightness(.62); } +.result-wash { position:absolute; inset:0; background:linear-gradient(90deg,rgba(17,33,37,.68),rgba(25,44,43,.26)); } +.result-party img { position:absolute; z-index:10; bottom:18px; width:104px; height:168px; object-fit:contain; object-position:bottom; filter:drop-shadow(0 5px 3px rgba(8,17,20,.45)); } +.result-kindhand { left:18px; }.result-farstring { left:102px; } +.result-order { position:absolute; z-index:12; bottom:9px; padding:3px 7px; color:white; background:rgba(24,33,39,.94); font-size:8px; border-top:2px solid var(--gold); } +.result-order.second { left:17px; }.result-order.lead { left:115px; } +.cause-path { position:absolute; z-index:20; left:204px; top:20px; width:326px; height:204px; display:flex; flex-direction:column; justify-content:space-between; } +.cause-node { position:relative; min-height:50px; padding:6px 9px 6px 13px; color:white; background:rgba(24,33,39,.92); border-left:4px solid var(--teal); box-shadow:0 2px 7px rgba(8,16,20,.28); } +.cause-node span,.cause-node small { display:block; font-size:8px; color:#c6d6d1; }.cause-node b { display:block; margin:1px 0; font-size:13px; }.cause-node.divergence { border:2px solid white; border-left:5px solid var(--coral); box-shadow:0 0 0 2px var(--ink),0 0 0 4px var(--cobalt); }.cause-node.response { border-left-color:var(--jade); } +.cause-path > i { align-self:center; width:2px; height:12px; background:var(--gold); position:relative; } +.cause-path > i:after { content:""; position:absolute; left:-3px; bottom:-1px; border-top:6px solid var(--gold); border-left:4px solid transparent; border-right:4px solid transparent; } +.result-rail { padding:10px 12px; } +.result-rail h2 { margin-bottom:5px; font-size:15px; line-height:18px; } +.result-copy { margin:0; } +.result-copy div { display:grid; grid-template-columns:76px 1fr; gap:6px; padding:3px 0; border-top:1px solid #ccd5d2; } +.result-copy dt { color:var(--teal); font-size:9px; line-height:12px; font-weight:800; }.result-copy dd { margin:0; font-size:9px; line-height:12px; } +.result-copy div:nth-child(3) { border-left:3px solid var(--coral); padding-left:5px; background:rgba(201,78,82,.07); } +.result-dock .receipt { color:#cce8dc; }.result-dock .receipt .icon { width:15px; height:15px; margin-right:4px; vertical-align:-3px; color:#65bf91; } + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; transition-duration: 0.001ms !important; } +} diff --git a/docs/runtime/p9_1/visual_slice/slice.js b/docs/runtime/p9_1/visual_slice/slice.js new file mode 100644 index 00000000..f8eefe98 --- /dev/null +++ b/docs/runtime/p9_1/visual_slice/slice.js @@ -0,0 +1,230 @@ +const FRAME_IDS = [ + "formation-review", + "approach-contact", + "damage-feedback", + "heal-response", + "ranged-finish", + "first-diagnosis", +]; + +const ASSETS = { + field: "assets/backgrounds/first_expedition_field.png", + farstring: "assets/actors/farstring.png", + kindhand: "assets/actors/kindhand.png", + farstringIdle: "assets/actions/farstring/idle.png", + farstringShot: "assets/actions/farstring/draw-release.png", + kindhandIdle: "assets/actions/kindhand/idle.png", + kindhandHeal: "assets/actions/kindhand/lantern-heal.png", + enemy: "assets/enemies/riftblade-idle.png", + damage: "assets/vfx/damage-heavy.png", + heal: "assets/vfx/heal-safe.png", +}; + +const frame = new URLSearchParams(window.location.search).get("frame") || FRAME_IDS[0]; +const selectedFrame = FRAME_IDS.includes(frame) ? frame : FRAME_IDS[0]; + +function img(src, className, alt) { + return `${alt}`; +} + +function icon(name) { + const icons = { + home: '', + restart: '', + pause: '', + inspect: '', + captions: '', + arrow: '', + check: '', + }; + return ``; +} + +function statusRail(context, battle = false) { + return ` +
+ +
+ 第一份委托 · 断裂风路 + ${context} +
+
+ ${battle ? '
战斗 1 / 5自动战斗
' : '
小队2
'} + + ${battle ? `` : ''} +
`; +} + +function actor({who, action = "idle", x, y, order, label, hp = 100, urgent = ""}) { + const actorSrc = who === "farstring" + ? (action === "shot" ? ASSETS.farstringShot : action === "full" ? ASSETS.farstring : ASSETS.farstringIdle) + : (action === "heal" ? ASSETS.kindhandHeal : action === "full" ? ASSETS.kindhand : ASSETS.kindhandIdle); + const name = who === "farstring" ? "妮娅" : "赛布尔"; + const role = who === "farstring" ? "弓手" : "治疗"; + return ` +
+
${order}${label}
+ ${name} · ${role} +
+ ${name} · ${role} + + ${hp} / 100${urgent ? ` · ${urgent}` : ""} +
+
`; +} + +function enemy(hp = 100, urgent = "") { + return ` +
+
裂影刃手路线守卫
+ ${img(ASSETS.enemy, "enemy-sprite", "裂影刃手")} + + ${hp} / 100${urgent ? ` · ${urgent}` : ""} +
`; +} + +function battlefieldBase(title, objective) { + return ` + ${statusRail(title, true)} +
+ 断裂风路遗迹战场 +
+
当前目标${objective}
+
+
`; +} + +function eventRibbon(step, text, source, action, target, result) { + return ` +
+ 事件 ${step} + ${text} +
${source}→${action}→${target}→${result}
+
`; +} + +function formationReview() { + return ` + ${statusRail("系统编队 · 检查两人的职责")} +
+ 断裂风路遗迹 +
+
+ ${actor({who:"kindhand", action:"full", x:64, y:82, order:"1", label:"第二位", hp:100})} + ${actor({who:"farstring", action:"full", x:282, y:74, order:"0", label:"先行位", hp:100, urgent:"暴露"})} +
敌人方向
+
第二位 · 1受击后举灯,为先行者恢复生命
+
先行位 · 0先接触威胁,以弓术回应敌人
+
+ +
+ + 选择顺序不会改变系统编队 + +
`; +} + +function approachContact() { + return ` + ${battlefieldBase("第一战 · 接敌", "守住断裂风路 · 读取敌人锁定")} + ${actor({who:"kindhand", x:112, y:137, order:"1", label:"第二位", hp:100})} + ${actor({who:"farstring", x:270, y:125, order:"0", label:"先行位", hp:100, urgent:"被锁定"})} + ${enemy(100, "蓄势")} +
锁定先行者
+
!
+ ${eventRibbon("01 · 1.20s", "危险:裂影刃手锁定妮娅", "裂影刃手", "重击预兆", "妮娅 · 先行位", "即将命中")}`; +} + +function damageFeedback() { + return ` + ${battlefieldBase("第一战 · 敌方压力", "确认受击对象与生命变化")} + ${actor({who:"kindhand", x:112, y:137, order:"1", label:"第二位", hp:100})} + ${actor({who:"farstring", x:270, y:125, order:"0", label:"先行位", hp:58, urgent:"高暴露"})} + ${enemy(100, "攻击已结算")} +
+
${img(ASSETS.damage, "vfx", "重击伤害")}
+
-42100 → 58
+ ${eventRibbon("02 · 2.35s", "妮娅在先行位承受高额伤害", "裂影刃手", "裂隙重击", "妮娅", "-42 · 58/100")}`; +} + +function healResponse() { + return ` + ${battlefieldBase("第一战 · 职业回应", "赛布尔恢复实际受伤的妮娅")} + ${actor({who:"kindhand", action:"heal", x:112, y:137, order:"1", label:"第二位", hp:100, urgent:"正在恢复"})} + ${actor({who:"farstring", x:270, y:125, order:"0", label:"先行位", hp:70, urgent:"恢复后"})} + ${enemy(100, "等待")} +
+
${img(ASSETS.heal, "vfx", "恢复效果")}
+
+1258 → 70
+ ${eventRibbon("03 · 3.60s", "赛布尔从第二位举灯恢复妮娅", "赛布尔", "灵灯恢复", "妮娅", "+12 · 70/100")}`; +} + +function rangedFinish() { + return ` + ${battlefieldBase("第一战 · 两人连携", "妮娅完成本轮远程回应")} + ${actor({who:"kindhand", x:112, y:137, order:"1", label:"第二位", hp:100})} + ${actor({who:"farstring", action:"shot", x:270, y:125, order:"0", label:"先行位", hp:70, urgent:"远程射击"})} + ${enemy(34, "即将倒下")} +
+
-3064 → 34
+ ${eventRibbon("04 · 5.10s", "妮娅以弓术把敌人压入终结线", "妮娅", "远程射击", "裂影刃手", "-30 · 34/100")}`; +} + +function firstDiagnosis() { + return ` + ${statusRail("第一战诊断 · 这支小队如何配合")} +
+ 战后断裂风路 +
+
+ ${img(ASSETS.kindhand, "result-kindhand", "赛布尔 · 治疗")} + ${img(ASSETS.farstring, "result-farstring", "妮娅 · 弓手")} + 1 · 第二位 + 0 · 先行位 +
+
+
系统编队妮娅先行远程职责,但高暴露
+ +
最早差异 · 2.35s重击 -42妮娅 100 → 58
+ +
职业回应 · 3.60s恢复 +12妮娅 58 → 70
+
+
+ + `; +} + +const renderers = { + "formation-review": formationReview, + "approach-contact": approachContact, + "damage-feedback": damageFeedback, + "heal-response": healResponse, + "ranged-finish": rangedFinish, + "first-diagnosis": firstDiagnosis, +}; + +const root = document.querySelector("#slice"); +root.dataset.frame = selectedFrame; +root.innerHTML = renderers[selectedFrame](); +document.documentElement.dataset.ready = "true"; diff --git a/docs/runtime/p9_1/visual_slice/verify.mjs b/docs/runtime/p9_1/visual_slice/verify.mjs new file mode 100644 index 00000000..5bc6221f --- /dev/null +++ b/docs/runtime/p9_1/visual_slice/verify.mjs @@ -0,0 +1,73 @@ +import { createHash } from "node:crypto"; +import { readFile, writeFile } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; +import { join } from "node:path"; +import { chromium } from "playwright"; +import { createServer } from "node:http"; + +const root = fileURLToPath(new URL(".", import.meta.url)); +const manifest = JSON.parse(await readFile(join(root, "frame-manifest.json"), "utf8")); +const assets = JSON.parse(await readFile(join(root, "asset-manifest.json"), "utf8")); +const errors = []; +const hashes = {}; + +for (const asset of assets.assets) { + const local = await readFile(join(root, asset.path)); + const source = await readFile(join(root, "../../../..", asset.source)); + const localHash = createHash("sha256").update(local).digest("hex"); + const sourceHash = createHash("sha256").update(source).digest("hex"); + hashes[asset.path] = { source: asset.source, sha256: localHash }; + if (localHash !== sourceHash) errors.push(`source hash mismatch: ${asset.path}`); +} + +const mime = { ".html":"text/html", ".css":"text/css", ".js":"text/javascript", ".png":"image/png", ".ttf":"font/ttf" }; +const server = createServer(async (request, response) => { + const rawPath = decodeURIComponent((request.url || "/").split("?")[0]); + const relativePath = rawPath === "/" ? "index.html" : rawPath.replace(/^\//, ""); + try { + const { extname } = await import("node:path"); + const body = await readFile(join(root, relativePath)); + response.writeHead(200, { "content-type": mime[extname(relativePath)] || "application/octet-stream" }); + response.end(body); + } catch { response.writeHead(404); response.end("Not found"); } +}); + +await new Promise(resolve => server.listen(0, "127.0.0.1", resolve)); +const { port } = server.address(); +const browser = await chromium.launch({ headless: true }); +const page = await browser.newPage({ viewport: { width: 844, height: 390 }, deviceScaleFactor: 1 }); + +for (const frame of manifest.frames) { + await page.goto(`http://127.0.0.1:${port}/index.html?frame=${frame.id}`, { waitUntil: "networkidle" }); + const state = await page.evaluate(() => { + const slice = document.querySelector("#slice"); + const nodes = [...document.querySelectorAll("body *")]; + const overflow = nodes.filter(node => { + const r = node.getBoundingClientRect(); + return r.width > 0 && r.height > 0 && (r.left < -0.5 || r.top < -0.5 || r.right > 844.5 || r.bottom > 390.5); + }).map(node => `${node.tagName}.${node.className}`); + return { + ready: document.documentElement.dataset.ready, + frame: slice?.dataset.frame, + overflow, + actors: document.querySelectorAll(".layer-actors img").length, + text: document.body.innerText + }; + }); + if (state.ready !== "true" || state.frame !== frame.id) errors.push(`frame not ready: ${frame.id}`); + if (state.overflow.length) errors.push(`viewport overflow ${frame.id}: ${state.overflow.join(", ")}`); + if (state.actors < 2) errors.push(`missing actors: ${frame.id}`); + if (!state.text.includes("妮娅") || !state.text.includes("赛布尔")) errors.push(`identity copy missing: ${frame.id}`); +} + +await browser.close(); +server.close(); + +const generation = JSON.parse(await readFile(join(root, "generation-record.json"), "utf8")); +if (generation.paid_calls_used > generation.authorized_call_ceiling) errors.push("paid call ceiling exceeded"); +if (generation.returned_cost_usd > generation.authorized_cost_ceiling_usd) errors.push("paid cost ceiling exceeded"); + +const report = { ok: errors.length === 0, viewport: manifest.viewport, frame_count: manifest.frames.length, asset_count: assets.assets.length, paid_calls_used: generation.paid_calls_used, paid_cost_usd: generation.returned_cost_usd, errors, hashes }; +await writeFile(join(root, "evidence", "verification.json"), JSON.stringify(report, null, 2) + "\n"); +console.log(JSON.stringify(report, null, 2)); +if (errors.length) process.exitCode = 1;