67 Commits

Author SHA1 Message Date
59b18b23bc Replace tasks with procedural generation backlog 2026-05-14 14:56:59 +02:00
6feaf84e39 Finish Godot campaign polish 2026-05-14 11:09:22 +02:00
ad5445b09d Complete Godot pulse UX integration 2026-05-14 11:04:36 +02:00
542c0cdc19 Add Godot level editing save path 2026-05-14 10:59:14 +02:00
6699b3b891 Add authored campaign levels 2026-05-14 10:56:15 +02:00
b68b87d475 Cleanup 2026-05-14 10:43:16 +02:00
fbc26bb3b8 Fix editor link validation regressions 2026-05-14 10:31:25 +02:00
ec8761f4e8 Tighten editor validation coverage 2026-05-14 10:29:36 +02:00
6decf2a9d2 Align frontend pulse contract and tasks 2026-05-14 10:26:56 +02:00
830c7aef14 Implement surface safety and powered props 2026-05-14 10:19:21 +02:00
2ad7feef96 Add sprinkler valve simulation contract 2026-05-14 10:15:12 +02:00
6db3e60fd1 Add pulse contract and isolation valves 2026-05-14 10:11:35 +02:00
c5688d2c0d godot imports 2026-05-14 10:00:19 +02:00
c149fe02a3 Rename coolant to water 2026-05-14 10:00:08 +02:00
4ac8a77e8e Refresh implementation task backlog 2026-05-14 03:06:12 +02:00
670ee5c59c Clarify powered terminals and campaign hazards 2026-05-14 02:58:54 +02:00
e90ab07e64 Align campaign with core design rules 2026-05-14 02:12:26 +02:00
04a72e4966 Draft campaign design document 2026-05-14 01:20:25 +02:00
fe1f46212b Document campaign choice rescue 2026-05-14 01:02:06 +02:00
1eb940ef15 Updated agents 2026-05-14 00:59:39 +02:00
57dd5d1e36 Document coolant sprinkler evaporation design 2026-05-14 00:36:08 +02:00
7777800a5d Document pulse-based simulation timing 2026-05-13 23:25:18 +02:00
4a3fd37ab4 Implement Godot grid viewport rendering 2026-05-13 03:43:05 +02:00
b939246ba4 Simulation bridge 2026-05-13 01:56:50 +02:00
251cfa5016 image imports and new TASKS.md 2026-05-13 01:18:49 +02:00
390a09015b Integrate Godot cutout art 2026-05-12 22:36:13 +02:00
3224f3768b Add Godot scene mood art 2026-05-12 21:46:50 +02:00
171d68e102 Add art style bible 2026-05-12 21:28:07 +02:00
33859d2cf6 Implement Godot UX scene scaffold 2026-05-12 21:06:48 +02:00
8cf554574b Main godot scene 2026-05-12 20:40:22 +02:00
ff8ee32c9a Add Godot UX blueprint 2026-05-12 20:39:07 +02:00
c38a9670ba Add empty Godot frontend shell 2026-05-12 20:20:36 +02:00
672f055a80 increased sim speed 2026-05-12 00:19:00 +02:00
adf1475fc0 Add editor simulation playback 2026-05-12 00:15:41 +02:00
06d37aac10 Fix editor drag feedback and surface overlays 2026-05-12 00:06:12 +02:00
99482c7011 UI iteration. 2026-05-12 00:01:07 +02:00
fbb7c0490c Refine editor inspector and drag painting 2026-05-11 23:18:47 +02:00
dfe0cb3b6a Add editor image badges and drag moves 2026-05-11 23:03:29 +02:00
884cc4503f cleanup 2026-05-11 22:38:33 +02:00
0651603fd2 Rework Win2D editor layers 2026-05-11 22:34:19 +02:00
69ed79ce86 Update task progress 2026-05-11 22:25:08 +02:00
e1ac56d201 Rework simulation rules 2026-05-11 22:18:43 +02:00
3d406179bf Restore complete design system documentation 2026-05-11 22:02:46 +02:00
787f1e5e85 Document approved design iteration 2026-05-11 21:53:55 +02:00
5ddd1b8ec8 Cleanup 2026-05-11 21:51:18 +02:00
1b9372ff7c Restore dual terrain tilemap rendering 2026-05-10 23:04:48 +02:00
3a52db0071 Finish rewrite task list 2026-05-10 22:35:25 +02:00
5a186fb606 Split simulation systems 2026-05-10 18:49:24 +02:00
6c7fa070f6 cleanup code 2026-05-10 18:37:30 +02:00
d22c4a7528 Update rewrite docs and cleanup 2026-05-10 18:09:43 +02:00
7ffaa140a8 Introduce simulation engine facade 2026-05-10 18:08:03 +02:00
3c5fc60ffe Parameterize surface interactions 2026-05-10 18:07:16 +02:00
9cd9defc0b Unify junction props 2026-05-10 18:05:32 +02:00
1aa9734e08 Split simulation models 2026-05-10 18:03:46 +02:00
a0b10423ac Expand rule event coverage 2026-05-10 17:38:43 +02:00
cb28eee1dd Add branch-aware junction flow 2026-05-10 17:29:19 +02:00
b232c0319f Track rewrite tooling setup 2026-05-10 19:14:28 +02:00
30963a9bde Rework Win2D editor for design model 2026-05-10 18:59:00 +02:00
851f6d27e8 Rewrite simulation core for design model 2026-05-10 18:41:17 +02:00
ca41e009bd Add rewrite task tracker 2026-05-10 18:29:53 +02:00
79f3219a72 Condense reactor design spec 2026-05-10 14:33:09 +02:00
071e6a1d48 Finalize v1 design spec 2026-05-10 14:27:49 +02:00
810478ddee Refine leak and remediation design 2026-05-10 13:56:20 +02:00
bb8d1adb10 Finalize simulation design document 2026-05-10 13:28:33 +02:00
c8795d582c gitignore 2026-05-10 13:22:17 +02:00
2376edab0d Revise simulation design doc 2026-05-09 13:10:49 +02:00
c406bf9d73 Latest 2026-05-09 12:29:32 +02:00
236 changed files with 100458 additions and 3430 deletions

File diff suppressed because it is too large Load Diff

8
.gitignore vendored
View File

@@ -1,3 +1,5 @@
.vs
bin
obj
.vs
bin
obj
.idea
.godot

View File

@@ -1,3 +1,7 @@
# Linux-specific instructions
- After every iteration, run `dotnet jb cleanupcode --build=False '$file1' '$file2' ...` for every C# file you touched.
# Linux-specific instructions
## Code work
- After every iteration, run `dotnet jb cleanupcode --verbosity:ERROR ./ReactorMaintenance.slnx`.
## Documentation work or planning
- No cleanup steps necessary

View File

@@ -1,30 +1,31 @@
# Platform and documentation
If this is a linux environment, read `AGENTS.linux.md`.
If this is a windows environment, read `AGENTS.windows.md`.
Follow the guidelines laid out in `CODESTYLE.md`.
Also see the other related technical documentation in the docs folder.
## Rules
- Prefer extracting code to a shared helper to be reused instead of duplicating code. Always keep high maintainability standards.
- If a class is to be used only once, consider nesting it inside of another class. Otherwise place each newly created class into its own file. The file name must match the class name.
- When asked to begin working on a task, create a detailed implementation plan first, present the plan to the user, and ask for approval before beginning with the actual implementation.
- Don't make assumptions in the plan. If necessary, ask all clarifying questions before presenting the final plan.
- When an task is finished, perform a code review to evaluate if the change is clean and maintainable with high software engineering standards. Iterate on the code and repeat the review process until satisfied.
- If there's documnentation present, always keep it updated.
- After every iteration, evaluate if the test coverage would fall below 100%, and write tests if necessary.
### Git
- Never change the .gitignore file without consent.
- Keep changes small with minimal churn and commit often. If one iteration encompasses many smaller tasks with more than one commit, create a git branch and do the commits there. Let me review the branch before merging it back to master.
- When multiple commits are necessary, pause after every commit and ask the user to give a command to proceed.
- After every iteration, do a git commit with a brief summary of the changes as a commit message.
- If you find unexpected changes in the code (deletions, changes, diff results that were not communicated), never revert them and never restore the old state. Assume that those changes happened with intent.
- Never use `git restore`, `git checkout --`, reset commands, or equivalent rollback actions to discard local changes unless the user explicitly asks for that exact rollback.
### Dotnet CLI
- If you need a separate output directory, use a subfolder under `artifacts`, and clean it up afterwards.
- Avoid running `dotnet build` and `dotnet test` in parallel in this repo; that can cause file-lock failures in `obj\Debug\net10.0`.
# Platform and documentation
Find out which platform you're running on.
If this is a linux environment, read `AGENTS.linux.md`.
If this is a windows environment, read `AGENTS.windows.md`.
Follow the guidelines laid out in `CODESTYLE.md`.
Also see the other related technical documentation in the docs folder.
## Rules
- Prefer extracting code to a shared helper to be reused instead of duplicating code. Always keep high maintainability standards.
- If a class is to be used only once, consider nesting it inside of another class. Otherwise place each newly created class into its own file. The file name must match the class name.
- When asked to begin working on a task, create a detailed implementation plan first, present the plan to the user, and ask for approval before beginning with the actual implementation.
- Don't make assumptions in the plan. If necessary, ask all clarifying questions before presenting the final plan.
- When an task is finished, perform a code review to evaluate if the change is clean and maintainable with high software engineering standards. Iterate on the code and repeat the review process until satisfied.
- If there's documnentation present, always keep it updated.
- After every iteration, evaluate if the test coverage would fall below 100%, and write tests if necessary.
### Git
- Never change the .gitignore file without consent.
- Keep changes small with minimal churn and commit often. If one iteration encompasses many smaller tasks with more than one commit, create a git branch and do the commits there. Let me review the branch before merging it back to master.
- When multiple commits are necessary, pause after every commit and ask the user to give a command to proceed.
- After every iteration, do a git commit with a brief summary of the changes as a commit message.
- If you find unexpected changes in the code (deletions, changes, diff results that were not communicated), never revert them and never restore the old state. Assume that those changes happened with intent.
- Never use `git restore`, `git checkout --`, reset commands, or equivalent rollback actions to discard local changes unless the user explicitly asks for that exact rollback.
### Dotnet CLI
- If you need a separate output directory, use a subfolder under `artifacts`, and clean it up afterwards.
- Avoid running `dotnet build` and `dotnet test` in parallel in this repo; that can cause file-lock failures in `obj\Debug\net10.0`.

View File

@@ -1,4 +1,8 @@
# Windows-specific instructions
- After the implementation is finished, run `python D:\Code\crlf.py $file1 $file2 ...` for changed files you recognize, in order to normalize all line endings of all touched files to CRLF.
- After every iteration, run `jb cleanupcode --build=False '$file1' '$file2' ...` for every C# file you touched.
# Windows-specific instructions
## Code work
- After the implementation is finished, run `python D:\Code\crlf.py $file1 $file2 ...` for changed files you recognize, in order to normalize all line endings of all touched files to CRLF.
- After every iteration, run `jb cleanupcode --verbosity:ERROR ReactorMaintenance.slnx`.
## Documentation work or planning
- No cleanup steps necessary

View File

@@ -1,51 +1,51 @@
# Code Style
This repository follows the local `.editorconfig` and the style visible in the current local changes. Use these notes when creating new code.
## Naming
- Use PascalCase for namespaces, types, methods, properties, enum members, and non-field members.
- Prefix enum type names with `E`, for example `ECellKind`, `EPipeMedium`, `EFailureKind`, and `EEditorTool`.
- Prefix struct type names with `S` when creating new structs.
- Prefix interfaces with `I`.
- Use camelCase for parameters and local variables.
- Prefix private instance fields with `m_` and keep the remainder PascalCase, for example `m_Level` and `m_SelectedTool`.
- Prefix private static fields and static readonly fields with `s_`.
- Prefix constants with `c_`.
- Avoid `this.` unless it is needed for clarity or disambiguation.
- Always use folder-based namespaces when creating types and refactoring.
## Files And Types
- Use file-scoped namespaces.
- Keep one reusable top-level class per file, with the file name matching the class name.
- If a helper type is used only by one class, prefer nesting it inside that class.
- Keep small, cohesive files and extract shared helpers instead of duplicating logic.
## Braces And Blocks
- Use braces for multi-line bodies.
- If nesting a for-loop under another for-loop, always include curly braces in the parent for-loop.
- Omit braces for simple single-line embedded statements when readability stays clear.
- Nested control flow with multi-line bodies should use braces at every multi-line level.
- Keep opening braces on the next line for types, methods, properties, accessors, and control blocks.
- Compact object initializers, switch expressions, and `with` expressions may keep the opening brace on the same line when cleanup formats them that way.
## Blank Lines
- Use a blank line to separate members.
- Use a blank line after control-flow transfer clauses such as `return`, `continue`, `break`, and `throw` when more code follows in the same scope.
- Avoid extra blank lines inside short methods and between tightly related statements.
- Keep at most one blank line in code and declarations.
## Expressions And Formatting
- Prefer `var` when the type is apparent or not useful to repeat; use explicit built-in types such as `int`, `bool`, and `string`.
- Prefer target-typed `new()` when the type is evident.
- Prefer object and collection initializers, including collection expressions such as `[".json"]`.
- Prefer pattern matching for combined checks, for example `cell is { HasPipe: true, Pressure: > 7 }`.
- Prefer switch expressions for simple value selection.
- Prefer expression-bodied properties and accessors when they remain simple.
- Keep simple object initializers and property patterns on one line when they are short and readable.
- Keep long boolean expressions and interpolated status strings readable without introducing unnecessary blank lines.
- Keep using directives outside namespaces.
# Code Style
This repository follows the local `.editorconfig` and the style visible in the current local changes. Use these notes when creating new code.
## Naming
- Use PascalCase for namespaces, types, methods, properties, enum members, and non-field members.
- Prefix enum type names with `E`, for example `ECellKind`, `EPipeMedium`, `EFailureKind`, and `EEditorTool`.
- Prefix struct type names with `S` when creating new structs.
- Prefix interfaces with `I`.
- Use camelCase for parameters and local variables.
- Prefix private instance fields with `m_` and keep the remainder PascalCase, for example `m_Level` and `m_SelectedTool`.
- Prefix private static fields and static readonly fields with `s_`.
- Prefix constants with `c_`.
- Avoid `this.` unless it is needed for clarity or disambiguation.
- Always use folder-based namespaces when creating types and refactoring.
## Files And Types
- Use file-scoped namespaces.
- Keep one reusable top-level class per file, with the file name matching the class name.
- If a helper type is used only by one class, prefer nesting it inside that class.
- Keep small, cohesive files and extract shared helpers instead of duplicating logic.
## Braces And Blocks
- Use braces for multi-line bodies.
- If nesting a for-loop under another for-loop, always include curly braces in the parent for-loop.
- Omit braces for simple single-line embedded statements when readability stays clear.
- Nested control flow with multi-line bodies should use braces at every multi-line level.
- Keep opening braces on the next line for types, methods, properties, accessors, and control blocks.
- Compact object initializers, switch expressions, and `with` expressions may keep the opening brace on the same line when cleanup formats them that way.
## Blank Lines
- Use a blank line to separate members.
- Use a blank line after control-flow transfer clauses such as `return`, `continue`, `break`, and `throw` when more code follows in the same scope.
- Avoid extra blank lines inside short methods and between tightly related statements.
- Keep at most one blank line in code and declarations.
## Expressions And Formatting
- Prefer `var` when the type is apparent or not useful to repeat; use explicit built-in types such as `int`, `bool`, and `string`.
- Prefer target-typed `new()` when the type is evident.
- Prefer object and collection initializers, including collection expressions such as `[".json"]`.
- Prefer pattern matching for combined checks, for example `cell is { HasPipe: true, Pressure: > 7 }`.
- Prefer switch expressions for simple value selection.
- Prefer expression-bodied properties and accessors when they remain simple.
- Keep simple object initializers and property patterns on one line when they are short and readable.
- Keep long boolean expressions and interpolated status strings readable without introducing unnecessary blank lines.
- Keep using directives outside namespaces.

6
NuGet.config Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="GodotSharpLocal" value="..\Godot_v4.5.1-stable_mono_win64\GodotSharp\Tools\nupkgs" />
</packageSources>
</configuration>

View File

@@ -1,18 +1,37 @@
# Reactor Maintenance
C# WinUI 3 + Win2D level editor for the deterministic grid simulation described in `design.md`.
## Projects
- `src/ReactorMaintenance.Simulation`: UI-independent level model, editor operations, forecasts, simulation turns, versioned JSON serialization, and swappable difficulty balancing profiles.
- `src/ReactorMaintenance.Win2D`: Win2D editor app for painting floor/wall terrain plus cell props, loading/saving levels, advancing simulation turns, and activating the reactor.
- `tests/ReactorMaintenance.Simulation.Tests`: unit tests for deterministic simulation behavior.
## Commands
```powershell
dotnet test tests\ReactorMaintenance.Simulation.Tests\ReactorMaintenance.Simulation.Tests.csproj
dotnet build src\ReactorMaintenance.Win2D\ReactorMaintenance.Win2D.csproj -p:Platform=x64
dotnet run --project src\ReactorMaintenance.Win2D\ReactorMaintenance.Win2D.csproj -p:Platform=x64
```
# Reactor Maintenance
C# simulation with WinUI 3 + Win2D editor and a Godot frontend shell for the deterministic grid simulation described in `docs/design.md`.
## Projects
- `src/ReactorMaintenance.Simulation`: UI-independent level model, editor operations, validation, forecasts, simulation turns, versioned JSON serialization, and deterministic balancing defaults.
- `src/ReactorMaintenance.Godot`: Godot 4.5 .NET frontend project with scene routing, UX blueprint screens, reusable UI controls, and a mock campaign manifest referencing future level JSON files.
- `src/ReactorMaintenance.Win2D`: Win2D editor app for authoring terrain, underground fuel/coolant/electricity networks, props, explicit leak access faces, door edges, reactor consumer bindings, rule events, surface hazards, robot start, loading/saving levels, ending turns, interacting with props, and activating a ready reactor.
- `tests/ReactorMaintenance.Simulation.Tests`: unit tests for deterministic simulation behavior, validation, serialization, and editor operations.
## Editor Controls
- Left click selects or paints with the current tool. Right click clears the selected cell's prop, surface hazards, leaks, doors, and reactor control.
- Door authoring is explicit: select the Door tool, click the door cell, then click the adjacent floor cell that defines the blocked edge.
- Electricity wall leaks are explicit: select the Electricity Leak tool, click the wall network cell, then click the adjacent floor access face.
- Reactor bindings are explicit: select or place a reactor control, select a matching consumer cell, then use the Fuel, Coolant, or Electric binding action in the inspector.
- Rule event authoring is available from the inspector for next-turn warnings and selected-cell leak events; authored events are saved in the version 2 JSON schema.
## Commands
```powershell
dotnet build src\ReactorMaintenance.Godot\ReactorMaintenance.Godot.csproj
..\Godot_v4.5.1-stable_mono_win64\Godot_v4.5.1-stable_mono_win64_console.exe --headless --editor --quit --path src\ReactorMaintenance.Godot
dotnet test tests\ReactorMaintenance.Simulation.Tests\ReactorMaintenance.Simulation.Tests.csproj
dotnet build src\ReactorMaintenance.Win2D\ReactorMaintenance.Win2D.csproj -p:Platform=x64 -p:EnableWindowsTargeting=true
dotnet run --project src\ReactorMaintenance.Win2D\ReactorMaintenance.Win2D.csproj -p:Platform=x64
```
The WinUI/XAML compiler is Windows-specific. On Linux, the simulation tests run normally, but the Win2D app build must be verified in a Windows-capable environment.
## Godot Frontend
The current Godot frontend is a navigable UX scaffold. It starts at a splash screen, routes through the main menu, campaign intro, random generation placeholder, level screen, win/loss overlays, options, tutorial, game over, and campaign complete screens.
The mock campaign manifest lives at `src\ReactorMaintenance.Godot\Data\default_campaign_manifest.json`. Each entry includes the future serialized simulation level path; those JSON files are intentionally placeholders for authored levels that will use the simulation `LevelSerializer` format.

View File

@@ -1,11 +1,12 @@
<Solution>
<Folder Name="/src/">
<Project Path="src/ReactorMaintenance.Simulation/ReactorMaintenance.Simulation.csproj" />
<Project Path="src/ReactorMaintenance.Win2D/ReactorMaintenance.Win2D.csproj">
<Platform Project="x86" />
</Project>
</Folder>
<Folder Name="/tests/">
<Project Path="tests/ReactorMaintenance.Simulation.Tests/ReactorMaintenance.Simulation.Tests.csproj" />
</Folder>
</Solution>
<Solution>
<Folder Name="/src/">
<Project Path="src/ReactorMaintenance.Godot/ReactorMaintenance.Godot.csproj"/>
<Project Path="src/ReactorMaintenance.Simulation/ReactorMaintenance.Simulation.csproj"/>
<Project Path="src/ReactorMaintenance.Win2D/ReactorMaintenance.Win2D.csproj">
<Platform Project="x86"/>
</Project>
</Folder>
<Folder Name="/tests/">
<Project Path="tests/ReactorMaintenance.Simulation.Tests/ReactorMaintenance.Simulation.Tests.csproj"/>
</Folder>
</Solution>

204
TASKS.md Normal file
View File

@@ -0,0 +1,204 @@
# Reactor Maintenance Tasks
This backlog tracks procedural level generation, reusable exhaustive solving, Godot random play, and Godot-assisted campaign authoring automation.
## Audit Snapshot
- Current simulation tests pass: `60/60` in `tests/ReactorMaintenance.Simulation.Tests`.
- Godot level play includes pulse step playback, terminal-gated layer controls, campaign loading, and in-game level editing with JSON save.
- Campaign data follows the tutorial plus six-group campaign from `docs/CAMPAIGN.md`.
- The next implementation replaces the placeholder random-level flow with seed-driven generation and reusable solver analysis.
- Win2D is not part of this backlog.
## P0 Generator Contract
- [ ] Add a parameterized `LevelGenerationRequest`.
- Include `Seed`, display name, optional max dimensions, `SolverDepthLimit`, target complexity, and `MaxAttempts`.
- Require `InvolvedSystems` to contain at least one of `Fuel`, `Water`, or `Electricity`.
- Specify exact available consumer counts per involved carrier.
- Specify exact required consumer counts per involved carrier.
- Specify the exact carrier set directly connected to the reactor control.
- Reject impossible specs before generation, including required consumers greater than available consumers.
- [ ] Add a player-facing seed profile expander.
- Use the seed to choose involved systems.
- Use the seed to choose available consumer counts.
- Use the seed to choose required consumers as none, some, or all of the placed consumers.
- Use the seed to choose how many involved systems directly connect to the reactor.
- Expand to a concrete `LevelGenerationRequest` so random play and authoring use one generator path.
- [ ] Generate levels without fixed archetypes.
- Compose generated content from the requested systems, consumers, reactor connectivity, and difficulty target.
- Avoid named campaign-group archetypes as public generation modes.
- Let multi-system hazards emerge from generated overlapping fuel, water, electricity, heat, leak, sprinkler, and powered-prop placements.
- [ ] Grow levels organically from the reactor.
- Start the generated graph at the `ReactorControlProp`.
- Grow carrier networks, branches, rooms, corridors, access routes, and player decision points outward from the reactor graph.
- Place surrounding walls after gameplay geometry exists.
- Calculate the occupied bounding box after generation.
- Offset all coordinates into positive level space before creating final arrays.
- Preserve valid wall geometry for doors, wall electricity leaks, and wall-mounted sprinkler valves.
- [ ] Generate structural-integrity and pressure interactions.
- Author weakened underground segments with controlled pressure or voltage exposure.
- Generate pressure-fed and voltage-fed leaks that can be isolated, repaired, or allowed to worsen.
- Generate hazards that restrict movement through `Unsafe` heat, electricity, or wet-electric combinations.
- Include pressure tradeoffs from sprinkler discharge and consumer/reactor starvation.
- Ensure generated hazards are reachable, readable, and tied to player choices.
## P0 Reusable Solver
- [ ] Add a reusable `LevelSolver` in the simulation project.
- Accept a `LevelState` and `SolverRequest`.
- Return a `SolverReport` with terminal counts, complexity metrics, diagnostics, and representative traces.
- Keep the solver independent from Godot so generator, editor, tests, and future tools can share it.
- [ ] Define solver choices as reachable lengthy actions.
- Collapse quick movement into safe reachability from the current robot position.
- Enumerate all reachable prop interactions, leak repairs, remedy uses, heat shield uses, and reactor activation attempts.
- Exclude inspection, selection, terminal viewing, and individual quick movement branches from primary metrics.
- Sort canonical choices deterministically before evaluation.
- [ ] Evaluate every bounded branch.
- Explore all possible choice permutations to the configured lengthy-action depth limit.
- Count `Win`, `Lose`, and `Inconclusive` leaves.
- Mark branches inconclusive when the depth limit or timeout is reached before terminal outcome.
- Treat no-choice non-ready states as losing dead ends.
- Preserve exact branch counts while using transposition caching to avoid duplicate expansion work.
- [ ] Add solver complexity metrics.
- Report min and max player choices to win.
- Report min and max player choices to loss.
- Report max and average available choices per decision state.
- Report lose/win ratio across all bounded permutations.
- Report inconclusive ratio.
- Report explored node count and cache hit count.
- Provide representative shortest winning and losing traces.
- [ ] Parallelize branch evaluation.
- Issue all canonical choices from a state in parallel when budget allows.
- Wait for all choice evaluations from that state.
- Aggregate results in canonical order for deterministic diagnostics.
- Use ThreadPool or TPL work stealing for nested parallel branch work.
- Respect max parallelism, timeout, and cancellation.
- [ ] Keep solver performance and memory usage controlled.
- Clone level states only at branch boundaries.
- Add per-thread reusable buffers or `LevelState` clone pools where practical.
- Avoid retaining full state graphs when only metrics are needed.
- Keep only bounded representative traces.
- Evaluate whether `record struct` or other value-oriented refactors reduce heap pressure without harming maintainability.
- Add allocation or stress tests for representative generated levels.
## P1 Generation Acceptance And Difficulty
- [ ] Validate every generated candidate.
- Run `LevelValidator`.
- Run bounded solver analysis.
- Reject candidates with validation errors.
- Reject candidates with no winning branch.
- Reject candidates whose required systems, consumer counts, or reactor-connected carrier set do not exactly match the request.
- Reject candidates whose hazards cannot affect movement or meaningful decisions.
- [ ] Add difficulty target filtering.
- Filter by minimum and maximum choices to win.
- Filter by maximum and average available choices.
- Filter by lose/win ratio range.
- Filter by maximum inconclusive ratio.
- Filter by required first-choice variety.
- Make defaults suitable for random play and configurable for campaign authoring.
- [ ] Support campaign progression targets.
- Early targets use fewer involved systems, lower available-choice counts, and low lose/win ratios.
- Mid targets increase first-choice variety and introduce more losing branches.
- Late targets allow more systems, more branching, higher lose/win ratio, and lower inconclusive tolerance.
- Make target definitions data-driven enough for campaign automation scripts or Godot UI.
- [ ] Add diagnostics for rejected candidates.
- Report invalid spec reasons.
- Report validation errors and warnings.
- Report solver failure reasons.
- Report mismatch against requested systems, consumers, reactor feeds, and complexity target.
- Keep diagnostics concise enough for Godot UI and detailed enough for tests.
## P1 Godot Random Play And Authoring Automation
- [ ] Replace placeholder random-level flow.
- Generate a level from a player seed profile.
- Save generated JSON under `user://generated/`.
- Load generated levels through `LevelStateLoader`.
- Retry the same generated level from the saved JSON.
- Complete random levels by returning to generation or regenerating from a new seed.
- [ ] Add authoring controls to `GenerationScreen`.
- Toggle involved systems.
- Set available consumers per carrier.
- Set required consumers per carrier.
- Toggle reactor-connected carriers.
- Set seed, max attempts, solver depth limit, and target complexity.
- Generate, regenerate, analyze, save JSON, begin play, and return to menu.
- [ ] Surface solver metrics in Godot.
- Show win, lose, and inconclusive counts.
- Show min and max choices to win.
- Show max and average available choices.
- Show lose/win ratio and inconclusive ratio.
- Show concise representative trace summaries for authoring review.
- [ ] Reuse generated JSON in Godot editor mode.
- Open generated levels in `LevelScreen`.
- Allow normal edit-mode changes.
- Validate and save back to the generated JSON path.
- Preserve generated metadata needed for retry and review.
- [ ] Keep Godot generation responsive.
- Run generation and solver analysis off the main UI path.
- Show progress, cancellation, and failure diagnostics.
- Prevent starting a stale or failed generated candidate.
## P1 Tests
- [ ] Add generator determinism tests.
- Same request and seed serialize to identical JSON.
- Different seeds produce meaningfully different layouts for the same request.
- Player seed profile expansion is deterministic.
- [ ] Add generation spec-compliance tests.
- Generated involved systems match the request exactly.
- Available consumer counts match the request exactly.
- Required consumer counts match the request exactly.
- Reactor-connected carriers match the request exactly.
- Organic bounding and coordinate offset produce valid positive coordinates and correct array sizes.
- [ ] Add generated hazard tests.
- Generated pressure-fed leaks inject only when fed.
- Structural-integrity hazards can create or worsen leaks under generated pressure or voltage.
- Generated hazards can restrict movement through `Unsafe` rules.
- Generated sprinkler pressure debt can affect consumer or reactor service.
- [ ] Add solver correctness tests.
- Known tiny levels report expected win, lose, and inconclusive counts.
- Depth-limited branches become inconclusive.
- No-choice non-ready states count as losing dead ends.
- Canonical action ordering produces stable diagnostics.
- Parallel and single-threaded solver runs produce identical reports.
- [ ] Add solver performance tests.
- Branch-heavy generated samples finish within the configured timeout.
- Representative solving does not retain unbounded traces.
- Clone or pooling behavior does not leak mutable state across branches.
- [ ] Add Godot integration coverage where practical.
- Generated JSON loads through `LevelStateLoader`.
- Generated JSON saves through the existing level save path.
- `FrontendSession` can point random mode at generated JSON.
## P2 Documentation And Tooling
- [ ] Update `docs/design.md`.
- Document procedural generation inputs.
- Document solver outcomes and metrics.
- Document generated structural-integrity and pressure-hazard expectations.
- Document random play seed behavior.
- [ ] Update `docs/UX.md`.
- Replace placeholder random-level wording.
- Describe generation controls, authoring controls, metrics display, progress, cancellation, and failure states.
- Describe generated JSON editing and save-back flow.
- [ ] Add authoring guidance.
- Explain how generated levels can be promoted into campaign candidates.
- Explain how increasing choices and lose/win ratio supports campaign progression.
- Explain recommended solver depth and target complexity ranges.
- [ ] Keep task status current.
- Check off items as implementation lands.
- Remove outdated task text instead of adding past-tense prose.
- Keep verification commands aligned with repo instructions.
## Verification Rules
- [ ] Run focused solver and generator tests after solver or generation changes.
- [ ] Run full `dotnet test` before committing simulation, serializer, or generated-data changes.
- [ ] Run `dotnet build ReactorMaintenance.slnx --no-restore` and `dotnet test ReactorMaintenance.slnx --no-restore` sequentially.
- [ ] For code changes on Windows, run `python D:\Code\crlf.py` for recognized touched files.
- [ ] For code changes on Windows, run `jb cleanupcode --verbosity:ERROR ReactorMaintenance.slnx`.
- [ ] For documentation-only iterations, no cleanup pass is required.
- [ ] Commit each completed iteration with a brief summary.

128
docs/ART.md Normal file
View File

@@ -0,0 +1,128 @@
# Reactor Maintenance Art Bible
This document defines the visual style for generated and hand-authored art. Include the prompt block when generating image assets, then add the asset-specific notes for the object being created.
## Prompt Block
Create readable top-down 2D game art for Reactor Maintenance: modern, elegant, stylized, and playful sci-fi with a scrappy industrial reactor-maintenance tone. Use crisp simplified shapes, functional machinery with charm, selective dark outlines, restrained lighting, and cool steel or graphite base materials with clear semantic accent colors. Preserve readability at small tile and icon sizes. Use sparse material texture based on hue variation, panel color shifts, subtle oxidation, heat tinting, and worn paint color changes instead of noisy luminance contrast. Avoid photorealism, excessive grime, tiny mechanical clutter, heavy glow, CRT effects, cyberpunk neon overload, and high-frequency scratches or speckles.
## Core Style
- Perspective: readable top-down tactical game view.
- Rendering: crisp stylized 2D, flat-to-soft shading, clean silhouettes, minimal atmospheric effects.
- Mood: scrappy industrial sci-fi, tense but approachable, not sterile and not horror.
- Shape language: chunky readable machinery, rounded bevels, practical handles, thick pipes, oversized state lights, and mild asymmetry.
- Edge treatment: selective dark outlines around gameplay-critical silhouettes, openings, and layered components. Do not outline every tiny surface detail.
- Detail density: large forms first, medium details second, small details only as accents.
## Color System
- Base materials: cool graphite, blue-gray steel, muted charcoal, dark desaturated teal.
- Neutral contrast: keep major surfaces in grouped value bands so details do not sparkle or flicker when downscaled.
- Fuel: saturated red accents and warning stains.
- Coolant: cyan or blue accents, slightly clean and cold.
- Electricity: yellow accents, sharp and high-energy.
- Heat: orange to white-hot accents, used sparingly.
- Ready or won: bright green-white accents.
- Lost or critical: red with limited orange heat support.
Use accent colors as gameplay information, not decoration. Do not let background panels compete with hazards, robot position, or reactor state.
## Texture Rules
- Prefer hue variation over luminance variation.
- Show wear through shifted paint colors, oxidized edges, faint coolant discoloration, heat-blued metal, and mismatched replacement panels.
- Keep texture marks broad enough to read at 64px and 32px.
- Use 1-3 controlled texture zones per asset, not all-over noise.
- Avoid fine scratches, random speckles, dense grime, noisy rust, photographic stains, and texture that changes the silhouette.
- If an asset must look dirty, make the dirt a clear shape with a distinct hue, not a field of dark dots.
## Downscale Readability
- Design every prop to remain recognizable at tile size before adding surface detail.
- Use 2-3 dominant shape groups per asset.
- Keep the outer silhouette simple and distinctive.
- Reserve the highest contrast for the object boundary, key interaction point, and state indicator.
- Leave quiet space inside each asset so icons, badges, and selection highlights can sit on top.
- Test mentally at 128px, 64px, and 32px. If the asset becomes a noisy blob, remove detail before adding more contrast.
## Asset Guidance
### Floors And Walls
Floors should be quiet, matte, and grid-readable: graphite plates, cool steel panels, rubberized seams, or worn maintenance decking. Use subtle hue shifts between plates instead of strong light/dark checkerboarding.
Walls should be heavier than floors with strong silhouettes and clear blocking. They may use darker steel, reinforced ribs, conduit hints, and warning trim, but must not look like interactable props.
### Pipes And Underground Networks
Pipes should be thick, continuous, and readable as routes. Use carrier color accents along otherwise muted metal forms:
- fuel pipes: red bands, valve marks, or inner glow kept low,
- coolant pipes: cyan-blue bands, cold tinting, condensation shapes,
- electricity conduits: yellow hazard marks, angular housings, insulated cable cues.
Avoid tiny pipe bundles. Junctions, sources, leaks, and breaks need larger silhouettes than ordinary pipe segments.
### Props
Props should look functional and slightly overbuilt. Each prop needs one obvious interaction feature: lever, screen, port, valve, hatch, supply cap, or activation core. State lights should be large enough to read at icon size.
Common prop rules:
- control terminals: screen-first silhouette, teal or green display, chunky base,
- diagnostic terminals: stronger sensor or scanner identity, purple-blue secondary accents allowed,
- cooling pumps: round tank or impeller identity, coolant accent,
- generators: coil, flywheel, or battery silhouette, electricity accent,
- pressure regulators: gauge and valve identity, simple face markings,
- reactor controls: largest authority object, central core or keyed activation surface.
### Robot
The robot should be compact, practical, and likable without becoming comedic. Use a strong top-down body shape, small tool arms or maintenance appendages, a visible direction cue, and one clear status light. Keep the robot brighter than floor tiles but less saturated than hazards.
### Hazards And Failures
Hazards must be immediately distinct from props and terrain:
- fuel: red slicks with smooth pooling shapes,
- coolant: blue-cyan spill shapes with colder edges,
- electricity: angular yellow arcs or charged patches,
- heat/fire: orange-white cores with simple flame or shimmer shapes.
Failure images may be more dramatic than gameplay tiles, but must keep a single readable focal point and avoid painterly chaos. Use large hazard shapes, clear silhouettes, and limited supporting debris.
### UI Icons And Badges
UI art should be flatter and cleaner than world props. Use the same semantic colors, simple pictograms, and selective outlines. Icons must remain readable at 24px. Avoid mini illustrations inside UI controls.
## Composition For Generated Images
- Use transparent or simple neutral backgrounds for props, icons, tiles, and pipe pieces.
- Center the asset with enough padding for selection outlines and Godot import cropping.
- Avoid dramatic camera angles for gameplay assets.
- Do not add labels, text, watermarks, borders, frames, or UI chrome unless the prompt asks for a UI asset.
- For tilemaps, keep lighting direction consistent and avoid shadows that imply non-grid depth.
## Negative Prompt
no photorealism, no noisy texture, no high-frequency detail, no dense grime, no random speckles, no tiny wires everywhere, no excessive scratches, no low-contrast muddy silhouette, no unreadable clutter, no dramatic perspective, no generic cyberpunk city neon, no heavy bloom, no CRT scanlines, no glitch effect, no horror gore, no text, no watermark
## Existing Asset Note
The current Win2D PNGs are useful subject references for the required asset categories, but they are not the target style. Future assets should be simpler, cleaner, more silhouette-driven, and more readable when scaled down.
## Generated Godot Cutouts
The first transparent Godot cutouts live under `src/ReactorMaintenance.Godot/Assets`. They were generated with `gpt-image-1.5` using the mood sheets in `output/imagegen/godot-mood` as references:
- `Assets/Characters/maintenance_robot.png`
- `Assets/Ui/primary_button_accent.png`
- `Assets/Ui/state_badge_frame.png`
- `Assets/Ui/fuel_icon.png`
- `Assets/Ui/coolant_icon.png`
- `Assets/Ui/electric_icon.png`
- `Assets/Ui/heat_shield_icon.png`
- `Assets/Ui/scanner_eye_icon.png`
The exact generation prompts are recorded in `src/ReactorMaintenance.Godot/Assets/generated_cutouts_prompts.jsonl`.

660
docs/CAMPAIGN.md Normal file
View File

@@ -0,0 +1,660 @@
# Reactor Maintenance Campaign Design
This document prepares the handcrafted campaign for implementation. It references `docs/design.md` terminology directly; campaign text should not introduce mechanics that are absent from the design document.
## Campaign Goals
- Teach one `LengthyAction` grammar at a time, then combine systems.
- Keep `MoveRobot`, inspection, and visible-state reading calm and free.
- Make every environment-changing action trigger exactly one `Pulse`.
- Keep `Forecast` output systemic and available only at an active and powered `AllSeeingEyeTerminal`.
- Avoid treating missing `ReactorReadiness` as a failure state; it only blocks `Ready`.
- Use `UnsafeEntryLoss` only when `MoveRobot` enters an `Unsafe` destination cell.
- Keep the campaign small enough for a two-week game jam.
## Campaign Structure
The campaign uses one tutorial plus six groups. Group 1 intentionally has two levels; Groups 2-6 have three levels each.
| Group | Levels | Networks | Main Lesson |
| ----- | ------ | -------- | ----------- |
| Tutorial | 1 | `Fuel` | `LengthyAction` triggers `Pulse`; `Ready` then `ActivateReactor` wins. |
| 1 | 2 | `Fuel` | `FlowProp`, `IsolationValveProp`, reachable leaks, and direct `ReactorReadiness`. |
| 2 | 3 | `Coolant` | `ConsumerProp`, `Producing`, `SprinklerControlProp`, wall-mounted `CoolantSprinklerValve`, `SprinklerWater`, pressure recovery. |
| 3 | 3 | `Electricity` | wall `Electricity` leaks, repair faces, powered `DoorProp`, and powered `AllSeeingEyeTerminal`. |
| 4 | 3 | `Fuel` + `Electricity` | first dual-system interaction: `Ignite` creates recurring `Heat`. |
| 5 | 3 | `Coolant` + `Electricity` | `SprinklerWater`, `Evaporation`, and wet-electricity `Conduct` risk. |
| 6 | 3 | `Fuel` + `Coolant` + `Electricity` | full startup with fire, suppression, wet conduction, and terminal-local `Forecast` use. |
## Authoring Rules
Before `AllSeeingEyeTerminal` appears in 3-2, all levels must be readable from surface props and visible effects alone. Use simple branches, clear source positions, visible leak access, and obvious valve placement.
Every non-tutorial level must offer at least two plausible first `LengthyAction` choices. The later the group, the more those choices should require sequencing rather than a single obvious answer.
Do not author level-specific forecast strings. The campaign can require that systemic `Forecast` output is useful, but the level spec must describe the underlying state that fixed forecast rules can detect.
Do not describe numeric surface thresholds or campaign-specific formulas. Use `Unsafe` only as the formal movement consequence defined in `docs/design.md`.
Each level below includes:
- `Purpose`: the reason the level exists.
- `Setup`: authored starting conditions.
- `Timeline`: intended reasoning and `Pulse` consequences.
- `Win`: required `ReactorReadiness` and `ActivateReactor` condition.
- `Lose`: terminal failure or `UnsafeEntryLoss` conditions.
- `ImplementationNotes`: content constraints for level data.
## Tutorial: First Pulse
### T0 - Wake The Feed
Purpose: Teach that one accepted `LengthyAction` triggers one `Pulse`, after which `Ready` can be activated.
Setup:
- Networks: `Fuel`.
- One disabled `Fuel` `FlowProp` feeds the `ReactorControlProp`.
- Required consumer counts are zero.
- No leaks, no surface hazards, no `DoorProp`, no `AllSeeingEyeTerminal`.
- Robot starts a few floor cells away from the `FlowProp`.
Timeline:
1. Player uses `MoveRobot` and inspection freely.
2. Player uses `InteractProp` on the `Fuel` `FlowProp`.
3. One `Pulse` propagates `Fuel` to the `ReactorControlProp`.
4. Level enters `Ready`.
5. Player uses `ActivateReactor`.
Win: `ReactorControlProp` has positive `Fuel` amount and pressure, required consumer counts are zero, and `ActivateReactor` changes level state to `Won`.
Lose: No designed loss condition beyond invalid actions; this is guided tutorial content.
ImplementationNotes:
- This is the only single-action setup.
- Do not include `Forecast`; no `AllSeeingEyeTerminal` exists.
## Group 1: `Fuel`
Group 1 teaches pressure-fed `Fuel` faults with no required `ConsumerProp` instances and no `Forecast`. Branches must be visually obvious from `FlowProp`, `IsolationValveProp`, leak access, and `ReactorControlProp` placement.
### 1-1 - Bleed Line
Purpose: Teach that source startup, reactor feed, and leak isolation are separate decisions.
Setup:
- Networks: `Fuel`.
- One disabled `Fuel` `FlowProp`.
- One `ReactorControlProp` on a main branch that starts blocked by a closed `IsolationValveProp`.
- One leaking `Fuel` segment on a short side branch with a reachable floor access cell.
- One open `IsolationValveProp` on the side branch before the leak.
- Required consumer counts are zero.
Timeline:
1. First choice A: enable the `FlowProp`; `Pulse` feeds the open leak branch and creates visible `LeakedFuel`, but the closed reactor-feed valve keeps `ReactorReadiness` missing.
2. First choice B: open the reactor-feed `IsolationValveProp`; `Pulse` changes the route but does not create `ReactorReadiness` because the source is still disabled.
3. First choice C: close the leak-branch `IsolationValveProp`; `Pulse` prevents fresh leak injection but leaves the reactor unfed.
4. Player isolates or repairs the leak, enables the source, and opens the reactor-feed branch.
5. Player uses `ActivateReactor`.
Win: `ReactorReadiness` is true for `Fuel` with zero required consumers, then `ActivateReactor`.
Lose: `Heat` terminal failure is not present; `LeakedFuel` alone does not cause `UnsafeEntryLoss`.
ImplementationNotes:
- The leak branch must be visually identifiable without underground visibility.
- No first `LengthyAction` should make the level `Ready`.
### 1-2 - Valve Choice
Purpose: Teach branch-specific isolation versus over-isolating the main feed.
Setup:
- Networks: `Fuel`.
- One enabled `Fuel` `FlowProp` feeds a fork.
- One fork branch reaches the `ReactorControlProp` through a closed reactor-feed `IsolationValveProp`.
- The other fork branch reaches a leaking `Fuel` segment.
- One main `IsolationValveProp` before the fork.
- One leak-branch `IsolationValveProp` before the leaking segment.
- Required consumer counts are zero.
Timeline:
1. First choice A: close the main `IsolationValveProp`; `Pulse` stops fresh leak injection and also prevents future reactor feed until reopened.
2. First choice B: close the leak-branch `IsolationValveProp`; `Pulse` stops fresh leak injection while preserving the main route.
3. First choice C: repair the leak; `Pulse` stops future leak injection but leaves the reactor-feed valve still closed.
4. Player opens the reactor-feed branch and keeps or restores the main feed.
5. Player uses `ActivateReactor`.
Win: `ReactorReadiness` is true for `Fuel` with zero required consumers, then `ActivateReactor`.
Lose: No designed `UnsafeEntryLoss` from `LeakedFuel` alone; terminal failure only if fixed rules mark the level `Lost`.
ImplementationNotes:
- Use surface labels or prop placement to distinguish main valve and leak-branch valve.
- The level must not begin `Ready`; the reactor-feed branch starts closed.
## Group 2: `Coolant`
Group 2 introduces `ConsumerProp` requirements and `CoolantSprinklerValve` pressure tradeoffs. Since `Coolant` alone is not a fire system yet, these levels focus on `Producing` consumers, `SprinklerControlProp`, `SprinklerWater`, `Evaporation`, and pressure recovery.
### 2-1 - Prime The Pump
Purpose: Introduce `ConsumerProp` as a required production checklist.
Setup:
- Networks: `Coolant`.
- One enabled `Coolant` `FlowProp`.
- Two visible `Coolant` `ConsumerProp` instances:
- one starts `Enabled` but is behind a closed branch `IsolationValveProp`, so it is not `Producing`,
- one starts `Disabled` on a supplied branch, so it is supplied but not `Producing`.
- One `ReactorControlProp`.
- Required consumers: two `Coolant`.
- The `ReactorControlProp` is fed from the main branch.
Timeline:
1. First choice A: enable the supplied but disabled `ConsumerProp`; `Pulse` makes only that consumer `Producing`.
2. First choice B: open the blocked branch `IsolationValveProp`; `Pulse` supplies the already enabled consumer.
3. First choice C: close the main branch; `Pulse` demonstrates that consumer production depends on supply and delays readiness.
4. Player sets both consumer branches so both `Coolant` `ConsumerProp` instances are `Enabled` and `Producing`.
5. Player uses `ActivateReactor`.
Win: `ReactorControlProp` has positive `Coolant` amount and pressure, two required `Coolant` `ConsumerProp` instances are `Enabled` and `Producing`, then `ActivateReactor`.
Lose: No designed terminal pressure; only invalid actions or explicit terminal rules.
ImplementationNotes:
- This level introduces the difference between `Enabled`, `Supplied`, and `Producing` before any `CoolantSprinklerValve`.
- Use visible prop placement; no `AllSeeingEyeTerminal`.
### 2-2 - Sprinkler Debt
Purpose: Teach that an enabled wall-mounted `CoolantSprinklerValve` can reduce pressure until disabled or isolated.
Setup:
- Networks: `Coolant`.
- One enabled `Coolant` `FlowProp`.
- One wall-mounted `CoolantSprinklerValve` starts discharging because its linked `SprinklerControlProp` starts `Enabled`.
- One nearby `SprinklerControlProp` is the only direct sprinkler interaction.
- The sprinkler has one adjacent floor outlet/access face and emits `SprinklerWater`.
- One `Coolant` `ConsumerProp` and the `ReactorControlProp` are on the same pressure-sensitive branch.
- Required consumers: one `Coolant`.
- One upstream `IsolationValveProp` can shut the sprinkler branch without shutting the consumer branch.
Timeline:
1. First choice A: disable the linked `SprinklerControlProp`; `Pulse` stops discharge and allows pressure recovery.
2. First choice B: close the sprinkler-branch `IsolationValveProp`; `Pulse` stops discharge by cutting supply to the sprinkler.
3. First choice C: enable or adjust the required `ConsumerProp` first; `Pulse` keeps sprinkler debt visible.
4. `SprinklerWater` evaporates through normal `Step` rules during pulses caused by useful actions.
5. Player reaches `ReactorReadiness` after `Coolant` pressure and required consumer production recover.
6. Player uses `ActivateReactor`.
Win: `ReactorControlProp` has positive `Coolant` amount and pressure, one `Coolant` `ConsumerProp` is `Enabled` and `Producing`, then `ActivateReactor`.
Lose: No authored `Heat` or fire; avoid loss pressure except explicit terminal rules.
ImplementationNotes:
- Do not author standalone `Heat` in this `Coolant`-only level.
- Sprinkler is wall-mounted with exactly one outlet/access floor face and exactly one linked `SprinklerControlProp`.
- This level visually teaches `SprinklerWater` and `Evaporation` without suppression yet.
### 2-3 - Split Flow
Purpose: Teach `JunctionProp` routing and consumer requirements in a readable `Coolant` network.
Setup:
- Networks: `Coolant`.
- One enabled `Coolant` `FlowProp`.
- One `JunctionProp` controls two consumer branches.
- Three visible `Coolant` `ConsumerProp` instances.
- One wall-mounted `CoolantSprinklerValve` starts `Disabled` because its linked `SprinklerControlProp` starts `Disabled`.
- Required consumers: two `Coolant`.
- One branch has a weakened or leaking `Coolant` segment that emits `SprinklerWater` when fed.
Timeline:
1. First choice A: cycle the `JunctionProp`; `Pulse` changes which consumers can become `Producing`.
2. First choice B: close the damaged branch `IsolationValveProp`; `Pulse` stops fresh `SprinklerWater` injection but reduces available production paths.
3. First choice C: repair the `Coolant` leak; `Pulse` stops future leak injection but does not remove existing `SprinklerWater`.
4. Player configures routing so two `Coolant` consumers are `Enabled` and `Producing`.
5. Player uses `ActivateReactor`.
Win: `ReactorControlProp` has positive `Coolant` amount and pressure, two `Coolant` `ConsumerProp` instances are `Enabled` and `Producing`, then `ActivateReactor`.
Lose: No designed `UnsafeEntryLoss` from `SprinklerWater` alone; otherwise no designed terminal pressure.
ImplementationNotes:
- Keep all relevant branches readable from surface prop placement.
- No `Forecast`; no `AllSeeingEyeTerminal`.
## Group 3: `Electricity`
Group 3 introduces wall `Electricity` leaks, powered `DoorProp` behavior, and then powered `AllSeeingEyeTerminal`. Level 3-1 must remain readable without underground visibility.
### 3-1 - Door Circuit
Purpose: Introduce `DoorProp` power requirements while reinforcing wall `Electricity` leak handling.
Setup:
- Networks: `Electricity`.
- One enabled `Electricity` `FlowProp`.
- One `DoorProp` begins `Closed` and its local supply branch starts blocked by an `IsolationValveProp`.
- One wall-based `Electricity` leak with exactly one adjacent floor emission/repair face.
- One `Electricity` `ConsumerProp` starts `Disabled` on the blocked door-supply branch.
- One `ReactorControlProp` is fed by the blocked door-supply branch.
- Required consumers: one `Electricity`.
- One `IsolationValveProp` can cut voltage to the leaking wall segment.
Timeline:
1. First choice A: interact with the unpowered `DoorProp`; `Pulse` resolves, the door stays `Closed`, and the leak can continue.
2. First choice B: open the door-supply `IsolationValveProp`; `Pulse` powers the door and keeps the leak state readable.
3. First choice C: isolate or repair the wall leak first; `Pulse` reduces `LeakedElectricity` risk before door operation.
4. Player powers and toggles the `DoorProp`, then restores consumer production and reactor feed.
5. Player uses `ActivateReactor`.
Win: `ReactorControlProp` has positive `Electricity` amount and voltage, one `Electricity` `ConsumerProp` is `Enabled` and `Producing`, then `ActivateReactor`.
Lose: `UnsafeEntryLoss` from moving into `Unsafe` `LeakedElectricity`; terminal failure if fixed rules mark `Lost`.
ImplementationNotes:
- Emission/repair face must be visually unambiguous.
- No `Forecast`; no `AllSeeingEyeTerminal`.
- Door supply and leak isolation must be readable from nearby props.
### 3-2 - The First Eye
Purpose: Introduce powered `AllSeeingEyeTerminal` access and terminal-local `Forecast`.
Setup:
- Networks: `Electricity`.
- One `AllSeeingEyeTerminal` in a side room.
- The terminal's local supply branch starts blocked by an `IsolationValveProp`.
- One `Electricity` `ConsumerProp` starts `Disabled` on a clearly separate supplied branch.
- One `ReactorControlProp` starts blocked by a closed reactor-feed `IsolationValveProp`.
- Required consumers: one `Electricity`.
- One simple wall `Electricity` leak is visible from the surface.
- One optional `DoorProp` is present only if its powered state remains obvious without terminal visibility.
Timeline:
1. First choice A: interact with the unpowered `AllSeeingEyeTerminal`; `Pulse` resolves, but no underground view or `Forecast` appears.
2. First choice B: power the terminal branch; `Pulse` makes later terminal use effective.
3. First choice C: isolate or repair the visible leak first; `Pulse` reduces visible risk but delays terminal information.
4. Player activates the powered terminal, uses systemic `Forecast`, restores required production and control feed.
5. Player uses `ActivateReactor`.
Win: `ReactorControlProp` has positive `Electricity` amount and voltage, one `Electricity` `ConsumerProp` is `Enabled` and `Producing`, then `ActivateReactor`.
Lose: `UnsafeEntryLoss` from entering `Unsafe` `LeakedElectricity`; terminal failure if fixed rules mark `Lost`.
ImplementationNotes:
- This is the first campaign level with `Forecast`.
- `Forecast` output must be systemic; do not author level text.
- The level remains solvable from visible information, but powering the terminal should clearly improve confidence.
### 3-3 - Blind Grid
Purpose: Require powered `AllSeeingEyeTerminal` use in a complex electricity routing puzzle.
Setup:
- Networks: `Electricity`.
- One `AllSeeingEyeTerminal` starts unpowered on a branch that can be energized early.
- Two required `Electricity` `ConsumerProp` instances sit on different branches.
- One `DoorProp` contains surface propagation along a useful route and requires local power.
- One visible wall `Electricity` leak and one weakened high-voltage segment are on different possible routes.
- Required consumers: two `Electricity`.
Timeline:
1. First choice A: power the terminal branch; `Pulse` enables a later useful terminal interaction while other risks advance.
2. First choice B: interact with the unpowered terminal first; `Pulse` changes nothing and demonstrates the cost of missing power.
3. First choice C: route power toward visible consumers without terminal information; `Pulse` may feed one consumer while worsening the hidden weak route.
4. Player activates the powered terminal, reads underground topology and systemic `Forecast`, then chooses which branches to isolate, repair, or power.
5. Player produces both required consumers and powers the `ReactorControlProp`.
6. Player uses `ActivateReactor`.
Win: `ReactorControlProp` has positive `Electricity` amount and voltage, two `Electricity` `ConsumerProp` instances are `Enabled` and `Producing`, then `ActivateReactor`.
Lose: `UnsafeEntryLoss` from `Unsafe` `LeakedElectricity`, terminal `Heat` if generated by fixed rules, or another fixed terminal rule.
ImplementationNotes:
- This is the first level where terminal use is essential rather than optional.
- The visible-only read should be plausible but incomplete.
## Group 4: `Fuel` + `Electricity`
Group 4 introduces the first dual-system interaction: `Fuel` and `Electricity` can `Ignite`, generating recurring `Heat`. From this point onward, `Heat` is a recurring campaign problem.
### 4-1 - First Spark
Purpose: Teach that combining `LeakedFuel` and `LeakedElectricity` can create `Heat`.
Setup:
- Networks: `Fuel`, `Electricity`.
- One visible `Fuel` leak emits near a corridor.
- One wall `Electricity` leak emits toward the same corridor.
- One `Fuel` `ConsumerProp`, one `Electricity` `ConsumerProp`, and one `ReactorControlProp` on both networks.
- Required consumers: one `Fuel`, one `Electricity`.
- One `IsolationValveProp` can isolate either damaged branch.
Timeline:
1. First choice A: enable `Fuel`; `Pulse` grows `LeakedFuel`.
2. First choice B: enable `Electricity`; `Pulse` grows `LeakedElectricity`.
3. First choice C: isolate or repair one leak before both networks are active.
4. If both leaked values meet, fixed surface rules can `Ignite` and create `Heat`.
5. Player stabilizes at least one leak path, restores required production, and activates.
Win: `ReactorReadiness` is true for `Fuel` and `Electricity`, required consumers are `Enabled` and `Producing`, then `ActivateReactor`.
Lose: terminal `Heat`, `UnsafeEntryLoss` from `Unsafe` `Heat`, `LeakedElectricity`, or wet-electric cells, or fixed terminal failure.
ImplementationNotes:
- Use only designed interactions from the hazard pair table.
- `Heat` appears as a consequence of `Ignite`, not as an arbitrary authored patch.
### 4-2 - Break Before Make
Purpose: Teach source sequencing: fixing or isolating first can be safer than powering both networks.
Setup:
- Networks: `Fuel`, `Electricity`.
- `Fuel` source starts enabled and a small visible `LeakedFuel` patch exists.
- `Electricity` source starts disabled.
- One wall `Electricity` leak is near the fuel patch.
- One powered `DoorProp` can contain surface spread if operated before electricity/fuel overlap grows.
- Required consumers: one `Fuel`, one `Electricity`.
Timeline:
1. First choice A: enable `Electricity`; `Pulse` may create `Heat` if leaked values interact.
2. First choice B: close or open a powered `DoorProp` to change surface propagation before energizing.
3. First choice C: isolate or repair the `Fuel` leak first.
4. Player prevents or contains `Ignite`, then restores both required services.
5. Player uses `ActivateReactor`.
Win: `ReactorReadiness` is true for `Fuel` and `Electricity`, required consumers are `Enabled` and `Producing`, then `ActivateReactor`.
Lose: terminal `Heat` or `UnsafeEntryLoss` from `Unsafe` cells.
ImplementationNotes:
- Door mechanics are centered on electricity supply: the `DoorProp` can only toggle while powered.
- If an `AllSeeingEyeTerminal` is present, its `Forecast` is optional and terminal-local.
### 4-3 - Hot Bypass
Purpose: Combine structural integrity, route choice, and fire risk.
Setup:
- Networks: `Fuel`, `Electricity`.
- Two visible branches can satisfy each required consumer set.
- One high-voltage branch threatens structural degradation near a `Fuel` leak path.
- One `DoorProp` can contain `Heat` or `LeakedElectricity` propagation if powered and closed.
- Required consumers: one `Fuel`, one `Electricity`.
- One `HeatShield` supply is available for movement safety.
Timeline:
1. First choice A: route `Electricity` through the high-voltage branch; `Pulse` may create a new wall leak by fixed integrity rules.
2. First choice B: isolate the `Fuel` bypass; `Pulse` reduces ignition risk but changes available production path.
3. First choice C: pick up `HeatShield`; `Pulse` improves future movement safety but does not solve the system.
4. Player chooses one stable production route per network.
5. Player uses `ActivateReactor`.
Win: `ReactorReadiness` is true for `Fuel` and `Electricity`, required consumers are `Enabled` and `Producing`, then `ActivateReactor`.
Lose: terminal `Heat`, `UnsafeEntryLoss` from `Unsafe` cells, or fixed terminal failure.
ImplementationNotes:
- `HeatShield` only affects movement safety; it does not quench `Heat`.
- Avoid perfect cleanup requirements.
## Group 5: `Coolant` + `Electricity`
Group 5 teaches that `SprinklerWater` is useful system material but dangerous with `Electricity`. `Evaporation` becomes important without adding a wait command.
### 5-1 - Charged Water
Purpose: Teach wet-electricity `Conduct`.
Setup:
- Networks: `Coolant`, `Electricity`.
- One wall-mounted `CoolantSprinklerValve` starts discharging because its linked `SprinklerControlProp` starts `Enabled`.
- Initial `SprinklerWater` is visible in a corridor between the sprinkler outlet and an electricity emission face.
- One wall `Electricity` leak emits toward that corridor.
- Required consumers: one `Coolant`, one `Electricity`.
- One `IsolationValveProp` can cut voltage to the leak branch.
Timeline:
1. First choice A: disable the linked `SprinklerControlProp`; `Pulse` stops new `SprinklerWater` and allows pressure recovery.
2. First choice B: isolate or repair the `Electricity` leak first; `Pulse` reduces wet-conduction risk before drying.
3. First choice C: energize electricity production first; `Pulse` shows why wet cells and `LeakedElectricity` are a bad combination.
4. If `LeakedElectricity` reaches wet cells, fixed rules use `Conduct`.
5. Player restores both required services and activates.
Win: `ReactorReadiness` is true for `Coolant` and `Electricity`, required consumers are `Enabled` and `Producing`, then `ActivateReactor`.
Lose: `UnsafeEntryLoss` from `Unsafe` wet-electric cells or fixed terminal failure.
ImplementationNotes:
- No authored forecast prose; terminal-local systemic `Forecast` may warn about wet conduction if a powered terminal exists.
- Do not present sprinkler activation as the intended first solution; the problem starts from an already wet corridor.
### 5-2 - Dry Before Live
Purpose: Teach that `Evaporation` can be part of useful action sequencing.
Setup:
- Networks: `Coolant`, `Electricity`.
- Initial `SprinklerWater` exists on a floor route near an `Electricity` leak face.
- A wall-mounted `CoolantSprinklerValve` maintains wetness and pressure debt because its linked `SprinklerControlProp` starts `Enabled`.
- A powered `DoorProp` can contain electrical surface propagation.
- Required consumers: one `Coolant`, one `Electricity`.
Timeline:
1. First choice A: disable the linked `SprinklerControlProp`; `Pulse` stops new `SprinklerWater` and allows `Evaporation`.
2. First choice B: close an upstream `IsolationValveProp`; `Pulse` cuts sprinkler supply and changes coolant pressure.
3. First choice C: power and operate the `DoorProp` before energizing the leak branch.
4. Player uses meaningful network actions while `Evaporation` reduces wetness.
5. Player energizes safely, restores both services, and activates.
Win: `ReactorReadiness` is true for `Coolant` and `Electricity`, required consumers are `Enabled` and `Producing`, then `ActivateReactor`.
Lose: `UnsafeEntryLoss` from `Unsafe` wet-electric cells or fixed terminal failure.
ImplementationNotes:
- Do not add a wait or fast-forward action.
- Do not author standalone `Heat`; drying comes from `Evaporation` rules during pulses.
### 5-3 - Eye In The Storm
Purpose: Combine `AllSeeingEyeTerminal` with wet-conduction routing.
Setup:
- Networks: `Coolant`, `Electricity`.
- One `AllSeeingEyeTerminal` is reachable but must be powered before activation reveals `Forecast`.
- Two wall-mounted `CoolantSprinklerValve` instances affect different outlet faces through linked `SprinklerControlProp` instances.
- One `Electricity` route overlaps one wettable corridor underground.
- Required consumers: one `Coolant`, one `Electricity`.
Timeline:
1. First choice A: power the `AllSeeingEyeTerminal` branch; a later terminal interaction enables terminal-local visibility and systemic `Forecast`.
2. First choice B: disable the nearest `SprinklerControlProp` or isolate its coolant supply without terminal information.
3. First choice C: isolate electricity before changing sprinkler state.
4. Player uses terminal-local information to avoid wet conduction and restore required production.
5. Player uses `ActivateReactor`.
Win: `ReactorReadiness` is true for `Coolant` and `Electricity`, required consumers are `Enabled` and `Producing`, then `ActivateReactor`.
Lose: `UnsafeEntryLoss` from `Unsafe` wet-electric cells, or fixed terminal failure.
ImplementationNotes:
- Terminal should reveal why one sprinkler/electric route combination is riskier.
- `Forecast` remains systemic and terminal-local.
## Group 6: Full Startup
Group 6 uses `Fuel`, `Coolant`, and `Electricity`. `Heat` can arise from `Fuel` + `Electricity`; `CoolantSprinklerValve` and `SprinklerWater` provide suppression while creating pressure and wet-conduction tradeoffs.
### 6-1 - Three-Key Start
Purpose: First all-network level with one readable problem per network.
Setup:
- Networks: `Fuel`, `Coolant`, `Electricity`.
- `ReactorControlProp` sits on all three networks.
- Required consumers: one `Fuel`, one `Coolant`, one `Electricity`.
- One visible `Fuel` leak, one wall-mounted `CoolantSprinklerValve` whose linked `SprinklerControlProp` starts `Enabled`, and one wall `Electricity` leak.
- One optional `AllSeeingEyeTerminal` is off the direct route and requires positive `Electricity` flow.
- One `HeatShield` and one relevant `RemedySupplyProp` are available for short-term mitigation.
Timeline:
1. First choice A: isolate or repair the `Fuel` leak.
2. First choice B: disable the linked `SprinklerControlProp` or isolate the sprinkler branch to restore pressure.
3. First choice C: isolate or repair the `Electricity` leak before wet cells conduct.
4. First choice D: power the `AllSeeingEyeTerminal` branch for later terminal-local `Forecast`.
5. First choice E: pick up `HeatShield` or a `RemedySupplyProp` to buy access to one risky intervention.
6. Player restores required production for all three carriers.
7. Player uses `ActivateReactor`.
Win: `ReactorReadiness` is true for all three carriers, one required `ConsumerProp` per carrier is `Enabled` and `Producing`, then `ActivateReactor`.
Lose: terminal `Heat`, `UnsafeEntryLoss` from `Unsafe` cells, wet-electric terminal failure, or other fixed terminal failure.
ImplementationNotes:
- Keep this forgiving and compact.
- Do not require hazard cleanup after `Ready`.
- Inventory mitigation should help with access or timing, not replace network repair and routing.
### 6-2 - Cascade Lockout
Purpose: Require ordering because one fix can worsen another surface interaction.
Setup:
- Networks: `Fuel`, `Coolant`, `Electricity`.
- A `Fuel` leak can meet an `Electricity` leak and `Ignite`.
- A wall-mounted `CoolantSprinklerValve` can quench or dilute but creates `SprinklerWater` while its linked `SprinklerControlProp` is `Enabled`.
- A powered `DoorProp` can contain one surface spread path.
- One `AllSeeingEyeTerminal` is reachable and requires positive `Electricity` flow.
- Required consumers: one per carrier.
Timeline:
1. First choice A: use the `SprinklerControlProp` to suppress/dilute; `Pulse` creates wet-conduction risk.
2. First choice B: isolate electricity before using sprinkler; `Pulse` delays electricity production.
3. First choice C: power and toggle the `DoorProp` to change containment.
4. First choice D: power the `AllSeeingEyeTerminal` branch for later systemic `Forecast`.
5. Player chooses containment, suppression, isolation, and production order.
6. Player uses `ActivateReactor`.
Win: `ReactorReadiness` is true for all three carriers, one required `ConsumerProp` per carrier is `Enabled` and `Producing`, then `ActivateReactor`.
Lose: terminal `Heat`, `UnsafeEntryLoss` from `Unsafe` cells, wet-electric terminal failure, or other fixed terminal failure.
ImplementationNotes:
- This is the main test of previous group lessons in one compact level.
- Door operation must depend on local `Electricity` supply.
### 6-3 - Critical Path
Purpose: Final capstone with multiple valid solution orders.
Setup:
- Networks: `Fuel`, `Coolant`, `Electricity`.
- `ReactorControlProp` sits on all three networks.
- Two `ConsumerProp` instances per carrier exist; one per carrier is required.
- One `Fuel` leak, one wall-mounted `CoolantSprinklerValve` whose linked `SprinklerControlProp` starts `Enabled`, one wall `Electricity` leak, one powered `DoorProp`, and one weakened structural segment.
- One powered-route `AllSeeingEyeTerminal`, one `HeatShield`, and one relevant `RemedySupplyProp` are available.
Timeline:
1. First choice A: power the `AllSeeingEyeTerminal` branch; a later terminal interaction grants terminal-local topology and `Forecast` while hazards advance.
2. First choice B: isolate or repair the `Fuel` leak before `Ignite`.
3. First choice C: disable the linked `SprinklerControlProp` or isolate the sprinkler before energizing wet areas.
4. First choice D: isolate or repair electricity before using sprinkler suppression.
5. First choice E: pick up `HeatShield` or a `RemedySupplyProp` for movement/access safety.
6. Player selects a minimum viable producing consumer set and stabilizes the most terminal interaction.
7. Player uses `ActivateReactor` as soon as `Ready` appears.
Win: `ReactorReadiness` is true for all three carriers, one required `ConsumerProp` per carrier is `Enabled` and `Producing`, then `ActivateReactor`.
Lose: terminal `Heat`, `UnsafeEntryLoss` from `Unsafe` cells, wet-electric terminal failure, or other fixed terminal failure.
ImplementationNotes:
- Winning with controlled remaining hazards is valid.
- `Forecast` output should come only from fixed systemic rules while at the active and powered terminal.
## Implementation Checklist
- Add campaign manifest entries in this document order once level data exists.
- Use stable level names because save data and UI can reference them.
- Implement one tutorial level first, then one representative level from each group before filling every group.
- Prioritize mechanics in this order:
1. `Pulse` playback and `ReactorReadiness`.
2. `Fuel` source, leak, repair, isolation, and `ActivateReactor`.
3. `Coolant` `ConsumerProp`, `SprinklerControlProp`, wall-mounted `CoolantSprinklerValve`, pressure drop, and `Evaporation`.
4. `Electricity` wall leaks, `UnsafeEntryLoss`, powered `DoorProp`.
5. Powered `AllSeeingEyeTerminal` and terminal-local systemic `Forecast`.
6. `Ignite`, `Heat`, `Dilute`, `Quench`, and wet-electric `Conduct`.
7. Full campaign content pass.
- If scope tightens, ship tutorial, Group 1, one level each from Groups 2-5, and one final Group 6 level.
## Test Expectations
Campaign implementation should add tests for:
- every level loading and validating,
- required consumer counts matching intended group mechanics,
- tutorial solvable with exactly one `LengthyAction` before `ActivateReactor`,
- every non-tutorial level exposing at least two valid first `LengthyAction` choices,
- win criteria reachable for each authored level,
- no level requiring a wait or fast-forward command,
- no `Forecast` visible away from an active and powered `AllSeeingEyeTerminal`,
- unpowered `DoorProp` and `AllSeeingEyeTerminal` interactions causing no state change while still triggering a `Pulse`,
- no direct interaction with `CoolantSprinklerValve`; sprinkler changes happen through linked `SprinklerControlProp`,
- no pulse-only stationary robot hazard loss,
- campaign manifest order matching this document.

367
docs/UX.md Normal file
View File

@@ -0,0 +1,367 @@
# Reactor Maintenance UX Blueprint
This document is the Godot implementation blueprint for the player-facing frontend. It describes scene responsibilities, control composition, navigation flow, and the minimum data concepts needed to populate the UI. It is not a final art direction document.
## UX Goals
- Put the level grid and current reactor state at the center of play.
- Make every simulation state readable before the player commits to a lengthy action.
- Keep menu flow short: splash, main menu, mode selection, level, outcome, next action.
- Treat campaign as a linear chain of handcrafted levels with names and short flavor text.
- Let players retry a lost level from its starting state and advance from a won level to the next campaign level.
- Keep one-action setups in guided tutorial content; campaign levels should present at least two plausible lengthy interventions with different pulse outcomes.
## Scene Flow
```text
SplashScreen
-> MainMenu
MainMenu
-> New Campaign -> CampaignIntro or LevelScreen
-> Continue Campaign -> LevelScreen
-> Play Random Level -> GenerationScreen -> LevelScreen
-> OptionsScreen
-> TutorialScreen
LevelScreen
-> LoseOverlay -> Retry Level | MainMenu
-> WinOverlay -> Next Level | MainMenu
-> GameOverScreen
-> GameWonScreen
GameOverScreen
-> Retry Current Level | MainMenu
GameWonScreen
-> MainMenu
```
Use one scene navigation service or autoload to own transitions. Individual screens emit intent signals such as `NewCampaignRequested`, `RetryRequested`, or `NextLevelRequested`; they should not load unrelated scenes directly.
## Shared Runtime Concepts
### Campaign Manifest
Represent campaign content as an ordered list. The exact storage format can be a Godot `Resource`, JSON file, or C# model, but the UI should receive these fields:
- `Id`: stable level identifier.
- `Name`: player-facing level name.
- `FlavorText`: one or two short lines of lore shown before the level starts and after menu selection.
- `LevelPath`: path or key used to load serialized simulation level data.
The first implementation can ship a single manifest for the default campaign. Branching paths, unlock maps, and difficulty variants are out of scope.
### Session State
The frontend session should track:
- current mode: campaign, random level, or tutorial,
- current campaign index when in campaign mode,
- current level start snapshot for retry,
- current mutable level state during play,
- whether a continue save exists,
- last outcome: won, lost, campaign complete, or abandoned.
Retry always restores the current level start snapshot. Continue campaign resumes the last saved campaign index and mutable level state if available.
## Scene Blueprints
### SplashScreen.tscn
Purpose: a short branded entry point before the main menu.
Recommended root: `Control`.
Controls:
- `CenterContainer`
- `VBoxContainer`
- title label: `Reactor Maintenance`
- small status label for build/version text if available
Behavior:
- Show for roughly 1-2 seconds.
- Any confirm, cancel, click, or tap skips immediately to `MainMenu`.
- Do not block on loading campaign data unless a clear loading label is shown.
### MainMenu.tscn
Purpose: primary navigation.
Recommended root: `Control`.
Controls:
- title label: `Reactor Maintenance`
- vertical command stack:
- `New Campaign`
- `Continue Campaign`
- `Play Random Level`
- `Options`
- `Tutorial`
- small footer for version or build label
Behavior:
- `Continue Campaign` is disabled when no continue state exists.
- `New Campaign` starts at campaign index `0`; if a save exists, confirm replacement before overwriting.
- `Play Random Level` loads a single non-campaign level. It can select from authored levels until random generation exists.
- `Options` and `Tutorial` return to this screen.
### CampaignIntro.tscn
Purpose: optional lightweight introduction before the first campaign level or before each level.
Recommended root: `Control`.
Controls:
- level name label
- flavor text label
- `Begin` button
- `Back` button when entered from a menu
Behavior:
- For a compact first pass, this may be embedded as a modal panel inside `LevelScreen`.
- The text should be short enough to read quickly and should not explain mechanics that belong in tutorial content.
### LevelScreen.tscn
Purpose: core gameplay screen.
Recommended root: `Control`.
Suggested layout:
```text
LevelScreen
MarginContainer
VBoxContainer
LevelHeader
HSplitContainer
GridViewport
InspectorPanel
ActionBar
OverlayLayer
```
Primary controls:
- `LevelHeader`
- level name
- campaign progress label such as `Level 2 / 8`
- global state badge: `Stable`, `Caution`, `Critical`, `Ready`
- reactor heat or readiness summary
- `GridViewport`
- visual grid
- robot marker
- terrain, props, leaks, hazards, and optional underground overlay
- selected cell highlight and reachable/actionable hints
- `InspectorPanel`
- selected cell coordinates
- terrain and prop summary
- underground values when visible through terminal access
- visible hazards, sprinkler water, and wet-electricity risk with safe/caution/critical bands
- forecast warnings
- `ActionBar`
- contextual action buttons: move, interact, toggle isolation valve, activate sprinkler valve, repair, remedy, heat shield, activate reactor
- disabled buttons must show why the action is unavailable
- `InventoryPanel`
- fuel neutralizer count
- coolant neutralizer count
- electricity neutralizer count
- heat shield count
Behavior:
- Selection and inspection are quick actions and must not trigger a pulse.
- Movement is a quick action and must update robot position without triggering a pulse.
- Interact, isolation valve toggles, sprinkler valve activation, repair, remedy, and heat shield are environment-changing actions and must trigger one animated pulse when accepted.
- Activate reactor must be visually prominent when ready and should resolve immediately without requiring a wait or extra pulse.
- Invalid actions should produce a short refusal message and not mutate level state.
- When the level state becomes `Lost`, show `LoseOverlay`.
- When the level state becomes `Won`, show `WinOverlay`.
- When the reactor is ready, make `Activate Reactor` visually prominent but keep other valid actions available.
- Before committing a lengthy action, the selected action state should expose the forecasted pulse consequence in plain terms such as isolated leak, restored pressure, downstream starvation, hazard growth, or reactor ready.
Pulse playback behavior:
- During a pulse, animate the configured steps as one short cascade of hazard motion, leak growth, quenching, evaporation, ignition, electrical conduction, and readiness updates.
- Isolation valve toggles must visibly mark the affected underground branch, show stopped or restored pressure flow, and warn when downstream consumers or reactor feed will starve.
- Sprinkler valve activation must visibly release blue sprinkler water at authored outlet cells and show the affected coolant service area losing pressure.
- Evaporation must be communicated through shrinking wet overlays and stronger steam/cooling feedback on hot cells.
- Wet cells that can spread electricity must use a distinct warning treatment so the player can distinguish helpful suppression water from dangerous electrified water.
- Disable conflicting action commands while pulse playback is running so the player cannot queue hidden actions into an unresolved environment state.
- Present the final post-pulse state as the next decision point.
- Do not make pulse length vary by action type, forecast outcome, or danger level.
- Do not expose a normal campaign fast-forward loop; the player goal is to solve the cascade and activate the reactor as soon as a pulse leaves its needs fulfilled.
### LoseOverlay.tscn
Purpose: immediate failure response while preserving context.
Recommended root: `PanelContainer` or `CanvasLayer` child.
Controls:
- title label: `Level Lost`
- short cause label from the terminal state or latest failure message
- `Retry Level`
- `Main Menu`
Behavior:
- Pauses input to the grid behind it.
- `Retry Level` reloads the current level from the start snapshot.
- `Main Menu` abandons the current run after confirmation if unsaved progress would be lost.
### WinOverlay.tscn
Purpose: immediate success response and transition to the next level.
Recommended root: `PanelContainer` or `CanvasLayer` child.
Controls:
- title label: `Reactor Online`
- level completion text
- `Next Level` when another campaign level exists
- `Finish Campaign` when this was the final campaign level
- `Main Menu`
Behavior:
- Saves campaign progress before presenting the next-level command.
- `Next Level` loads the next handcrafted campaign level and shows its name and flavor text.
- In random level mode, replace `Next Level` with `Play Another` or `Main Menu`.
### GameOverScreen.tscn
Purpose: full-screen campaign failure or abandoned-run endpoint.
Recommended root: `Control`.
Controls:
- title label: `Game Over`
- summary label with the failed level name
- `Retry Current Level`
- `Main Menu`
Behavior:
- Use this when leaving the level context after a loss, not for the immediate in-level failure modal.
- Retry uses the same start snapshot rules as `LoseOverlay`.
### GameWonScreen.tscn
Purpose: full-screen campaign completion endpoint.
Recommended root: `Control`.
Controls:
- title label: `Campaign Complete`
- short final lore text
- completed level count
- `Main Menu`
Behavior:
- Reached after winning the final handcrafted campaign level.
- Clears any in-progress continue marker or marks campaign complete.
### OptionsScreen.tscn
Purpose: global settings.
Recommended root: `Control`.
Controls:
- audio sliders
- fullscreen toggle
- UI scale selector if supported
- color/accessibility toggles when implemented
- `Back`
Behavior:
- Changes apply immediately where practical.
- Persist settings separately from campaign progress.
### TutorialScreen.tscn
Purpose: teach interaction grammar before or outside campaign.
Recommended root: `Control`.
Controls:
- tutorial topic list
- content panel
- `Start Tutorial Level`
- `Back`
Behavior:
- Keep tutorial text focused on action economy, pulses, isolation valves, sprinkler suppression, evaporation, wet-electricity hazards, forecasts, remedies, and reactor activation.
- Tutorial level can reuse `LevelScreen` with a tutorial mode flag and guided prompts.
- A tutorial level may demonstrate a single safe lengthy action. The first campaign level should not be a single-action demonstration; it should ask the player to choose between service restoration, isolation, repair, or activation timing.
## Reusable Controls
- `PrimaryButton`: consistent command button style and focus behavior.
- `StateBadge`: color-coded label for `Stable`, `Caution`, `Critical`, `Ready`, `Lost`, and `Won`.
- `LevelHeader`: level metadata, global state, and campaign progress.
- `CellInspector`: selected cell details, visible hazards, sprinkler water, wet-electricity risk, prop state, isolation valve branch impact, underground details when available.
- `ForecastList`: ordered warnings from soonest to latest pulse.
- `InventoryStrip`: remedy and heat shield counts.
- `OutcomeOverlay`: shared base layout for win and loss overlays.
- `ConfirmDialog`: save overwrite, abandon run, and return-to-menu confirmations.
Keep repeated controls as separate scenes so the level screen, tutorial, and overlays can reuse them without duplicating layout logic.
## Input And Focus
- Support mouse and keyboard from the first implementation.
- Confirm activates the focused button or selected contextual action.
- Cancel closes overlays or returns to the previous screen when safe.
- Arrow keys or WASD can move the robot when the level grid has focus.
- Tab cycles between grid, inspector, action bar, and menu buttons.
- Disabled commands must remain inspectable by focus or hover so the player can read why they are unavailable.
## Visual Language
- The game should feel technical, readable, and tense, not decorative.
- Use restrained panels and clear hierarchy: grid first, status second, supporting details third.
- Color roles:
- fuel: red
- coolant/sprinkler water: blue
- electricity: yellow
- heat: orange or white-hot accent
- isolated branch: muted carrier color with a broken-flow or valve marker
- wet-electricity risk: yellow over blue or a distinct charged-water pattern
- safe: neutral or green
- caution: amber
- critical/lost: red
- ready/won: bright green or white
- Do not rely on color alone; pair colored states with text labels or icons.
## Implementation Acceptance Checklist
- Every scene has one clear root responsibility and emits navigation intent instead of directly owning campaign policy.
- The main menu exposes all requested buttons.
- Campaign levels show `Name` and `FlavorText`.
- Lost levels can be retried from the original starting snapshot.
- Won levels can advance to the next campaign level.
- Final campaign win reaches `GameWonScreen`.
- Full failure endpoint reaches `GameOverScreen`.
- The level screen distinguishes quick actions from lengthy actions.
- The level screen previews selected lengthy-action pulse consequences before commitment.
- The level screen makes isolation valve branch effects, sprinkler valve outlets, evaporation, and wet-electricity risk readable before and after a pulse.
- Forecasts, inventory, selected cell inspection, and global level state have dedicated UI space.
- Documentation stays aligned with `docs/design.md` when simulation rules change.

File diff suppressed because it is too large Load Diff

13
dotnet-tools.json Normal file
View File

@@ -0,0 +1,13 @@
{
"version": 1,
"isRoot": true,
"tools": {
"jetbrains.resharper.globaltools": {
"version": "2026.1.1",
"commands": [
"jb"
],
"rollForward": false
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

View File

@@ -0,0 +1,22 @@
{"out":"01-splash-screen.png","use_case":"stylized-concept","size":"1536x1024","quality":"medium","prompt":"Create a mood image for the Godot SplashScreen of Reactor Maintenance. Use the art bible: modern elegant stylized playful sci-fi, scrappy industrial reactor-maintenance tone, readable top-down 2D game art language, crisp simplified shapes, selective dark outlines, restrained lighting, cool graphite and blue-gray steel base with semantic accents, texture through hue variation rather than luminance noise. Scene: a short branded entry screen, quiet reactor control bay floor plates seen from top-down/isometric-flat 2D, compact maintenance robot silhouette near a dormant reactor emblem, one calm pulsing green-white status light. Composition: centered title-space mood without legible title text, clean negative space, first-screen polish. Avoid photorealism, noisy texture, tiny mechanical clutter, heavy bloom, CRT, cyberpunk neon overload, text, watermark."}
{"out":"02-main-menu.png","use_case":"ui-mockup","size":"1536x1024","quality":"medium","prompt":"Create a mood image for the Godot MainMenu of Reactor Maintenance. Apply the full art bible: readable stylized 2D sci-fi, scrappy industrial, crisp simplified panels, selective dark outlines, cool graphite steel, hue-varied worn paint, semantic accents only. Screen concept: elegant main menu over a top-down reactor maintenance floor, vertical command stack area suggested with clean button silhouettes but no readable text, compact reactor core icon motif, muted pipe routes in background, playful but functional. Composition: 16:9 game UI mockup, restrained panels, readable hierarchy, menu commands prominent. Avoid text, watermark, photorealism, noisy grime, tiny wires everywhere, heavy glow."}
{"out":"03-campaign-intro.png","use_case":"ui-mockup","size":"1536x1024","quality":"medium","prompt":"Create a mood image for CampaignIntro scene in Reactor Maintenance. Use the art bible style: modern elegant stylized playful sci-fi, scrappy industrial reactor-maintenance, clean silhouettes, top-down tactical asset language, cool steel and graphite with restrained teal and amber accents, hue variation for worn panels. Screen: level briefing panel with level name/flavor text areas represented as abstract unreadable blocks, small handcrafted level map thumbnail, worn maintenance docket feel without paper fantasy, Begin/Back button silhouettes. Mood: lore-rich but compact, industrial and approachable. Avoid readable text, watermark, photorealism, dense clutter, high-frequency scratches."}
{"out":"04-generation-screen-random-level.png","use_case":"ui-mockup","size":"1536x1024","quality":"medium","prompt":"Create a mood image for the GenerationScreen / Play Random Level loading screen of Reactor Maintenance. Art bible: crisp stylized 2D, scrappy industrial sci-fi, selective dark outlines, cool graphite/blue-gray steel, semantic red/cyan/yellow/orange/green accents, hue-based texture only. Screen: procedural level assembly mood, modular tiles and thick pipe route fragments sliding into a grid, small maintenance robot waiting at edge, compact progress panel with abstract unreadable glyph rows. Composition: clear center grid construction, minimal animation implication, not decorative. Avoid readable text, watermark, noisy texture, neon cyberpunk, heavy bloom."}
{"out":"05-options-screen.png","use_case":"ui-mockup","size":"1536x1024","quality":"medium","prompt":"Create a mood image for the OptionsScreen of Reactor Maintenance. Follow art bible: modern elegant stylized playful sci-fi with scrappy industrial reactor-maintenance tone, crisp simplified UI panels, cool steel/graphite palette, hue variation not noise, selective outlines. Screen: utilitarian settings panel with audio sliders, fullscreen toggle, UI scale selector, accessibility toggles, and Back button represented visually without readable text. Style: quiet operational UI, dense but organized, no marketing hero, no nested decorative cards. Avoid text, watermark, photorealism, noisy grime, tiny details."}
{"out":"06-tutorial-screen.png","use_case":"ui-mockup","size":"1536x1024","quality":"medium","prompt":"Create a mood image for TutorialScreen in Reactor Maintenance. Use the art bible style: readable stylized 2D, modern elegant playful sci-fi, scrappy industrial, restrained lighting, graphite/steel base, semantic accents, hue-varied wear, clean silhouettes. Screen: tutorial topic list beside a content panel, small top-down grid vignette demonstrating robot movement, hazard remedy, forecast, and reactor activation through icons and abstract diagrams, no readable text. Composition: teach interaction grammar clearly, practical UI for mouse/keyboard. Avoid text, watermark, photorealism, clutter, tiny unreadable icon noise."}
{"out":"07-game-over-screen.png","use_case":"ui-mockup","size":"1536x1024","quality":"medium","prompt":"Create a mood image for GameOverScreen in Reactor Maintenance. Art bible: stylized top-down sci-fi, scrappy industrial, clean silhouettes, cool graphite/blue-gray steel, selective dark outlines, hue variation over luminance noise. Screen: full-screen campaign failure endpoint, darkened reactor floor, one failed level map silhouette, restrained red critical accents and orange heat support, retry and main menu button shapes without readable text. Mood: tense but not horror, readable and elegant. Avoid gore, photorealism, heavy smoke, noisy texture, text, watermark."}
{"out":"08-game-won-screen.png","use_case":"ui-mockup","size":"1536x1024","quality":"medium","prompt":"Create a mood image for GameWonScreen in Reactor Maintenance. Use art bible: modern elegant stylized playable sci-fi, scrappy industrial but hopeful, crisp 2D shapes, selective outlines, cool steel/graphite base, green-white ready/won accents, hue-shifted worn metal. Screen: campaign completion endpoint, reactor core safely online, repaired pipe networks softly organized, final lore panel and completed-count area represented as abstract unreadable blocks, single main menu button shape. Avoid text, watermark, photorealism, noisy texture, excessive glow."}
{"out":"09-lose-overlay.png","use_case":"ui-mockup","size":"1536x1024","quality":"medium","prompt":"Create a mood image for LoseOverlay in Reactor Maintenance. Apply art bible: crisp stylized 2D, scrappy industrial reactor UI, cool steel graphite palette, semantic critical red/orange accents, selective outlines, hue variation not noisy luminance. Screen: level grid visible dimmed behind immediate failure modal, modal has cause indicator area, Retry Level and Main Menu button silhouettes, clear focus, paused grid input. Mood: urgent but clean, no horror. Avoid readable text, watermark, photorealism, dense debris, heavy bloom."}
{"out":"10-win-overlay.png","use_case":"ui-mockup","size":"1536x1024","quality":"medium","prompt":"Create a mood image for WinOverlay in Reactor Maintenance. Art bible: modern elegant stylized sci-fi, scrappy industrial but repaired, crisp simplified UI, selective dark outlines, cool steel/graphite with bright green-white ready accents. Screen: level grid dimmed behind success modal, reactor online state, next level / finish campaign / main menu button silhouettes without readable text, compact completion text area represented abstractly. Mood: satisfying, functional, not celebratory fireworks. Avoid text, watermark, photorealism, excessive glow, noisy texture."}
{"out":"11-reusable-controls-kit.png","use_case":"ui-mockup","size":"1536x1024","quality":"medium","prompt":"Create a mood board image for reusable Godot controls in Reactor Maintenance: PrimaryButton, StateBadge, LevelHeader, CellInspector, ForecastList, InventoryStrip, OutcomeOverlay, ConfirmDialog. Use art bible: stylized readable 2D sci-fi, scrappy industrial, crisp simplified panels, selective outlines, cool graphite and blue-gray steel with semantic red/cyan/yellow/orange/green accents, hue variation instead of noisy texture. Composition: organized UI component sheet with visual examples but no readable text, button states, badges, inventory icons, forecast rows. Avoid text labels, watermark, photorealism, high-frequency detail."}
{"out":"12-level-surface-standard.png","use_case":"stylized-concept","size":"2048x1152","quality":"medium","prompt":"Create a detailed mood image for LevelScreen surface gameplay in Reactor Maintenance. Use the art bible exactly: readable top-down tactical 2D game art, modern elegant stylized playful sci-fi, scrappy industrial reactor-maintenance, crisp simplified shapes, selective dark outlines, cool graphite/blue-gray steel base, semantic fuel red/coolant cyan/electric yellow/heat orange/ready green accents, hue-variation texture not luminance noise. Scene: full Godot LevelScreen layout with top header, central grid viewport, right inspector panel, bottom action bar, inventory strip. Grid shows floor plates, walls, thick pipes, robot marker, control terminals, cooling pump, generator, pressure regulator, reactor control. UI has abstract blocks/icons, no readable text. Composition: playable, readable, dense but organized. Avoid photorealism, noisy grime, tiny wires, text, watermark."}
{"out":"13-level-surface-critical.png","use_case":"stylized-concept","size":"2048x1152","quality":"medium","prompt":"Create a mood image for a critical LevelScreen moment in Reactor Maintenance. Art bible: readable top-down stylized 2D, scrappy industrial sci-fi, clear silhouettes, selective outlines, cool graphite steel, hue-based wear, semantic accents. Scene: same Godot LevelScreen layout with grid, inspector, action bar. Gameplay state: reactor near critical, fuel slick red pooling, coolant spill cyan, electric arcs yellow, heat patches orange-white, robot positioned near a valve and terminal, forecast warnings in side panel as icon rows, Activate Reactor command visually prominent but other actions present. Keep hazards immediately distinct and large-shaped, not noisy. Avoid readable text, watermark, photorealism, heavy bloom, chaotic debris."}
{"out":"14-level-underground-network.png","use_case":"stylized-concept","size":"2048x1152","quality":"medium","prompt":"Create a mood image for the underground network view of Reactor Maintenance LevelScreen. Use art bible: crisp stylized top-down 2D, scrappy industrial reactor maintenance, cool graphite/steel, semantic route colors, hue variation not noise, selective dark outlines. Scene: beneath-floor pipe and conduit layer shown as readable thick continuous routes: fuel pipes with red bands, coolant pipes with cyan-blue bands, electricity conduits with yellow insulated housings. Include junctions, valves, leaks, breaks, source nodes, and terminal access points with oversized state lights. Composition: gameplay map view, clear route logic, no tiny pipe bundles. Avoid text, watermark, photorealism, tangled clutter, high-frequency scratches."}
{"out":"15-level-surface-underground-split.png","use_case":"stylized-concept","size":"2048x1152","quality":"medium","prompt":"Create a mood image for LevelScreen showing combined surface and underground awareness in Reactor Maintenance. Art bible: modern elegant stylized 2D sci-fi, scrappy industrial, readable top-down tactical, selective outlines, restrained lighting, cool steel/graphite, hue-varied texture, semantic accents. Scene: central grid uses a surface/underground split view: left half normal floor with robot, props and hazards; right half ghosted cutaway under-floor pipe/conduit network, with a terminal creating an all-seeing scan cone. Include inspector panel highlighting both surface hazards and underground values as abstract icon rows. Avoid readable text, watermark, photorealism, clutter, excessive glow."}
{"out":"16-all-seeing-eye-terminal-interface.png","use_case":"ui-mockup","size":"2048x1152","quality":"medium","prompt":"Create a mood image for the all-seeing-eye interface in Reactor Maintenance, a Godot LevelScreen terminal mode that reveals underground systems. Use art bible: elegant stylized playful sci-fi, scrappy industrial reactor maintenance, crisp simplified shapes, selective dark outlines, cool graphite/blue-gray steel, muted teal/purple-blue secondary diagnostic accents allowed, hue variation over luminance noise. Screen: top-down grid background with a central diagnostic terminal icon shaped like a practical industrial eye/scanner, radial scan overlay revealing hidden pipes, leaks, pressure values, forecast warnings as icon rows, inspector panel with clean diagnostic readouts represented by abstract glyph blocks. Avoid readable text, watermark, CRT scanlines, glitch effects, cyberpunk neon overload, noisy details."}
{"out":"17-all-seeing-eye-overlay-detail.png","use_case":"stylized-concept","size":"1536x1024","quality":"medium","prompt":"Create a close mood image for the all-seeing-eye diagnostic overlay visual language in Reactor Maintenance. Art bible: readable stylized 2D game art, scrappy industrial sci-fi, crisp shapes, functional charm, selective outlines, cool steel/graphite with teal diagnostic light and semantic hazard colors, texture through hue shifts. Subject: a top-down terminal access panel projecting an eye-like scanner overlay onto a grid tile, revealing underground fuel/coolant/electric routes below the surface, with large readable icons and state lights, no readable text. Composition: asset/UI mood reference, focused on overlay treatment and scan aesthetics. Avoid text, watermark, photorealism, heavy bloom, tiny wires, noisy texture."}
{"out":"18-prop-sheet-machinery.png","use_case":"stylized-concept","size":"2048x1152","quality":"medium","prompt":"Create a mood sheet of designed gameplay props for Reactor Maintenance. Use art bible: top-down readable 2D, modern elegant stylized playful sci-fi, scrappy industrial, crisp simplified shapes, selective dark outlines, cool graphite/blue-gray steel, hue-variation wear, semantic accents. Include separate top-down props with padding: control terminal with teal/green screen, diagnostic all-seeing-eye terminal with scanner identity, cooling pump with round tank/impeller and cyan accent, generator with coil/flywheel and yellow electricity accent, pressure regulator with gauge and valve, reactor control with largest authority silhouette and green activation core. Each prop has one obvious interaction feature and large state light. No labels or text. Avoid photorealism, noisy grime, tiny clutter, watermark."}
{"out":"19-hazard-and-failure-sheet.png","use_case":"stylized-concept","size":"2048x1152","quality":"medium","prompt":"Create a mood sheet of hazards and failure visuals for Reactor Maintenance. Follow art bible: stylized readable 2D, clear silhouettes, selective outlines, semantic colors, hue variation rather than luminance noise, no painterly chaos. Include top-down hazard shapes with padding: red fuel slick with smooth pooling, cyan coolant spill with colder edges, angular yellow electric arcs or charged patch, orange-white heat/fire patch, broken pipe leak, ruptured conduit, critical reactor warning tile. Use large readable shapes and quiet interiors for icons/highlights. Avoid text, watermark, photorealism, gore, random speckles, dense debris, heavy bloom."}
{"out":"20-robot-design-sheet.png","use_case":"stylized-concept","size":"1536x1024","quality":"medium","prompt":"Create a mood sheet for the Reactor Maintenance player robot. Art bible: compact practical likable but not comedic, readable top-down body shape, small tool arms or maintenance appendages, visible direction cue, one clear status light, brighter than floor tiles but less saturated than hazards, cool graphite/blue-gray steel body with restrained accent colors, hue-varied worn paint. Include several top-down pose/state variations: idle, moving, interacting with valve, shielded from heat, carrying remedy canister. Crisp stylized 2D, selective outlines, generous padding. Avoid text, watermark, photorealism, noisy texture, tiny mechanical clutter."}
{"out":"21-tile-floor-wall-pipe-kit.png","use_case":"stylized-concept","size":"2048x1152","quality":"medium","prompt":"Create a mood sheet for floor, wall, and pipe tile art in Reactor Maintenance. Use art bible: readable top-down tactical 2D, modern elegant stylized scrappy industrial sci-fi, cool graphite/blue-gray steel, subtle hue shifts between plates, selective outlines, texture by broad hue variation not luminance noise. Include quiet floor plates, rubberized seams, maintenance decking, heavy wall blockers with reinforced ribs, warning trim, thick pipe segments, elbows, junctions, valves, source nodes, leaks and breaks for fuel/coolant/electricity. Keep tiles grid-readable and not decorative. Avoid text, watermark, photorealism, noisy scratches, tiny pipe bundles."}
{"out":"22-level-lore-and-campaign-map.png","use_case":"ui-mockup","size":"1536x1024","quality":"medium","prompt":"Create a mood image for campaign level selection/lore presentation in Reactor Maintenance, supporting the CampaignIntro and campaign chain concept. Apply art bible: stylized readable 2D sci-fi, scrappy industrial reactor maintenance, crisp simplified UI, cool graphite/steel, restrained semantic accents, hue variation texture. Scene: linear chain of handcrafted level nodes inside a maintenance facility schematic, each node represented by icon-only badges and small abstract flavor panels, current level highlighted, completed levels green-white, dangerous future levels amber/red. No readable text. Avoid watermark, photorealism, noisy clutter, neon overload, heavy glow."}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://g8njonkahvns"
path="res://.godot/imported/maintenance_robot.png-77cbb0f8610a91dc82df7b4565ee0a3a.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Assets/Characters/maintenance_robot.png"
dest_files=["res://.godot/imported/maintenance_robot.png-77cbb0f8610a91dc82df7b4565ee0a3a.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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c8ni4xlm0da87"
path="res://.godot/imported/terrain_tilemap.png-c82b5455f5449af21e1ce16efe8292b9.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Assets/Terrain/terrain_tilemap.png"
dest_files=["res://.godot/imported/terrain_tilemap.png-c82b5455f5449af21e1ce16efe8292b9.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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c460u833mc8wd"
path="res://.godot/imported/coolant_icon.png-5f62705f72d18fbac1995d1563570521.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Assets/Ui/coolant_icon.png"
dest_files=["res://.godot/imported/coolant_icon.png-5f62705f72d18fbac1995d1563570521.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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c15hva5fpyqb0"
path="res://.godot/imported/electric_icon.png-f93d44cfb4888519024e3d64de9dbe62.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Assets/Ui/electric_icon.png"
dest_files=["res://.godot/imported/electric_icon.png-f93d44cfb4888519024e3d64de9dbe62.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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://2v0gglovpy7t"
path="res://.godot/imported/fuel_icon.png-8e723683b233f30e6235d28f228f5d83.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Assets/Ui/fuel_icon.png"
dest_files=["res://.godot/imported/fuel_icon.png-8e723683b233f30e6235d28f228f5d83.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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://6lpfwf7pkx0c"
path="res://.godot/imported/heat_shield_icon.png-80a3ecc066be8caa1d77f8459e73bf57.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Assets/Ui/heat_shield_icon.png"
dest_files=["res://.godot/imported/heat_shield_icon.png-80a3ecc066be8caa1d77f8459e73bf57.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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bj45xeusdpf2g"
path="res://.godot/imported/primary_button_accent.png-99d4e7a7f8a491f0ae2203e71d886e65.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Assets/Ui/primary_button_accent.png"
dest_files=["res://.godot/imported/primary_button_accent.png-99d4e7a7f8a491f0ae2203e71d886e65.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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c3v3xokega3lk"
path="res://.godot/imported/scanner_eye_icon.png-003ae6769bc5ea847f0ea3422bbd05b3.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Assets/Ui/scanner_eye_icon.png"
dest_files=["res://.godot/imported/scanner_eye_icon.png-003ae6769bc5ea847f0ea3422bbd05b3.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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bmpb1t3muf78i"
path="res://.godot/imported/state_badge_frame.png-391c9f73136d3046614e3bac00f56da2.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Assets/Ui/state_badge_frame.png"
dest_files=["res://.godot/imported/state_badge_frame.png-391c9f73136d3046614e3bac00f56da2.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

View File

@@ -0,0 +1,8 @@
{"out":"res://Assets/Characters/maintenance_robot.png","model":"gpt-image-1.5","source":"output/imagegen/godot-mood/20-robot-design-sheet.png","prompt":"Create one transparent PNG cutout of the compact Reactor Maintenance player robot from the reference sheet. Keep the same modern elegant stylized playful sci-fi art direction: readable top-down 2D game asset, compact practical likable maintenance robot, cool graphite and blue-gray steel body, one cyan status light, selective dark outline, broad hue-varied wear instead of noisy luminance scratches. Center a single robot with generous transparent padding, no floor, no shadow, no text, no watermark, no UI frame. The robot must read clearly at 128px, 64px, and 32px."}
{"out":"res://Assets/Ui/primary_button_accent.png","model":"gpt-image-1.5","source":"output/imagegen/godot-mood/11-reusable-controls-kit.png","prompt":"Create one transparent PNG cutout for a Reactor Maintenance primary command button accent plate from the reusable controls reference. It should be a compact sci-fi UI button ornament or left icon plate, not a full button with text. Style: modern elegant stylized playful sci-fi, scrappy industrial, cool graphite and blue-gray steel, cyan edge light, selective dark outline, broad hue-varied wear, readable at 44px height. Center it with transparent padding. No text, no watermark, no background, no shadow."}
{"out":"res://Assets/Ui/state_badge_frame.png","model":"gpt-image-1.5","source":"output/imagegen/godot-mood/11-reusable-controls-kit.png","prompt":"Create one transparent PNG cutout for a Reactor Maintenance state badge frame from the reusable controls reference. It should be a small rounded industrial sci-fi badge backing with selective dark outline, cool graphite steel, subtle cyan rim detail, quiet center area for overlaid state text, broad hue variation not noisy scratches. Center with transparent padding. No text, no watermark, no background, no shadow."}
{"out":"res://Assets/Ui/fuel_icon.png","model":"gpt-image-1.5","source":"output/imagegen/godot-mood/11-reusable-controls-kit.png","prompt":"Create one transparent PNG cutout UI inventory icon for fuel neutralizer in Reactor Maintenance. Use the reusable controls reference style and art bible: flat-cleaner UI art, red fuel droplet/container pictogram, compact industrial badge silhouette, selective outline, readable at 24px. Center with transparent padding. No text, no watermark, no background, no shadow."}
{"out":"res://Assets/Ui/coolant_icon.png","model":"gpt-image-1.5","source":"output/imagegen/godot-mood/11-reusable-controls-kit.png","prompt":"Create one transparent PNG cutout UI inventory icon for coolant neutralizer in Reactor Maintenance. Use the reusable controls reference style and art bible: flat-cleaner UI art, cyan-blue droplet/canister pictogram, compact industrial badge silhouette, selective outline, readable at 24px. Center with transparent padding. No text, no watermark, no background, no shadow."}
{"out":"res://Assets/Ui/electric_icon.png","model":"gpt-image-1.5","source":"output/imagegen/godot-mood/11-reusable-controls-kit.png","prompt":"Create one transparent PNG cutout UI inventory icon for electricity neutralizer in Reactor Maintenance. Use the reusable controls reference style and art bible: flat-cleaner UI art, yellow lightning/insulated cell pictogram, compact industrial badge silhouette, selective outline, readable at 24px. Center with transparent padding. No text, no watermark, no background, no shadow."}
{"out":"res://Assets/Ui/heat_shield_icon.png","model":"gpt-image-1.5","source":"output/imagegen/godot-mood/11-reusable-controls-kit.png","prompt":"Create one transparent PNG cutout UI inventory icon for heat shield in Reactor Maintenance. Use the reusable controls reference style and art bible: flat-cleaner UI art, green-white shield with restrained orange heat edge, compact industrial badge silhouette, selective outline, readable at 24px. Center with transparent padding. No text, no watermark, no background, no shadow."}
{"out":"res://Assets/Ui/scanner_eye_icon.png","model":"gpt-image-1.5","source":"output/imagegen/godot-mood/11-reusable-controls-kit.png","prompt":"Create one transparent PNG cutout UI icon for the all-seeing-eye diagnostic scanner in Reactor Maintenance. Use the reusable controls reference style and art bible: flat-cleaner UI art, practical industrial eye/scanner symbol, teal/cyan diagnostic glow with muted graphite casing, selective outline, readable at 24px and 48px. Center with transparent padding. No text, no watermark, no background, no shadow."}

View File

@@ -0,0 +1,107 @@
using Godot;
using ReactorMaintenance.Simulation;
using System.Text;
namespace ReactorMaintenance.Godot.Controls;
public partial class CellInspector : PanelContainer
{
public override void _Ready()
{
var body = new VBoxContainer();
AddChild(body);
var header = new HBoxContainer();
header.AddChild(FrontendAssets.CreateIcon(FrontendAssets.ScannerEyeIcon, new(34, 34)));
header.AddChild(new Label {
Text = "Inspector",
VerticalAlignment = VerticalAlignment.Center
});
body.AddChild(header);
body.AddChild(m_Text);
m_Text.AutowrapMode = TextServer.AutowrapMode.WordSmart;
}
public void SetCellInfo(LevelState? level, GridPosition? position, bool canSeeUnderground)
{
var pos = position ?? new(-1, -1);
var sb = new StringBuilder();
sb.AppendLine($"Selected Cell: {pos.X},{pos.Y}");
if (level is null || position is null || !level.InBounds(pos))
{
m_Text.Text = sb.ToString();
return;
}
var terrain = level.GetTerrain(pos);
var prop = level.GetProp(pos);
var surface = level.GetSurface(pos);
sb.AppendLine($"Terrain: {terrain}");
sb.AppendLine($"Prop: {FormatProp(prop)}");
if (prop.Type != EPropType.None)
sb.AppendLine($"Service: {FormatService(prop)}");
sb.AppendLine($"Surface: {FormatHazards(surface.Fuel, surface.Water, surface.Electricity, surface.Heat)}");
sb.AppendLine(surface.IsUnsafe() ? "Movement: Unsafe" : "Movement: Safe");
if (canSeeUnderground)
sb.AppendLine($"Underground: {FormatUnderground(level, pos)}");
else
sb.AppendLine("Underground: terminal access required");
m_Text.Text = sb.ToString();
}
private static string FormatProp(PropState prop)
{
return prop.Type switch {
EPropType.Flow => $"{prop.Carrier} Flow {prop.SwitchState}",
EPropType.Consumer => $"Consumer {prop.SwitchState}",
EPropType.IsolationValve => $"{prop.Carrier} Valve {(prop.IsOpen ? "Open" : "Closed")}",
EPropType.SprinklerControl => $"Sprinkler Control {prop.SwitchState}",
EPropType.SprinklerValve => $"Sprinkler Valve -> {FormatPosition(prop.OutletPosition)}",
EPropType.Door => $"Door {prop.DoorState}",
EPropType.AllSeeingEyeTerminal => prop.Active ? "All-Seeing-Eye Active" : "All-Seeing-Eye",
EPropType.ReactorControl => $"Reactor Control {prop.ReactorId}",
EPropType.RemedySupply => prop.Depleted ? $"{prop.RemedyType} Empty" : $"{prop.RemedyType} Supply",
_ => prop.Type.ToString()
};
}
private static string FormatService(PropState prop)
{
if (prop.Type == EPropType.Consumer)
return $"fuel {prop.FuelServiceState}, water {prop.WaterServiceState}, electricity {prop.ElectricityServiceState}";
return prop.ServiceState.ToString();
}
private static string FormatUnderground(LevelState level, GridPosition position)
{
var parts = new List<string>();
foreach (var carrier in Enum.GetValues<ECarrierType>())
{
var cell = level.GetUnderground(position, carrier);
if (cell.IsPresent)
parts.Add($"{carrier} {cell.State} {cell.Amount:F1}/{cell.Intensity:F1}");
}
return parts.Count > 0 ? string.Join(", ", parts) : "none";
}
private static string FormatPosition(GridPosition? position)
{
return position is { } p ? $"{p.X},{p.Y}" : "unlinked";
}
private static string FormatHazards(float fuel, float water, float electricity, float heat)
{
var parts = new List<string>();
if (fuel > 0) parts.Add($"fuel {fuel:F1}");
if (water > 0) parts.Add($"water {water:F1}");
if (electricity > 0) parts.Add($"electricity {electricity:F1}");
if (heat > 0) parts.Add($"heat {heat:F1}");
return parts.Count > 0 ? string.Join(", ", parts) : "none";
}
private readonly Label m_Text = new();
}

View File

@@ -0,0 +1 @@
uid://d0d3kglv6s32m

View File

@@ -0,0 +1,33 @@
using Godot;
namespace ReactorMaintenance.Godot.Controls;
public partial class ConfirmDialog : PanelContainer
{
public event Action? Confirmed;
public event Action? Canceled;
public void Configure(string title, string message, string confirmText = "Confirm")
{
foreach (var child in GetChildren())
child.QueueFree();
var body = new VBoxContainer();
AddChild(body);
body.AddChild(new Label { Text = title, HorizontalAlignment = HorizontalAlignment.Center });
body.AddChild(new Label { Text = message, AutowrapMode = TextServer.AutowrapMode.WordSmart });
var actions = new HBoxContainer();
body.AddChild(actions);
var confirm = new PrimaryButton();
confirm.Configure(confirmText);
confirm.Pressed += () => Confirmed?.Invoke();
actions.AddChild(confirm);
var cancel = new PrimaryButton();
cancel.Configure("Cancel");
cancel.Pressed += () => Canceled?.Invoke();
actions.AddChild(cancel);
}
}

View File

@@ -0,0 +1 @@
uid://cp5qe2n4nwb32

View File

@@ -0,0 +1,64 @@
using Godot;
using ReactorMaintenance.Simulation;
namespace ReactorMaintenance.Godot.Controls;
public partial class ForecastList : PanelContainer
{
public override void _Ready()
{
AddChild(m_Items);
SetForecasts(Array.Empty<Forecast>());
}
public void SetForecasts(IReadOnlyList<Forecast> forecasts)
{
foreach (var child in m_Items.GetChildren())
child.QueueFree();
m_Items.AddChild(new Label { Text = "Forecasts" });
foreach (var forecast in forecasts)
{
var label = new Label {
Text = FormatForecast(forecast),
AutowrapMode = TextServer.AutowrapMode.WordSmart
};
ApplyForecastColor(label, forecast.Kind);
m_Items.AddChild(label);
}
}
public void SetUnavailable(string reason)
{
foreach (var child in m_Items.GetChildren())
child.QueueFree();
m_Items.AddChild(new Label { Text = "Forecasts" });
m_Items.AddChild(new Label {
Text = reason,
AutowrapMode = TextServer.AutowrapMode.WordSmart
});
}
private static string FormatForecast(Forecast forecast)
{
var pos = forecast.Position;
var posStr = pos != null ? $" [{pos.X},{pos.Y}]" : "";
return $"Pulse +{forecast.Turns}: {forecast.Message}{posStr}";
}
private static void ApplyForecastColor(Label label, EForecastKind kind)
{
var color = kind switch {
EForecastKind.TerminalLoss => new(1.0f, 0.36f, 0.32f),
EForecastKind.ConsumerStarved => new(1.0f, 0.6f, 0.2f),
EForecastKind.HazardGrowth => new(1.0f, 0.8f, 0.2f),
EForecastKind.StructuralIntegrity => new(0.6f, 0.8f, 1.0f),
EForecastKind.ReactorReady => new(0.45f, 1.0f, 0.58f),
_ => Colors.White
};
label.AddThemeColorOverride("font_color", color);
}
private readonly VBoxContainer m_Items = new();
}

View File

@@ -0,0 +1 @@
uid://bswy75n15jxl5

View File

@@ -0,0 +1,32 @@
using Godot;
namespace ReactorMaintenance.Godot.Controls;
internal static class FrontendAssets
{
public static TextureRect CreateIcon(string path, Vector2 size)
{
return new() {
Texture = LoadTexture(path),
CustomMinimumSize = size,
ExpandMode = TextureRect.ExpandModeEnum.FitWidthProportional,
StretchMode = TextureRect.StretchModeEnum.KeepAspectCentered,
MouseFilter = Control.MouseFilterEnum.Ignore
};
}
public static Texture2D? LoadTexture(string path)
{
return ResourceLoader.Exists(path) ? ResourceLoader.Load<Texture2D>(path) : null;
}
public const string WaterIcon = "res://Assets/Ui/water_icon.png";
public const string ElectricIcon = "res://Assets/Ui/electric_icon.png";
public const string FuelIcon = "res://Assets/Ui/fuel_icon.png";
public const string HeatShieldIcon = "res://Assets/Ui/heat_shield_icon.png";
public const string MaintenanceRobot = "res://Assets/Characters/maintenance_robot.png";
public const string PrimaryButtonAccent = "res://Assets/Ui/primary_button_accent.png";
public const string ScannerEyeIcon = "res://Assets/Ui/scanner_eye_icon.png";
public const string StateBadgeFrame = "res://Assets/Ui/state_badge_frame.png";
public const string TerrainTilemap = "res://Assets/Terrain/terrain_tilemap.png";
}

View File

@@ -0,0 +1 @@
uid://bfvrecxfcutol

View File

@@ -0,0 +1,656 @@
using Godot;
using ReactorMaintenance.Simulation;
namespace ReactorMaintenance.Godot.Controls;
public partial class GridViewport : Control
{
private readonly record struct SGridLayout(float CellSize, Vector2 Origin)
{
public Rect2 CellRect(GridPosition position)
{
return new(Origin + new Vector2(position.X * CellSize, position.Y * CellSize), new(CellSize, CellSize));
}
public Rect2 DualTileRect(int x, int y)
{
return new(Origin + new Vector2((x - 0.5f) * CellSize, (y - 0.5f) * CellSize), new(CellSize, CellSize));
}
public Vector2 CellCenter(GridPosition position)
{
return CellRect(position).GetCenter();
}
}
public override void _Ready()
{
MouseFilter = MouseFilterEnum.Stop;
FocusMode = FocusModeEnum.Click;
ClipContents = true;
m_TerrainTilemap = FrontendAssets.LoadTexture(FrontendAssets.TerrainTilemap);
m_RobotTexture = FrontendAssets.LoadTexture(FrontendAssets.MaintenanceRobot);
}
public override void _Draw()
{
DrawRect(new(Vector2.Zero, Size), c_BackgroundColor);
if (m_LevelState is null)
return;
var layout = GetLayout();
DrawTerrain(layout, SurfaceOpacity());
DrawUnderground(layout);
DrawSurfaceHazards(layout, SurfaceOpacity());
DrawUnsafeWarnings(layout, SurfaceOpacity());
DrawDoors(layout, SurfaceOpacity());
DrawProps(layout, SurfaceOpacity());
DrawLeaks(layout, SurfaceOpacity());
DrawReachableHints(layout);
DrawRobot(layout, SurfaceOpacity());
DrawGridOverlays(layout);
}
public override void _GuiInput(InputEvent @event)
{
if (m_LevelState is null)
return;
if (@event is InputEventMouseMotion motion)
{
if (m_IsPanning)
{
PanOffset += motion.Relative;
QueueRedraw();
return;
}
SetHoveredCell(ScreenToCell(motion.Position));
return;
}
if (@event is not InputEventMouseButton mouseButton)
return;
if (mouseButton.ButtonIndex is MouseButton.WheelUp or MouseButton.WheelDown && mouseButton.Pressed)
{
ZoomAt(mouseButton.Position, mouseButton.ButtonIndex == MouseButton.WheelUp ? 1.12f : 1 / 1.12f);
AcceptEvent();
return;
}
if (mouseButton.ButtonIndex == MouseButton.Middle)
{
m_IsPanning = mouseButton.Pressed;
AcceptEvent();
return;
}
if (mouseButton.ButtonIndex == MouseButton.Left && mouseButton.Pressed)
{
if (ScreenToCell(mouseButton.Position) is { } cell)
{
SelectedCell = cell;
EmitSignal(SignalName.OnCellSelected, cell);
EmitSignal(SignalName.OnGridClicked, cell);
QueueRedraw();
}
AcceptEvent();
}
}
public void SetLevelState(LevelState levelState)
{
m_LevelState = levelState;
RobotPosition = ToVector(levelState.Robot.Position);
if (!IsValidCell(SelectedCell))
SelectedCell = RobotPosition;
QueueRedraw();
}
private void DrawTerrain(SGridLayout layout, float opacity)
{
if (m_LevelState is null)
return;
for (var y = 0; y <= m_LevelState.Height; y++)
{
for (var x = 0; x <= m_LevelState.Width; x++)
DrawDualTerrainTile(layout.DualTileRect(x, y), GetDualTileFloorMask(x, y), opacity);
}
}
private void DrawDualTerrainTile(Rect2 rect, int floorMask, float opacity)
{
if (m_TerrainTilemap is null)
{
DrawFallbackTerrainTile(rect, floorMask, opacity);
return;
}
var wallMask = c_AllCorners ^ floorMask;
DrawTextureRectRegion(m_TerrainTilemap, rect, TilemapSourceRect(wallMask), new(1, 1, 1, opacity));
}
private void DrawFallbackTerrainTile(Rect2 rect, int floorMask, float opacity)
{
var color = floorMask == c_AllCorners ? new(0.13f, 0.16f, 0.17f, opacity) : new Color(0.18f, 0.20f, 0.22f, opacity);
DrawRect(rect, color);
}
private void DrawUnderground(SGridLayout layout)
{
foreach (var carrier in OrderedUndergroundLayers())
DrawUndergroundLayer(layout, carrier, CarrierColor(carrier), UndergroundOpacity(carrier));
}
private IEnumerable<ECarrierType> OrderedUndergroundLayers()
{
var carriers = new[] { ECarrierType.Fuel, ECarrierType.Water, ECarrierType.Electricity }.Where(IsLayerVisible).ToArray();
return ActiveUndergroundLayer is { } activeCarrier && carriers.Contains(activeCarrier)
? carriers.Where(carrier => carrier != activeCarrier).Append(activeCarrier)
: carriers;
}
private void DrawUndergroundLayer(SGridLayout layout, ECarrierType carrier, Color color, float opacity)
{
if (m_LevelState is null)
return;
var layerColor = WithOpacity(color, opacity);
var lineWidth = Math.Max(4, layout.CellSize * 0.16f);
var cellDotRadius = Math.Max(2, layout.CellSize * 0.08f);
var sourceDotRadius = Math.Max(5, layout.CellSize * 0.22f);
foreach (var position in AllPositions())
{
var cell = m_LevelState.GetUnderground(position, carrier);
if (!cell.IsPresent)
continue;
var center = layout.CellCenter(position);
DrawNetworkConnection(layout, carrier, position, new(position.X + 1, position.Y), layerColor, lineWidth);
DrawNetworkConnection(layout, carrier, position, new(position.X, position.Y + 1), layerColor, lineWidth);
DrawCircle(center, cellDotRadius, layerColor);
if (cell.State == EUndergroundState.Leaking)
DrawArc(center, sourceDotRadius * 0.7f, 0, Mathf.Tau, 32, c_LeakColor, Math.Max(2, lineWidth * 0.25f));
var prop = m_LevelState.GetProp(position);
if (prop is { Type: EPropType.Flow } && prop.Carrier == carrier)
DrawCircle(center, sourceDotRadius, layerColor);
}
}
private void DrawNetworkConnection(SGridLayout layout, ECarrierType carrier, GridPosition position, GridPosition neighbor, Color color, float lineWidth)
{
if (m_LevelState is null || !m_LevelState.InBounds(neighbor) || !m_LevelState.GetUnderground(neighbor, carrier).IsPresent)
return;
DrawLine(layout.CellCenter(position), layout.CellCenter(neighbor), color, lineWidth);
}
private void DrawSurfaceHazards(SGridLayout layout, float opacity)
{
if (m_LevelState is null)
return;
foreach (var position in AllPositions().Where(m_LevelState.IsFloor))
{
var surface = m_LevelState.GetSurface(position);
var rect = layout.CellRect(position);
FillHazard(rect, surface.Fuel, c_FuelColor, 0.08f, opacity, Balancing.Current.FuelCaution, Balancing.Current.FuelCritical);
FillHazard(rect, surface.Water, c_WaterColor, 0.18f, opacity, Balancing.Current.WaterCaution, Balancing.Current.WaterCritical);
FillHazard(rect, surface.Electricity, c_ElectricityColor, 0.28f, opacity, Balancing.Current.ElectricityCaution, Balancing.Current.ElectricityCritical);
FillHazard(rect, surface.Heat, c_HeatColor, 0.34f, opacity, Balancing.Current.HeatCaution, Balancing.Current.HeatCritical);
}
}
private void DrawUnsafeWarnings(SGridLayout layout, float opacity)
{
if (m_LevelState is null)
return;
foreach (var position in AllPositions().Where(m_LevelState.IsFloor))
{
if (!m_LevelState.GetSurface(position).IsUnsafe())
continue;
var rect = Inset(layout.CellRect(position), 0.08f);
DrawRect(rect, WithOpacity(c_UnsafeColor, opacity), false, Math.Max(2, layout.CellSize * 0.07f));
}
}
private void FillHazard(Rect2 rect, float amount, Color color, float inset, float opacity, float caution, float critical)
{
var overlayOpacity = SurfaceOverlayOpacity(amount, caution, critical);
if (overlayOpacity <= 0)
return;
DrawRect(Inset(rect, inset), WithOpacity(color, overlayOpacity * opacity * 0.68f));
}
private void DrawDoors(SGridLayout layout, float opacity)
{
if (m_LevelState is null)
return;
foreach (var position in AllPositions())
{
var prop = m_LevelState.GetProp(position);
if (prop.Type != EPropType.Door)
continue;
var rect = layout.CellRect(position);
var center = layout.CellCenter(position);
var color = WithOpacity(prop.DoorState == EDoorState.Open ? c_ReadyColor : c_LeakColor, opacity);
var width = Math.Max(3, layout.CellSize * 0.1f);
if (IsWall(new(position.X, position.Y - 1)) && IsWall(new(position.X, position.Y + 1)))
DrawLine(new(center.X, rect.Position.Y), new(center.X, rect.End.Y), color, width);
else
DrawLine(new(rect.Position.X, center.Y), new(rect.End.X, center.Y), color, width);
}
}
private void DrawProps(SGridLayout layout, float opacity)
{
if (m_LevelState is null)
return;
foreach (var position in AllPositions())
{
var prop = m_LevelState.GetProp(position);
if (prop.Type == EPropType.None || prop.Type == EPropType.Door)
continue;
DrawPropBadge(Inset(layout.CellRect(position), 0.18f), prop, opacity);
}
}
private void DrawPropBadge(Rect2 rect, PropState prop, float opacity)
{
var color = WithOpacity(PropColor(prop), opacity);
DrawRect(rect, color);
DrawRect(rect, WithOpacity(Colors.White, opacity * 0.4f), false, Math.Max(1, rect.Size.X * 0.04f));
DrawString(ThemeDB.FallbackFont, rect.GetCenter() + new Vector2(-rect.Size.X * 0.3f, rect.Size.Y * 0.09f), PropLabel(prop), HorizontalAlignment.Center, rect.Size.X * 0.6f, (int)Math.Max(9, rect.Size.Y * 0.24f), WithOpacity(Colors.White, opacity));
if (prop.Type is EPropType.Flow or EPropType.Consumer)
{
var indicatorColor = prop.IsEnabled ? c_ReadyColor : c_DisabledColor;
DrawCircle(rect.Position + new Vector2(rect.Size.X * 0.82f, rect.Size.Y * 0.18f), Math.Max(3, rect.Size.X * 0.08f), WithOpacity(indicatorColor, opacity));
}
}
private void DrawLeaks(SGridLayout layout, float opacity)
{
if (m_LevelState is null)
return;
foreach (var leak in m_LevelState.Leaks.Where(leak => !leak.Repaired))
{
var rect = Inset(layout.CellRect(leak.AccessPosition), 0.12f);
DrawArc(rect.GetCenter(), rect.Size.X * 0.32f, 0, Mathf.Tau, 24, WithOpacity(CarrierColor(leak.Carrier), opacity), Math.Max(3, rect.Size.X * 0.08f));
DrawLine(rect.Position + new Vector2(rect.Size.X * 0.25f, rect.Size.Y * 0.75f), rect.Position + new Vector2(rect.Size.X * 0.75f, rect.Size.Y * 0.25f), WithOpacity(c_LeakColor, opacity), Math.Max(3, rect.Size.X * 0.07f));
}
}
private void DrawReachableHints(SGridLayout layout)
{
if (m_LevelState is null || !IsValidCell(RobotPosition))
return;
var robot = ToGridPosition(RobotPosition);
foreach (var neighbor in robot.Neighbors().Where(m_LevelState.IsFloor))
DrawRect(Inset(layout.CellRect(neighbor), 0.29f), c_ReachableColor);
}
private void DrawRobot(SGridLayout layout, float opacity)
{
if (m_LevelState is null)
return;
var rect = Inset(layout.CellRect(m_LevelState.Robot.Position), 0.04f);
if (m_RobotTexture is not null)
{
DrawTextureRect(m_RobotTexture, rect, false, new(1, 1, 1, opacity));
return;
}
DrawRect(rect, WithOpacity(Colors.White, opacity));
DrawString(ThemeDB.FallbackFont, rect.GetCenter() + new Vector2(-rect.Size.X * 0.28f, rect.Size.Y * 0.08f), "BOT", HorizontalAlignment.Center, rect.Size.X * 0.56f, (int)Math.Max(10, rect.Size.Y * 0.25f), c_BackgroundColor);
}
private void DrawGridOverlays(SGridLayout layout)
{
if (m_LevelState is null)
return;
foreach (var position in AllPositions())
DrawRect(layout.CellRect(position), c_GridLineColor, false, 1);
if (IsValidCell(HoveredCell) && HoveredCell != SelectedCell)
DrawRect(layout.CellRect(ToGridPosition(HoveredCell)), c_HoverColor, false, 2);
if (IsValidCell(SelectedCell))
DrawRect(layout.CellRect(ToGridPosition(SelectedCell)), c_SelectedColor, false, 3);
}
private int GetDualTileFloorMask(int x, int y)
{
var mask = 0;
if (GetTerrainOrWall(x - 1, y - 1) == ECellTerrain.Floor)
mask |= c_TopLeftCorner;
if (GetTerrainOrWall(x, y - 1) == ECellTerrain.Floor)
mask |= c_TopRightCorner;
if (GetTerrainOrWall(x - 1, y) == ECellTerrain.Floor)
mask |= c_BottomLeftCorner;
if (GetTerrainOrWall(x, y) == ECellTerrain.Floor)
mask |= c_BottomRightCorner;
return mask;
}
private ECellTerrain GetTerrainOrWall(int x, int y)
{
if (m_LevelState is null)
return ECellTerrain.Wall;
var position = new GridPosition(x, y);
return m_LevelState.InBounds(position) ? m_LevelState.GetTerrain(position) : ECellTerrain.Wall;
}
private bool IsWall(GridPosition position)
{
return m_LevelState is not null && m_LevelState.InBounds(position) && m_LevelState.GetTerrain(position) == ECellTerrain.Wall;
}
private Rect2 TilemapSourceRect(int wallMask)
{
var tilePosition = wallMask switch {
c_BottomLeftCorner => new(0, 0),
c_TopRightCorner | c_BottomRightCorner => new(1, 0),
c_TopLeftCorner | c_BottomLeftCorner | c_BottomRightCorner => new(2, 0),
c_BottomLeftCorner | c_BottomRightCorner => new(3, 0),
c_TopLeftCorner | c_BottomRightCorner => new(0, 1),
c_BottomLeftCorner | c_TopRightCorner | c_BottomRightCorner => new(1, 1),
c_AllCorners => new(2, 1),
c_TopLeftCorner | c_BottomLeftCorner | c_TopRightCorner => new(3, 1),
c_TopRightCorner => new(0, 2),
c_TopLeftCorner | c_TopRightCorner => new(1, 2),
c_TopLeftCorner | c_TopRightCorner | c_BottomRightCorner => new(2, 2),
c_BottomLeftCorner | c_TopLeftCorner => new(3, 2),
0 => new(0, 3),
c_BottomRightCorner => new(1, 3),
c_BottomLeftCorner | c_TopRightCorner => new(2, 3),
c_TopLeftCorner => new(3, 3),
_ => Vector2I.Zero
};
return new(tilePosition * c_TilemapTileSize, new Vector2I(c_TilemapTileSize, c_TilemapTileSize));
}
private SGridLayout GetLayout()
{
if (m_LevelState is null)
return new(TileSize * Zoom, Vector2.Zero);
var cellSize = TileSize * Zoom;
var contentSize = new Vector2(m_LevelState.Width * cellSize, m_LevelState.Height * cellSize);
var origin = ((Size - contentSize) / 2) + PanOffset;
return new(cellSize, origin);
}
private Vector2I? ScreenToCell(Vector2 point)
{
if (m_LevelState is null)
return null;
var layout = GetLayout();
var x = Mathf.FloorToInt((point.X - layout.Origin.X) / layout.CellSize);
var y = Mathf.FloorToInt((point.Y - layout.Origin.Y) / layout.CellSize);
var cell = new Vector2I(x, y);
return IsValidCell(cell) ? cell : null;
}
private void ZoomAt(Vector2 point, float factor)
{
var oldLayout = GetLayout();
var cell = (point - oldLayout.Origin) / oldLayout.CellSize;
m_Zoom = Math.Clamp(m_Zoom * factor, c_MinZoom, c_MaxZoom);
var newCellSize = TileSize * m_Zoom;
var contentSize = m_LevelState is null ? Vector2.Zero : new(m_LevelState.Width * newCellSize, m_LevelState.Height * newCellSize);
var centeredOrigin = (Size - contentSize) / 2;
m_PanOffset = point - centeredOrigin - (cell * newCellSize);
QueueRedraw();
}
private void SetHoveredCell(Vector2I? cell)
{
var next = cell ?? c_InvalidCell;
if (next == HoveredCell)
return;
HoveredCell = next;
EmitSignal(SignalName.OnCellHovered, HoveredCell);
QueueRedraw();
}
private bool IsLayerVisible(ECarrierType carrier)
{
return carrier switch {
ECarrierType.Fuel => ShowFuelLayer,
ECarrierType.Water => ShowWaterLayer,
ECarrierType.Electricity => ShowElectricityLayer,
_ => false
};
}
private float SurfaceOpacity()
{
return ActiveUndergroundLayer is null ? 1.0f : 0.5f;
}
private float UndergroundOpacity(ECarrierType carrier)
{
if (ActiveUndergroundLayer is null)
return 0.25f;
return ActiveUndergroundLayer == carrier ? 1.0f : 0.25f;
}
private IEnumerable<GridPosition> AllPositions()
{
if (m_LevelState is null)
yield break;
for (var y = 0; y < m_LevelState.Height; y++)
{
for (var x = 0; x < m_LevelState.Width; x++)
yield return new(x, y);
}
}
private bool IsValidCell(Vector2I cell)
{
return m_LevelState is not null && cell.X >= 0 && cell.Y >= 0 && cell.X < m_LevelState.Width && cell.Y < m_LevelState.Height;
}
private static Vector2I ToVector(GridPosition position)
{
return new(position.X, position.Y);
}
private static GridPosition ToGridPosition(Vector2I cell)
{
return new(cell.X, cell.Y);
}
private static Rect2 Inset(Rect2 rect, float fraction)
{
var inset = rect.Size.X * fraction;
return new(rect.Position + new Vector2(inset, inset), rect.Size - new Vector2(inset * 2, inset * 2));
}
private static float SurfaceOverlayOpacity(float amount, float caution, float critical)
{
if (amount < caution)
return 0;
if (amount >= critical)
return 0.9f;
var cautionRange = Math.Max(0.001f, critical - caution);
var t = (amount - caution) / cautionRange;
return 0.3f + (t * 0.35f);
}
private static Color WithOpacity(Color color, float opacity)
{
return new(color.R, color.G, color.B, color.A * Math.Clamp(opacity, 0, 1));
}
private static Color CarrierColor(ECarrierType carrier)
{
return carrier switch {
ECarrierType.Fuel => c_FuelColor,
ECarrierType.Water => c_WaterColor,
ECarrierType.Electricity => c_ElectricityColor,
_ => Colors.White
};
}
private static Color PropColor(PropState prop)
{
return prop.Type switch {
EPropType.Flow => CarrierColor(prop.Carrier),
EPropType.Consumer => new(0.36f, 0.48f, 0.67f),
EPropType.Junction => new(0.56f, 0.44f, 0.70f),
EPropType.AllSeeingEyeTerminal => new(0.33f, 0.59f, 0.61f),
EPropType.IsolationValve => CarrierColor(prop.Carrier),
EPropType.SprinklerControl => new(0.21f, 0.59f, 0.73f),
EPropType.SprinklerValve => new(0.15f, 0.44f, 0.59f),
EPropType.RemedySupply => new(0.30f, 0.57f, 0.34f),
EPropType.ReactorControl => new(0.69f, 0.28f, 0.29f),
_ => Colors.Gray
};
}
private static string PropLabel(PropState prop)
{
return prop.Type switch {
EPropType.Flow => $"{CarrierShort(prop.Carrier)} SRC",
EPropType.Consumer => "CON",
EPropType.Junction => $"J {prop.JunctionMode}",
EPropType.AllSeeingEyeTerminal => "EYE",
EPropType.IsolationValve => prop.IsOpen ? "V OPEN" : "V CLOSED",
EPropType.SprinklerControl => prop.IsEnabled ? "SPR ON" : "SPR OFF",
EPropType.SprinklerValve => "SPR",
EPropType.RemedySupply => RemedyShort(prop.RemedyType),
EPropType.ReactorControl => "REACT",
_ => string.Empty
};
}
private static string CarrierShort(ECarrierType carrier)
{
return carrier switch {
ECarrierType.Fuel => "F",
ECarrierType.Water => "C",
ECarrierType.Electricity => "E",
_ => "?"
};
}
private static string RemedyShort(ERemedyType remedy)
{
return remedy switch {
ERemedyType.FuelNeutralizer => "F REM",
ERemedyType.WaterNeutralizer => "C REM",
ERemedyType.ElectricityNeutralizer => "E REM",
ERemedyType.HeatShield => "H SHD",
_ => "REM"
};
}
public int TileSize
{
get => m_TileSize;
set
{
m_TileSize = Math.Max(12, value);
QueueRedraw();
}
}
public float Zoom
{
get => m_Zoom;
set
{
m_Zoom = Math.Clamp(value, c_MinZoom, c_MaxZoom);
QueueRedraw();
}
}
public Vector2 PanOffset
{
get => m_PanOffset;
set
{
m_PanOffset = value;
QueueRedraw();
}
}
public Vector2I SelectedCell { get; set; } = c_InvalidCell;
public Vector2I HoveredCell { get; private set; } = c_InvalidCell;
public Vector2I RobotPosition { get; private set; } = c_InvalidCell;
public bool ShowFuelLayer { get; set; } = true;
public bool ShowWaterLayer { get; set; } = true;
public bool ShowElectricityLayer { get; set; } = true;
public ECarrierType? ActiveUndergroundLayer { get; set; }
[Signal]
public delegate void OnCellHoveredEventHandler(Vector2I cell);
[Signal]
public delegate void OnCellSelectedEventHandler(Vector2I cell);
[Signal]
public delegate void OnGridClickedEventHandler(Vector2I cell);
private const float c_MinZoom = 0.5f;
private const float c_MaxZoom = 3.0f;
private const int c_TilemapTileSize = 512;
private const int c_TopLeftCorner = 1;
private const int c_TopRightCorner = 2;
private const int c_BottomLeftCorner = 4;
private const int c_BottomRightCorner = 8;
private const int c_AllCorners = c_TopLeftCorner | c_TopRightCorner | c_BottomLeftCorner | c_BottomRightCorner;
private static readonly Vector2I c_InvalidCell = new(-1, -1);
private static readonly Color c_BackgroundColor = new(0.06f, 0.07f, 0.08f);
private static readonly Color c_WaterColor = new(0.20f, 0.78f, 0.92f);
private static readonly Color c_DisabledColor = new(0.32f, 0.34f, 0.35f);
private static readonly Color c_ElectricityColor = new(0.96f, 0.78f, 0.20f);
private static readonly Color c_FuelColor = new(0.86f, 0.20f, 0.18f);
private static readonly Color c_GridLineColor = new(0.36f, 0.41f, 0.45f, 0.35f);
private static readonly Color c_HeatColor = new(1.0f, 0.42f, 0.12f);
private static readonly Color c_HoverColor = new(0.60f, 0.74f, 0.86f, 0.72f);
private static readonly Color c_LeakColor = new(1.0f, 0.27f, 0.16f);
private static readonly Color c_ReachableColor = new(0.78f, 0.96f, 0.84f, 0.20f);
private static readonly Color c_ReadyColor = new(0.46f, 0.95f, 0.52f);
private static readonly Color c_SelectedColor = new(1.0f, 1.0f, 1.0f, 0.95f);
private static readonly Color c_UnsafeColor = new(1.0f, 0.9f, 0.25f, 0.9f);
private bool m_IsPanning;
private LevelState? m_LevelState;
private Vector2 m_PanOffset;
private Texture2D? m_RobotTexture;
private Texture2D? m_TerrainTilemap;
private int m_TileSize = 48;
private float m_Zoom = 1;
}

View File

@@ -0,0 +1 @@
uid://dtsjjdlvxopty

View File

@@ -0,0 +1,49 @@
using Godot;
namespace ReactorMaintenance.Godot.Controls;
public partial class InventoryStrip : HBoxContainer
{
public override void _Ready()
{
AddChild(CreateItem("Fuel Neutralizer", 2, FrontendAssets.FuelIcon, out var fuelLabel));
AddChild(CreateItem("Water Neutralizer", 2, FrontendAssets.WaterIcon, out var waterLabel));
AddChild(CreateItem("Electric Neutralizer", 1, FrontendAssets.ElectricIcon, out var electricLabel));
AddChild(CreateItem("Heat Shield", 1, FrontendAssets.HeatShieldIcon, out var heatLabel));
m_FuelLabel = fuelLabel;
m_WaterLabel = waterLabel;
m_ElectricLabel = electricLabel;
m_HeatLabel = heatLabel;
}
public void SetInventory(int fuelNeutralizers, int waterNeutralizers, int electricNeutralizers, int heatShields)
{
m_FuelLabel.Text = $"{fuelNeutralizers}";
m_WaterLabel.Text = $"{waterNeutralizers}";
m_ElectricLabel.Text = $"{electricNeutralizers}";
m_HeatLabel.Text = $"{heatShields}";
}
private static HBoxContainer CreateItem(string name, int count, string iconPath, out Label countLabel)
{
var item = new HBoxContainer {
SizeFlagsHorizontal = SizeFlags.ExpandFill
};
item.AddChild(FrontendAssets.CreateIcon(iconPath, new(30, 30)));
countLabel = new() {
Text = $"{count}",
Name = name,
HorizontalAlignment = HorizontalAlignment.Center,
SizeFlagsHorizontal = SizeFlags.ExpandFill
};
item.AddChild(countLabel);
return item;
}
private Label m_ElectricLabel = null!;
private Label m_FuelLabel = null!;
private Label m_HeatLabel = null!;
private Label m_WaterLabel = null!;
}

View File

@@ -0,0 +1 @@
uid://d1pdwv570am3v

View File

@@ -0,0 +1,46 @@
using Godot;
using ReactorMaintenance.Simulation;
namespace ReactorMaintenance.Godot.Controls;
public partial class LevelHeader : HBoxContainer
{
public override void _Ready()
{
AddChild(m_Title);
AddChild(m_Progress);
AddChild(m_Badge);
AddChild(m_Summary);
m_Title.SizeFlagsHorizontal = SizeFlags.ExpandFill;
m_Title.AddThemeFontSizeOverride("font_size", 24);
m_Progress.HorizontalAlignment = HorizontalAlignment.Center;
m_Summary.HorizontalAlignment = HorizontalAlignment.Right;
}
public void SetLevel(string title, int levelNumber, int levelCount, ELevelState state)
{
m_Title.Text = title;
m_Progress.Text = $"Level {levelNumber} / {levelCount}";
m_Badge.SetState(StateToString(state));
m_Summary.Text = "Heat: nominal | Reactor: offline";
}
private static string StateToString(ELevelState state)
{
return state switch {
ELevelState.Stable => "Stable",
ELevelState.Caution => "Caution",
ELevelState.Critical => "Critical",
ELevelState.Ready => "Ready",
ELevelState.Lost => "Lost",
ELevelState.Won => "Won",
_ => "Unknown"
};
}
private readonly StateBadge m_Badge = new();
private readonly Label m_Progress = new();
private readonly Label m_Summary = new();
private readonly Label m_Title = new();
}

View File

@@ -0,0 +1 @@
uid://cp50vj0rystlk

View File

@@ -0,0 +1,41 @@
using Godot;
namespace ReactorMaintenance.Godot.Controls;
public partial class OutcomeOverlay : PanelContainer
{
public override void _Ready()
{
CustomMinimumSize = new(420, 220);
AddChild(m_Body);
m_Body.AddChild(m_Title);
m_Body.AddChild(m_Message);
m_Body.AddChild(m_Actions);
m_Title.HorizontalAlignment = HorizontalAlignment.Center;
m_Title.AddThemeFontSizeOverride("font_size", 26);
m_Message.AutowrapMode = TextServer.AutowrapMode.WordSmart;
m_Message.HorizontalAlignment = HorizontalAlignment.Center;
}
protected void Configure(string title, string message, IReadOnlyList<(string Text, Action Pressed)> actions)
{
m_Title.Text = title;
m_Message.Text = message;
foreach (var child in m_Actions.GetChildren())
child.QueueFree();
foreach (var action in actions)
{
var button = new PrimaryButton();
button.Configure(action.Text);
button.Pressed += action.Pressed;
m_Actions.AddChild(button);
}
}
private readonly HBoxContainer m_Actions = new();
private readonly VBoxContainer m_Body = new();
private readonly Label m_Message = new();
private readonly Label m_Title = new();
}

View File

@@ -0,0 +1 @@
uid://65iow3r0egvo

View File

@@ -0,0 +1,24 @@
using Godot;
namespace ReactorMaintenance.Godot.Controls;
public partial class PrimaryButton : Button
{
public override void _Ready()
{
FocusMode = FocusModeEnum.All;
CustomMinimumSize = new(220, 44);
SizeFlagsHorizontal = SizeFlags.ExpandFill;
Icon = FrontendAssets.LoadTexture(FrontendAssets.PrimaryButtonAccent);
ExpandIcon = true;
IconAlignment = HorizontalAlignment.Left;
}
public void Configure(string text, string tooltip = "", bool disabled = false)
{
Text = text;
TooltipText = tooltip;
Disabled = disabled;
}
}

View File

@@ -0,0 +1 @@
uid://bxk3mw5bybqxr

View File

@@ -0,0 +1,51 @@
using Godot;
namespace ReactorMaintenance.Godot.Controls;
public partial class StateBadge : PanelContainer
{
public override void _Ready()
{
CustomMinimumSize = new(96, 28);
var stack = new Control {
CustomMinimumSize = CustomMinimumSize,
SizeFlagsHorizontal = SizeFlags.ExpandFill
};
AddChild(stack);
var frame = new TextureRect {
Texture = FrontendAssets.LoadTexture(FrontendAssets.StateBadgeFrame),
AnchorRight = 1,
AnchorBottom = 1,
ExpandMode = TextureRect.ExpandModeEnum.FitWidthProportional,
StretchMode = TextureRect.StretchModeEnum.Scale,
MouseFilter = MouseFilterEnum.Ignore
};
stack.AddChild(frame);
m_Text.SetAnchorsPreset(LayoutPreset.FullRect);
m_Text.HorizontalAlignment = HorizontalAlignment.Center;
m_Text.VerticalAlignment = VerticalAlignment.Center;
stack.AddChild(m_Text);
}
public void SetState(string state)
{
m_Text.Text = state;
m_Text.AddThemeColorOverride("font_color", GetColor(state));
}
private static Color GetColor(string state)
{
return state.ToLowerInvariant() switch {
"stable" => new(0.72f, 0.86f, 0.76f),
"caution" => new(1.0f, 0.76f, 0.24f),
"critical" or "lost" => new(1.0f, 0.36f, 0.32f),
"ready" or "won" => new(0.45f, 1.0f, 0.58f),
_ => Colors.White
};
}
private readonly Label m_Text = new();
}

View File

@@ -0,0 +1 @@
uid://y7qn5eixkr7j

View File

@@ -0,0 +1,9 @@
namespace ReactorMaintenance.Godot.Data;
public sealed record CampaignLevel
{
public string Id { get; init; } = string.Empty;
public string Name { get; init; } = string.Empty;
public string FlavorText { get; init; } = string.Empty;
public string LevelPath { get; init; } = string.Empty;
}

View File

@@ -0,0 +1 @@
uid://uis6i7yfkrty

View File

@@ -0,0 +1,6 @@
namespace ReactorMaintenance.Godot.Data;
public sealed record CampaignManifest
{
public IReadOnlyList<CampaignLevel> Levels { get; init; } = Array.Empty<CampaignLevel>();
}

View File

@@ -0,0 +1 @@
uid://bnyc3qlyksmua

View File

@@ -0,0 +1,39 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using FileAccess = Godot.FileAccess;
namespace ReactorMaintenance.Godot.Data;
public static class CampaignRepository
{
public static CampaignManifest LoadDefault()
{
var json = FileAccess.GetFileAsString(c_DefaultManifestPath);
if (string.IsNullOrWhiteSpace(json))
return CreateFallback();
var manifest = JsonSerializer.Deserialize<CampaignManifest>(json, s_Options);
return manifest is { Levels.Count: > 0 } ? manifest : CreateFallback();
}
private static CampaignManifest CreateFallback()
{
return new() {
Levels = [
new() {
Id = "fallback",
Name = "Fallback Reactor",
FlavorText = "A placeholder level loaded because the campaign manifest was unavailable.",
LevelPath = "res://Data/Levels/fallback.json"
}
]
};
}
private const string c_DefaultManifestPath = "res://Data/default_campaign_manifest.json";
private static readonly JsonSerializerOptions s_Options = new() {
PropertyNameCaseInsensitive = true,
Converters = { new JsonStringEnumConverter() }
};
}

View File

@@ -0,0 +1 @@
uid://bufo8npdbreb1

View File

@@ -0,0 +1,62 @@
namespace ReactorMaintenance.Godot.Data;
public sealed class FrontendSession
{
public FrontendSession(CampaignManifest campaign)
{
m_Campaign = campaign;
}
public void StartNewCampaign()
{
IsRandomLevel = false;
CampaignIndex = 0;
HasContinue = true;
}
public void ContinueCampaign()
{
IsRandomLevel = false;
HasContinue = true;
}
public void StartRandomLevel()
{
IsRandomLevel = true;
}
public void MarkCurrentLevelLoaded()
{
if (!IsRandomLevel)
HasContinue = true;
}
public void AdvanceToNextLevel()
{
if (HasNextLevel)
CampaignIndex++;
}
public void CompleteCampaign()
{
IsRandomLevel = false;
CampaignIndex = 0;
HasContinue = false;
}
public bool HasContinue { get; private set; }
public bool IsRandomLevel { get; private set; }
public int CampaignIndex { get; private set; }
public CampaignLevel CurrentLevel => IsRandomLevel ? m_RandomLevel : m_Campaign.Levels[CampaignIndex];
public int LevelNumber => IsRandomLevel ? 1 : CampaignIndex + 1;
public int LevelCount => IsRandomLevel ? 1 : m_Campaign.Levels.Count;
public bool HasNextLevel => !IsRandomLevel && CampaignIndex + 1 < m_Campaign.Levels.Count;
private readonly CampaignManifest m_Campaign;
private readonly CampaignLevel m_RandomLevel = new() {
Id = "random-maintenance",
Name = "Random Maintenance Shift",
FlavorText = "A generated shift will use authored JSON level data until generation is implemented.",
LevelPath = "res://Data/Levels/random_placeholder.json"
};
}

View File

@@ -0,0 +1 @@
uid://bsbukw3qpo0by

View File

@@ -0,0 +1,197 @@
using ReactorMaintenance.Simulation;
namespace ReactorMaintenance.Godot.Data;
public sealed class GameSession
{
public event StateChangedHandler? LevelStateChanged;
public event StateChangedHandler? RobotMoved;
public event StateChangedHandler? PulseAdvanced;
public event StateChangedHandler? LevelWon;
public event StateChangedHandler? LevelLost;
public void Initialize(LevelState levelState, string levelPath)
{
LevelState = levelState;
LevelPath = levelPath;
m_StartSnapshot = LevelSerializer.Deserialize(LevelSerializer.Serialize(levelState));
}
public bool MoveRobot(GridPosition destination)
{
if (!LevelState.InBounds(destination) || !LevelState.IsFloor(destination) || LevelState.Robot.Position.ManhattanDistance(destination) != 1)
return false;
var previousPosition = LevelState.Robot.Position;
LevelState = m_Engine.MoveRobot(LevelState, destination);
if (LevelState.Robot.Position == previousPosition)
return false;
RobotMoved?.Invoke(this);
LevelStateChanged?.Invoke(this);
return true;
}
public bool InteractProp()
{
if (LevelState.Global.LevelState is ELevelState.Lost or ELevelState.Won)
return false;
var pulse = LevelState.Global.Pulse;
var trace = m_Engine.InteractPropWithPulseTrace(LevelState);
LevelState = trace.FinalState;
LastPulseSteps = trace.Steps;
LastPulseFeedback = CreatePulseFeedback(LevelState);
if (LevelState.Global.Pulse == pulse)
{
LevelStateChanged?.Invoke(this);
return false;
}
OnPulseAdvanced();
return true;
}
public bool InteractLeak(ECarrierType carrier, bool useRemedy)
{
if (LevelState.Global.LevelState is ELevelState.Lost or ELevelState.Won)
return false;
var pulse = LevelState.Global.Pulse;
var trace = m_Engine.InteractLeakWithPulseTrace(LevelState, carrier, useRemedy);
LevelState = trace.FinalState;
LastPulseSteps = trace.Steps;
LastPulseFeedback = CreatePulseFeedback(LevelState);
if (LevelState.Global.Pulse == pulse)
{
LevelStateChanged?.Invoke(this);
return false;
}
OnPulseAdvanced();
return true;
}
public bool ApplyHeatShield()
{
if (LevelState.Global.LevelState is ELevelState.Lost or ELevelState.Won)
return false;
var pulse = LevelState.Global.Pulse;
var trace = m_Engine.ApplyHeatShieldWithPulseTrace(LevelState);
LevelState = trace.FinalState;
LastPulseSteps = trace.Steps;
LastPulseFeedback = CreatePulseFeedback(LevelState);
if (LevelState.Global.Pulse == pulse)
{
LevelStateChanged?.Invoke(this);
return false;
}
OnPulseAdvanced();
return true;
}
public bool ActivateReactor()
{
if (LevelState.Global.LevelState is ELevelState.Lost or ELevelState.Won)
return false;
LevelState = m_Engine.ActivateReactor(LevelState);
CheckOutcome();
return true;
}
public IReadOnlyList<Forecast> GetForecasts()
{
return m_Engine.Forecast(LevelState);
}
public void Retry()
{
LevelState = m_StartSnapshot;
LastPulseSteps = Array.Empty<LevelState>();
LastPulseFeedback = Array.Empty<string>();
LevelStateChanged?.Invoke(this);
}
public void ApplyEditorTool(GridPosition position, EditorToolCommand command)
{
LevelState = LevelEditor.Apply(LevelState, position, command);
LevelStateChanged?.Invoke(this);
}
public string ValidateForSave()
{
var report = m_Validator.Validate(LevelState);
if (!report.IsValid)
return report.Errors[0].Message;
return report.Warnings.Count > 0 ? report.Warnings[0].Message : "Level is valid.";
}
public void SaveLevel()
{
LevelStateLoader.Save(LevelPath, LevelState);
m_StartSnapshot = LevelSerializer.Deserialize(LevelSerializer.Serialize(LevelState));
}
private void OnPulseAdvanced()
{
CheckOutcome();
PulseAdvanced?.Invoke(this);
}
private static IReadOnlyList<string> CreatePulseFeedback(LevelState level)
{
var feedback = new List<string>();
if (level.Global.LevelState == ELevelState.Ready)
feedback.Add("Reactor ready");
if (level.Props.Any(prop => prop.Type == EPropType.Consumer && prop.ServiceStateFor(ECarrierType.Fuel) == EConsumerServiceState.Starved))
feedback.Add("Fuel consumer starved");
if (level.Props.Any(prop => prop.Type == EPropType.Consumer && prop.ServiceStateFor(ECarrierType.Water) == EConsumerServiceState.Starved))
feedback.Add("Water consumer starved");
if (level.Props.Any(prop => prop.Type == EPropType.Consumer && prop.ServiceStateFor(ECarrierType.Electricity) == EConsumerServiceState.Starved))
feedback.Add("Electricity consumer starved");
if (level.Surface.Any(surface => surface.IsWetElectricUnsafe()))
feedback.Add("Wet-electric risk");
if (level.Surface.Any(surface => surface.Heat >= Balancing.Current.RobotHeatSafetyThreshold))
feedback.Add("Heat danger");
return feedback.Count > 0 ? feedback : [level.Global.Status];
}
private void CheckOutcome()
{
if (LevelState.Global.LevelState == ELevelState.Won)
LevelWon?.Invoke(this);
else if (LevelState.Global.LevelState == ELevelState.Lost)
LevelLost?.Invoke(this);
}
public LevelState LevelState { get; private set; } = null!;
public GridPosition RobotPosition => LevelState.Robot.Position;
public GlobalState GlobalState => LevelState.Global;
public IReadOnlyList<Forecast> Forecasts => LevelState.Forecasts;
public IReadOnlyList<string> LastPulseFeedback { get; private set; } = Array.Empty<string>();
public IReadOnlyList<LevelState> LastPulseSteps { get; private set; } = Array.Empty<LevelState>();
public IReadOnlyList<LeakState> Leaks => LevelState.Leaks;
public string LevelPath { get; private set; } = string.Empty;
public IReadOnlyList<ReactorState> Reactors => LevelState.Reactors;
public IReadOnlyList<PropState> Props => LevelState.Props;
public ECellTerrain[] Terrain => LevelState.Terrain;
public SurfaceState[] Surface => LevelState.Surface;
public UndergroundCell[] UndergroundFuel => LevelState.Fuel;
public UndergroundCell[] UndergroundWater => LevelState.Water;
public UndergroundCell[] UndergroundElectricity => LevelState.Electricity;
public delegate void StateChangedHandler(GameSession sender);
private readonly SimulationEngine m_Engine = new();
private readonly LevelValidator m_Validator = new();
private LevelState m_StartSnapshot = null!;
}

View File

@@ -0,0 +1 @@
uid://c32a61f2pslf8

View File

@@ -0,0 +1,50 @@
using ReactorMaintenance.Simulation;
using FileAccess = Godot.FileAccess;
namespace ReactorMaintenance.Godot.Data;
public static class LevelStateLoader
{
public static LevelState Load(string path)
{
if (string.IsNullOrWhiteSpace(path))
throw new ArgumentException("Level path must not be null or empty.", nameof(path));
if (!FileAccess.FileExists(path))
throw new FileNotFoundException($"Level file not found: {path}");
var json = FileAccess.GetFileAsString(path);
if (string.IsNullOrWhiteSpace(json))
throw new InvalidOperationException($"Level file is empty: {path}");
try
{
return LevelSerializer.Deserialize(json);
}
catch (InvalidOperationException)
{
throw;
}
catch (Exception ex)
{
throw new InvalidOperationException(
$"Failed to deserialize level from {path}: {ex.Message}", ex);
}
}
public static void Save(string path, LevelState level)
{
if (string.IsNullOrWhiteSpace(path))
throw new ArgumentException("Level path must not be null or empty.", nameof(path));
var report = new LevelValidator().Validate(level);
if (!report.IsValid)
throw new InvalidOperationException(report.Errors[0].Message);
using var file = FileAccess.Open(path, FileAccess.ModeFlags.Write);
if (file is null)
throw new IOException($"Could not open level file for writing: {path}");
file.StoreString(LevelSerializer.Serialize(level));
}
}

View File

@@ -0,0 +1 @@
uid://dded4q1wrmfdr

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More