feat(p9.1): add representative battle visual slice
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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=<id>` 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.
|
||||
@@ -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.
|
||||
@@ -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"}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 165 KiB |
|
After Width: | Height: | Size: 143 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 3.5 MiB |
|
After Width: | Height: | Size: 43 KiB |
@@ -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.
|
||||
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 32 KiB |
@@ -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();
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
]
|
||||
}
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=844, initial-scale=1">
|
||||
<title>P9.1 首战视觉切片灰盒</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
html, body { width: 844px; height: 390px; margin: 0; overflow: hidden; }
|
||||
body { font-family: sans-serif; color: #182127; background: #d7e0dd; }
|
||||
.canvas { position: relative; width: 844px; height: 390px; }
|
||||
.box { position: absolute; border: 2px solid #34424a; background: rgba(255,255,255,.52); }
|
||||
.box b { display: block; padding: 4px 7px; color: white; background: #34424a; font-size: 12px; }
|
||||
.top { left: 12px; top: 10px; width: 820px; height: 54px; }
|
||||
.world { left: 18px; top: 72px; width: 544px; height: 242px; background: #a9c3b8; }
|
||||
.rail { left: 570px; top: 72px; width: 256px; height: 242px; }
|
||||
.dock { left: 18px; top: 322px; width: 808px; height: 56px; }
|
||||
.party { position: absolute; left: 54px; top: 100px; width: 345px; height: 116px; border-bottom: 4px solid #34424a; }
|
||||
.actor { position: absolute; bottom: 0; width: 84px; height: 104px; display: grid; place-items: center; color: white; background: #566970; border: 3px solid white; }
|
||||
.second { left: 34px; }.lead { right: 30px; background: #c94e52; }
|
||||
.enemy { position: absolute; right: 32px; top: 68px; width: 92px; height: 115px; display: grid; place-items: center; color: white; background: #855579; border: 3px solid #182127; }
|
||||
.gap { position: absolute; left: 382px; top: 128px; width: 72px; border-top: 4px dashed #c94e52; }
|
||||
.focal { position: absolute; padding: 3px 7px; color: white; font-weight: bold; font-size: 11px; border-radius: 2px; }
|
||||
.first { left: 294px; top: 80px; background: #c94e52; }
|
||||
.second-focal { left: 588px; top: 84px; background: #13818a; }
|
||||
.third { right: 26px; bottom: 67px; background: #d6a72e; color: #182127; }
|
||||
.rail ul { margin: 14px 12px; padding-left: 18px; font-size: 12px; line-height: 1.65; }
|
||||
.dock .button { position: absolute; right: 8px; top: -2px; width: 248px; height: 56px; display: grid; place-items: center; color: #182127; background: #d6a72e; border: 2px solid #182127; font-weight: bold; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="canvas">
|
||||
<section class="box top"><b>TOP STATUS RAIL · 820 x 54</b></section>
|
||||
<section class="box world">
|
||||
<b>MAIN WORLD / CAUSAL EVENT · 544 x 242</b>
|
||||
<div class="party"><span class="actor second">1<br>第二位</span><span class="actor lead">0<br>先行位</span></div>
|
||||
<div class="gap"></div><div class="enemy">敌人</div>
|
||||
</section>
|
||||
<aside class="box rail"><b>CONTEXT RAIL · 256 x 242</b><ul><li>当前证据</li><li>行动与目标</li><li>精确前后值</li><li>因果解释</li></ul></aside>
|
||||
<footer class="box dock"><b>BOTTOM ACTION DOCK · 808 x 56</b><span class="button">PRIMARY ACTION · 248 x 56</span></footer>
|
||||
<span class="focal first">1 · 当前事件 / 受影响角色</span>
|
||||
<span class="focal second-focal">2 · 原因 / 精确变化</span>
|
||||
<span class="focal third">3 · 下一步 ↓</span>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=844, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<title>P9.1 首战视觉切片</title>
|
||||
<link rel="stylesheet" href="slice.css">
|
||||
</head>
|
||||
<body>
|
||||
<main id="slice" class="slice" aria-live="polite"></main>
|
||||
<script src="slice.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 402 KiB |
|
After Width: | Height: | Size: 399 KiB |
|
After Width: | Height: | Size: 212 KiB |
|
After Width: | Height: | Size: 276 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 401 KiB |
|
After Width: | Height: | Size: 401 KiB |
@@ -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; }
|
||||
}
|
||||
@@ -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 `<img class="${className}" src="${src}" alt="${alt}">`;
|
||||
}
|
||||
|
||||
function icon(name) {
|
||||
const icons = {
|
||||
home: '<path d="M4 11 12 4l8 7v8h-5v-5H9v5H4z"/>',
|
||||
restart: '<path d="M5 8a8 8 0 1 1-1 7m1-7V3m0 5h5"/>',
|
||||
pause: '<path d="M7 5h3v14H7zm7 0h3v14h-3z"/>',
|
||||
inspect: '<circle cx="11" cy="11" r="6"/><path d="m16 16 4 4"/>',
|
||||
captions: '<path d="M4 6h16v11H9l-4 3v-3H4z"/><path d="M8 10h3m2 0h3m-8 3h5"/>',
|
||||
arrow: '<path d="m7 12 9-7v14z"/>',
|
||||
check: '<path d="m5 12 4 4L19 6"/>',
|
||||
};
|
||||
return `<svg class="icon" viewBox="0 0 24 24" aria-hidden="true">${icons[name]}</svg>`;
|
||||
}
|
||||
|
||||
function statusRail(context, battle = false) {
|
||||
return `
|
||||
<header class="status-rail layer-ui">
|
||||
<button class="tool" aria-label="返回首页">${icon("home")}</button>
|
||||
<div class="status-copy">
|
||||
<span class="eyebrow">第一份委托 · 断裂风路</span>
|
||||
<strong>${context}</strong>
|
||||
</div>
|
||||
<div class="status-spacer"></div>
|
||||
${battle ? '<div class="battle-index"><span>战斗 1 / 5</span><b>自动战斗</b></div>' : '<div class="resource"><span>小队</span><b>2</b></div>'}
|
||||
<button class="tool" aria-label="重新开始">${icon("restart")}</button>
|
||||
${battle ? `<button class="tool" aria-label="暂停">${icon("pause")}</button><button class="tool" aria-label="查看事件">${icon("inspect")}</button><button class="tool active" aria-label="字幕开启">${icon("captions")}</button>` : ''}
|
||||
</header>`;
|
||||
}
|
||||
|
||||
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 `
|
||||
<section class="actor actor-${who} ${urgent ? "urgent" : ""} layer-actors" style="--x:${x}px;--y:${y}px" aria-label="${order} ${name} ${role} 生命 ${hp}">
|
||||
<div class="actor-badge"><b>${order}</b><span>${label}</span></div>
|
||||
<img src="${actorSrc}" alt="${name} · ${role}">
|
||||
<div class="actor-meta">
|
||||
<strong>${name} · ${role}</strong>
|
||||
<span class="hp"><i style="--hp:${hp}%"></i></span>
|
||||
<small>${hp} / 100${urgent ? ` · ${urgent}` : ""}</small>
|
||||
</div>
|
||||
</section>`;
|
||||
}
|
||||
|
||||
function enemy(hp = 100, urgent = "") {
|
||||
return `
|
||||
<section class="enemy ${urgent ? "urgent" : ""} layer-actors" aria-label="裂影刃手 生命 ${hp}">
|
||||
<div class="enemy-name"><strong>裂影刃手</strong><span>路线守卫</span></div>
|
||||
${img(ASSETS.enemy, "enemy-sprite", "裂影刃手")}
|
||||
<span class="hp enemy-hp"><i style="--hp:${hp}%"></i></span>
|
||||
<small>${hp} / 100${urgent ? ` · ${urgent}` : ""}</small>
|
||||
</section>`;
|
||||
}
|
||||
|
||||
function battlefieldBase(title, objective) {
|
||||
return `
|
||||
${statusRail(title, true)}
|
||||
<section class="battlefield layer-environment">
|
||||
<img class="field-art" src="${ASSETS.field}" alt="断裂风路遗迹战场">
|
||||
<div class="field-wash"></div>
|
||||
<div class="objective layer-ui"><span>当前目标</span><strong>${objective}</strong></div>
|
||||
<div class="party-ground layer-connectors"><i></i><i></i></div>
|
||||
</section>`;
|
||||
}
|
||||
|
||||
function eventRibbon(step, text, source, action, target, result) {
|
||||
return `
|
||||
<footer class="event-ribbon layer-captions">
|
||||
<span class="event-step">事件 ${step}</span>
|
||||
<strong>${text}</strong>
|
||||
<div class="event-chain"><span>${source}</span><i>→</i><span>${action}</span><i>→</i><span>${target}</span><i>→</i><b>${result}</b></div>
|
||||
</footer>`;
|
||||
}
|
||||
|
||||
function formationReview() {
|
||||
return `
|
||||
${statusRail("系统编队 · 检查两人的职责")}
|
||||
<section class="formation-world layer-environment">
|
||||
<img class="field-art" src="${ASSETS.field}" alt="断裂风路遗迹">
|
||||
<div class="formation-wash"></div>
|
||||
<div class="formation-cord layer-connectors"><span></span></div>
|
||||
${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:"暴露"})}
|
||||
<div class="threat-gap layer-connectors"><span>敌人方向</span><i></i></div>
|
||||
<div class="formation-note note-second layer-ui"><b>第二位 · 1</b><span>受击后举灯,为先行者恢复生命</span></div>
|
||||
<div class="formation-note note-lead layer-ui"><b>先行位 · 0</b><span>先接触威胁,以弓术回应敌人</span></div>
|
||||
</section>
|
||||
<aside class="context-rail layer-ui">
|
||||
<span class="rail-kicker">系统排序原因</span>
|
||||
<h2>职责决定顺序</h2>
|
||||
<p>妮娅先接近威胁;赛布尔留在受保护位置,等待实际受伤目标。</p>
|
||||
<div class="evidence-row"><i class="symbol order">0</i><span><b>妮娅 · 弓手</b>远程职责先行,但会承受更高暴露。</span></div>
|
||||
<div class="evidence-row"><i class="symbol heal">+</i><span><b>赛布尔 · 治疗</b>第二位行动,恢复实际受伤成员。</span></div>
|
||||
<p class="system-note">系统依据两人的当前职责形成此顺序。</p>
|
||||
</aside>
|
||||
<footer class="action-dock layer-ui">
|
||||
<button class="secondary">重新选择成员</button>
|
||||
<span>选择顺序不会改变系统编队</span>
|
||||
<button class="primary">${icon("arrow")}确认编队并出发</button>
|
||||
</footer>`;
|
||||
}
|
||||
|
||||
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, "蓄势")}
|
||||
<div class="target-line layer-connectors"><span>锁定先行者</span></div>
|
||||
<div class="threat-mark layer-feedback"><i></i><i></i><span>!</span></div>
|
||||
${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, "攻击已结算")}
|
||||
<div class="impact-line layer-connectors"></div>
|
||||
<div class="damage-burst layer-feedback">${img(ASSETS.damage, "vfx", "重击伤害")}</div>
|
||||
<div class="number damage-number layer-feedback"><b>-42</b><span>100 → 58</span></div>
|
||||
${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, "等待")}
|
||||
<div class="heal-path layer-connectors"><i></i><i></i><i></i></div>
|
||||
<div class="heal-burst layer-feedback">${img(ASSETS.heal, "vfx", "恢复效果")}</div>
|
||||
<div class="number heal-number layer-feedback"><b>+12</b><span>58 → 70</span></div>
|
||||
${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, "即将倒下")}
|
||||
<div class="projectile-line layer-connectors"><span></span></div>
|
||||
<div class="number enemy-number layer-feedback"><b>-30</b><span>64 → 34</span></div>
|
||||
${eventRibbon("04 · 5.10s", "妮娅以弓术把敌人压入终结线", "妮娅", "远程射击", "裂影刃手", "-30 · 34/100")}`;
|
||||
}
|
||||
|
||||
function firstDiagnosis() {
|
||||
return `
|
||||
${statusRail("第一战诊断 · 这支小队如何配合")}
|
||||
<section class="result-world layer-environment">
|
||||
<img class="field-art" src="${ASSETS.field}" alt="战后断裂风路">
|
||||
<div class="result-wash"></div>
|
||||
<div class="result-party layer-actors">
|
||||
${img(ASSETS.kindhand, "result-kindhand", "赛布尔 · 治疗")}
|
||||
${img(ASSETS.farstring, "result-farstring", "妮娅 · 弓手")}
|
||||
<span class="result-order second">1 · 第二位</span>
|
||||
<span class="result-order lead">0 · 先行位</span>
|
||||
</div>
|
||||
<div class="cause-path layer-connectors">
|
||||
<div class="cause-node decision"><span>系统编队</span><b>妮娅先行</b><small>远程职责,但高暴露</small></div>
|
||||
<i></i>
|
||||
<div class="cause-node divergence"><span>最早差异 · 2.35s</span><b>重击 -42</b><small>妮娅 100 → 58</small></div>
|
||||
<i></i>
|
||||
<div class="cause-node response"><span>职业回应 · 3.60s</span><b>恢复 +12</b><small>妮娅 58 → 70</small></div>
|
||||
</div>
|
||||
</section>
|
||||
<aside class="context-rail result-rail layer-ui">
|
||||
<span class="rail-kicker">首战因果结果</span>
|
||||
<h2>治疗补回部分损失</h2>
|
||||
<dl class="result-copy">
|
||||
<div><dt>发生了什么</dt><dd>系统让妮娅先行;她承受重击后完成远程回应。</dd></div>
|
||||
<div><dt>为什么</dt><dd>赛布尔随后为实际受伤的妮娅恢复 12 生命。</dd></div>
|
||||
<div><dt>系统编队造成的差异</dt><dd>远程成员先行而高暴露;治疗只补回部分损失。</dd></div>
|
||||
<div><dt>现在改变了</dt><dd>妮娅 70/100;金币 0 → 20(仅一次)。</dd></div>
|
||||
<div><dt>下一步</dt><dd>比较安全与节奏两种应对方案。</dd></div>
|
||||
</dl>
|
||||
</aside>
|
||||
<footer class="action-dock result-dock layer-ui">
|
||||
<button class="secondary">${icon("inspect")}查看完整事件</button>
|
||||
<span class="receipt">${icon("check")}奖励已结算一次 · 金币 +20</span>
|
||||
<button class="primary">${icon("arrow")}查看下一场威胁</button>
|
||||
</footer>`;
|
||||
}
|
||||
|
||||
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";
|
||||
@@ -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;
|
||||