feat: rebuild first-play opening as game flow
@@ -1,9 +1,19 @@
|
||||
# Current Checkpoint
|
||||
|
||||
Active Goal: `docs/goals/PHASE9_0_BOOT_TITLE_GOAL.md`
|
||||
Active contract: `docs/runtime/PHASE9_0_BOOT_TITLE_CONTRACT.md`
|
||||
Active Goal: `docs/goals/PHASE9_0R1_GAME_OPENING_REDESIGN_GOAL.md`
|
||||
Active contract: `docs/runtime/PHASE9_0R1_GAME_OPENING_CONTRACT.md`
|
||||
Active ledger: `docs/reviews/PHASE9_GODOT_VERTICAL_SLICE_LEDGER.md`
|
||||
Baseline: `b3968b0f41f841efb0f699d8fb04a9bcb958182a`
|
||||
Redesign baseline: `00b019543e571c89c20adaa97b4e805ecbd3e6f4`
|
||||
Stage: `P9_0R1_GAME_OPENING_ACTIVE`
|
||||
|
||||
Current P9.0R1 replaces the Owner-rejected form-like Web opening with one
|
||||
scene-based flow: Title -> Guild Yard front/rear choice -> deterministic first
|
||||
battle -> explicit first-expedition victory. Contract/difficulty, paid Market,
|
||||
equipment/order configuration, persistence, chapter expansion, authoritative
|
||||
combat, device packaging, TestFlight, stores and release remain frozen. The
|
||||
older Phase 8/P9 history below is retained as provenance, not current scope.
|
||||
|
||||
Historical portfolio baseline: `b3968b0f41f841efb0f699d8fb04a9bcb958182a`
|
||||
Accepted P8.1 source: `0b4a64fe549af0ff965e654aa4f5fb7f2ff054b4`
|
||||
P8.2 candidate source: `86a8a1adec39de40b4adfee6b01033407e3e3421`
|
||||
Accepted P8.2 closure: `d308f8bc14b043ed133ce2d99cbce461aecec81f`
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
# Phase 9.0R1 Game Opening Redesign Goal
|
||||
|
||||
## Owner Decision
|
||||
|
||||
The Owner rejected remote Web source `f0aa84738041cebf4968d91c6bf170668804e203`.
|
||||
The opening looked like a form rather than a game, exposed development labels,
|
||||
split recruitment into two unexplained systems, hid revision/reset affordances,
|
||||
and stopped before any playable encounter.
|
||||
|
||||
This Goal replaces that first-play structure. It does not preserve the rejected
|
||||
Contract/First Market presentation merely because its domain tests were green.
|
||||
|
||||
## Decision Enabled
|
||||
|
||||
Decide whether a fresh player can enter a recognizable Japanese hand-painted
|
||||
fantasy game, build one understandable two-person Company, see those characters
|
||||
act in a short battle, understand the result, and restart without coaching.
|
||||
|
||||
## Player Chain
|
||||
|
||||
```text
|
||||
title -> guild_yard_team_build -> departure -> tutorial_battle -> first_victory
|
||||
^ | |
|
||||
+-- replace+---------------- home / restart -------+
|
||||
```
|
||||
|
||||
## Owned Scope
|
||||
|
||||
- Keep the verified cold boot and Title technical behavior but remove internal
|
||||
milestone language from all player-facing screens.
|
||||
- Replace Contract, difficulty, four-provisional-plus-two-paid recruitment,
|
||||
equipment purchase/equip, order optimization and receipt screens with one
|
||||
scene-based opening Company choice.
|
||||
- Present two explicit positions:
|
||||
- front: Hearthguard or Wayrunner;
|
||||
- rear: Farstring or Kindhand.
|
||||
- Show the selected characters standing in the yard before departure. Choosing
|
||||
the other candidate replaces the current occupant; it never dead-ends.
|
||||
- Run one 30-60 second authored tutorial battle with visible advance, front-line
|
||||
contact, enemy attack, front role response, rear ranged attack or healing,
|
||||
enemy defeat and victory.
|
||||
- Always expose Home and a confirmed Restart Experience action after Title.
|
||||
- End with `First expedition complete`, the chosen Company and one explicit next
|
||||
goal. No chapter-complete claim is legal.
|
||||
|
||||
## Experience Contract
|
||||
|
||||
- Fantasy: assemble a small guild squad and watch the formation prove itself on
|
||||
a dangerous route.
|
||||
- First focal read: characters acting in the world.
|
||||
- Second focal read: the current choice or threat.
|
||||
- Third focal read: compact health/role feedback and one primary action.
|
||||
- First action by 20 seconds: place or replace one character.
|
||||
- First complete Company by 60 seconds without coaching.
|
||||
- First encounter begins by 90 seconds and resolves by 150 seconds.
|
||||
- The player can explain front versus rear contribution after the result.
|
||||
|
||||
## Presentation Contract
|
||||
|
||||
- Japanese 2D hand-painted fantasy environment and full-body character art are
|
||||
the visual authority already accepted in Phase 6.
|
||||
- World/actors occupy at least 70% of the landscape play field. HUD and commands
|
||||
overlay the world; they do not replace it with a dashboard.
|
||||
- UI materials use dark indigo lacquer, warm parchment, brushed gold edge,
|
||||
teal magical glass and crimson danger. Rectangular controls need layered
|
||||
border, highlight, shadow and pressed states; generic flat gray/gold buttons
|
||||
are prohibited.
|
||||
- Cards may frame one candidate choice, but a card wall or page-sized panel is
|
||||
prohibited. Each candidate owns one clear role, consequence and Join/Replace
|
||||
action.
|
||||
- No `P9`, `CTR`, `OFF`, `MKT`, `EQ`, receipt, vertical-slice, locked-future or
|
||||
implementation terminology appears in player copy.
|
||||
|
||||
## Dynamic Slice
|
||||
|
||||
Representative screen: tutorial battle at `844x390` landscape.
|
||||
|
||||
Focal order:
|
||||
|
||||
1. selected front and rear characters versus the Riftblade on one ground line;
|
||||
2. the current attack/guard/heal/projectile consequence;
|
||||
3. compact health and one pause/restart surface.
|
||||
|
||||
Motion-state table:
|
||||
|
||||
| Event | Actor | Anticipation | Impact | Recovery/final | Reduced motion |
|
||||
|---|---|---|---|---|---|
|
||||
| departure | Company | 250 ms lean | advance across ground | contact stance | snap to contact |
|
||||
| enemy attack | Riftblade | telegraph | front HP loss | idle | telegraph + final |
|
||||
| guard/dodge | front | brace/lean | reduced damage | ready | final pose |
|
||||
| arrow | Farstring | draw | projectile + enemy HP loss | ready | projectile final |
|
||||
| heal | Kindhand | lantern raise | front HP restoration | ready | value + final |
|
||||
| victory | Company/enemy | final attack | enemy falls | victory banner | final poses |
|
||||
|
||||
## Acceptance
|
||||
|
||||
1. Fresh launch and in-game Restart both reach the same Title/yard state.
|
||||
2. Home and Restart remain visible and touchable on yard, battle and victory.
|
||||
3. Exactly one front and one rear choice are required; every candidate can be
|
||||
selected, replaced and selected again with no stale state.
|
||||
4. No pre-battle Contract, difficulty, paid hire, equipment or order step exists.
|
||||
5. Parsed ScreenTouch drives the complete chain with fallback/direct zero.
|
||||
6. Normal rendering covers Title, empty yard, one selection, ready Company,
|
||||
battle pressure and victory at `1280x720`, `844x390` and `390x844` in
|
||||
zh_CN/English and 130% text without clipping.
|
||||
7. A 15-30 second normal-render recording proves visible actor motion and state
|
||||
change; screenshots alone cannot pass the dynamic slice.
|
||||
8. Strict script/parse/resource/import/ObjectDB/RID diagnostics are clean and
|
||||
no owned process remains.
|
||||
|
||||
## Frozen
|
||||
|
||||
- Contract selection, difficulty, paid Market recruitment, equipment and manual
|
||||
formation optimization.
|
||||
- Persistent save/migration, chapters, expanded battles, progression, economy,
|
||||
final audio, bulk assets, device packaging, stores and release.
|
||||
- Changes to complete content counts or later-game authority.
|
||||
|
||||
## Legal Terminal
|
||||
|
||||
`Aetherbound Guild P9.0R1 game opening candidate ready`
|
||||
|
||||
or
|
||||
|
||||
`Aetherbound Guild P9.0R1 redesign required: <earliest failed player boundary>`
|
||||
@@ -12,3 +12,4 @@ separate.
|
||||
| 2026-08-12 | Owner Web preview request | `P9_0_WEB_PREVIEW_AUTHORIZED` | Owner requested a dedicated remote H5 page for phone review. Only a non-threaded Godot Web export, browser verification and private Artifact Gallery deployment of fixed P9.0 content are authorized. Product behavior, later pages, native packages, stores and release remain frozen. |
|
||||
| 2026-08-12 | Web source `f0aa847`; Gallery item `PRji-yhJo0nQ`; `p9-0-web-f0aa847-attempt01` | `P9_0_REMOTE_WEB_READY` | Private signed item redirects to the full-screen runtime entry with an artifact-scoped HttpOnly cookie. Remote HTML/JS/WASM/PCK response types and executable CSP pass; unauthorized entry remains gated. Chrome phone-viewport verification reaches the Chinese Title and Contract by real canvas input, portrait shows the rotate overlay, and browser warnings/errors are zero. Final evidence inventory verifies 34/34 with SHA-256 `cb3111f86012d313c85d1eebed6574b8b5936531bf30821421f89f1442aeb945`; the signed token is not recorded in Git. Owner phone comprehension, fun, listening, physical device, packaging, store and release remain open. |
|
||||
| 2026-08-12 | Owner remote H5 playtest of `f0aa847` | `P9_0_OWNER_REJECTED_REDESIGN_REQUIRED` | Earliest product failure is the first post-Title action: the runtime reads as a configuration form rather than a game. There is no visible in-game restart/home path; Contract, First Market, provisional two-of-four selection, two later forced hires, equipment/order revision and the stopping point are not understandable without coaching. Engineering labels and receipt language are exposed as if they were chapter content. The current H5 is retained only as a problem reference. P9.0/P9.1 are not Owner-accepted; P9.2, content expansion, packaging and release remain frozen. A successor must compare structural onboarding options and reach a short encounter before advanced preparation systems. |
|
||||
| 2026-08-12 | Owner authorization for opening redesign | `P9_0R1_GAME_OPENING_ACTIVE` | Owner authorized a full first-play and UI redesign. The active slice is Title -> scene-based two-position Company choice -> deterministic short battle -> explicit first-expedition victory, with always-visible Home/Restart. Contract, difficulty, paid Market hire, equipment/order configuration, persistence, chapters, later combat, packaging and release remain frozen. Existing accepted Japanese hand-painted environment, character, Riftblade and VFX assets are reused before any new generation. |
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
# Phase 9.0R1 Game Opening Runtime Contract
|
||||
|
||||
## State
|
||||
|
||||
The successor runtime owns only:
|
||||
|
||||
```text
|
||||
BOOT -> TITLE -> YARD -> BATTLE -> VICTORY
|
||||
```
|
||||
|
||||
`home()` returns to Title and clears transient team/battle state. `restart()`
|
||||
performs the same reset and returns to Title after confirmation. Browser reload
|
||||
also starts from Boot because this slice has no persistence.
|
||||
|
||||
## Team
|
||||
|
||||
Front choices:
|
||||
|
||||
- Hearthguard: takes the enemy contact and visibly guards.
|
||||
- Wayrunner: takes the enemy contact and visibly evades/counterattacks.
|
||||
|
||||
Rear choices:
|
||||
|
||||
- Farstring: attacks safely with a visible projectile.
|
||||
- Kindhand: restores the injured front character.
|
||||
|
||||
One member per position. Selecting another candidate replaces that position.
|
||||
No currency, offer ID, purchase, dismissal or capacity system is involved.
|
||||
|
||||
## Battle
|
||||
|
||||
The battle is a deterministic tutorial presentation, not the later authoritative
|
||||
combat system. It must bind visible values and motion to one fixed sequence:
|
||||
|
||||
1. Company and one Riftblade advance to contact.
|
||||
2. Riftblade damages the selected front.
|
||||
3. Front guard/dodge response reduces or answers pressure.
|
||||
4. Farstring damages the Riftblade, or Kindhand heals the front.
|
||||
5. The Company performs a final combined action.
|
||||
6. Riftblade reaches zero and falls; victory appears once.
|
||||
|
||||
The two rear choices must visibly change the sequence and final explanation.
|
||||
Home/Restart cannot grant a victory or retain transient HP.
|
||||
|
||||
## Reused Runtime Art
|
||||
|
||||
No new provider generation is owned by this slice. Runtime copies are
|
||||
byte-identical to accepted Phase 6 authority:
|
||||
|
||||
- `runtime/assets/backgrounds/first_expedition_field.png` <-
|
||||
`phase6_battle_scale_04/outputs/01_empty_battlefield.png`;
|
||||
- Riftblade idle/attack/hit/death <- accepted Phase 6 motion frames;
|
||||
- `protect_absorb`, `protect_gain` and `damage_heavy` <- accepted Phase 6 VFX
|
||||
`@2x` files.
|
||||
|
||||
The existing four full-body Recruit anchors remain the Company identity source.
|
||||
Godot `.import` sidecars are import metadata, not new art sources.
|
||||
|
||||
## Player Copy
|
||||
|
||||
Allowed player labels describe place, role, action, threat and result. Internal
|
||||
IDs and delivery milestones are forbidden. The victory state says the first
|
||||
expedition is complete and that the guild can next learn hiring and equipment;
|
||||
it does not claim a chapter is complete.
|
||||
|
||||
## Evidence Markers
|
||||
|
||||
- `ABG_P9_0R1_DOMAIN_OK`
|
||||
- `ABG_P9_0R1_TOUCH_OK`
|
||||
- `ABG_P9_0R1_LAYOUT_OK`
|
||||
- `ABG_P9_0R1_RUNTIME_CAPTURE_OK`
|
||||
- `ABG_P9_0R1_DYNAMIC_SLICE_OK`
|
||||
|
After Width: | Height: | Size: 3.5 MiB |
@@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://d0t6j8kk3l478"
|
||||
path="res://.godot/imported/first_expedition_field.png-512185aef842432d998b716fbdbda726.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/backgrounds/first_expedition_field.png"
|
||||
dest_files=["res://.godot/imported/first_expedition_field.png-512185aef842432d998b716fbdbda726.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
|
After Width: | Height: | Size: 36 KiB |
@@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dp0vd7ysqlxhq"
|
||||
path="res://.godot/imported/attack.png-f368cb5dce3612ada5cc3dc0654ef93d.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/enemies/riftblade/attack.png"
|
||||
dest_files=["res://.godot/imported/attack.png-f368cb5dce3612ada5cc3dc0654ef93d.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
|
After Width: | Height: | Size: 20 KiB |
@@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://3koj8sncknch"
|
||||
path="res://.godot/imported/death.png-e68dfbb00e5ad6224a0d831754a35124.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/enemies/riftblade/death.png"
|
||||
dest_files=["res://.godot/imported/death.png-e68dfbb00e5ad6224a0d831754a35124.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
|
After Width: | Height: | Size: 54 KiB |
@@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://m5i4rlks6f5b"
|
||||
path="res://.godot/imported/hit.png-48d85e065b0cdc6acc16f8869b8964d4.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/enemies/riftblade/hit.png"
|
||||
dest_files=["res://.godot/imported/hit.png-48d85e065b0cdc6acc16f8869b8964d4.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
|
After Width: | Height: | Size: 43 KiB |
@@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dmvrgg6a8gtcd"
|
||||
path="res://.godot/imported/idle.png-b4995d90d56d1cbedcb6819a1c5eddc9.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/enemies/riftblade/idle.png"
|
||||
dest_files=["res://.godot/imported/idle.png-b4995d90d56d1cbedcb6819a1c5eddc9.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
|
After Width: | Height: | Size: 29 KiB |
@@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://h3dvkx4rq4ok"
|
||||
path="res://.godot/imported/damage_heavy.png-59e6de32464699ffe87188d4361ab7d2.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/vfx/damage_heavy.png"
|
||||
dest_files=["res://.godot/imported/damage_heavy.png-59e6de32464699ffe87188d4361ab7d2.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
|
After Width: | Height: | Size: 38 KiB |
@@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://iwlvuwmmlice"
|
||||
path="res://.godot/imported/protect_absorb.png-33f11958ef887c357bc8746b262471ac.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/vfx/protect_absorb.png"
|
||||
dest_files=["res://.godot/imported/protect_absorb.png-33f11958ef887c357bc8746b262471ac.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
|
After Width: | Height: | Size: 32 KiB |
@@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://btnelcc7vr06g"
|
||||
path="res://.godot/imported/protect_gain.png-d3bfbe940288931ab6022856af268d59.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/vfx/protect_gain.png"
|
||||
dest_files=["res://.godot/imported/protect_gain.png-d3bfbe940288931ab6022856af268d59.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
@@ -0,0 +1,134 @@
|
||||
extends SceneTree
|
||||
|
||||
const MainScene = preload("res://main.tscn")
|
||||
|
||||
const STATES := [
|
||||
"title",
|
||||
"yard_empty",
|
||||
"yard_front",
|
||||
"yard_ready",
|
||||
"battle_pressure",
|
||||
"victory",
|
||||
]
|
||||
|
||||
var output_root := ""
|
||||
var viewport_size := Vector2i(1280, 720)
|
||||
var locale := "zh_CN"
|
||||
var text_scale := 1.0
|
||||
var failures: Array[String] = []
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("run_test")
|
||||
|
||||
|
||||
func run_test() -> void:
|
||||
output_root = OS.get_environment("ABG_P9_0R1_CAPTURE_ROOT")
|
||||
if output_root.is_empty():
|
||||
output_root = "/tmp/abg-p9-0r1-captures"
|
||||
viewport_size = Vector2i(
|
||||
read_int("ABG_P9_0R1_VIEWPORT_WIDTH", 1280),
|
||||
read_int("ABG_P9_0R1_VIEWPORT_HEIGHT", 720)
|
||||
)
|
||||
locale = read_string("ABG_P9_0R1_LOCALE", "zh_CN")
|
||||
text_scale = read_float("ABG_P9_0R1_TEXT_SCALE", 1.0)
|
||||
DirAccess.make_dir_recursive_absolute(output_root)
|
||||
var viewport := SubViewport.new()
|
||||
viewport.size = viewport_size
|
||||
viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
|
||||
root.add_child(viewport)
|
||||
var game := MainScene.instantiate()
|
||||
game.auto_boot = false
|
||||
viewport.add_child(game)
|
||||
await settle()
|
||||
game.locale = locale
|
||||
game.text_scale = text_scale
|
||||
game.reduced_motion = true
|
||||
game.muted = true
|
||||
game.boot_verified = true
|
||||
for index in range(STATES.size()):
|
||||
await capture_state(viewport, game, index + 1, STATES[index])
|
||||
game.queue_free()
|
||||
viewport.queue_free()
|
||||
await drain_cleanup()
|
||||
if failures.is_empty():
|
||||
print("ABG_P9_0R1_RUNTIME_CAPTURE_OK frames=6 viewport=%sx%s locale=%s text_scale=%.1f renderer=normal input_claim=none root=%s" % [viewport_size.x, viewport_size.y, locale, text_scale, output_root])
|
||||
call_deferred("finish", 0)
|
||||
else:
|
||||
for failure in failures:
|
||||
push_error(failure)
|
||||
call_deferred("finish", 1)
|
||||
|
||||
|
||||
func finish(exit_code: int) -> void:
|
||||
quit(exit_code)
|
||||
|
||||
|
||||
func capture_state(viewport: SubViewport, game: Control, index: int, state_name: String) -> void:
|
||||
game.clear_experience()
|
||||
apply_state(game, state_name)
|
||||
game.render()
|
||||
await settle()
|
||||
await RenderingServer.frame_post_draw
|
||||
var image := viewport.get_texture().get_image()
|
||||
var path := output_root.path_join("%02d-%s.png" % [index, state_name])
|
||||
var error := image.save_png(path)
|
||||
if error != OK:
|
||||
failures.append("capture %s failed with %s" % [state_name, error])
|
||||
|
||||
|
||||
func apply_state(game: Control, state_name: String) -> void:
|
||||
match state_name:
|
||||
"title":
|
||||
game.page = game.AppPage.TITLE
|
||||
"yard_empty":
|
||||
game.page = game.AppPage.YARD
|
||||
game.notice = game.tr_text("先选前排:盾卫更稳,游侠更灵活。", "Choose the front: Guardian is steady, Runner is agile.")
|
||||
"yard_front":
|
||||
game.page = game.AppPage.YARD
|
||||
game.front_choice = "Hearthguard"
|
||||
game.notice = game.tr_text("艾连进入前排。现在选择一名后排。", "Elian joined the front. Now choose a rear role.")
|
||||
"yard_ready":
|
||||
game.page = game.AppPage.YARD
|
||||
game.front_choice = "Hearthguard"
|
||||
game.rear_choice = "Farstring"
|
||||
game.notice = game.tr_text("妮娅进入后排。队伍已准备好。", "Nia joined the rear. The squad is ready.")
|
||||
"battle_pressure":
|
||||
game.page = game.AppPage.BATTLE
|
||||
game.front_choice = "Hearthguard"
|
||||
game.rear_choice = "Farstring"
|
||||
game.battle_playing = false
|
||||
game.battle_phase = 2
|
||||
game.front_hp = 82
|
||||
game.enemy_hp = 100
|
||||
"victory":
|
||||
game.page = game.AppPage.VICTORY
|
||||
game.front_choice = "Hearthguard"
|
||||
game.rear_choice = "Farstring"
|
||||
game.front_hp = 82
|
||||
game.enemy_hp = 0
|
||||
|
||||
|
||||
func read_string(key: String, fallback: String) -> String:
|
||||
var value := OS.get_environment(key)
|
||||
return value if not value.is_empty() else fallback
|
||||
|
||||
|
||||
func read_int(key: String, fallback: int) -> int:
|
||||
var value := OS.get_environment(key)
|
||||
return int(value) if not value.is_empty() else fallback
|
||||
|
||||
|
||||
func read_float(key: String, fallback: float) -> float:
|
||||
var value := OS.get_environment(key)
|
||||
return float(value) if not value.is_empty() else fallback
|
||||
|
||||
|
||||
func settle() -> void:
|
||||
for frame in range(10):
|
||||
await process_frame
|
||||
|
||||
|
||||
func drain_cleanup() -> void:
|
||||
for frame in range(120):
|
||||
await process_frame
|
||||
@@ -0,0 +1 @@
|
||||
uid://bdsp4a43800ei
|
||||
@@ -0,0 +1,107 @@
|
||||
extends SceneTree
|
||||
|
||||
const MainScene = preload("res://main.tscn")
|
||||
|
||||
var failures: Array[String] = []
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("run_test")
|
||||
|
||||
|
||||
func run_test() -> void:
|
||||
var game := MainScene.instantiate()
|
||||
game.auto_boot = false
|
||||
root.add_child(game)
|
||||
await settle()
|
||||
expect(game.page == game.AppPage.BOOT, "fresh instance starts at Boot")
|
||||
expect(game.front_choice.is_empty() and game.rear_choice.is_empty(), "fresh team is empty")
|
||||
|
||||
game.boot_paths_override.assign(["res://missing/p9_0r1_required_resource.tres"])
|
||||
await game.run_boot_verification()
|
||||
expect(game.page == game.AppPage.BOOT, "failed boot stays at Boot")
|
||||
expect(game.boot_failed_path == "res://missing/p9_0r1_required_resource.tres", "failed boot retains path")
|
||||
expect(find_control(game, "BootRetry") != null, "failed boot exposes Retry")
|
||||
|
||||
game.boot_paths_override.clear()
|
||||
await game.run_boot_verification()
|
||||
expect(game.boot_verified and game.page == game.AppPage.TITLE, "verified boot reaches Title")
|
||||
expect(find_control(game, "StartAdventure") != null, "Title exposes one game entry")
|
||||
|
||||
game.start_adventure()
|
||||
expect(game.page == game.AppPage.YARD, "entry reaches Guild Yard")
|
||||
game.choose_candidate("Hearthguard")
|
||||
game.choose_candidate("Wayrunner")
|
||||
expect(game.front_choice == "Wayrunner", "front candidate replaces same position")
|
||||
game.choose_candidate("Farstring")
|
||||
game.choose_candidate("Kindhand")
|
||||
expect(game.rear_choice == "Kindhand", "rear candidate replaces same position")
|
||||
game.choose_candidate("Farstring")
|
||||
expect(game.rear_choice == "Farstring", "rear candidate can be selected again")
|
||||
|
||||
game.depart()
|
||||
expect(game.page == game.AppPage.BATTLE and game.battle_playing, "ready team begins battle")
|
||||
for phase in range(6):
|
||||
game.advance_battle_phase()
|
||||
expect(game.page == game.AppPage.VICTORY, "fixed tutorial sequence reaches victory")
|
||||
expect(game.enemy_hp == 0 and game.victory_events == 1, "victory commits once")
|
||||
expect(game.guard_events == 1 and game.rear_events == 1, "front and rear roles both act")
|
||||
game.advance_battle_phase()
|
||||
expect(game.victory_events == 1, "victory cannot duplicate")
|
||||
|
||||
game.request_restart()
|
||||
expect(game.restart_confirmation, "restart requires confirmation")
|
||||
game.cancel_restart()
|
||||
expect(not game.restart_confirmation and game.page == game.AppPage.VICTORY, "cancel preserves current state")
|
||||
game.request_restart()
|
||||
game.confirm_restart()
|
||||
expect(game.page == game.AppPage.TITLE, "confirmed restart returns Title")
|
||||
expect(game.front_choice.is_empty() and game.rear_choice.is_empty(), "restart clears team")
|
||||
expect(game.front_hp == 100 and game.enemy_hp == 100 and game.victory_events == 0, "restart clears battle state")
|
||||
|
||||
game.start_adventure()
|
||||
game.choose_candidate("Hearthguard")
|
||||
game.choose_candidate("Kindhand")
|
||||
game.depart()
|
||||
for phase in range(6):
|
||||
game.advance_battle_phase()
|
||||
expect(game.page == game.AppPage.VICTORY, "healer route reaches victory")
|
||||
expect(game.front_hp == 94, "healer visibly restores front after guarded hit")
|
||||
expect(game.rear_events == 1, "healer contribution recorded")
|
||||
game.go_home()
|
||||
expect(game.page == game.AppPage.TITLE and game.front_choice.is_empty(), "Home clears transient experience")
|
||||
|
||||
game.queue_free()
|
||||
await drain_cleanup()
|
||||
if failures.is_empty():
|
||||
print("ABG_P9_0R1_DOMAIN_OK routes=2 replace=front,rear restart=confirmed home=1 battle_phases=6 victory_once=1")
|
||||
quit(0)
|
||||
else:
|
||||
for failure in failures:
|
||||
push_error(failure)
|
||||
quit(1)
|
||||
|
||||
|
||||
func find_control(node: Node, node_name: String) -> Control:
|
||||
if node.name == node_name and node is Control and not node.is_queued_for_deletion():
|
||||
return node
|
||||
for child in node.get_children():
|
||||
var found := find_control(child, node_name)
|
||||
if found != null:
|
||||
return found
|
||||
return null
|
||||
|
||||
|
||||
func settle() -> void:
|
||||
for frame in range(8):
|
||||
await process_frame
|
||||
|
||||
|
||||
func drain_cleanup() -> void:
|
||||
for frame in range(40):
|
||||
await process_frame
|
||||
|
||||
|
||||
func expect(condition: bool, message: String) -> void:
|
||||
if not condition:
|
||||
failures.append("P9.0R1 domain: %s" % message)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bixsojbce2foq
|
||||
@@ -0,0 +1,53 @@
|
||||
extends SceneTree
|
||||
|
||||
const MainScene = preload("res://main.tscn")
|
||||
|
||||
var game: Control
|
||||
var failures: Array[String] = []
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("run_test")
|
||||
|
||||
|
||||
func run_test() -> void:
|
||||
game = MainScene.instantiate()
|
||||
game.auto_boot = false
|
||||
root.add_child(game)
|
||||
await settle(12)
|
||||
game.locale = "zh_CN"
|
||||
game.text_scale = 1.0
|
||||
game.reduced_motion = false
|
||||
game.muted = true
|
||||
game.boot_verified = true
|
||||
game.front_choice = "Hearthguard"
|
||||
game.rear_choice = "Farstring"
|
||||
game.depart()
|
||||
var elapsed := 0.0
|
||||
while game.page != game.AppPage.VICTORY and elapsed < 22.0:
|
||||
await process_frame
|
||||
elapsed += 1.0 / 30.0
|
||||
if game.page != game.AppPage.VICTORY:
|
||||
failures.append("dynamic slice did not reach victory")
|
||||
if game.guard_events != 1 or game.rear_events != 1 or game.victory_events != 1:
|
||||
failures.append("dynamic slice role events were not committed exactly once")
|
||||
await create_timer(1.6).timeout
|
||||
game.queue_free()
|
||||
game = null
|
||||
await settle(120)
|
||||
if failures.is_empty():
|
||||
print("ABG_P9_0R1_DYNAMIC_SLICE_OK duration=%.2f phases=6 advance=1 front_response=1 rear_action=1 enemy_defeat=1 victory=1 renderer=normal" % elapsed)
|
||||
call_deferred("finish", 0)
|
||||
else:
|
||||
for failure in failures:
|
||||
push_error(failure)
|
||||
call_deferred("finish", 1)
|
||||
|
||||
|
||||
func finish(exit_code: int) -> void:
|
||||
quit(exit_code)
|
||||
|
||||
|
||||
func settle(frames: int) -> void:
|
||||
for frame in range(frames):
|
||||
await process_frame
|
||||
@@ -0,0 +1,128 @@
|
||||
extends SceneTree
|
||||
|
||||
const MainScene = preload("res://main.tscn")
|
||||
|
||||
const LANDSCAPE_FIXTURES := [
|
||||
{"size": Vector2i(1280, 720), "locale": "zh_CN", "scale": 1.0},
|
||||
{"size": Vector2i(844, 390), "locale": "en", "scale": 1.0},
|
||||
{"size": Vector2i(844, 390), "locale": "zh_CN", "scale": 1.3},
|
||||
]
|
||||
const PAGES := ["title", "yard", "battle", "victory"]
|
||||
|
||||
var failures: Array[String] = []
|
||||
var fixture_count := 0
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("run_test")
|
||||
|
||||
|
||||
func run_test() -> void:
|
||||
for fixture in LANDSCAPE_FIXTURES:
|
||||
for page_name in PAGES:
|
||||
await verify_landscape(fixture, page_name)
|
||||
await verify_portrait()
|
||||
if failures.is_empty():
|
||||
print("ABG_P9_0R1_LAYOUT_OK fixtures=%d landscape=12 portrait=1 pages=4 locales=2 text_130=true min_touch=44" % fixture_count)
|
||||
quit(0)
|
||||
else:
|
||||
for failure in failures:
|
||||
push_error(failure)
|
||||
quit(1)
|
||||
|
||||
|
||||
func verify_landscape(fixture: Dictionary, page_name: String) -> void:
|
||||
var viewport := SubViewport.new()
|
||||
viewport.size = fixture.size
|
||||
viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
|
||||
root.add_child(viewport)
|
||||
var game := MainScene.instantiate()
|
||||
game.auto_boot = false
|
||||
viewport.add_child(game)
|
||||
await settle()
|
||||
game.locale = fixture.locale
|
||||
game.text_scale = fixture.scale
|
||||
game.front_choice = "Hearthguard"
|
||||
game.rear_choice = "Farstring"
|
||||
game.boot_verified = true
|
||||
match page_name:
|
||||
"title": game.page = game.AppPage.TITLE
|
||||
"yard": game.page = game.AppPage.YARD
|
||||
"battle":
|
||||
game.page = game.AppPage.BATTLE
|
||||
game.battle_playing = false
|
||||
game.battle_phase = 2
|
||||
game.front_hp = 82
|
||||
"victory":
|
||||
game.page = game.AppPage.VICTORY
|
||||
game.enemy_hp = 0
|
||||
game.render()
|
||||
await settle()
|
||||
fixture_count += 1
|
||||
var label := "%sx%s/%s/%s/%s" % [fixture.size.x, fixture.size.y, fixture.locale, str(fixture.scale), page_name]
|
||||
verify_interactive_bounds(game, Rect2(Vector2.ZERO, Vector2(fixture.size)), label)
|
||||
expect(find_control(game, "RotateNotice") == null, "%s unexpectedly shows portrait notice" % label)
|
||||
if page_name != "title":
|
||||
expect(find_control(game, "HomeButton") != null, "%s missing Home" % label)
|
||||
expect(find_control(game, "RestartButton") != null, "%s missing Restart" % label)
|
||||
game.queue_free()
|
||||
viewport.queue_free()
|
||||
await drain_cleanup()
|
||||
|
||||
|
||||
func verify_portrait() -> void:
|
||||
var viewport := SubViewport.new()
|
||||
viewport.size = Vector2i(390, 844)
|
||||
viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
|
||||
root.add_child(viewport)
|
||||
var game := MainScene.instantiate()
|
||||
game.auto_boot = false
|
||||
viewport.add_child(game)
|
||||
await settle()
|
||||
game.page = game.AppPage.YARD
|
||||
game.render()
|
||||
await settle()
|
||||
fixture_count += 1
|
||||
expect(find_control(game, "RotateNotice") != null, "portrait missing rotate guidance")
|
||||
verify_interactive_bounds(game, Rect2(Vector2.ZERO, Vector2(390, 844)), "390x844/portrait")
|
||||
game.queue_free()
|
||||
viewport.queue_free()
|
||||
await drain_cleanup()
|
||||
|
||||
|
||||
func verify_interactive_bounds(node: Node, bounds: Rect2, label: String) -> void:
|
||||
if node is BaseButton and node.is_visible_in_tree():
|
||||
var rect: Rect2 = node.get_global_rect()
|
||||
expect(contains_rect(bounds, rect), "%s button %s outside viewport: %s" % [label, node.name, rect])
|
||||
expect(rect.size.x >= 44.0 and rect.size.y >= 44.0, "%s button %s below 44pt: %s" % [label, node.name, rect.size])
|
||||
for child in node.get_children():
|
||||
verify_interactive_bounds(child, bounds, label)
|
||||
|
||||
|
||||
func find_control(node: Node, node_name: String) -> Control:
|
||||
if node.name == node_name and node is Control and not node.is_queued_for_deletion():
|
||||
return node
|
||||
for child in node.get_children():
|
||||
var found := find_control(child, node_name)
|
||||
if found != null:
|
||||
return found
|
||||
return null
|
||||
|
||||
|
||||
func contains_rect(outer: Rect2, inner: Rect2) -> bool:
|
||||
return inner.position.x >= -0.5 and inner.position.y >= -0.5 and inner.end.x <= outer.end.x + 0.5 and inner.end.y <= outer.end.y + 0.5
|
||||
|
||||
|
||||
func settle() -> void:
|
||||
for frame in range(8):
|
||||
await process_frame
|
||||
|
||||
|
||||
func drain_cleanup() -> void:
|
||||
for frame in range(30):
|
||||
await process_frame
|
||||
|
||||
|
||||
func expect(condition: bool, message: String) -> void:
|
||||
if not condition:
|
||||
failures.append("P9.0R1 layout: %s" % message)
|
||||
@@ -0,0 +1 @@
|
||||
uid://be0d7430yxb01
|
||||
@@ -0,0 +1,105 @@
|
||||
extends SceneTree
|
||||
|
||||
const MainScene = preload("res://main.tscn")
|
||||
|
||||
var game: Control
|
||||
var touch_count := 0
|
||||
var failures: Array[String] = []
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("run_test")
|
||||
|
||||
|
||||
func run_test() -> void:
|
||||
game = MainScene.instantiate()
|
||||
game.auto_boot = false
|
||||
root.add_child(game)
|
||||
await settle()
|
||||
game.boot_verified = true
|
||||
game.page = game.AppPage.TITLE
|
||||
game.render()
|
||||
await settle()
|
||||
|
||||
await tap("StartAdventure")
|
||||
expect(game.page == game.AppPage.YARD, "Title touch reaches yard")
|
||||
await tap("Candidate_Hearthguard")
|
||||
await tap("Candidate_Wayrunner")
|
||||
expect(game.front_choice == "Wayrunner", "touch replaces front")
|
||||
await tap("Candidate_Farstring")
|
||||
await tap("Candidate_Kindhand")
|
||||
expect(game.rear_choice == "Kindhand", "touch replaces rear")
|
||||
await tap("DepartButton")
|
||||
expect(game.page == game.AppPage.BATTLE, "touch starts battle")
|
||||
await tap("RestartButton")
|
||||
expect(game.restart_confirmation, "restart touch opens confirmation")
|
||||
await tap("RestartCancelButton")
|
||||
expect(game.page == game.AppPage.BATTLE and not game.restart_confirmation, "cancel touch preserves battle")
|
||||
await tap("HomeButton")
|
||||
expect(game.page == game.AppPage.TITLE, "Home touch reaches Title")
|
||||
expect(game.front_choice.is_empty() and game.rear_choice.is_empty(), "Home touch clears team")
|
||||
await tap("StartAdventure")
|
||||
await tap("Candidate_Hearthguard")
|
||||
await tap("Candidate_Farstring")
|
||||
await tap("DepartButton")
|
||||
await tap("RestartButton")
|
||||
await tap("RestartConfirmButton")
|
||||
expect(game.page == game.AppPage.TITLE, "confirmed restart touch reaches Title")
|
||||
expect(game.front_hp == 100 and game.enemy_hp == 100, "confirmed restart clears HP")
|
||||
|
||||
game.queue_free()
|
||||
game = null
|
||||
await drain_cleanup()
|
||||
if failures.is_empty():
|
||||
print("ABG_P9_0R1_TOUCH_OK touch=%d fallback=0 direct=0 replace=2 battle=1 home=1 restart=confirmed" % touch_count)
|
||||
quit(0)
|
||||
else:
|
||||
for failure in failures:
|
||||
push_error(failure)
|
||||
quit(1)
|
||||
|
||||
|
||||
func tap(node_name: String) -> void:
|
||||
var control := find_control(game, node_name)
|
||||
if control == null or not control.is_visible_in_tree() or (control is BaseButton and control.disabled):
|
||||
failures.append("unavailable touch target %s" % node_name)
|
||||
return
|
||||
var position := root.get_final_transform() * control.get_global_rect().get_center()
|
||||
var down := InputEventScreenTouch.new()
|
||||
down.index = 0
|
||||
down.position = position
|
||||
down.pressed = true
|
||||
Input.parse_input_event(down)
|
||||
await process_frame
|
||||
var up := InputEventScreenTouch.new()
|
||||
up.index = 0
|
||||
up.position = position
|
||||
up.pressed = false
|
||||
Input.parse_input_event(up)
|
||||
touch_count += 1
|
||||
await settle()
|
||||
|
||||
|
||||
func find_control(node: Node, node_name: String) -> Control:
|
||||
if node.name == node_name and node is Control and not node.is_queued_for_deletion():
|
||||
return node
|
||||
for child in node.get_children():
|
||||
var found := find_control(child, node_name)
|
||||
if found != null:
|
||||
return found
|
||||
return null
|
||||
|
||||
|
||||
func settle() -> void:
|
||||
for frame in range(8):
|
||||
await process_frame
|
||||
|
||||
|
||||
func drain_cleanup() -> void:
|
||||
for frame in range(50):
|
||||
await process_frame
|
||||
|
||||
|
||||
func expect(condition: bool, message: String) -> void:
|
||||
if not condition:
|
||||
failures.append("P9.0R1 touch: %s" % message)
|
||||
@@ -0,0 +1 @@
|
||||
uid://b7i5n0vbinkxx
|
||||