Compare commits

...

33 Commits

Author SHA1 Message Date
nathan e28945a076 Add LinaAI terminal command 2026-05-24 15:35:00 +00:00
nathan 63490e044f Load LinaAI memory into Aider 2026-05-24 14:37:25 +00:00
nathan 90c15fdf84 Prepare LinaAI project memory 2026-05-24 07:54:37 +00:00
nathan 2d3e0454cd Add LinaAI automatic Codex routing 2026-05-24 07:24:58 +00:00
nathan e82045a7f9 Document LinaAI Codex login requirement 2026-05-24 03:47:05 +00:00
nathan a7c50f651a Add self-hosted AI worker bootstrap 2026-05-24 03:44:23 +00:00
nathan 6d7cc534c4 Add AGR market trade flow roadmap 2026-05-22 05:02:12 +00:00
nathan 507f7ad2f1 Add AGR wallet integration roadmap 2026-05-22 04:38:22 +00:00
nathan c77708ee80 Move project handoff into game repo 2026-05-22 03:18:40 +00:00
nathan 810a92372b Add 2027 awards direction to roadmap 2026-05-22 03:14:14 +00:00
nathan 6eb262acc3 Add pause save exit settings shell 2026-05-22 03:06:27 +00:00
nathan e7bd783309 Sequence startup story before character selection 2026-05-21 22:55:52 +00:00
nathan 5cd0c9c6d5 Harden gameplay input restore after frontend 2026-05-21 22:44:27 +00:00
nathan fd1a8ce477 Queue Ground Zero assets and biome resource plan 2026-05-21 22:31:53 +00:00
nathan fc74a7b129 Lock asset pipeline to free sources 2026-05-21 22:21:16 +00:00
nathan f0713c6c46 Add Agrarian asset pipeline policy 2026-05-21 22:16:57 +00:00
nathan 3f27be7f88 Fix frontend input release and vegetation proxies 2026-05-21 20:44:02 +00:00
nathan 03dbcbc5f8 Restore gameplay input after MVP frontend 2026-05-21 16:56:54 +00:00
nathan 40f7b7e814 Update M_AGR_CharacterProxy_Workwear_Male.uasset 2026-05-21 11:49:14 -05:00
nathan dd3d247539 Upgrade Ground Zero vegetation assets 2026-05-21 15:43:14 +00:00
nathan 98ab61a7a4 Upgrade Ground Zero terrain material 2026-05-21 15:12:30 +00:00
nathan 13e931eb04 Prioritize investor visual credibility 2026-05-21 14:58:12 +00:00
nathan 106b3bd01b Reset roadmap for 0.2 development 2026-05-21 10:07:53 +00:00
nathan 4e17cede2d Stabilize investor visual smoke build 2026-05-19 22:28:35 -07:00
nathan d2b8185333 Stabilize investor frontend entry 2026-05-19 20:46:09 -07:00
nathan d59f613e2b Improve investor visual recovery pass 2026-05-19 18:11:17 -07:00
nathan c742a172da Prepare roadmap for 0.2 homesteading 2026-05-19 16:15:35 -07:00
nathan 64d0603680 Define multiplayer learning rules 2026-05-19 15:53:11 -07:00
nathan 0aa1802949 Define knowledge persistence requirements 2026-05-19 15:51:07 -07:00
nathan 66c6052e91 Add learning exploit guardrails 2026-05-19 15:48:46 -07:00
nathan 766ceac5d7 Define deeper question timing 2026-05-19 15:45:57 -07:00
nathan b5416e0453 Add elementary survival question bank 2026-05-19 15:44:34 -07:00
nathan 7b1f9b81c0 Define subject content format 2026-05-19 15:42:55 -07:00
71 changed files with 14339 additions and 452 deletions
+268 -88
View File
@@ -29,7 +29,6 @@ Core commitments:
- [ ] Build toward an Earth-scale world made from real-world terrain tiles.
- [ ] Keep travel paced by believable real-world movement, vehicles, terrain, and character condition.
- [ ] Treat terrain, bathymetry, biomes, resources, rivers, and mountains as data-driven long-term infrastructure, not one-off maps.
- [ ] Represent each real-world tile with layered climate, biome, ecology, and human-use metadata so the in-game environment feels geographically recognizable rather than using generic biome labels.
- [ ] Keep base world time grounded; the world should progress without feeling artificially sped up.
- [ ] Make player skill, tools, infrastructure, cooperation, and knowledge improve efficiency, yield, reliability, quality, and capacity rather than breaking natural biological time.
- [ ] Treat learning as a core play loop: knowledge unlocks understanding, practice builds competence, and infrastructure makes advanced work possible.
@@ -37,6 +36,41 @@ Core commitments:
- [ ] Let players choose depth: basic survival should be approachable, while advanced civilization, nuclear power, spaceflight, and colonization should reward real study, planning, and mastery.
- [ ] Avoid trivia-only gates; complex achievements require the right knowledge, repeated hands-on experience, tools, safety systems, materials, teams, and institutions.
## 2027 Awards Direction
Target: build Agrarian toward credible nomination conversations for visual
art/art direction, new intellectual property, design, debut/independent, and
innovation-style game awards in 2027.
Agrarian should not chase generic Unreal showcase beauty. The award-worthy
identity is geographically truthful, emotionally grounded, and systemically
alive: a realistic post-collapse Earth simulation where every tile feels like a
real place, every resource has consequence, and rebuilding civilization feels
intimate, fragile, and earned.
Every milestone must be judged against these production bars:
- [ ] Visual credibility: no investor-facing build may rely on placeholder
terrain, mannequins, cubes, spheres, undressed water, fake-looking vegetation,
or unexplained debug geometry.
- [ ] Geographic truth: biome, lighting, water, terrain, flora, fauna,
resource availability, weather, and human remnants should match the real-world
tile being represented or a documented near-future extrapolation.
- [ ] Art direction: realism must still have a signature look: recovered nature
overtaking abandoned infrastructure, human-made warmth against harsh natural
systems, and hopeful reconstruction rather than generic apocalypse.
- [ ] Systemic originality: each gameplay layer should reinforce persistent
civilization recovery, real resource consequence, real time, family/community
continuity, and Earth-scale simulation.
- [ ] Emotional attachment: players should care about people, places, tools,
land, shelter, animals, family, and community because those things persist,
age, improve, decay, or can be lost.
- [ ] Demo discipline: investor builds should be showable without explaining
that major first-impression visuals are temporary.
- [ ] Roadmap filter: if a task does not improve visual credibility, systemic
originality, player emotional attachment, stability, tooling, or demo
readiness, defer it.
## Time And Progression Philosophy
Baseline rule:
@@ -60,37 +94,46 @@ Design intent:
Primary development repository:
```text
git@github.com:pacificao/AgrarianGameBuild.git
http://192.168.5.21:3000/nathan/agrarian-game.git
```
Primary local Codex/server checkout:
Primary Unreal/Codex build checkout:
```text
/mnt/projects/AgrarianGameBulid
/home/nathan/UnrealProjects/AgrarianGame
```
Ubuntu-Codex host:
Primary Unreal build host:
```text
192.168.5.10
unreal-engine / 192.168.5.20
```
Unraid project share:
Primary engine install:
```text
\\DevBox\projects\AgrarianGameBulid
/opt/UnrealEngine-5.7
```
Windows build VM:
Current engine source tag:
```text
Windows-Builder / 192.168.5.12
5.7.4-release
```
Codex headless editor build command:
Headless Linux editor build command:
```text
UNRAID_PASSWORD=<set in environment> /home/nathan/bin/agrarian-build-editor
cd /opt/UnrealEngine-5.7
./Engine/Build/BatchFiles/Linux/Build.sh AgrarianGameEditor Linux Development -Project=/home/nathan/UnrealProjects/AgrarianGame/AgrarianGame.uproject
```
Historical/secondary locations:
```text
GitHub mirror/source before Gitea migration: git@github.com:pacificao/AgrarianGameBuild.git
Old network-share workflow: \\DevBox\projects\AgrarianGameBulid
Windows build/visual QA VM: Windows-Builder / 192.168.5.12
```
Important tracked project root files/folders:
@@ -139,7 +182,34 @@ Roadmap headings now use release-style milestone numbers. Subsections use letter
labels such as `0.1.A`, `0.1.B`, and `0.1.C` so they do not look like
separate versions.
## Active Milestone - Version 0.01 Foundation Baseline
## Active Milestone - Version 0.2.0 Investor Visual Credibility Baseline
Status: not started.
Current reset baseline as of 2026-05-21:
- [x] Active game repository moved to self-hosted Gitea.
- [x] Active Unreal build machine moved to Ubuntu VM `unreal-engine`.
- [x] Unreal Engine `5.7.4-release` source build completed on Linux.
- [x] Agrarian Linux editor target builds on the Ubuntu VM.
- [x] Headless project load succeeds with `NullRHI`.
- [x] Git working tree is clean at the reset point.
- [x] Git LFS fsck passes.
- [x] Generated Unreal folders remain ignored.
- [x] Version 0.1 implementation track is complete through `0.1.S`.
- [ ] Start the next milestone with only `0.2.0 Investor Visual Credibility Baseline`.
- [ ] After `0.2.0` is complete and verified, move to `0.2.A Tile Biome And Natural Resource Foundation`.
- [ ] After `0.2.A` is complete and verified, choose the next milestone deliberately rather than letting the roadmap sprawl drive work out of order.
Coding rule for the next phase:
- Work one roadmap item or one tightly scoped milestone at a time.
- Keep every gameplay change server-authoritative unless explicitly documented otherwise.
- Preserve the Ubuntu Unreal VM and Gitea workflow as the default development path.
- Build or run a focused verifier before each commit.
- Do not use the old mapped-network-drive workflow for primary Unreal editing.
## Previous Milestone - Version 0.01 Foundation Baseline
Status: completed.
@@ -826,65 +896,60 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe
- [x] Replace the placeholder Ground Zero environment presentation with investor-facing biome dressing: believable terrain material, grass, brush, shrubs, bushes, trees, rocks, water visuals, and local coastal-scrub color variation. Upgraded the repeatable Ground Zero setup to require denser investor-facing foliage counts and twenty-three labeled variation actors covering trees, brush, shrubs, dry grass mats, rock slabs, water-bank pieces, reeds, and freshwater surface material variation, then extended the verifier/docs so the map no longer qualifies if the visual dressing falls back to sparse placeholder presentation.
- [x] Add a real water-source visual pass with surface material, edge treatment, scale, and placement that reads as collectable freshwater instead of a placeholder plane. Formalized the MVP freshwater presentation around the native `AAgrarianWaterSource` water-surface, stone-bank, and collect-marker proxies, documented the Ground Zero drainage-candidate placement and nearby water-bank/reed dressing, and added an Unreal verifier that checks surface material, edge treatment, scale, placement, and nearby dressing actors in the actual map.
- [x] Add density and sightline tuning so grasses, shrubs, trees, and resource clusters are visible enough to sell the world without hiding gameplay-critical objects. Added protected foliage clearances around early survival targets and biome resource nodes, sampled first-look sightline corridors from the player start to wood, fiber, campfire, shelter, wildlife, and freshwater, plus a dedicated Unreal verifier/documentation gate so investor-facing density cannot regress into object-hiding clutter.
- [x] Preserve realism as the target: use assets, materials, lighting, and environmental dressing that can survive toward MVP production rather than cosmetic throwaways where practical. Added MVP realism target rules to the shared art/UX/code/asset standards, tied current 0.1.O visual passes to production-directed proxy expectations, and added a verifier that requires Ground Zero materials, water, density/sightline, character, and survival-object docs to point toward production realism instead of cosmetic throwaways.
- [x] Define default, recommended, and cinematic investor rendering presets, with ray tracing available only as an optional high-end/cinematic mode and never required for baseline visual credibility. Added `Config/AgrarianRenderingPresets.ini`, `Docs/Rendering/InvestorRenderingPresets.md`, and a verifier that requires Default and Recommended to remain non-ray-traced while Cinematic is the only optional high-end ray-tracing profile.
- [x] Verify the non-ray-traced compatibility/default path still looks credible on common investor, tester, and remote-session hardware. Added a dedicated non-ray-traced default verifier that checks Default and Recommended disable `r.RayTracing`, `r.Lumen.HardwareRayTracing`, and `r.PathTracing`, keeps Cinematic as the only optional ray-tracing profile, and documents that packaged investor demos should launch on Default unless another profile is explicitly selected.
- [x] Add packaged-demo visual QA screenshots or short clips for startup credits, character selection, first spawn, terrain, vegetation, water, campfire, shelter, pause menu, and save/quit before each investor build is called ready. Added a full investor-demo visual QA evidence runbook, Windows helper, startup capture expansion, and verifier requiring Sunshine/Moonlight or equivalent real-GPU captures for startup credits, character selection, first spawn, terrain, vegetation, water, campfire, shelter, pause, and save/quit before a packaged build is called investor-ready.
- [x] Add an investor-demo acceptance gate: no current build should be described as investor visual MVP if menus are confusing, character art is mannequin-only, terrain is flat/tan, foliage is absent or unreadable, or core objects still read as primitive debug shapes. Added an investor-demo acceptance gate document, updated demo status wording, and added verification that hard-fail conditions cover confusing menus, mannequin-only characters, flat/tan terrain, absent/unreadable foliage, unreadable water, primitive debug objects, non-ray-traced default credibility, and missing visual QA evidence.
- [ ] Preserve realism as the target: use assets, materials, lighting, and environmental dressing that can survive toward MVP production rather than cosmetic throwaways where practical.
- [ ] Define default, recommended, and cinematic investor rendering presets, with ray tracing available only as an optional high-end/cinematic mode and never required for baseline visual credibility.
- [ ] Verify the non-ray-traced compatibility/default path still looks credible on common investor, tester, and remote-session hardware.
- [ ] Add packaged-demo visual QA screenshots or short clips for startup credits, character selection, first spawn, terrain, vegetation, water, campfire, shelter, pause menu, and save/quit before each investor build is called ready.
- [x] Add an investor-demo acceptance gate: no current build should be described as investor visual MVP if menus are confusing, character art is mannequin-only, terrain is flat/tan, foliage is absent or unreadable, or core objects still read as primitive debug shapes.
## 0.1.P MVP Audio And Atmosphere
- [x] Add ambient biome audio. Extended the placed `AAgrarianWeatherAudioController` so its ambient component explicitly owns a Ground Zero coastal-scrub biome loop slot with separate day/night volume targets, keeping the current MVP silent until placeholder or final audio assets are assigned while giving the map a real ambient audio attachment point.
- [x] Add footstep placeholders. Added native player-character footstep hooks with assignable walk, sprint, crouch, and prone sound slots plus movement-state cadence, keeping the MVP silent until placeholder or final surface-aware audio assets are assigned.
- [x] Add gathering sounds. Added spatialized resource-node gathering audio hooks with assignable normal/depleted gathering cues and a server-authoritative multicast trigger after successful harvests, keeping multiplayer clients aligned while remaining silent until audio assets are assigned.
- [x] Add fire sounds. Added campfire loop, ignition, and extinguish audio hooks with spatialized components, replicated lit-state loop control, and server-triggered multicast event cues so fire audio follows the authoritative campfire state once assets are assigned.
- [x] Add unattended and poorly maintained fire risk for campfires and other open-flame sources. Added server-side campfire risk state for lit duration, seconds since maintenance, cleared area, containment, high fuel, wet weather mitigation, and a replicated `FireRiskScore` that later ignition/spread systems can consume.
- [x] Add grass and forest ignition checks from irresponsible fire placement, wind/weather, dry fuel, nearby vegetation, and burn duration. Added foliage fuel counting, campfire vegetation ignition risk scores, grass/brush and forest ignition flags, and weather/wind/burn-duration modifiers so unsafe fire placement near dry fuel can now become a server-authoritative ignition risk.
- [x] Add shelter/structure ignition risk when fires are placed too close to primitive shelters, wood piles, flammable crafting stations, or settlement objects. Added campfire structure ignition risk for nearby primitive shelters and flammable wood/fiber resource nodes, with containment, burn-duration, weather/wind, and fire-risk modifiers before a replicated structure ignition flag is set.
- [x] Add server-authoritative fire spread rules for grass, brush, trees, shelters, and other burnable actors, including fuel, distance, wind, weather, and suppression hooks. Added replicated grass, forest, and structure fire intensities plus active spread radius that grow only on the server from nearby fuel, ignition distance, wind/weather, and a suppression-pressure hook for later rain, carried water, dirt/sand, firebreaks, and tools.
- [x] Add fire maintenance gameplay so watched, cleared, contained, or extinguished fires are safe, while neglected fires can become dangerous. Updated lit campfire interaction to maintain the fire, added watch, clear-area, and contain-fire hooks, and made maintenance reduce campfire, vegetation, forest, and structure ignition risks while extinguishing resets active risk state.
- [x] Add fire suppression hooks for rain, water carrying, dirt/sand, cleared firebreaks, and future firefighting tools. Added shared server-side suppression hooks plus water, dirt/sand, firebreak, and tool wrappers that raise suppression pressure, reduce ignition risks, reduce active fire intensity, shrink spread radius, and let rain/water drain fuel.
- [x] Persist active grass, forest, and structure fires across save/load without corrupting world state. Extended campfire persistence coverage for ignition flags, ignition risk scores, active grass/forest/structure fire intensities, spread radius, and suppression pressure so save/load recovery preserves active and partially suppressed fire state.
- [x] Add QA coverage for safe campfires, unsafe campfires, vegetation spread, shelter ignition, suppression, and save/load recovery. Added a fire-risk QA coverage document and verifier requiring safe/unsafe campfire, vegetation spread, shelter ignition, suppression, and save/load recovery scenarios plus the supporting fire-risk verification scripts.
- [x] Add weather sounds. Formalized the existing placed weather audio controller as the MVP weather-sound path, documenting rain, wind, storm, clear ambient, and biome loop slots plus verification that weather playback follows replicated weather state, provider wind speed, and day/night state while remaining silent until assets are assigned.
- [x] Add wildlife sounds. Added spatialized wildlife audio hooks with assignable idle, flee/chase, death, and harvest sound slots plus server-triggered multicast playback from authoritative wildlife state changes and harvest events.
- [x] Add UI sounds. Added optional 2D confirm, back, selection, and save/quit sound hooks to the MVP frontend widget, with keyboard and mouse actions sharing the same feedback path while remaining silent until UI audio assets are assigned.
- [x] Add mix settings. Added MVP audio mix settings for master, ambient, weather, foley, fire, wildlife, and UI buses with conservative investor-build defaults and documentation for future SoundClass/MetaSound replacement.
- [x] Add volume sliders. Added MVP frontend volume sliders for master, ambient, weather, effects, wildlife, and UI levels, with runtime value storage and immediate UI-volume application to frontend feedback sounds while leaving final SoundClass binding for authored audio assets.
- [ ] Add ambient biome audio.
- [ ] Add footstep placeholders.
- [ ] Add gathering sounds.
- [ ] Add fire sounds.
- [ ] Add unattended and poorly maintained fire risk for campfires and other open-flame sources.
- [ ] Add grass and forest ignition checks from irresponsible fire placement, wind/weather, dry fuel, nearby vegetation, and burn duration.
- [ ] Add shelter/structure ignition risk when fires are placed too close to primitive shelters, wood piles, flammable crafting stations, or settlement objects.
- [ ] Add server-authoritative fire spread rules for grass, brush, trees, shelters, and other burnable actors, including fuel, distance, wind, weather, and suppression hooks.
- [ ] Add fire maintenance gameplay so watched, cleared, contained, or extinguished fires are safe, while neglected fires can become dangerous.
- [ ] Add fire suppression hooks for rain, water carrying, dirt/sand, cleared firebreaks, and future firefighting tools.
- [ ] Persist active grass, forest, and structure fires across save/load without corrupting world state.
- [ ] Add QA coverage for safe campfires, unsafe campfires, vegetation spread, shelter ignition, suppression, and save/load recovery.
- [ ] Add weather sounds.
- [ ] Add wildlife sounds.
- [ ] Add UI sounds.
- [ ] Add mix settings.
- [ ] Add volume sliders.
## 0.1.Q MVP QA Gates
- [x] Can launch packaged client. Added an MVP QA gate that requires the Windows package script, packaged executable, installed investor launchers, and the real-GPU visual QA readiness check before the client launch gate qualifies.
- [x] Can launch server. Added an MVP QA gate for the Linux gameplay host requiring the true dedicated build path or current binary-engine fallback, deployment to `/opt/agrarian/server`, `agrarian-game-server.service` active state, UDP `7777` listener evidence, and Ground Zero map browse evidence.
- [x] Can connect two clients. Added a two-client connection QA gate and Windows helper that checks the packaged client, launches two client instances against the same `play.agrariangame.com:7777` or LAN endpoint, and ties the manual observation steps to the multiplayer latency smoke plan.
- [x] Can gather resources. Added a resource gathering QA gate tied to Ground Zero wood/fiber nodes, server-authoritative resource interaction, replicated harvest depletion, inventory grants, resource persistence coverage, and the natural shelter playable-loop smoke test.
- [x] Can craft a fire. Added a craft-fire QA gate tied to `DA_Recipe_Campfire`, the player recipe setup, `BP_Campfire`, replicated campfire lit/fuel state, fire interaction prompts, campfire persistence, and fire-risk QA coverage.
- [x] Can craft a shelter. Added a craft-shelter QA gate tied to primitive frame/wall/roof/shelter recipes, native build placement, `BP_PrimitiveShelter`, shelter persistence/protection hooks, and the natural shelter playable-loop smoke test.
- [x] Can survive one full day/night cycle. Added a full day/night survival QA gate tied to the `4 real hours = 1 in-game day` calendar, replicated world time and solar phase, authoritative hunger/thirst/stamina/body-temperature/health pressure, fire and shelter mitigation, critical survival HUD visibility, and save/load persistence coverage before investor demos treat the gate as play-proven.
- [x] Can die from survival pressure. Added a survival-pressure death QA gate requiring starvation, dehydration, cold exposure, sickness, and bleeding to reduce health on server authority, trigger `UpdateDeathState`, replicate `bIsDead` and `LastDeathReason`, show death/respawn UI feedback, support server respawn, and remain covered by player stat persistence.
- [x] Can reconnect and retain state. Added a reconnect state-retention QA gate tied to logout/restart player snapshots, safe player identity, transform, survival, care history, inventory restore, normal-spawn fallback behavior, and the two-client manual reconnect evidence path.
- [x] Can restart server and retain placed shelter. Added a server-restart shelter persistence QA gate tied to `primitive_shelter` persistent actor state, world actor save/load, game-mode class registration, load-on-server-start behavior, shelter weather protection, and a release smoke requirement to place, save, restart, and confirm the shelter transform remains.
- [x] No critical log spam during 30-minute test. Added a 30-minute critical log soak QA gate plus `scan_critical_log_spam.py` so client/server/release logs can be checked for fatal, crash, assertion, ensure, access-violation, callstack, and critical-error spam before a milestone package is treated as investor-stable.
- [x] Clean up Unreal API deprecation warnings from packaged builds, starting
- [ ] Can launch packaged client.
- [ ] Can launch server.
- [ ] Can connect two clients.
- [ ] Can gather resources.
- [ ] Can craft a fire.
- [ ] Can craft a shelter.
- [ ] Can survive one full day/night cycle.
- [ ] Can die from survival pressure.
- [ ] Can reconnect and retain state.
- [ ] Can restart server and retain placed shelter.
- [ ] No critical log spam during 30-minute test.
- [ ] Clean up Unreal API deprecation warnings from packaged builds, starting
with direct `NetCullDistanceSquared` access on replicated world actors before
future Unreal upgrades turn the warning into a compile blocker. Replaced direct
`NetCullDistanceSquared = FMath::Square(...)` assignments with
`SetNetCullDistanceSquared(FMath::Square(...))` on item pickups, resource
nodes, campfires, shelters, wildlife, water sources, weather exposure zones,
and wildlife spawn managers, then added verifier coverage to prevent the
deprecated assignment style from returning.
- [x] Server remains stable with target test player count. Added a target player count server-stability QA gate using the MVP audience definition: 2-player minimum proof, 4-player closed-test smoke target, and 8-player stretch test only after the server path is stable, with evidence tied to server launch, two-client connection, reconnect retention, critical log scanning, active service state, and UDP `7777` listener checks.
future Unreal upgrades turn the warning into a compile blocker.
- [ ] Server remains stable with target test player count.
## 0.1.R Knowledge And Skill Foundation
- [x] Define the MVP separation between knowledge, practical experience, physical stats, tools, and infrastructure. Added `Docs/KnowledgeAndSkillFoundation.md` with a five-part MVP model separating knowledge, practical experience, physical stats, tools, and infrastructure so basic survival remains possible but outcomes improve through understanding, practice, equipment, and durable world improvements.
- [x] Add a first-pass skill taxonomy for survival, gathering, tool use, crafting, fire, shelter, navigation, first aid, food safety, and weather awareness. Added the first MVP taxonomy to `Docs/KnowledgeAndSkillFoundation.md`, covering survival, gathering, tool use, crafting, fire, shelter, navigation, first aid, food safety, and weather awareness as non-lockout skill domains that modify risk, quality, speed, yield, readability, and confidence.
- [x] Define how knowledge affects survival actions: fewer mistakes, safer attempts, better yields, lower injury risk, and more reliable outcomes. Added the knowledge action-effects model to `Docs/KnowledgeAndSkillFoundation.md`, defining how knowledge changes warnings, failed-action reasons, safety, yield/waste, injury risk, and outcome reliability without silently guaranteeing success or replacing practical experience.
- [x] Define how practical experience grows through use, repetition, mistakes, and recovery from failure. Added practical experience growth rules to `Docs/KnowledgeAndSkillFoundation.md`, defining gain from meaningful use, diminishing returns for rote repetition, learning from readable mistakes, and extra credit for recovering well from failure.
- [x] Add first contextual learning prompts for fire safety, potable water, exposure, shelter placement, injury care, and resource identification. Added first contextual prompt specs to `Docs/KnowledgeAndSkillFoundation.md` for fire safety, potable water, exposure, shelter placement, injury care, and resource identification, with trigger examples, prompt intent, sample wording, and the rule that prompts explain immediate risk without pausing the game or forcing a quiz.
- [x] Design optional knowledge checks that appear when relevant to the action instead of interrupting basic play. Added optional knowledge-check rules to `Docs/KnowledgeAndSkillFoundation.md`, defining inline/skippable presentation, action-relevant timing, calm review moments, non-punitive wrong answers, and the rule that checks deepen understanding without gating the first survival loop.
- [x] Add player-facing feedback that explains why an action failed or produced poor results. Added failed-action and poor-result feedback rules to `Docs/KnowledgeAndSkillFoundation.md`, requiring short messages that say what happened, name one likely cause, offer one useful next step, avoid blame, and avoid revealing hidden formulas.
- [x] Define accessibility rules for the learning system: hints, retries, readable wording, no hard lockout from basic survival, and non-punitive practice paths. Added learning accessibility rules to `Docs/KnowledgeAndSkillFoundation.md`, covering reusable hints, retries, readable wording, non-color-only warnings, no lockout from basic survival, safer practice paths, and diminishing returns to prevent exploit loops.
- [ ] Define the MVP separation between knowledge, practical experience, physical stats, tools, and infrastructure.
- [ ] Add a first-pass skill taxonomy for survival, gathering, tool use, crafting, fire, shelter, navigation, first aid, food safety, and weather awareness.
- [ ] Define how knowledge affects survival actions: fewer mistakes, safer attempts, better yields, lower injury risk, and more reliable outcomes.
- [ ] Define how practical experience grows through use, repetition, mistakes, and recovery from failure.
- [ ] Add first contextual learning prompts for fire safety, potable water, exposure, shelter placement, injury care, and resource identification.
- [ ] Design optional knowledge checks that appear when relevant to the action instead of interrupting basic play.
- [ ] Add player-facing feedback that explains why an action failed or produced poor results.
- [ ] Define accessibility rules for the learning system: hints, retries, readable wording, no hard lockout from basic survival, and non-punitive practice paths.
- [ ] Define the first subject content format: topic, concepts, difficulty tier, prerequisite concepts, in-game effect, practice action, and source note.
- [ ] Add a small MVP question bank for elementary survival knowledge.
- [ ] Define when deeper questions should matter: quality improvements, safer work, complex crafting, teaching others, and advanced branches.
@@ -892,13 +957,68 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe
- [ ] Add persistence requirements for knowledge, skill experience, learned concepts, failed attempts, and tutorial state.
- [ ] Add multiplayer rules for teaching, observation, shared work, and group skill benefits.
## 0.1.S Investor Visual Recovery
- [x] Prevent broken first-view character/menu presentation before 0.2. Added a dedicated frontend presentation state that hides and freezes the pawn, suppresses gameplay/death HUD overlays, uses a clean temporary menu camera, and restores gameplay camera/input only after the player enters Ground Zero.
- [x] Improve MVP character presentation enough for investor builds before final character art. Added practical workwear, backpack, bedroll, and boot proxy layers on top of the selected Manny/Quinn meshes so the current characters read as near-future frontier survivors instead of untouched template dummies.
- [x] Raise the Ground Zero first-look vegetation density before 0.2. Expanded the repeatable Ground Zero setup from 64 trees, 148 shrubs, and 260 grass clumps to 96 trees, 220 shrubs, and 420 grass clumps while preserving spawn and sightline reservations.
- [x] Add first-look environment dressing near spawn. Added a focused ring of coastal oak, brush, grass mat, and rock variation actors around the safe spawn approach so the first investor camera view reads as a dressed coastal-scrub environment rather than empty tan terrain.
- [x] Tighten visual recovery verification. Updated character and Ground Zero environment verifiers so missing workwear proxy layers, lower foliage counts, or missing first-look variation actors fail before an investor build is called ready.
---
# Version 0.2 - Persistent Homesteading
Goal: Transition from temporary survival into lasting settlement and land stewardship.
Goal: first make the version 0.1 systems foundation visually credible enough
for investor review, then transition from temporary survival into lasting
settlement and land stewardship.
## 0.2.A Land And Claiming
## 0.2.0 Investor Visual Credibility Baseline
Purpose: replace the most visible MVP placeholders before adding deeper
homesteading systems, so every new 0.2 feature is built on a world that already
looks intentional, grounded, and investor-readable.
Awards bar: this milestone is not just cosmetic cleanup. It is the first pass
toward Agrarian's art-direction identity: real Earth tiles, believable materials,
coastal-scrub ecology, readable water, realistic frontier/post-collapse
characters, and a world that looks alive before the player touches anything.
Required order:
- [x] Replace or upgrade the terrain material first so Ground Zero no longer reads as flat tan placeholder ground. Rebuilt `M_AGR_GZ_Terrain_CoastalScrub` as a procedural coastal scrub material that blends dry soil, scrub green, and sandy path color families with broad and fine noise, documented the visual baseline, and extended the natural-environment verifier so flat constant-color terrain fails.
- [x] Replace or upgrade grasses, shrubs, and trees with believable coastal-scrub vegetation assets, density, color variation, scale variation, and LOD/performance limits. Added native generated coastal oak, coyote brush, and dry grass clump mesh assets under `/Game/Agrarian/Environment/Vegetation`, switched the Ground Zero foliage patch off engine basic shapes, rebuilt foliage materials with per-instance color variation, preserved investor-facing density and scale variation, added explicit HISM cull/shadow performance limits, and extended verifiers so basic-shape vegetation or missing cull limits fail.
- [x] Add the Asset acquisition and ingest pipeline before pulling more visuals: created approved staging folders, added `Docs/Art/AssetLicenses.md`, documented the pipeline in `Docs/Art/AgrarianAssetPipeline.md`, added `Scripts/verify_asset_pipeline_policy.py`, defaulted to Fab/free, Quixel, CC0/public-domain, team-created, or Nathan-supplied assets only, rejected random scraped internet assets, and prioritized trees, shrubs, grass, water, rocks, character bodies/outfits, and old abandoned equipment being reclaimed by nature.
- [x] Add the Ground Zero asset acquisition queue for the investor visual pass: documented free-only Fab candidates for shrubs, Mediterranean/coastal plants, grass, rocks/water-support props, and rural/reclaimed set dressing in `Docs/Art/GroundZeroAssetAcquisitionQueue.md`; added `Scripts/verify_ground_zero_asset_queue.py`; tied every candidate to the license register and staging workflow.
- [x] Add stable MVP pause menu save/exit/settings shell: Resume, Save Game, Settings, Save & Exit, and Quit Without Saving now have separate player-facing actions, keyboard shortcuts, and verifier coverage while deeper settings stay roadmapped.
- [ ] Replace or upgrade freshwater visuals with readable water surface, edge treatment, bank dressing, reflection/roughness tuning, and collectability cues.
- [ ] Replace or upgrade character bodies and clothing so selected characters read as realistic near-future post-collapse frontier people rather than template mannequins or proxy stacks.
- [ ] Replace or upgrade resource objects so wood, stone, fiber, edible plants, pickups, and gathered items look like world objects rather than debug primitives.
- [ ] Replace or upgrade fire and smoke so campfires have believable flame, ember, smoke, heat, and fuel-state visuals without requiring ray tracing.
- [ ] Replace or upgrade shelter pieces so primitive structures read as plausible built objects with material identity, not composed placeholder geometry.
- [ ] Replace or upgrade wildlife visuals so the first animal prototype reads as a living creature with appropriate silhouette, scale, material, and animation target.
- [ ] Add abandoned/reclaimed human-made set dressing near Ground Zero such as worn fencing, old equipment, broken pavement, weathered containers, and nature-overgrowth details that establish the near-future recovery tone without turning the game into generic apocalypse.
- [ ] Establish an investor screenshot composition checklist for first boot, story, credits, character selection, first spawn, water, vegetation, shelter, fire, resource interaction, and pause/save flow.
- [ ] Add automated visual placeholder audits for basic engine primitives, flat tan materials, missing foliage/water meshes, mannequin-only character presentation, and map-start camera placement.
- [ ] Verify the non-ray-traced default still looks credible; ray tracing remains optional/cinematic only.
- [ ] Capture fresh investor screenshots after the pass: startup/credits, character selection, first spawn, terrain, vegetation, water, campfire, shelter, pause/save flow, and one gameplay interaction.
- [ ] Do not start `0.2.A Tile Biome And Natural Resource Foundation` until this visual baseline is good enough to show without explaining that the world is still placeholder-heavy.
## 0.2.A Tile Biome And Natural Resource Foundation
- [x] Document the automatic biome selection and natural resource lifecycle direction in `Docs/World/BiomeAndNaturalResourceGenerationPlan.md`.
- [ ] Define tile biome profile schema with weighted biome blends derived from real-world signals.
- [ ] Add Ground Zero biome profile metadata.
- [ ] Register biome asset sets for terrain, trees, shrubs, grasses, rocks, water, wildlife, and reclaimed human-made props.
- [ ] Connect biome weights to procedural asset selection and placement density.
- [ ] Add persistent removable natural resource records for trees and shrubs first.
- [ ] Ensure removed trees and shrubs stay removed across save/load and server restart.
- [ ] Add stump/deadwood/disturbed-ground aftermath states.
- [ ] Add slow natural reseeding/regrowth rules based on realistic in-game time.
- [ ] Extend persistent resource records to rocks, edible plants, fiber, puddles, lakes, and reclaimed props.
- [ ] Add verifier coverage for biome profile loading, asset-set lookup, removal persistence, and regrowth timing.
## 0.2.B Land And Claiming
- [ ] Design land claim philosophy.
- [ ] Define claim size limits.
@@ -911,7 +1031,7 @@ Goal: Transition from temporary survival into lasting settlement and land stewar
- [ ] Add claim conflict rules.
- [ ] Add abandoned claim decay rules.
## 0.2.B Farming
## 0.2.C Farming
- [ ] Design soil model.
- [ ] Add basic soil quality.
@@ -975,13 +1095,38 @@ Goal: Transition from temporary survival into lasting settlement and land stewar
- [ ] Add barter container.
- [ ] Add simple trade UI.
- [ ] Add local market listing model for player-produced crops, meat, wood,
stone, fiber, tools, animals, fuel, and other transferable resources.
- [ ] Track market-facing item attributes: quantity, quality, freshness,
location, seller, ownership, storage condition, and pickup/delivery terms.
- [ ] Add item reservation/lock rules so listed goods cannot be double-sold,
consumed, moved, spoiled invisibly, or transferred while a sale is pending.
- [ ] Add ownership transfer.
- [ ] Add local price notes if needed.
- [x] Add AGR placeholder integration planning.
- [ ] Add AGR wallet visibility MVP: let a player link a public AGR wallet
address, query a trusted AGR node/API, and show the read-only AGR balance in
the HUD without storing private keys or enabling spending.
- [ ] Add linked-wallet profile storage with address format validation,
refresh/error states, and clear non-custodial language.
- [ ] Add server-side AGR balance lookup service contract so the Unreal client
does not talk directly to private node credentials.
- [ ] Add transaction logging.
- [ ] Add early business knowledge for bookkeeping, inventory, profit/loss, fair trade, basic credit, risk, and customer trust.
- [ ] Add simple workshop/business ownership rules for homestead-scale production.
## 0.2.F1 Player Options And Settings
- [ ] Define the settings persistence model so options survive save/load, packaged demos, and future multiplayer profile storage.
- [ ] Add preferred units for metric/imperial distance, weight, temperature, speed, volume, and field-size display.
- [ ] Add controls remapping UI for keyboard, mouse, and gamepad while preserving sane defaults for movement, sprint, crouch, prone, interact, menus, and camera.
- [ ] Add gameplay settings for autosave cadence, UI scale, hints, camera behavior, interaction prompts, and accessibility-friendly timing.
- [ ] Add graphics and hardware settings for quality presets, resolution/window mode, frame cap, foliage density, shadows, water, post-process, ray tracing optional toggles, and reset-to-safe defaults.
- [ ] Add audio settings for master, music, effects, ambient, voice, and cinematic volume.
- [ ] Add accessibility settings for subtitles, color/contrast, text scale, hold-versus-toggle interactions, motion comfort, and input assistance.
- [ ] Add account/server preferences for default server, last-used address, privacy-safe telemetry choice, and multiplayer connection display.
- [ ] Add verifier coverage for settings save/load, default migration, reset-to-default behavior, and packaged-demo menu access.
## 0.2.G Homesteading Knowledge Progression
- [ ] Define early profession paths: farmer, herder, carpenter, mason, cook, medic, hunter, fisher, trapper, trader, and scout.
@@ -1016,6 +1161,11 @@ Goal: Let player communities form organically through trade, trust, conflict, la
- [ ] Add positive reputation events.
- [ ] Add negative reputation events.
- [ ] Add reputation decay or locality rules.
- [ ] Add AGR wallet ownership verification by signed message or equivalent
non-custodial proof so a player can prove they control a linked address
without sharing a private key.
- [ ] Add privacy-safe wallet display rules so players can choose whether their
public AGR address or balance is visible to others.
## 0.3.B Trade And Contracts
@@ -1023,11 +1173,31 @@ Goal: Let player communities form organically through trade, trust, conflict, la
- [ ] Add secure trade window.
- [ ] Add barter offer records.
- [ ] Add basic contract data model.
- [ ] Add market stall and settlement market board flows where sellers can list
produced goods and buyers can browse, reserve, and purchase them naturally.
- [ ] Add direct player trade flow for face-to-face exchange of resources,
animals, crafted goods, tools, labor promises, or delivery contracts.
- [ ] Add delivery contract.
- [ ] Add labor contract.
- [ ] Add rental contract placeholder.
- [ ] Add contract completion rules.
- [ ] Add dispute placeholder.
- [ ] Add AGR payment request flow for player-to-player trade: seller creates a
payment request, buyer pays through their own wallet, and the server verifies
chain confirmation before marking the contract paid.
- [ ] Add `Buy with AGR` market flow: reserve item, create payment request,
wait for confirmation, transfer ownership, release pickup/delivery rights, and
record seller/buyer receipts.
- [ ] Add AGR transaction receipt records linked to game accounts, trades,
contracts, timestamps, confirmations, and settlement/dispute state.
- [ ] Add timeout and cancellation rules so unpaid or failed AGR purchases
unlock the item and return the listing to the market.
- [ ] Add low-value fast-confirmation policy research for future UX, with fraud
limits and rollback/dispute handling before any pending-payment delivery is
allowed.
- [ ] Decide whether player-to-player AGR trades use direct payment only,
server-observed escrow, or a separate escrow service after legal and security
review.
## 0.3.C Crime And Consequences
@@ -1211,10 +1381,14 @@ Goal: Enable cities, citizenship, taxation, diplomacy, organized law, warfare, a
- [ ] Add treasury model.
- [ ] Add tax rules.
- [ ] Add settlement market fee rules for market stalls, board listings,
storage usage, delivery contracts, and public trade infrastructure.
- [ ] Add public storage.
- [ ] Add road funding.
- [ ] Add public building funding.
- [ ] Add treasury audit logs.
- [ ] Add optional AGR-backed settlement treasury design only after wallet
linking, payment verification, audit logs, and legal review are mature.
## 0.5.D Diplomacy
@@ -1355,29 +1529,16 @@ Goal: Expand from a small test world toward a huge, regionally diverse, persiste
## 0.7.B Biome Diversity
- [ ] Define a layered real-world biome architecture instead of a simplified school-model biome list.
- [ ] Define the biome data contract for each 1 km tile: macro biome weights, regional ecological region, local sub-biome blend, confidence score, source datasets, generation version, and manual override fields.
- [ ] Define core climate bands that drive temperature, daylight, seasonality, and weather behavior: polar, subpolar, boreal, cool temperate, warm temperate, subtropical, tropical, and highland/alpine.
- [ ] Define roughly 15-25 macro biome families as the maintainable top-level simulation vocabulary.
- [ ] Define roughly 60-120 regional biome variants so recognizable places such as the Pacific Northwest, Siberia, Patagonia, the Great Plains, Mongolian Steppe, Scottish Highlands, Amazon Basin, and Mediterranean coasts can emerge without one-off labels.
- [ ] Define procedural local sub-biome blending so tiles can carry weighted mixtures such as prairie, riparian woodland, marsh, rocky slope, or scrub edge instead of hard biome borders.
- [ ] Derive biome weights from latitude, elevation, rainfall, prevailing wind, ocean proximity, ocean currents, soil type, drainage, seasonal temperature swing, rain shadow effects, and river/wetland systems.
- [ ] Add biome inference rules for real Earth patterns: subtropical west-coast Mediterranean climates, humid east coasts, interior continental steppe/prairie, leeward mountain deserts, equatorial alpine tundra, river-valley fertility, and coastal/marine moderation.
- [ ] Add forest biome variants: tropical rainforest, tropical seasonal forest, temperate deciduous forest, temperate rainforest, boreal conifer forest, cloud forest, and mangrove forest.
- [ ] Add grassland biome variants: savanna, prairie, steppe, pampas, and alpine meadow.
- [ ] Add dryland biome variants: hot desert, cold desert, semi-arid scrubland, Mediterranean shrubland, and badlands.
- [ ] Add cold-region biome variants: arctic tundra, alpine tundra, ice sheet, and glacier.
- [ ] Add wet-system biome variants: swamp, marsh, fen/bog, river delta, and floodplain.
- [ ] Add aquatic and coastal biome variants: freshwater river, freshwater lake, estuary, rocky coast, sandy coast, coral reef, and kelp forest.
- [ ] Add a climate-engine layer that produces the environmental variables biome generation consumes.
- [ ] Add a biome-generator layer that assigns weighted biome and regional-variant outputs to each tile.
- [ ] Add an ecology layer that converts biome weights into plausible plants, animals, diseases, soil fertility, water access, fuel availability, and building-material availability.
- [ ] Add a human-use layer that converts biome/ecology outputs into crop viability, settlement density, trade value, culture pressure, transportation difficulty, warfare strategy, and long-term economic specialization.
- [ ] Make crop viability, animal species, disease pressure, water access, building materials, fuel availability, trade economics, settlement density, cultural evolution, transportation difficulty, and warfare strategy consume the same biome/ecology tile metadata instead of separate hand-authored rules.
- [ ] Map natural resources to likely real-world geology, flora, water, climate, soil, and land-cover data.
- [ ] Add biome-specific survival pressure for exposure, thirst, food reliability, fire risk, disease risk, travel friction, shelter material scarcity, and visibility.
- [ ] Add biome plausibility QA checks that reject tiles whose climate, vegetation, water, soil, and resource placement contradict the represented real-world location.
- [ ] Keep Ground Zero as an early coastal-scrub proof tile, but require future generated tiles to move toward this layered biome contract as the Earth-scale pipeline matures.
- [ ] Derive biome candidates from real-world land-cover, climate, elevation, and water data.
- [ ] Add forest biome.
- [ ] Add plains biome.
- [ ] Add mountain biome.
- [ ] Add wetland biome.
- [ ] Add desert/dryland biome.
- [ ] Add cold biome.
- [ ] Add biome-specific resources.
- [ ] Map natural resources to likely real-world geology, flora, water, and climate.
- [ ] Add biome-specific survival pressure.
## 0.7.C Logistics And Transportation
@@ -1662,6 +1823,25 @@ These tracks run across all phases and must not be left as afterthoughts.
- [x] Define market transaction logs.
- [x] Define bridge between web wallet and game account.
- [x] Define legal/compliance review points.
- [ ] Phase 1 / `0.2.F`: non-custodial wallet link and read-only AGR balance
display in the HUD.
- [ ] Phase 2 / `0.3.A`: prove wallet ownership with signed messages or an
equivalent non-custodial verification flow.
- [ ] Phase 3 / `0.3.B`: verify player-to-player AGR payment requests from
external wallets before in-game trade completion.
- [ ] Phase 4 / `0.3.B` to `0.5.C`: add transaction receipts, dispute records,
settlement treasury concepts, and audit logs.
- [ ] Phase 5 / post-security review: decide whether Agrarian ever offers a
website wallet or custodial wallet; do not store player private keys in the
game client or dedicated server before that review.
- [ ] Outside-game dependency: operate a trusted AGR full node, indexer or
explorer API, and backend balance/transaction verification service reachable
by the game server.
- [ ] Outside-game dependency: provide Qt wallet, website wallet, or companion
wallet signing/payment UX for players before spending is enabled.
- [ ] Outside-game dependency: complete legal/compliance review for real-value
currency use, marketplace payments, fees, custody, refunds, taxes, and
regional availability before enabling player-to-player AGR spending.
## E. Admin And Moderation
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+90
View File
@@ -0,0 +1,90 @@
# LinaAI Knowledge Map
This map defines the project memory LinaAI should consult before working. It is
not a replacement for reading source files. It is the first-read order for
context.
## First Read For Agrarian
1. `Docs/AI/LinaAIOperatingManual.md`
2. `Docs/AI/LocalAgentGuardrails.md`
3. `Docs/AI/LinaAISecretsPolicy.md`
4. `Docs/Ops/HANDOFF.md`
5. `AGRARIAN_DEVELOPMENT_ROADMAP.md`
6. `Docs/CoreDesignDocument.md`
7. `Docs/TechnicalDesignDocument.md`
8. `Docs/SixMonthMvpDefinition.md`
9. `Docs/MvpSurvivalReadinessCriteria.md`
10. `Docs/Investor/InvestorDemoAcceptanceGate.md`
## System-Specific Internal Docs
Use these before touching the named area.
- Visuals and assets:
`Docs/Art/AgrarianAssetPipeline.md`,
`Docs/Art/AssetLicenses.md`,
`Docs/Art/GroundZeroAssetAcquisitionQueue.md`,
`Docs/Rendering/InvestorRenderingPresets.md`.
- Terrain and world generation:
`Docs/Terrain/GroundZeroTile.md`,
`Docs/Terrain/GroundZeroNaturalEnvironmentPass.md`,
`Docs/Terrain/UnrealLandscapeImportPlan.md`,
`Docs/World/BiomeAndNaturalResourceGenerationPlan.md`.
- Persistence:
`Docs/PersistenceDesignDocument.md`,
`Docs/Ops/PersistenceSaveRecoveryPlan.md`.
- Multiplayer:
`Docs/MultiplayerNetworkingDesign.md`,
`Docs/Ops/DedicatedServerBuildRunbook.md`,
`Docs/Ops/MultiplayerLatencyTestPlan.md`.
- Economy and AGR:
`Docs/EconomyAndAgrDesignDocument.md`.
- Infrastructure and repo policy:
`Docs/RepositoryStoragePolicy.md`,
`Docs/BranchingConventions.md`,
`Docs/CommitMessageConventions.md`,
`Docs/BackupExpectations.md`.
## Official Vendor Documentation
Refresh local cached copies with `Scripts/linaai_refresh_knowledge.sh`. The
tracked repo stores URLs and notes; downloaded vendor pages stay under ignored
`Saved/LinaAIKnowledge/`.
- Unraid docs: `https://docs.unraid.net/`
- Unreal Engine 5.7 docs: `https://dev.epicgames.com/documentation/en-us/unreal-engine`
- Laravel 12 docs: `https://laravel.com/docs/12.x`
- MySQL 8.4 Reference Manual: `https://dev.mysql.com/doc/refman/8.4/en/`
- Gitea docs: `https://docs.gitea.com/`
- Ollama docs: `https://docs.ollama.com/`
- Open WebUI docs: `https://docs.openwebui.com/`
- Aider docs: `https://aider.chat/docs/`
## Infrastructure Map
- Gitea: `http://192.168.5.21:3000`
- Agrarian game repo:
`http://192.168.5.21:3000/nathan/agrarian-game.git`
- Ollama: `http://192.168.5.23:11434`
- Open WebUI: `http://192.168.5.26:8085`
- LinaAI VM: `192.168.5.27`
- Unreal build VM: `192.168.5.20`
- Unraid host: `192.168.5.8`
Credentials are intentionally not listed here. See
`Docs/AI/LinaAISecretsPolicy.md`.
## Knowledge Refresh Rule
Run a knowledge refresh before large planning, repo migrations, infrastructure
changes, or when vendor behavior matters:
```bash
Scripts/linaai_refresh_knowledge.sh
Scripts/linaai_bootstrap_context.sh
```
If downloaded docs are unavailable, LinaAI should continue with tracked project
docs and state the gap in its evidence.
+100
View File
@@ -0,0 +1,100 @@
# LinaAI Operating Manual
LinaAI is the self-hosted AI development worker for Agrarian and related
projects. Its purpose is to provide project memory, safe local assistance,
repeatable repo inspection, small supervised edits, build/test automation, and
clean escalation to Codex when the local model is not the right tool.
LinaAI is not a fully autonomous developer and must not pretend to be one.
## Primary Workflow
Use `Scripts/linaai_task.sh` from the repository root for normal work.
1. Gather repo evidence before any edits.
2. Run local Qwen/Ollama preflight for risk and confidence.
3. Route to Codex when confidence is below threshold or task risk is high.
4. Use Aider only for narrow, supervised local edits.
5. Verify with the most relevant script, test, compile, or dry-run command.
6. Leave a clear status file under `Saved/AiTaskStatus/`.
7. Commit only after human-approved workflow requires it.
Default confidence threshold is `0.75`. This is intentionally conservative.
## What LinaAI Can Handle Locally
- Summarize project docs and repo structure.
- Create or update documentation.
- Generate focused tests or verification scripts.
- Inspect logs and produce likely causes.
- Make small, low-risk code or script edits.
- Prepare structured Codex handoffs.
- Update project memory after a completed task.
## What Must Escalate
Escalate to Codex or human review before editing when a task touches:
- Unreal core architecture.
- Save/load and persistence.
- Multiplayer, networking, or replication.
- AGR wallet, payments, marketplace, or economy transfer logic.
- Auth, security, secrets, deployment keys, or production migrations.
- Broad refactors or large cross-system changes.
- Anything where local evidence is thin or contradictory.
## Required Evidence
Before proposing changes, LinaAI must identify the evidence it checked:
- Internal docs read.
- Files inspected.
- Commands run.
- Build/test/log results.
- Official vendor docs consulted when framework behavior matters.
If it has not inspected evidence, confidence must be below `0.65`.
## Branch And Commit Rules
- Never merge directly to `main`.
- Never commit generated caches under `Saved/`.
- Never commit raw credentials.
- Prefer small branches and small commits.
- Do not rewrite unrelated history.
- Do not revert user work unless explicitly instructed.
## Build Host Boundaries
- `LinaAI` owns AI tooling, Aider, Codex CLI escalation, repo memory, scripts,
and small supervised branch work.
- `unreal-engine` owns Unreal Engine source, editor builds, commandlets, and
package verification.
- Gitea is the source of truth for current private development repositories.
## Normal Commands
Refresh the local knowledge cache:
```bash
Scripts/linaai_refresh_knowledge.sh
```
Build a compact local context file from tracked docs:
```bash
Scripts/linaai_bootstrap_context.sh
```
Run a supervised task:
```bash
Scripts/linaai_task.sh "Summarize what docs should be read before changing terrain visuals."
```
Force-test Codex routing:
```bash
Scripts/linaai_task.sh --force-escalate "Test Codex route only. Do not edit files."
```
+56
View File
@@ -0,0 +1,56 @@
# LinaAI Secrets Policy
LinaAI must never store raw secrets in tracked docs, model prompts, logs,
knowledge caches, commits, issue bodies, or handoff summaries.
This includes:
- Passwords.
- API keys and tokens.
- SSH private keys.
- Wallet private keys, seed phrases, or recovery phrases.
- Database passwords.
- Production webhook secrets.
- Cloud provider credentials.
## Allowed Context
LinaAI may store and use non-secret operational context:
- Hostnames and IP addresses.
- Public ports.
- Repository URLs.
- Service roles.
- Usernames when needed for operational clarity.
- Credential source names, such as "human approval required" or "use existing
SSH agent".
## Disallowed Context
LinaAI must not copy plaintext credentials from chat, terminal history, handoff
files, screenshots, `.env` files, config files, or password managers into its
own docs or prompts.
If a task requires a secret, LinaAI should:
1. Explain which credential is needed.
2. Use an existing secure mechanism if already configured, such as SSH keys,
an OS credential store, or an environment variable.
3. Ask the human to perform the login or provide the credential interactively.
4. Redact the credential from logs and summaries.
## Repo And Cache Hygiene
- `Saved/` is ignored and may hold local task state, but it is still not a safe
place for raw secrets.
- Knowledge refresh scripts must not scrape or package `.env`, private key,
wallet, token, browser profile, or password manager files.
- Before commits, run `git status --short` and inspect any newly tracked docs or
scripts for accidental secrets.
## AI Prompt Rule
When prompting Qwen, Aider, or Codex, include service names and endpoints only.
Do not include passwords or tokens. If Codex needs a privileged action, use the
existing shell/SSH session or ask for explicit human approval.
+53
View File
@@ -0,0 +1,53 @@
# Local AI Agent Guardrails
These rules apply to any self-hosted AI coding assistant working on Agrarian.
## Mandatory Behavior
- Inspect existing project patterns before proposing changes.
- Classify every task as `low`, `medium`, or `high` risk.
- State evidence checked: files, docs, commands, logs, and build results.
- Make the smallest useful change.
- Do not refactor unrelated code.
- Do not invent APIs or project conventions.
- Do not merge directly to `main`.
- Do not store secrets, private keys, wallet keys, passwords, or tokens in the
repo.
- Passing tests/builds matter more than finishing the task.
## Stop And Escalate
Stop local work and prepare a Codex handoff when any of these are true:
- confidence is below `0.75`,
- tests fail twice,
- build fails twice,
- Unreal compile errors persist after one focused fix,
- the task touches security, auth, payments, AGR wallet integration, save/load,
multiplayer, marketplace logic, migrations, or core engine architecture,
- the diff touches more files than the brief allows,
- local model context is overloaded,
- the model cannot point to evidence for its recommendation.
## Required Handoff Format
```text
Project:
Branch:
Goal:
Risk:
Confidence:
Files inspected:
Files changed:
Commands run:
Errors:
What local AI tried:
Why local AI stopped:
Requested Codex action:
```
## Quality Bar
Local AI is useful only when it is disciplined. It should be allowed to be
uncertain, but it should not be allowed to be vague, overconfident, or
unverified.
+122
View File
@@ -0,0 +1,122 @@
# Agrarian Self-Hosted AI Development Stack
This stack is meant to reduce pressure on Codex over time, not replace it
immediately. The first production target is supervised local assistance:
repository awareness, documentation, small safe edits, tests/builds, and clear
Codex escalation when local tooling is over its head.
## Current Services
- Gitea:
`http://192.168.5.21:3000/nathan/agrarian-game.git`
- Ollama:
`http://192.168.5.23:11434`
- Open WebUI:
`http://192.168.5.26:8085`
- Local AI worker VM:
`LinaAI / 192.168.5.27`
- Primary Unreal/Linux development VM:
`unreal-engine / 192.168.5.20`
## Current Local Model
- `qwen2.5-coder:7b`
- Role:
repo summaries, documentation, small patch suggestions, test generation,
straightforward scripts, and structured handoff preparation.
- Not the role:
broad Unreal architecture changes, risky save/multiplayer/economy rewrites,
security-sensitive code, or autonomous merges.
## Operating Model
1. Refresh LinaAI project memory when context may be stale:
`Scripts/linaai_refresh_knowledge.sh`.
2. Build a compact local context pack when needed:
`Scripts/linaai_bootstrap_context.sh`.
3. Start with `Scripts/linaai_task.sh`, not raw Aider, for normal work.
4. Qwen/Ollama performs a preflight risk and confidence check.
5. Default confidence threshold is `0.75`.
6. High-risk tasks or low-confidence tasks route to Codex automatically.
7. Aider runs only for acceptable supervised local work.
8. If Aider fails, `Scripts/linaai_task.sh` writes a status file and calls
Codex through `Scripts/ai_codex_escalate.sh`.
9. Codex escalation uses the npm Codex CLI, not the API.
10. Human review controls merges.
The operating manual, knowledge map, and secrets policy live in:
- `Docs/AI/LinaAIOperatingManual.md`
- `Docs/AI/LinaAIKnowledgeMap.md`
- `Docs/AI/LinaAISecretsPolicy.md`
## Codex Escalation
Use `Scripts/ai_codex_escalate.sh` with a completed task status file. The
script prefers a locally installed `codex` command and falls back to
`npx -y @openai/codex exec`.
For normal tasks, use:
```bash
cd ~/repos/AgrarianGame
Scripts/linaai_refresh_knowledge.sh
Scripts/linaai_bootstrap_context.sh
Scripts/linaai_task.sh "your task here"
```
For a terminal conversation that feels closer to this Codex workflow, use:
```bash
linaai
```
That opens an interactive prompt. Each instruction is routed through
`Scripts/linaai_task.sh`, so Qwen preflight, Aider local edits, and Codex
fallback still apply. Use `/status`, `/refresh`, `/dry on`, `/codex on`, and
`/exit` inside the prompt.
To test automatic escalation without editing files:
```bash
Scripts/linaai_task.sh --dry-run --force-escalate "Test escalation path only."
```
On `LinaAI`, the npm Codex CLI is installed, but it still needs an authenticated
Codex login before cloud escalation can run:
```bash
ssh nathan@192.168.5.27
codex login
```
Codex should be called for:
- confidence below `0.75`,
- two failed build/test attempts,
- Unreal compile errors that persist,
- tasks touching save systems, multiplayer, auth, payments, AGR wallet
integration, marketplace logic, migrations, or core architecture,
- patches that grow beyond the intended small scope,
- contradictions between local model output and official/project docs.
## VM Boundaries
- `LinaAI` owns local AI coding tools, Aider, Codex CLI escalation wrappers,
repo indexing, documentation generation, and small supervised branch work.
- `unreal-engine` owns Unreal Engine source, editor builds, commandlets, and
game compile/package verification.
- Keep these roles separated so AI tooling experiments do not destabilize the
Unreal build host.
## Immediate Next Work
- Verify Open WebUI model selection uses the Ollama backend at
`http://192.168.5.23:11434`.
- Use Aider from `LinaAI`, not from `unreal-engine`.
- Authenticate the npm Codex CLI on `LinaAI` so escalation can run from the AI
worker VM.
- Build project memory inside this repo under `Docs/` rather than creating a
separate documentation repository.
- Add small local-agent tasks first: summarize systems, write docs, generate
tests, inspect logs, and prepare Codex handoffs.
+88
View File
@@ -0,0 +1,88 @@
# Agrarian Asset Pipeline
Purpose: replace placeholder visuals with realistic, licensed, performant
assets while keeping the project clean enough to scale across Earth-sized
tiles.
## Visual Direction
Agrarian should read as realistic modern post-collapse frontier survival:
damaged but recoverable, practical, lived-in, and grounded. The world should
not look cartoonish, old-west, or exaggerated apocalypse junkyard.
## First Asset Priorities
1. Coastal scrub trees.
2. Shrubs and bushes.
3. Grasses and ground cover.
4. Water, banks, wet edges, and shoreline dressing.
5. Rocks, terrain decals, and material detail.
6. Two to four human character bodies/outfits.
7. Old abandoned equipment starting to be overtaken by nature.
## Approved Sources
- Fab free assets. Paid Fab assets are blocked unless Nathan explicitly approves
the purchase in a later task.
- Quixel/Megascans assets available under the current Epic/Unreal terms.
- CC0/public-domain art libraries.
- Assets created internally.
- Assets Nathan manually adds to the staging folder with permission to use.
Do not scrape random internet images or models. If an asset cannot be traced to
a usable license, reject it.
## Free-Only Lockdown
The asset pipeline is currently free-only. Before download or import, verify
that the asset page is marked free or that the asset is project-owned/internal.
Record the cost in `Docs/Art/AssetLicenses.md` as `Free`, `$0`, `0`, or `N/A`.
Do not click purchase, checkout, add payment, subscription, or paid-license
flows. If an asset looks useful but is not free, record it as a candidate in
notes outside the import flow and wait for explicit approval.
## Staging Workflow
1. Place downloaded/manual assets in:
`/home/nathan/AssetStaging/Agrarian/Incoming`
2. Save license evidence in:
`/home/nathan/AssetStaging/Agrarian/LicenseEvidence`
3. Review license and visual fit.
4. Move acceptable assets to:
`/home/nathan/AssetStaging/Agrarian/Approved`
5. Import into Unreal under the correct project path:
`/Game/Agrarian/Environment`, `/Game/Agrarian/Characters`,
`/Game/Agrarian/Props`, or `/Game/Agrarian/Effects`.
6. Rename using Agrarian naming policy.
7. Generate or verify:
- materials/material instances
- collision
- LODs or Nanite settings
- texture size limits
- foliage cull distances where relevant
- gameplay tags or placement metadata where relevant
8. Record the asset in `Docs/Art/AssetLicenses.md`.
9. Run visual and placeholder verifiers before packaging a demo.
## Unreal Import Notes
- Foliage should use HISM/foliage-friendly meshes with cull distances and
sensible material complexity.
- Nanite may be used for rigid static meshes where it improves visual density,
but grass and alpha-heavy foliage still need performance testing.
- Characters and animals must not be imported as static showcase meshes if
gameplay requires animation. They need skeletal meshes, animation targets,
collision, and gameplay integration.
- Water should be handled as a shader/system problem, not a generated model.
## Rejection Rules
Reject or quarantine assets that:
- have unclear licensing.
- require attribution we cannot satisfy in-game or in shipped notices.
- are visibly stylized against the realism target.
- are too high-poly or texture-heavy without a practical optimization path.
- include unrelated branding, logos, watermarks, or embedded marketplace demo
content.
+65
View File
@@ -0,0 +1,65 @@
# Agrarian Asset License Register
Every non-original art asset imported into Agrarian must be recorded here
before it is used in a playable map, packaged demo, screenshot, or trailer.
Allowed default sources:
- Fab assets explicitly marked free. Paid Fab assets are blocked unless Nathan
explicitly approves a purchase in a later task.
- Quixel/Megascans assets available under the current Epic/Unreal terms for
this project.
- CC0 or public-domain assets.
- Assets created by the Agrarian team.
- Assets manually supplied by Nathan with permission to use in the game.
Do not import random internet images, models, scans, or textures unless the
license is clear, compatible with commercial game use, and recorded below.
## Staging Policy
Asset staging root on the Unreal Ubuntu VM:
`/home/nathan/AssetStaging/Agrarian`
Expected subfolders:
- `Incoming`: newly downloaded or manually supplied assets.
- `LicenseEvidence`: screenshots, text exports, or links proving license terms.
- `Approved`: assets reviewed and ready for Unreal import.
- `Processed`: assets imported, optimized, renamed, and verified.
- `Rejected`: assets that should not be used.
## Free-Only Acquisition Gate
Until Nathan explicitly approves a paid purchase, every third-party asset must
have `Cost` recorded as `Free`, `$0`, `0`, or `N/A` for project-owned/internal
assets. Do not use "purchased", "paid", a dollar amount above zero, or blank
cost values in the register.
Any asset with uncertain cost, marketplace bundle requirements, subscription
requirements, or unclear entitlement belongs in `Rejected` until reviewed.
## Naming Policy
Use project-readable names before import:
- Static meshes: `SM_<Category>_<SpeciesOrObject>_<Variant>`
- Skeletal meshes: `SK_<Category>_<Name>_<Variant>`
- Materials: `M_<Category>_<Name>` or `MI_<Category>_<Name>_<Variant>`
- Textures: `T_<Category>_<Name>_<MapType>`
- Niagara systems: `NS_<Effect>_<Variant>`
Examples:
- `SM_Tree_CoastalOak_A`
- `SM_Shrub_CoyoteBrush_B`
- `MI_Ground_CoastalScrub_Dry`
- `SK_Human_FrontierAdult_A`
- `SM_Equipment_OvergrownTractor_A`
## License Entries
| Asset | Type | Source | License | Cost | Imported Path | Notes |
| --- | --- | --- | --- | --- | --- | --- |
| Native Ground Zero proxy vegetation | Tree/shrub/grass placeholders | Created in project | Project-owned | N/A | `/Game/Agrarian/Environment/Vegetation` | Generated proxy meshes; replace with licensed/production assets during 0.2.0 visual credibility work. |
@@ -0,0 +1,55 @@
# Ground Zero Asset Acquisition Queue
Purpose: make the first playable tile visually investor-ready using only
free/approved assets, while keeping every source traceable before import.
Current rule: free-only. Do not download or import paid assets unless Nathan
explicitly approves a purchase in a later task.
## Priority Look
Ground Zero should feel like a believable coastal-scrub survival location after
a modern social collapse: dry soil, uneven grasses, hardy shrubs, believable
trees, readable water, natural rocks, practical frontier characters, and old
equipment being reclaimed by vegetation.
## Free Fab Candidates
| Priority | Asset | Source | Use | Cost | Status | Notes |
| --- | --- | --- | --- | --- | --- | --- |
| 1 | Free Shrubs Pack (Ultra Realistic Wind) | https://www.fab.com/listings/7ca465ab-fb9c-4d6b-bddb-82c20f604657 | Replace Ground Zero shrub proxies | Free | Candidate | Realistic shrub pack with 11 shrubs, opaque Nanite meshes, and Pivot Painter wind. Good near-term replacement for coyote-brush proxy silhouettes. |
| 2 | Mediterranean Vegetation: Plant Pack I | https://www.fab.com/listings/41b3889d-1a98-41e1-850e-c8b417840da0 | Coastal-scrub plants, agave-like accent plants, scanned ground detail | Free | Candidate | Free but currently flagged as not available in this region from unauthenticated browsing; check again through the Epic/Fab account before relying on it. |
| 3 | temperate Vegetation: optimized Grass Library | https://www.fab.com/listings/8b68642e-35f4-438e-82b4-799fc2228303 | Replace dry grass cards and add wind/detail | Free | Candidate | Free but currently flagged as not available in this region from unauthenticated browsing; verify through the account. |
| 4 | Soul: Cave | https://www.fab.com/listings/75f42402-40bb-4a1b-b557-18e2c9604273 | Rock, wet-edge, ruin, and water-support props/materials | Free | Candidate | Epic sample content with rock and water props/materials; useful for banks, stone resources, and visual dressing even if not coastal-specific. |
| 5 | Modular Rural House & Pine Forest Environment | https://www.fab.com/listings/a081748c-6a49-4ba4-9008-9b10fadf8f73 | Abandoned rural structures, props, roads, weeds, and potential reclaimed-equipment set dressing | Free | Candidate | Useful for post-collapse frontier settlement mood; pine assets may not fit Ground Zero coastal scrub but props/roads/house pieces may. |
## Acquisition Steps
1. Log into Fab/Epic only in an active browser or launcher session.
2. Confirm each listing still shows `Free`.
3. Add only free assets to the library/cart.
4. Download/export into `/home/nathan/AssetStaging/Agrarian/Incoming`.
5. Save a license/cost screenshot or text export into
`/home/nathan/AssetStaging/Agrarian/LicenseEvidence`.
6. Move only approved free assets to `/home/nathan/AssetStaging/Agrarian/Approved`.
7. Import into Unreal under:
- `/Game/Agrarian/Environment/Vegetation`
- `/Game/Agrarian/Environment/Water`
- `/Game/Agrarian/Environment/Rocks`
- `/Game/Agrarian/Props/Reclaimed`
- `/Game/Agrarian/Characters`
8. Add imported assets to `Docs/Art/AssetLicenses.md`.
9. Run:
- `Scripts/verify_asset_pipeline_policy.py`
- `Scripts/verify_asset_license_free_only.py`
- relevant visual/placeholder verifiers
## Import Standards
- Replace proxy assets only after imported meshes have collision, materials,
cull distances, and LOD/Nanite decisions.
- Do not rely on showcase maps. Extract only the specific assets needed for
Agrarian's map, biome, and resource systems.
- Any tree, shrub, rock, puddle, lake, resource node, or large prop placed in a
playable tile must eventually be represented by a persistent world-resource record
so player removal is durable.
+17 -31
View File
@@ -1,39 +1,25 @@
# MVP Character Proxies
The 0.1.O investor visual pass introduces first playable character proxies for
the startup character selection flow. These are not final production humans;
they are practical, human-scale stand-ins that remove the single default dummy
presentation while the final realistic character art pipeline is still pending.
The current investor build uses MVP character proxies, not final production humans.
## Current Proxies
Selection currently supports:
- Young adult male:
`/Game/Characters/Mannequins/Meshes/SKM_Manny_Simple`
- Young adult female:
`/Game/Characters/Mannequins/Meshes/SKM_Quinn_Simple`
- Male workwear material:
`/Game/Agrarian/Characters/Materials/M_AGR_CharacterProxy_Workwear_Male`
- Female workwear material:
`/Game/Agrarian/Characters/Materials/M_AGR_CharacterProxy_Workwear_Female`
- `SKM_Manny_Simple` as the young adult male base.
- `SKM_Quinn_Simple` as the young adult female base.
The materials use muted earth-tone workwear colors so the character reads as an
Agrarian survivor/pioneer proxy instead of an untouched template dummy.
Agrarian applies project-owned workwear materials and runtime presentation
layers for torso clothing, backpack, bedroll, and boots. These layers are meant
to move the build away from untouched Unreal template dummies while still
remaining honest prototype art.
## Runtime Flow
Visual direction:
The MVP frontend stores the selected young-adult archetype. When the loading
segment closes, it issues `AgrarianSelectCharacter male` or
`AgrarianSelectCharacter female`. The player controller records the selected
proxy and applies the matching mesh/material to the possessed Agrarian player
character.
- realistic near-future frontier survival;
- practical modern workwear;
- post-collapse scarcity without apocalypse-warrior exaggeration;
- settlement-builder tone;
- a few archetypes that can later respond to age, health, injury, nutrition,
and long-term physical condition.
The character asset folder is always cooked for investor builds so both proxy
materials remain available in packaged clients.
## Replacement Path
Final character work should replace these proxies with grounded realistic human
assets, production clothing, age/condition variation, and replication/persistence
of visual state. The current C++ selection path is intentionally simple so final
assets can slot into the same male/female archetype switch without reworking the
menu flow.
Final character work should replace these proxies with original or properly
licensed free character bases before public testing.
+112
View File
@@ -0,0 +1,112 @@
# Codebase Readiness Review
Date: 2026-05-21
Scope: source code, build scripts, verification scripts, config files, and the
roadmap after completion of `0.1.S` and migration to the Ubuntu Unreal build
VM.
## 2026-05-21 Restart Audit Summary
- Active game checkout is clean on `main` at
`/home/nathan/UnrealProjects/AgrarianGame`.
- Active remote is self-hosted Gitea:
`http://192.168.5.21:3000/nathan/agrarian-game.git`.
- Repository currently tracks 723 files, including 300 Git LFS assets.
- `git lfs fsck` passes.
- Ignored Unreal-generated folders exist locally from verification builds:
`Binaries/`, `Intermediate/`, `Saved/`, and `DerivedDataCache/`.
- Those generated folders are ignored by `.gitignore` and are not part of the
repository state.
- Unreal Engine `5.7.4-release` is built from source at
`/opt/UnrealEngine-5.7`.
- `AgrarianGameEditor Linux Development` builds successfully on the Ubuntu VM;
the latest incremental check reported `Target is up to date` and
`Result: Succeeded`.
- Headless project load through the Linux source-built editor succeeds in
`NullRHI` mode with `0 error(s)`.
- The old roadmap project-location block was stale and has been reset to the
Ubuntu Unreal VM plus Gitea workflow.
- The next coding phase should begin with exactly
`0.2.0 Investor Visual Credibility Baseline`, then move to
`0.2.A Land And Claiming` after the investor-facing world no longer reads as
placeholder-heavy.
## 0.1 Completion Check
All `0.1.A` through `0.1.S` roadmap checkboxes are complete. The remaining
unchecked roadmap items before `Version 0.2` are North Star and philosophy
statements, not incomplete `0.1` implementation tasks.
## Current Strengths
- Core gameplay systems are separated into recognizable Unreal classes:
character, controller, game state, survival, inventory, crafting, building,
persistence, weather, resource nodes, wildlife, shelter, campfire, water, UI,
and automation.
- Server-authoritative paths are already present for core multiplayer actions
such as crafting, item use/drop/splitting, travel, save/load, respawn, fire,
resource depletion, wildlife state, world actors, and weather state.
- Persistence has a central subsystem and explicit world actor/resource/player
capture paths, which is the right base for 0.2 homesteading.
- The project now has broad verifier coverage for roadmap promises and MVP
guardrails. This is useful for preventing accidental regression while the team
moves into larger systems.
- The Windows build pipeline, package script, visual QA gate, and Linux server
target are established enough to keep milestone work shippable.
- The Ubuntu source-built Unreal lane now gives Codex a headless, local-disk
development path that is better suited to automated C++ work than the old
mapped-drive Windows workflow.
## Cleanup Findings
- `AgrarianEditorAutomationLibrary.cpp` is doing too much. It contains map
import, gameplay smoke tests, persistence tests, and setup helpers in one
large file. It should be split by responsibility before 0.2 adds claiming,
farming, storage, and household tests.
- The MVP frontend is still implemented as runtime-created widget trees. This
has been effective for fast iteration, but 0.2 should move toward reusable
UMG widgets, shared styling, and a cleaner input/navigation model.
- Placeholder/proxy meshes are intentionally still present for resources,
shelters, campfires, water, wildlife, and characters. 0.2 should replace the
highest-impact placeholders with durable realistic assets while preserving
data-driven class behavior.
- Several systems use hard-coded Ground Zero, MVP, and automation constants.
These are acceptable for 0.1 but should move toward data assets, config, or
tile metadata as 0.2 introduces claims, crops, animals, storage, and
homestead state.
- Admin/dev console commands currently share runtime controller paths. Before
broader testing, privileged commands need a clearer authority, role, and
audit boundary so tester tools do not become gameplay exploits.
- Verifier scripts are useful but numerous. 0.2 should add a grouped verifier
runner so milestone verification is one command with explicit categories.
- Some status docs predate the automated setup passes and should be treated as
historical unless their claims are repeated in the main roadmap or this
readiness review.
- `ripgrep` is not installed on the Ubuntu Unreal VM. This is not blocking, but
adding it would make future audits and code search faster.
## Low-Risk Cleanup Applied
- Removed duplicate `SavingAndQuit` branch logic from the MVP frontend continue
flow. Behavior is unchanged, but the control path is now easier to read.
## 0.2 Engineering Priorities
- Start with `0.2.0 Investor Visual Credibility Baseline`: terrain material,
grasses/shrubs/trees, water, character bodies/clothing, resource objects,
fire/smoke, shelter pieces, and wildlife, in that order.
- Then finish `0.2.A Land And Claiming` before moving to farming,
domestication, storage, or economy work.
- Keep land claims, farming, storage, household tasks, and future economy state
server-authoritative from the first implementation pass.
- Add schema/version fields when introducing new persistent records.
- Use data assets for claim rules, crop definitions, animal definitions,
storage container definitions, and early profession/knowledge topics.
- Create focused automation helpers by domain instead of expanding the current
monolithic editor automation file.
- Replace placeholder visuals in the order investors and testers notice them:
terrain material, grasses/shrubs/trees, water, character bodies/clothing,
resource objects, fire/smoke, shelter pieces, and wildlife.
- Keep ray tracing optional. The default non-ray-traced path must remain the
baseline for demo readability and remote testing.
+249
View File
@@ -379,3 +379,252 @@ Non-punitive practice paths:
Accessibility rule: learning should make players more capable without making
them feel trapped, shamed, or forced into a classroom flow.
## Subject Content Format
Learning content should use a small structured format so topics can become data
assets later without rewriting design intent.
Required fields:
- `topic`: stable identifier and display name, such as `fire.clearance`.
- `concepts`: one or more concept tags the player may learn or demonstrate.
- `difficulty_tier`: `elementary`, `practical`, `advanced`, or `expert`.
- `prerequisite_concepts`: concept tags that should be known first, if any.
- `in_game_effect`: the warning, modifier, feedback, unlock, or quality effect
this topic can influence.
- `practice_action`: the action that can build practical experience for the
topic.
- `source_note`: a short design/source note explaining why the topic matters.
Example:
```text
topic: fire.clearance
concepts: fire_safety, dry_fuel, wind_spread
difficulty_tier: elementary
prerequisite_concepts: none
in_game_effect: clearer fire-risk warning and lower unsafe-placement mistakes
practice_action: clear area, contain fire, maintain fire, extinguish fire
source_note: Open flame near dry fuel and wind can spread beyond the campfire.
```
Format rule: content records should be small enough to review in source control
and explicit enough to become data assets later.
## MVP Elementary Survival Question Bank
The first question bank is intentionally small and elementary. These questions
support optional checks, camp review, and future teaching objects.
```text
id: fire.clearance.001
topic: fire.clearance
question: Why clear dry brush away from a campfire?
answers: A) It lowers fire-spread risk. B) It makes the fire colder. C) It makes rain stronger.
correct: A
feedback: Open flame, dry fuel, and wind can spread fire beyond the campfire.
```
```text
id: water.potable.001
topic: water.potable
question: What is the safest first assumption about unknown water?
answers: A) It may need treatment. B) It is always safe. C) It restores warmth.
correct: A
feedback: Water access and safe drinking water are not always the same thing.
```
```text
id: exposure.cold.001
topic: exposure.cold
question: What helps most when cold wind and rain are lowering body temperature?
answers: A) Shelter, warmth, dry conditions, and rest. B) Sprinting forever. C) Dropping all food.
correct: A
feedback: Wind, rain, fatigue, and nightfall can make exposure dangerous.
```
```text
id: shelter.drainage.001
topic: shelter.drainage
question: Why avoid placing shelter in a drainage path?
answers: A) Water can pool or flow through it. B) It makes tools sharper. C) It prevents all wind.
correct: A
feedback: Stable, drained ground improves shelter reliability.
```
```text
id: injury.bleeding.001
topic: injury.bleeding
question: What should you do before heavy work while bleeding?
answers: A) Treat bleeding and rest if possible. B) Ignore it and sprint. C) Stand in smoke.
correct: A
feedback: Bleeding and exhaustion make further mistakes and injury more likely.
```
```text
id: resource.fiber.001
topic: resource.fiber
question: Why is fiber useful early?
answers: A) Binding, panels, and shelter parts. B) It replaces all water. C) It stops night.
correct: A
feedback: Fiber is a basic binding material for primitive crafting and shelter.
```
Question-bank rule: elementary questions should be short, practical, and tied to
actions the player can immediately recognize in the MVP.
## When Deeper Questions Matter
Deeper questions should not decide whether a new player can survive the first
night. They should matter when the player is trying to do better, teach others,
or enter more complex branches.
Quality improvements:
- Use deeper checks when the player wants better yield, stronger construction,
more durable tools, safer food, better medicine, or more efficient work.
Safer work:
- Use deeper checks when incorrect assumptions could create fire spread, injury,
sickness, structural failure, bad weather exposure, or wasted scarce supplies.
Complex crafting:
- Use deeper checks for multi-stage recipes, material substitutions, tool
maintenance, preserved food, medicines, buildings, and later machinery.
Teaching others:
- Use deeper checks when a player or NPC attempts to teach a concept, create a
lesson note, train family/community members, or evaluate whether someone
understood a dangerous task.
Advanced branches:
- Use deeper checks for farming, animal care, medicine, engineering, trade,
navigation, governance, science, and future advanced technology.
Depth rule: deeper questions should improve mastery and responsibility. They
should not be busywork for actions the character has already demonstrated
through repeated safe practice.
## Exploit And Rote-Memorization Guardrails
The learning system should reward understanding and practice, not repetitive
input farming or memorizing answer order.
Exploit farming risks:
- Repeating the same safe action for unlimited experience.
- Creating harmless failures just to farm mistake feedback.
- Spamming prompts or questions for repeated rewards.
- Using alternate accounts or group work to duplicate teaching credit.
- Performing actions with no meaningful cost, context, or outcome.
Guardrails:
- Apply diminishing returns to identical repeated actions.
- Require meaningful context for experience: time, resource cost, risk,
environmental variation, or consequence.
- Track recent prompt/question exposure and suppress repeated rewards.
- Give recovery credit only when the player takes a useful corrective action.
- Separate "learned concept" from "perfect mastery" so one answer does not solve
every future situation.
- Prefer concept families and varied wording over fixed answer-order memorizing.
Anti-exploit rule: rewards should come from meaningful decisions, varied
practice, and good recovery, not from low-cost repetition.
## Knowledge Persistence Requirements
Knowledge and skill state must survive save/load, server restarts, and future
character handoff systems without resetting learning progress or tutorial
fatigue.
Persisted state:
- Stable knowledge profile ID linked to the player character or NPC.
- Learned concepts by stable concept ID.
- Practical skill experience by taxonomy domain.
- Mastery/confidence tier per topic, separate from raw experience.
- Failed attempts that produced useful feedback, including cause category,
recovery action, and recent cooldown state.
- Tutorial and contextual prompt state: seen, dismissed, repeated, snoozed, and
next eligible display time.
- Optional knowledge checks answered, skipped, failed, or recently displayed.
- Teaching, observation, and shared-work learning events that granted credit.
- Schema version, migration marker, and source build/version for future cleanup.
Save/load rules:
- The server is authoritative for writes in multiplayer.
- Client UI may cache prompt visibility, but save data comes from validated
server state.
- Save enough recent action and prompt history to prevent reset-based farming.
- Keep learned concepts stable across wording changes in the question bank.
- Preserve failed attempts long enough to support better feedback and avoid
repeating the same hint every few seconds.
- Store tutorial state per character/profile, not globally across all worlds.
- Never store private credentials, personal account data, or real-world source
notes in player save records.
Persistence rule: learning state should make a returning character feel
continuous while giving designers enough history to tune teaching, feedback, and
anti-exploit behavior.
## Multiplayer Learning Rules
Learning in multiplayer should make cooperation valuable without turning nearby
players into passive experience sources.
Teaching:
- The server validates teaching credit.
- The teacher must know the concept, have enough practical experience, or
demonstrate the action in the current context.
- The learner must be nearby, attentive, and eligible for the concept.
- Teaching grants bounded awareness or practice credit, not instant mastery.
- Dangerous concepts require supervised practice before full confidence is
awarded.
Observation:
- Nearby observation can unlock awareness of a concept when the action is
visible and relevant.
- Passive observation grants less credit than direct practice.
- Repeated passive observation has diminishing returns.
- Observation does not grant credit while idle, disconnected, hidden from the
action, or outside the relevant range.
Shared work:
- Group tasks can grant role-specific experience to active contributors.
- Roles should be explicit enough to audit later: gatherer, builder, fire
watcher, first aid helper, scout, teacher, cook, hauler, or defender.
- Failed group work can teach useful feedback when a player helps diagnose or
correct the problem.
- Idle proximity, item handoff spam, and repeated low-risk loops should not
grant shared-work credit.
Group skill benefits:
- Skilled contributors can reduce risk, improve quality, increase speed, or
make warnings more visible for the group task.
- Benefits should be capped so one expert does not remove all challenge.
- Benefits apply only while the contributor is present, equipped, and actively
helping.
- No global aura: skill benefits do not apply across the map or while logged
off unless a future offline-simulation rule explicitly grants protection.
Network rules:
- Client learning requests are hints only.
- Server authority validates distance, visibility, participation, role,
cooldown, tools, environmental context, and outcome before granting credit.
- Teaching, observation, and shared-work events must be persisted for tuning and
anti-exploit review.
Multiplayer rule: cooperation should transfer awareness, improve group outcomes,
and reward active contribution without bypassing practice, risk, or context.
+9975
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,57 @@
# Investor Readiness Audit - 2026-05-19
## Current Verdict
The current build should not be presented as investor visual MVP until it passes
a real-GPU packaged-client visual and stability check. The systems are further
along than the presentation, but the player-facing first impression is still at
risk.
## Findings
- Packaged crash logs show a Slate/UI paint crash while the character-selection
frontend is active. The crash callstack centers on `SButton::OnPaint` and
`FSlateDrawElement::MakeBox`, so the immediate fix removes custom button brush
styling, defers frontend actions to the next tick, and shortens the investor
path from character selection directly into Ground Zero.
- The project content library still contains only placeholder meshes plus the
Manny/Quinn mannequin assets for characters. The current character and object
visuals are proxy-quality, not final investor-quality realism.
- Ground Zero editor verification confirms landscape, water/resource actors,
foliage variation actors, and environment materials exist, but this does not
prove the packaged executable looks good through the real GPU path.
- Cooked runtime logs warned that tree, shrub, and grass materials were missing
instanced-static-mesh usage. Those flags are now set on the assets and in the
repeatable map setup script.
- The generated investor roadmap/report can show old milestone completion state
if it is not regenerated from the current roadmap after fixes. The source
roadmap still contains explicit visual QA acceptance items that remain open
until real packaged screenshots or clips are captured.
## Required Before Investor-Ready Label
- Launch the packaged Windows demo through the real GPU desktop path.
- Capture startup credits, character selection, first spawn, terrain, vegetation,
water, campfire, shelter, pause menu, and save/quit.
- Confirm selecting a character and entering Ground Zero does not crash.
- Confirm the first player view is not ground/legs, the menu does not appear
after gameplay starts, water is visible, foliage is visible, and debug-looking
primitives are not the dominant read.
- Replace proxy/mannequin art with real or production-directed free/internal art
assets before calling the build visually investor-ready.
## Verification Completed In This Pass
- `verify_mvp_menu_input_and_quit_flow.py`
- `verify_mvp_character_archetype_choice.py`
- `verify_mvp_character_proxies.py`
- `verify_mvp_frontend_umg_flow.py`
- `verify_ground_zero_natural_environment_pass.py`
- Windows package BuildCookRun completed successfully after the fixes.
## Remaining Risk
The packaged build still needs a real interactive visual pass through
Sunshine/Moonlight or direct Windows display access. Static checks and editor
commandlets are not enough to clear the user-reported crash and visual quality
concerns.
@@ -6,9 +6,14 @@ space.
## Scope
- Terrain receives a warm coastal scrub ground material.
- Foliage patch instances keep the current prototype meshes but use distinct
tree, shrub, and dry grass materials.
- Terrain receives a procedural coastal scrub terrain material that blends
dry soil, scrub green, and sandy path color families with broad and fine
noise so Ground Zero does not read as flat tan placeholder ground.
- Foliage patch instances use native low-poly coastal scrub vegetation meshes
under `/Game/Agrarian/Environment/Vegetation`: a coastal oak proxy, coyote
brush proxy, and dry grass clump proxy. The foliage materials include
per-instance color variation so repeated instances do not read as copied
engine primitives.
- Wood, fiber, stone, and freshwater actors receive distinct first-pass
materials.
- Investor-facing asset variation actors add additional tree canopies/trunks,
@@ -48,7 +53,13 @@ space.
`Scripts/verify_ground_zero_natural_environment_pass.py` checks that the
materials exist, the landscape uses the terrain material, the foliage actor has
the expected investor-facing instance counts and material assignments, and
resource/water actors are visually dressed. It also checks the asset variation
resource/water actors are visually dressed. It also checks that foliage no
longer points at `/Engine/BasicShapes` or template meshes, that the three
coastal-scrub vegetation assets are assigned, and that tree, shrub, and grass
components have explicit cull distances for performance. It also checks that the terrain
material contains procedural color breakup rather than a flat constant color:
noise, blend, and coastal-scrub color-vector expression families must be present
in the saved material package. It also checks the asset variation
layer: twenty-three labeled variation actors, at least four mesh silhouettes,
unique scale profiles, and coverage across tree, bush, grass, rock, and water
visual families. `Scripts/verify_native_placeholder_meshes.py` checks that playable
@@ -0,0 +1,97 @@
# Biome And Natural Resource Generation Plan
Agrarian needs Earth-aware tile generation, not hand-authored placeholder maps.
Every 1 km tile should derive its biome and natural resources from real-world
environmental signals, then place removable and persistent objects that behave
like real natural resources.
## Biome Selection Inputs
Each tile should eventually derive biome weights from:
- latitude
- elevation
- slope and aspect
- rainfall
- seasonal temperature swing
- ocean proximity
- prevailing wind
- ocean currents
- rain-shadow effects
- soil type
- drainage
- river, lake, wetland, and coast proximity
- land-use history when available
## Layered Generation
1. Climate engine: produces temperature, rainfall, seasonality, and weather
pressure.
2. Biome generator: assigns weighted macro/regional/local biome blends.
3. Ecology layer: chooses plant communities, wildlife, disease pressure, soil
behavior, and water availability.
4. Resource layer: places trees, shrubs, grasses, rocks, water bodies, edible
plants, fuel, fiber, and old human-made objects.
5. Human layer: derives agriculture suitability, settlement pressure, trade
value, travel difficulty, and cultural/economic implications.
## Biome Output Shape
Avoid hard borders. A tile should produce weighted blends such as:
- `70% coastal scrub`
- `20% riparian woodland`
- `10% seasonal marsh`
Those weights drive asset selection, density, color variation, seasonality, and
resource availability.
## Persistent Natural Resources
Every removable world object should have durable state:
- unique tile-local resource id
- resource type
- species or object profile
- transform
- growth stage
- health
- age
- quantity remaining
- removed/depleted timestamp
- player/planted/natural origin
- respawn or regrowth policy
- last simulation timestamp
## Removal And Regrowth Rules
- Trees, shrubs, rocks, puddles, lakes, ruins, shelters, and large props should
not silently pop back after removal.
- Player-removed trees and shrubs stay removed unless replanted or naturally
reseeded over realistic in-game time.
- Stumps, deadwood, brush piles, and disturbed ground should be valid aftermath
states rather than instant deletion.
- Water bodies are removable only through believable terrain/drainage changes,
drought, construction, or simulation systems.
- Edible plants, grasses, brush, and fiber resources can replenish, but only on
realistic seasonal timelines and only when biome, weather, grazing, and human
use allow it.
- Player-planted resources use explicit planting/cultivation rules rather than
random respawn.
## First Implementation Slice
1. Add tile biome profile data assets or JSON metadata for Ground Zero.
2. Register asset sets by biome weight: terrain, trees, shrubs, grasses, rocks,
water, wildlife, reclaimed props.
3. Convert placed foliage/resource proxies into persistent resource records.
4. Add removal state and save/load durability for trees and shrubs first.
5. Add slow regrowth/reseeding simulation for grasses/shrubs/trees.
6. Extend to water bodies, edible plants, rocks, and reclaimed objects.
## Investor Relevance
This system should make new tiles feel recognizable without hand-sculpting each
one. A Pacific coastal tile, a prairie tile, a boreal tile, and a river valley
tile should choose different assets, resource densities, travel difficulty, and
survival pressures automatically.
+8
View File
@@ -0,0 +1,8 @@
@echo off
setlocal
set "PACKAGE_DIR=%~dp0..\Builds\WindowsDevelopment"
set "GAME_EXE=%PACKAGE_DIR%\AgrarianGame\Binaries\Win64\AgrarianGame.exe"
cd /d "%PACKAGE_DIR%"
"%GAME_EXE%" -windowed -ResX=1280 -ResY=720 -ExecCmds=AgrarianInvestorSmokeTest -nosound
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
set -euo pipefail
OLLAMA_URL="${OLLAMA_URL:-http://192.168.5.23:11434}"
OPEN_WEBUI_URL="${OPEN_WEBUI_URL:-http://192.168.5.26:8085}"
GITEA_URL="${GITEA_URL:-http://192.168.5.21:3000}"
MODEL="${MODEL:-qwen2.5-coder:7b}"
echo "Gitea: ${GITEA_URL}"
curl -fsSI "${GITEA_URL}/user/login" >/dev/null
echo " ok"
echo "Ollama: ${OLLAMA_URL}"
curl -fsS "${OLLAMA_URL}/api/tags" >/tmp/agrarian_ollama_tags.json
if ! grep -q "\"${MODEL}\"" /tmp/agrarian_ollama_tags.json; then
echo " model ${MODEL} not found"
cat /tmp/agrarian_ollama_tags.json
exit 1
fi
echo " ok, model ${MODEL} available"
echo "Ollama chat smoke test"
curl -fsS "${OLLAMA_URL}/api/chat" \
-H "Content-Type: application/json" \
-d "{\"model\":\"${MODEL}\",\"messages\":[{\"role\":\"user\",\"content\":\"Reply with exactly: ok\"}],\"stream\":false}" \
| grep -Eiq '"content":"[[:space:]]*ok[[:space:]]*"'
echo " ok"
echo "Open WebUI: ${OPEN_WEBUI_URL}"
curl -fsSI "${OPEN_WEBUI_URL}" >/dev/null
echo " ok"
echo "Self-hosted AI stack reachable."
+74
View File
@@ -0,0 +1,74 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -lt 1 ]]; then
echo "Usage: $0 <task-status-json-or-handoff-text> [extra prompt]" >&2
exit 2
fi
STATUS_FILE="$1"
EXTRA_PROMPT="${2:-}"
if [[ ! -f "$STATUS_FILE" ]]; then
echo "Missing status/handoff file: $STATUS_FILE" >&2
exit 2
fi
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
OUT_DIR="${ROOT}/Saved/AiEscalations/${STAMP}"
mkdir -p "$OUT_DIR"
PROMPT_FILE="${OUT_DIR}/codex_prompt.txt"
LOG_FILE="${OUT_DIR}/codex_exec.log"
BYPASS_LOG_FILE="${OUT_DIR}/codex_exec_bypass.log"
{
echo "You are Codex being called as an escalation worker for Agrarian."
echo "Use the repository at: ${ROOT}"
echo
echo "Local AI stopped and requested escalation. Review the status below,"
echo "inspect the repo, make only the needed changes, run verification, and"
echo "summarize the result. Do not hide uncertainty."
echo
echo "Status / handoff:"
cat "$STATUS_FILE"
if [[ -n "$EXTRA_PROMPT" ]]; then
echo
echo "Extra prompt:"
echo "$EXTRA_PROMPT"
fi
} > "$PROMPT_FILE"
echo "Prompt written to ${PROMPT_FILE}"
run_codex_sandboxed() {
if command -v codex >/dev/null 2>&1; then
codex exec --sandbox workspace-write -C "$ROOT" - < "$PROMPT_FILE" 2>&1 | tee "$LOG_FILE"
else
npx -y @openai/codex exec --sandbox workspace-write -C "$ROOT" - < "$PROMPT_FILE" 2>&1 | tee "$LOG_FILE"
fi
}
run_codex_bypass() {
{
echo "LinaAI note: Codex sandbox failed inside the isolated LinaAI VM."
echo "Retrying with Codex sandbox bypass so escalation can inspect/run commands."
echo "This should only be used from LinaAI, not shared production hosts."
echo
if command -v codex >/dev/null 2>&1; then
codex exec --dangerously-bypass-approvals-and-sandbox -C "$ROOT" - < "$PROMPT_FILE"
else
npx -y @openai/codex exec --dangerously-bypass-approvals-and-sandbox -C "$ROOT" - < "$PROMPT_FILE"
fi
} 2>&1 | tee "$BYPASS_LOG_FILE"
}
run_codex_sandboxed
if grep -q "bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted" "$LOG_FILE"; then
run_codex_bypass
echo "Codex escalation bypass log written to ${BYPASS_LOG_FILE}"
else
echo "Codex escalation log written to ${LOG_FILE}"
fi
+16
View File
@@ -0,0 +1,16 @@
{
"task": "",
"project": "AgrarianGame",
"branch": "",
"risk": "low|medium|high",
"confidence": 0.0,
"attempts": 0,
"files_inspected": [],
"files_changed": [],
"commands_run": [],
"tests_passed": false,
"build_passed": false,
"blocked_reason": "",
"recommended_escalation": "none|codex|human",
"requested_codex_action": ""
}
@@ -0,0 +1,43 @@
import unreal
MATERIAL_PATHS = [
"/Game/Agrarian/Materials/M_AGR_GZ_Tree_CoastalOak",
"/Game/Agrarian/Materials/M_AGR_GZ_Shrub_CoyoteBrush",
"/Game/Agrarian/Materials/M_AGR_GZ_Grass_DryCoastal",
]
def set_instanced_usage(material):
applied = False
for property_name in (
"used_with_instanced_static_meshes",
"bUsedWithInstancedStaticMeshes",
):
try:
material.set_editor_property(property_name, True)
applied = True
except Exception:
pass
return applied
dirty_assets = []
for material_path in MATERIAL_PATHS:
material = unreal.EditorAssetLibrary.load_asset(material_path)
if not material:
raise RuntimeError(f"Missing material: {material_path}")
if not set_instanced_usage(material):
raise RuntimeError(f"Could not set instanced static mesh usage on {material_path}")
unreal.MaterialEditingLibrary.recompile_material(material)
dirty_assets.append(material_path)
for material_path in dirty_assets:
if not unreal.EditorAssetLibrary.save_asset(material_path, only_if_is_dirty=False):
raise RuntimeError(f"Failed to save updated material: {material_path}")
print("Updated instanced static mesh usage for Ground Zero foliage materials:")
for material_path in dirty_assets:
print(f" - {material_path}")
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
cd "$ROOT"
CACHE_DIR="${LINAAI_KNOWLEDGE_DIR:-Saved/LinaAIKnowledge}"
OUT="${CACHE_DIR}/context.md"
mkdir -p "$CACHE_DIR"
: > "$OUT"
printf '# LinaAI Bootstrap Context\n\n' >> "$OUT"
printf 'Generated: %s\n\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$OUT"
printf 'This is a compact, local-only context pack built from tracked project docs. It intentionally excludes raw secrets.\n\n' >> "$OUT"
include_doc() {
local doc="$1"
local lines="${2:-120}"
if [[ -f "$doc" ]]; then
printf '\n---\n\n## %s\n\n' "$doc" >> "$OUT"
sed -n "1,${lines}p" "$doc" >> "$OUT"
printf '\n' >> "$OUT"
else
printf '\n---\n\n## Missing: %s\n\n' "$doc" >> "$OUT"
fi
}
include_doc "Docs/AI/LinaAIOperatingManual.md" 220
include_doc "Docs/AI/LocalAgentGuardrails.md" 180
include_doc "Docs/AI/LinaAISecretsPolicy.md" 180
include_doc "Docs/AI/LinaAIKnowledgeMap.md" 220
include_doc "Docs/Ops/HANDOFF.md" 220
include_doc "AGRARIAN_DEVELOPMENT_ROADMAP.md" 260
include_doc "Docs/CoreDesignDocument.md" 160
include_doc "Docs/TechnicalDesignDocument.md" 160
include_doc "Docs/SixMonthMvpDefinition.md" 160
include_doc "Docs/Investor/InvestorDemoAcceptanceGate.md" 160
printf 'LinaAI bootstrap context written: %s\n' "$OUT"
+96
View File
@@ -0,0 +1,96 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
cd "$ROOT"
CACHE_DIR="${LINAAI_KNOWLEDGE_DIR:-Saved/LinaAIKnowledge}"
RAW_DIR="${CACHE_DIR}/vendor/raw"
INTERNAL_DIR="${CACHE_DIR}/internal"
INDEX="${CACHE_DIR}/INDEX.md"
mkdir -p "$RAW_DIR" "$INTERNAL_DIR"
write_line() {
printf '%s\n' "$1" >> "$INDEX"
}
: > "$INDEX"
write_line "# LinaAI Knowledge Cache"
write_line ""
write_line "Generated: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
write_line ""
write_line "This directory is ignored by Git. It is a local retrieval/cache area for LinaAI."
write_line "Do not place raw credentials here."
write_line ""
write_line "## Tracked Project Memory"
write_line ""
project_docs=(
"Docs/AI/LinaAIOperatingManual.md"
"Docs/AI/LocalAgentGuardrails.md"
"Docs/AI/LinaAISecretsPolicy.md"
"Docs/AI/LinaAIKnowledgeMap.md"
"Docs/Ops/HANDOFF.md"
"AGRARIAN_DEVELOPMENT_ROADMAP.md"
"Docs/CoreDesignDocument.md"
"Docs/TechnicalDesignDocument.md"
"Docs/SixMonthMvpDefinition.md"
"Docs/MvpSurvivalReadinessCriteria.md"
"Docs/Investor/InvestorDemoAcceptanceGate.md"
"Docs/Art/AgrarianAssetPipeline.md"
"Docs/Terrain/GroundZeroTile.md"
"Docs/World/BiomeAndNaturalResourceGenerationPlan.md"
"Docs/EconomyAndAgrDesignDocument.md"
"Docs/MultiplayerNetworkingDesign.md"
"Docs/PersistenceDesignDocument.md"
)
for doc in "${project_docs[@]}"; do
if [[ -f "$doc" ]]; then
safe_name="$(printf '%s' "$doc" | tr '/ ' '__')"
{
printf '# %s\n\n' "$doc"
grep -E '^(#|##|###) ' "$doc" 2>/dev/null || true
} > "${INTERNAL_DIR}/${safe_name}.headings.md"
write_line "- ${doc} -> ${INTERNAL_DIR}/${safe_name}.headings.md"
else
write_line "- missing: ${doc}"
fi
done
write_line ""
write_line "## Official Vendor Docs"
write_line ""
fetch_doc() {
local name="$1"
local url="$2"
local out="${RAW_DIR}/${name}.html"
if curl -L --fail --retry 2 --connect-timeout 10 --max-time 45 -o "$out" "$url"; then
write_line "- ${name}: ${url} -> ${out}"
else
write_line "- ${name}: ${url} -> fetch failed"
rm -f "$out"
fi
}
fetch_doc "unraid" "https://docs.unraid.net/"
fetch_doc "unreal-engine-5-7" "https://dev.epicgames.com/documentation/en-us/unreal-engine"
fetch_doc "laravel-12" "https://laravel.com/docs/12.x"
fetch_doc "mysql-8-4" "https://dev.mysql.com/doc/refman/8.4/en/"
fetch_doc "gitea" "https://docs.gitea.com/"
fetch_doc "ollama" "https://docs.ollama.com/"
fetch_doc "open-webui" "https://docs.openwebui.com/"
fetch_doc "aider" "https://aider.chat/docs/"
write_line ""
write_line "## Usage"
write_line ""
write_line "- Use this index as evidence that project/vendor docs were refreshed."
write_line "- Use tracked docs as the source of truth for project policy."
write_line "- Treat fetched vendor docs as reference material; verify details before risky edits."
printf 'LinaAI knowledge cache refreshed: %s\n' "$INDEX"
+295
View File
@@ -0,0 +1,295 @@
#!/usr/bin/env bash
set -euo pipefail
MODEL="${MODEL:-qwen2.5-coder:7b}"
OLLAMA_URL="${OLLAMA_URL:-http://192.168.5.23:11434}"
THRESHOLD="${LINAAI_CONFIDENCE_THRESHOLD:-0.75}"
FORCE_ESCALATE=0
DRY_RUN=0
usage() {
cat >&2 <<'EOF'
Usage:
Scripts/linaai_task.sh [--threshold 0.75] [--dry-run] [--force-escalate] "task"
Routes a task through LinaAI's supervised local workflow:
1. Qwen/Ollama preflight risk and confidence check.
2. Automatic Codex escalation if confidence is too low or task is high risk.
3. Aider local execution for acceptable supervised tasks.
4. Automatic Codex escalation if Aider fails.
Use --dry-run to test routing without editing files.
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--threshold)
THRESHOLD="${2:-}"
shift 2
;;
--dry-run)
DRY_RUN=1
shift
;;
--force-escalate)
FORCE_ESCALATE=1
shift
;;
-h|--help)
usage
exit 0
;;
--)
shift
break
;;
-*)
echo "Unknown option: $1" >&2
usage
exit 2
;;
*)
break
;;
esac
done
TASK="${*:-}"
if [[ -z "$TASK" ]]; then
usage
exit 2
fi
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
cd "$ROOT"
mkdir -p Saved/AiTaskStatus
STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
PREFLIGHT_JSON="Saved/AiTaskStatus/linaai_preflight_${STAMP}.json"
STATUS_JSON="Saved/AiTaskStatus/linaai_status_${STAMP}.json"
current_branch="$(git branch --show-current 2>/dev/null || echo unknown)"
repo_evidence="$(
{
echo "cwd: ${ROOT}"
echo "branch: ${current_branch}"
echo "git_status:"
git status --short 2>/dev/null || true
echo "top_level:"
find . -maxdepth 1 -mindepth 1 -printf "%f\n" 2>/dev/null | sort | head -80
echo "project_markers:"
test -f AgrarianGame.uproject && echo "AgrarianGame.uproject"
test -d Source && echo "Source/"
test -d Config && echo "Config/"
test -d Content && echo "Content/"
test -d Scripts && echo "Scripts/"
test -d Docs && echo "Docs/"
echo "script_samples:"
find Scripts -maxdepth 1 -type f -printf "%f\n" 2>/dev/null | sort | head -25
echo "doc_samples:"
find Docs -maxdepth 2 -type f -printf "%p\n" 2>/dev/null | sort | head -25
echo "linaai_required_docs:"
for doc in \
Docs/AI/LinaAIOperatingManual.md \
Docs/AI/LocalAgentGuardrails.md \
Docs/AI/LinaAISecretsPolicy.md \
Docs/AI/LinaAIKnowledgeMap.md \
Docs/Ops/HANDOFF.md \
AGRARIAN_DEVELOPMENT_ROADMAP.md
do
test -f "$doc" && echo "$doc"
done
echo "linaai_cache:"
test -f Saved/LinaAIKnowledge/INDEX.md && echo "Saved/LinaAIKnowledge/INDEX.md"
test -f Saved/LinaAIKnowledge/context.md && echo "Saved/LinaAIKnowledge/context.md"
} | sed 's/"/'\''/g'
)"
system_prompt='You are LinaAI, a supervised local coding assistant for Agrarian. Follow Docs/AI/LinaAIOperatingManual.md, Docs/AI/LocalAgentGuardrails.md, Docs/AI/LinaAISecretsPolicy.md, and Docs/AI/LinaAIKnowledgeMap.md when present. You must not pretend certainty. Classify task risk and confidence before any edits. Confidence must be based on concrete evidence. If you lack evidence, confidence must be below 0.65. Do not include raw secrets in prompts, docs, logs, or commits. High-risk areas include Unreal core architecture, save/load, multiplayer, networking/replication, AGR wallet/payments, marketplace/economy transfer logic, auth, security, migrations, deployment secrets, and broad refactors. Return JSON only.'
user_prompt=$(cat <<EOF
Task:
${TASK}
Repo evidence gathered by wrapper before any edits:
${repo_evidence}
Return compact JSON only with these keys:
risk: low|medium|high
confidence: number from 0.0 to 1.0
evidence_checked: array of strings
reason: short string
recommended_escalation: none|codex|human
requested_codex_action: short string
EOF
)
payload="$(
python3 - "$MODEL" "$system_prompt" "$user_prompt" <<'PY'
import json
import sys
model, system_prompt, user_prompt = sys.argv[1:4]
print(json.dumps({
"model": model,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
],
"options": {
"num_ctx": 4096,
"num_predict": 220,
"temperature": 0.1
},
"stream": False,
}))
PY
)"
echo "LinaAI preflight with ${MODEL}..."
response="$(curl -fsS --max-time "${LINAAI_PREFLIGHT_TIMEOUT:-120}" "${OLLAMA_URL}/api/chat" -H "Content-Type: application/json" -d "$payload")"
content="$(printf '%s' "$response" | python3 -c 'import json,sys; print(json.load(sys.stdin)["message"]["content"])')"
python3 - "$content" "$PREFLIGHT_JSON" <<'PY'
import json
import re
import sys
content, output = sys.argv[1:3]
match = re.search(r"\{.*\}", content, re.S)
if not match:
raise SystemExit(f"No JSON object found in preflight response: {content}")
data = json.loads(match.group(0))
with open(output, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
f.write("\n")
PY
risk="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("risk","high"))' "$PREFLIGHT_JSON")"
confidence="$(python3 -c 'import json,sys; print(float(json.load(open(sys.argv[1])).get("confidence",0)))' "$PREFLIGHT_JSON")"
recommended="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("recommended_escalation","codex"))' "$PREFLIGHT_JSON")"
reason="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("reason",""))' "$PREFLIGHT_JSON")"
evidence_count="$(python3 -c 'import json,sys; v=json.load(open(sys.argv[1])).get("evidence_checked",[]); print(len(v) if isinstance(v,list) else 0)' "$PREFLIGHT_JSON")"
if [[ "$evidence_count" -lt 2 ]]; then
confidence="$(python3 - "$confidence" <<'PY'
import sys
print(min(float(sys.argv[1]), 0.64))
PY
)"
fi
high_risk_regex='(save/load|save system|multiplayer|replication|networking|\bAGR\b|wallet|payment|marketplace|auth|security|migration|core architecture|engine source|broad refactor|private key|secret)'
keyword_escalate=0
if printf '%s' "$TASK" | grep -Eiq "$high_risk_regex"; then
keyword_escalate=1
fi
should_escalate=0
escalation_reason=""
if [[ "$FORCE_ESCALATE" -eq 1 ]]; then
should_escalate=1
escalation_reason="forced escalation test"
elif [[ "$risk" == "high" ]]; then
should_escalate=1
escalation_reason="high risk task"
elif [[ "$recommended" != "none" ]]; then
should_escalate=1
escalation_reason="local preflight recommended ${recommended}"
elif [[ "$keyword_escalate" -eq 1 ]]; then
should_escalate=1
escalation_reason="task matched high-risk escalation keywords"
else
below_threshold="$(python3 - "$confidence" "$THRESHOLD" <<'PY'
import sys
print("1" if float(sys.argv[1]) < float(sys.argv[2]) else "0")
PY
)"
if [[ "$below_threshold" == "1" ]]; then
should_escalate=1
escalation_reason="confidence ${confidence} below threshold ${THRESHOLD}"
fi
fi
write_status() {
local tests_passed="$1"
local build_passed="$2"
local blocked_reason="$3"
local requested_action="$4"
python3 - "$STATUS_JSON" "$TASK" "$current_branch" "$risk" "$confidence" "$tests_passed" "$build_passed" "$blocked_reason" "$requested_action" <<'PY'
import json
import sys
path, task, branch, risk, confidence, tests_passed, build_passed, blocked_reason, requested_action = sys.argv[1:10]
data = {
"task": task,
"project": "AgrarianGame",
"branch": branch,
"risk": risk,
"confidence": float(confidence),
"attempts": 1,
"files_inspected": [],
"files_changed": [],
"commands_run": [],
"tests_passed": tests_passed == "true",
"build_passed": build_passed == "true",
"blocked_reason": blocked_reason,
"recommended_escalation": "codex",
"requested_codex_action": requested_action,
}
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
f.write("\n")
PY
}
echo "Risk: ${risk}"
echo "Confidence: ${confidence}"
echo "Evidence entries: ${evidence_count}"
echo "Reason: ${reason}"
echo "Preflight: ${PREFLIGHT_JSON}"
if [[ "$should_escalate" -eq 1 ]]; then
echo "Routing to Codex: ${escalation_reason}"
write_status false false "$escalation_reason" "Handle this task or provide a precise implementation plan. Do not edit files unless the task explicitly requires edits."
if [[ "$DRY_RUN" -eq 1 ]]; then
echo "Dry run: would run Scripts/ai_codex_escalate.sh ${STATUS_JSON}"
exit 0
fi
exec Scripts/ai_codex_escalate.sh "$STATUS_JSON"
fi
if [[ -x Scripts/linaai_bootstrap_context.sh && ! -f Saved/LinaAIKnowledge/context.md ]]; then
echo "Building LinaAI bootstrap context..."
Scripts/linaai_bootstrap_context.sh >/dev/null || true
fi
if [[ "$DRY_RUN" -eq 1 ]]; then
echo "Dry run: would run Aider locally."
exit 0
fi
echo "Routing to Aider local execution."
aider_read_args=()
for read_doc in \
Docs/AI/LinaAIOperatingManual.md \
Docs/AI/LocalAgentGuardrails.md \
Docs/AI/LinaAISecretsPolicy.md \
Docs/AI/LinaAIKnowledgeMap.md \
Saved/LinaAIKnowledge/context.md
do
if [[ -f "$read_doc" ]]; then
aider_read_args+=(--read "$read_doc")
fi
done
set +e
aider --model "ollama/${MODEL}" --no-auto-commits --yes-always "${aider_read_args[@]}" --message "$TASK"
aider_exit=$?
set -e
if [[ "$aider_exit" -ne 0 ]]; then
echo "Aider failed with exit code ${aider_exit}; escalating to Codex."
write_status false false "Aider failed with exit code ${aider_exit}" "Review the task, Aider failure, and repository state; complete or advise next steps."
exec Scripts/ai_codex_escalate.sh "$STATUS_JSON"
fi
echo "Aider completed. Review git diff and run verification before committing."
+175
View File
@@ -0,0 +1,175 @@
#!/usr/bin/env bash
set -euo pipefail
DEFAULT_REPO="${LINAAI_REPO:-$HOME/repos/AgrarianGame}"
REPO="$DEFAULT_REPO"
DRY_RUN=0
FORCE_ESCALATE=0
usage() {
cat >&2 <<'EOF'
Usage:
linaai [--repo PATH] [--dry-run] [--force-escalate] [task text...]
Without task text, opens an interactive LinaAI terminal prompt. Each instruction
is routed through Scripts/linaai_task.sh from the selected repo.
Examples:
linaai
linaai --dry-run "Confirm repo access and list LinaAI docs."
linaai "Update docs for the current deployment workflow."
Interactive commands:
/help Show commands.
/repo PATH Change repo path.
/status Show git status for current repo.
/refresh Refresh LinaAI knowledge cache.
/dry on|off Toggle dry-run mode.
/codex on|off Toggle forced Codex escalation.
/exit Quit.
EOF
}
run_status() {
cd "$REPO"
printf 'Repo: %s\n' "$REPO"
git branch --show-current 2>/dev/null || true
git status --short
}
refresh_context() {
cd "$REPO"
if [[ -x Scripts/linaai_refresh_knowledge.sh ]]; then
Scripts/linaai_refresh_knowledge.sh
fi
if [[ -x Scripts/linaai_bootstrap_context.sh ]]; then
Scripts/linaai_bootstrap_context.sh
fi
}
run_task() {
local task="$1"
cd "$REPO"
if [[ ! -x Scripts/linaai_task.sh ]]; then
echo "Missing Scripts/linaai_task.sh in ${REPO}" >&2
return 2
fi
local args=()
if [[ "$DRY_RUN" -eq 1 ]]; then
args+=(--dry-run)
fi
if [[ "$FORCE_ESCALATE" -eq 1 ]]; then
args+=(--force-escalate)
fi
Scripts/linaai_task.sh "${args[@]}" "$task"
}
while [[ $# -gt 0 ]]; do
case "$1" in
--repo)
REPO="${2:-}"
shift 2
;;
--dry-run)
DRY_RUN=1
shift
;;
--force-escalate)
FORCE_ESCALATE=1
shift
;;
-h|--help)
usage
exit 0
;;
--)
shift
break
;;
-*)
echo "Unknown option: $1" >&2
usage
exit 2
;;
*)
break
;;
esac
done
if [[ ! -d "$REPO" ]]; then
echo "Repo does not exist: ${REPO}" >&2
exit 2
fi
if [[ $# -gt 0 ]]; then
run_task "$*"
exit $?
fi
cat <<EOF
LinaAI terminal
Repo: ${REPO}
Dry run: $([[ "$DRY_RUN" -eq 1 ]] && echo on || echo off)
Forced Codex: $([[ "$FORCE_ESCALATE" -eq 1 ]] && echo on || echo off)
Type /help for commands or /exit to quit.
EOF
while true; do
printf '\nlinaai> '
if ! IFS= read -r line; then
printf '\n'
break
fi
[[ -z "${line// }" ]] && continue
case "$line" in
/exit|/quit)
break
;;
/help)
usage
;;
/status)
run_status
;;
/refresh)
refresh_context
;;
/dry\ on)
DRY_RUN=1
echo "Dry run on."
;;
/dry\ off)
DRY_RUN=0
echo "Dry run off."
;;
/codex\ on)
FORCE_ESCALATE=1
echo "Forced Codex escalation on."
;;
/codex\ off)
FORCE_ESCALATE=0
echo "Forced Codex escalation off."
;;
/repo\ *)
next_repo="${line#/repo }"
if [[ -d "$next_repo" ]]; then
REPO="$next_repo"
echo "Repo set to ${REPO}"
else
echo "Repo does not exist: ${next_repo}" >&2
fi
;;
/*)
echo "Unknown command: ${line}" >&2
;;
*)
run_task "$line"
;;
esac
done
+463 -15
View File
@@ -18,6 +18,8 @@ LANDSCAPE_MIN_XY = -50000.0
FOLIAGE_LABEL = "AGR_GroundZeroFoliage_FirstPass"
FOLIAGE_RANDOM_SEED = 4160544
PLACEHOLDER_MESH_FOLDER = "/Game/Agrarian/Environment/PlaceholderMeshes"
VEGETATION_MESH_FOLDER = "/Game/Agrarian/Environment/Vegetation"
VEGETATION_SOURCE_FOLDER = PROJECT_ROOT / "Saved" / "CodexGenerated" / "Vegetation"
PLACEHOLDER_MESH_SOURCES = {
"SM_AGR_Placeholder_Cube": "/Game/LevelPrototyping/Meshes/SM_Cube",
"SM_AGR_Placeholder_ChamferCube": "/Game/LevelPrototyping/Meshes/SM_ChamferCube",
@@ -30,9 +32,9 @@ PLACEHOLDER_MESHES = {
for name in PLACEHOLDER_MESH_SOURCES
}
FOLIAGE_MESHES = {
"tree": PLACEHOLDER_MESHES["SM_AGR_Placeholder_Cylinder"],
"shrub": PLACEHOLDER_MESHES["SM_AGR_Placeholder_Cube"],
"grass": PLACEHOLDER_MESHES["SM_AGR_Placeholder_Cylinder"],
"tree": f"{VEGETATION_MESH_FOLDER}/SM_AGR_GZ_CoastalOak_Proxy",
"shrub": f"{VEGETATION_MESH_FOLDER}/SM_AGR_GZ_CoyoteBrush_Proxy",
"grass": f"{VEGETATION_MESH_FOLDER}/SM_AGR_GZ_DryGrassClump_Proxy",
}
VARIATION_MESHES = {
"cube": PLACEHOLDER_MESHES["SM_AGR_Placeholder_Cube"],
@@ -40,28 +42,43 @@ VARIATION_MESHES = {
"cylinder": PLACEHOLDER_MESHES["SM_AGR_Placeholder_Cylinder"],
"quarter_cylinder": PLACEHOLDER_MESHES["SM_AGR_Placeholder_QuarterCylinder"],
"plane": PLACEHOLDER_MESHES["SM_AGR_Placeholder_Plane"],
"coastal_oak": FOLIAGE_MESHES["tree"],
"coyote_brush": FOLIAGE_MESHES["shrub"],
"dry_grass_clump": FOLIAGE_MESHES["grass"],
}
MATERIAL_FOLDER = "/Game/Agrarian/Materials"
ENVIRONMENT_MATERIALS = {
"terrain": {
"path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Terrain_CoastalScrub",
"color": unreal.LinearColor(0.28, 0.24, 0.16, 1.0),
"color": unreal.LinearColor(0.16, 0.23, 0.12, 1.0),
"dry_soil_color": unreal.LinearColor(0.28, 0.24, 0.16, 1.0),
"scrub_green_color": unreal.LinearColor(0.12, 0.22, 0.10, 1.0),
"sandy_path_color": unreal.LinearColor(0.42, 0.36, 0.23, 1.0),
"noise_scale": 42.0,
"roughness": 0.92,
},
"tree": {
"path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Tree_CoastalOak",
"color": unreal.LinearColor(0.18, 0.31, 0.16, 1.0),
"color": unreal.LinearColor(0.07, 0.18, 0.06, 1.0),
"variation_color": unreal.LinearColor(0.14, 0.24, 0.09, 1.0),
"roughness": 0.88,
"used_with_instanced_static_meshes": True,
},
"shrub": {
"path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Shrub_CoyoteBrush",
"color": unreal.LinearColor(0.31, 0.39, 0.20, 1.0),
"color": unreal.LinearColor(0.15, 0.28, 0.10, 1.0),
"variation_color": unreal.LinearColor(0.24, 0.34, 0.15, 1.0),
"roughness": 0.9,
"used_with_instanced_static_meshes": True,
"two_sided": True,
},
"grass": {
"path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Grass_DryCoastal",
"color": unreal.LinearColor(0.47, 0.42, 0.23, 1.0),
"color": unreal.LinearColor(0.32, 0.34, 0.13, 1.0),
"variation_color": unreal.LinearColor(0.52, 0.45, 0.22, 1.0),
"roughness": 0.95,
"used_with_instanced_static_meshes": True,
"two_sided": True,
},
"wood_resource": {
"path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Wood_Resource",
@@ -85,7 +102,7 @@ ENVIRONMENT_MATERIALS = {
},
"fresh_water": {
"path": f"{MATERIAL_FOLDER}/M_AGR_GZ_FreshWater",
"color": unreal.LinearColor(0.08, 0.28, 0.38, 1.0),
"color": unreal.LinearColor(0.02, 0.16, 0.30, 1.0),
"roughness": 0.35,
},
}
@@ -346,7 +363,7 @@ WEATHER_EXPOSURE_ZONES = [
ENVIRONMENT_VARIATION_ACTORS = [
{
"label": "AGR_GZ_EnvVar_Tree_Canopy_01",
"mesh_key": "chamfer_cube",
"mesh_key": "coastal_oak",
"material_key": "tree",
"location_xy": unreal.Vector(-27500.0, 6900.0, 0.0),
"z_offset": 390.0,
@@ -364,7 +381,7 @@ ENVIRONMENT_VARIATION_ACTORS = [
},
{
"label": "AGR_GZ_EnvVar_Tree_Canopy_02",
"mesh_key": "chamfer_cube",
"mesh_key": "coastal_oak",
"material_key": "tree",
"location_xy": unreal.Vector(17600.0, 31800.0, 0.0),
"z_offset": 430.0,
@@ -382,7 +399,7 @@ ENVIRONMENT_VARIATION_ACTORS = [
},
{
"label": "AGR_GZ_EnvVar_Bush_Rounded_01",
"mesh_key": "chamfer_cube",
"mesh_key": "coyote_brush",
"material_key": "shrub",
"location_xy": unreal.Vector(-33400.0, -15200.0, 0.0),
"z_offset": 70.0,
@@ -391,7 +408,7 @@ ENVIRONMENT_VARIATION_ACTORS = [
},
{
"label": "AGR_GZ_EnvVar_Bush_Rounded_02",
"mesh_key": "chamfer_cube",
"mesh_key": "coyote_brush",
"material_key": "shrub",
"location_xy": unreal.Vector(30400.0, -3900.0, 0.0),
"z_offset": 75.0,
@@ -551,6 +568,78 @@ ENVIRONMENT_VARIATION_ACTORS = [
"scale": unreal.Vector(0.07, 0.07, 1.15),
"rotation": unreal.Rotator(0.0, -19.0, 0.0),
},
{
"label": "AGR_GZ_EnvVar_FirstLook_OakCanopy_01",
"mesh_key": "chamfer_cube",
"material_key": "tree",
"location_xy": unreal.Vector(-26200.0, -7200.0, 0.0),
"z_offset": 410.0,
"scale": unreal.Vector(2.2, 2.6, 1.55),
"rotation": unreal.Rotator(0.0, 24.0, 0.0),
},
{
"label": "AGR_GZ_EnvVar_FirstLook_OakTrunk_01",
"mesh_key": "cylinder",
"material_key": "wood_resource",
"location_xy": unreal.Vector(-26200.0, -7200.0, 0.0),
"z_offset": 180.0,
"scale": unreal.Vector(0.28, 0.28, 3.7),
"rotation": unreal.Rotator(0.0, 24.0, 0.0),
},
{
"label": "AGR_GZ_EnvVar_FirstLook_Brush_01",
"mesh_key": "chamfer_cube",
"material_key": "shrub",
"location_xy": unreal.Vector(-19100.0, -8350.0, 0.0),
"z_offset": 72.0,
"scale": unreal.Vector(1.65, 1.25, 0.68),
"rotation": unreal.Rotator(0.0, -14.0, 0.0),
},
{
"label": "AGR_GZ_EnvVar_FirstLook_Brush_02",
"mesh_key": "chamfer_cube",
"material_key": "shrub",
"location_xy": unreal.Vector(-23900.0, 1300.0, 0.0),
"z_offset": 76.0,
"scale": unreal.Vector(1.25, 1.95, 0.72),
"rotation": unreal.Rotator(0.0, 39.0, 0.0),
},
{
"label": "AGR_GZ_EnvVar_FirstLook_GrassMat_01",
"mesh_key": "plane",
"material_key": "grass",
"location_xy": unreal.Vector(-20500.0, -6400.0, 0.0),
"z_offset": 15.0,
"scale": unreal.Vector(3.8, 2.4, 1.0),
"rotation": unreal.Rotator(0.0, 18.0, 0.0),
},
{
"label": "AGR_GZ_EnvVar_FirstLook_GrassMat_02",
"mesh_key": "plane",
"material_key": "grass",
"location_xy": unreal.Vector(-24200.0, -1800.0, 0.0),
"z_offset": 15.0,
"scale": unreal.Vector(2.9, 3.7, 1.0),
"rotation": unreal.Rotator(0.0, -31.0, 0.0),
},
{
"label": "AGR_GZ_EnvVar_FirstLook_Rock_01",
"mesh_key": "quarter_cylinder",
"material_key": "stone_resource",
"location_xy": unreal.Vector(-18400.0, -2400.0, 0.0),
"z_offset": 45.0,
"scale": unreal.Vector(1.15, 1.55, 0.52),
"rotation": unreal.Rotator(0.0, 66.0, 0.0),
},
{
"label": "AGR_GZ_EnvVar_FirstLook_Rock_02",
"mesh_key": "chamfer_cube",
"material_key": "stone_resource",
"location_xy": unreal.Vector(-27900.0, -3600.0, 0.0),
"z_offset": 42.0,
"scale": unreal.Vector(0.98, 0.82, 0.36),
"rotation": unreal.Rotator(0.0, -52.0, 0.0),
},
]
RUIN_PLACEHOLDER_ACTORS = [
@@ -604,7 +693,7 @@ RUIN_PLACEHOLDER_ACTORS = [
FOLIAGE_ZONES = {
"trees": {
"count": 64,
"count": 96,
"x_range": (-25000.0, 42000.0),
"y_range": (-12000.0, 42000.0),
"min_elevation_m": 18.0,
@@ -612,7 +701,7 @@ FOLIAGE_ZONES = {
"scale_range": (0.75, 1.35),
},
"shrubs": {
"count": 148,
"count": 220,
"x_range": (-42000.0, 45000.0),
"y_range": (-38000.0, 45000.0),
"min_elevation_m": 7.0,
@@ -620,7 +709,7 @@ FOLIAGE_ZONES = {
"scale_range": (0.45, 1.05),
},
"grass": {
"count": 260,
"count": 420,
"x_range": (-46000.0, 46000.0),
"y_range": (-46000.0, 46000.0),
"min_elevation_m": 4.0,
@@ -702,6 +791,209 @@ def ensure_native_placeholder_meshes():
unreal.log(f"Created native placeholder mesh: {destination_path}")
def write_obj_mesh(path, vertices, faces):
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", encoding="utf-8") as handle:
handle.write("# Agrarian generated coastal scrub vegetation proxy\n")
for vertex in vertices:
handle.write(f"v {vertex[0]:.3f} {vertex[1]:.3f} {vertex[2]:.3f}\n")
for face in faces:
handle.write("f " + " ".join(str(index + 1) for index in face) + "\n")
def add_box(vertices, faces, center, size):
cx, cy, cz = center
sx, sy, sz = size[0] * 0.5, size[1] * 0.5, size[2] * 0.5
base = len(vertices)
vertices.extend(
[
(cx - sx, cy - sy, cz - sz),
(cx + sx, cy - sy, cz - sz),
(cx + sx, cy + sy, cz - sz),
(cx - sx, cy + sy, cz - sz),
(cx - sx, cy - sy, cz + sz),
(cx + sx, cy - sy, cz + sz),
(cx + sx, cy + sy, cz + sz),
(cx - sx, cy + sy, cz + sz),
]
)
faces.extend(
[
(base + 0, base + 1, base + 2, base + 3),
(base + 4, base + 7, base + 6, base + 5),
(base + 0, base + 4, base + 5, base + 1),
(base + 1, base + 5, base + 6, base + 2),
(base + 2, base + 6, base + 7, base + 3),
(base + 3, base + 7, base + 4, base + 0),
]
)
def add_leaf_card(vertices, faces, center, width, height, yaw_degrees, lean_degrees=0.0):
yaw = math.radians(yaw_degrees)
lean = math.radians(lean_degrees)
right = (math.cos(yaw) * width * 0.5, math.sin(yaw) * width * 0.5, 0.0)
up_offset = (math.sin(lean) * height * 0.35, 0.0, math.cos(lean) * height)
cx, cy, cz = center
base = len(vertices)
vertices.extend(
[
(cx - right[0], cy - right[1], cz),
(cx + right[0], cy + right[1], cz),
(cx + right[0] + up_offset[0], cy + right[1] + up_offset[1], cz + up_offset[2]),
(cx - right[0] + up_offset[0], cy - right[1] + up_offset[1], cz + up_offset[2]),
]
)
faces.append((base + 0, base + 1, base + 2, base + 3))
def add_irregular_leaf_card(vertices, faces, center, width, height, yaw_degrees, lean_degrees=0.0, pinch=0.18):
yaw = math.radians(yaw_degrees)
lean = math.radians(lean_degrees)
right = (math.cos(yaw) * width * 0.5, math.sin(yaw) * width * 0.5, 0.0)
up_offset = (math.sin(lean) * height * 0.35, 0.0, math.cos(lean) * height)
cx, cy, cz = center
base = len(vertices)
vertices.extend(
[
(cx - right[0] * 0.86, cy - right[1] * 0.86, cz),
(cx + right[0] * 0.92, cy + right[1] * 0.92, cz + height * 0.08),
(cx + right[0] * pinch + up_offset[0], cy + right[1] * pinch + up_offset[1], cz + up_offset[2]),
(cx - right[0] * 0.68 + up_offset[0] * 0.55, cy - right[1] * 0.68 + up_offset[1] * 0.55, cz + up_offset[2] * 0.58),
]
)
faces.append((base + 0, base + 1, base + 2, base + 3))
def add_tapered_cylinder(vertices, faces, base_center, height, base_radius, top_radius, segments=9, yaw_offset_degrees=0.0, top_offset=(0.0, 0.0)):
bx, by, bz = base_center
yaw_offset = math.radians(yaw_offset_degrees)
base_indices = []
top_indices = []
for index in range(segments):
angle = yaw_offset + (math.tau * index) / segments
cos_a = math.cos(angle)
sin_a = math.sin(angle)
base_indices.append(len(vertices))
vertices.append((bx + cos_a * base_radius, by + sin_a * base_radius, bz))
top_indices.append(len(vertices))
vertices.append((bx + top_offset[0] + cos_a * top_radius, by + top_offset[1] + sin_a * top_radius, bz + height))
base_center_index = len(vertices)
vertices.append((bx, by, bz))
top_center_index = len(vertices)
vertices.append((bx + top_offset[0], by + top_offset[1], bz + height))
for index in range(segments):
next_index = (index + 1) % segments
faces.append((base_indices[index], base_indices[next_index], top_indices[next_index], top_indices[index]))
faces.append((base_center_index, base_indices[index], base_indices[next_index]))
faces.append((top_center_index, top_indices[next_index], top_indices[index]))
def add_low_poly_ellipsoid(vertices, faces, center, radius_x, radius_y, radius_z, segments=10):
cx, cy, cz = center
top_index = len(vertices)
vertices.append((cx, cy, cz + radius_z))
upper = []
lower = []
for ring_z, ring_radius_scale, target in ((0.18, 0.9, upper), (-0.25, 1.0, lower)):
for index in range(segments):
angle = (math.tau * index) / segments
target.append(len(vertices))
vertices.append(
(
cx + math.cos(angle) * radius_x * ring_radius_scale,
cy + math.sin(angle) * radius_y * ring_radius_scale,
cz + (radius_z * ring_z),
)
)
bottom_index = len(vertices)
vertices.append((cx, cy, cz - radius_z * 0.45))
for index in range(segments):
next_index = (index + 1) % segments
faces.append((top_index, upper[index], upper[next_index]))
faces.append((upper[index], lower[index], lower[next_index], upper[next_index]))
faces.append((bottom_index, lower[next_index], lower[index]))
def coastal_oak_mesh():
vertices = []
faces = []
add_tapered_cylinder(vertices, faces, (0.0, 0.0, 0.0), 275.0, 23.0, 12.0, 11, 8.0, (18.0, -10.0))
add_tapered_cylinder(vertices, faces, (-8.0, 2.0, 180.0), 145.0, 12.0, 6.0, 8, 21.0, (-86.0, 36.0))
add_tapered_cylinder(vertices, faces, (12.0, -4.0, 205.0), 150.0, 11.0, 5.5, 8, 12.0, (92.0, -42.0))
add_tapered_cylinder(vertices, faces, (5.0, 0.0, 235.0), 120.0, 9.0, 5.0, 8, 44.0, (22.0, 90.0))
for center, rx, ry, rz, segments in (
((0.0, 4.0, 362.0), 165.0, 126.0, 86.0, 13),
((-108.0, 38.0, 330.0), 116.0, 82.0, 66.0, 11),
((105.0, -34.0, 342.0), 122.0, 88.0, 70.0, 11),
((24.0, 106.0, 330.0), 88.0, 70.0, 58.0, 9),
):
add_low_poly_ellipsoid(vertices, faces, center, rx, ry, rz, segments)
for yaw in (8.0, 52.0, 96.0, 141.0):
add_irregular_leaf_card(vertices, faces, (0.0, 0.0, 285.0), 235.0, 150.0, yaw, 6.0, 0.3)
return vertices, faces
def coyote_brush_mesh():
vertices = []
faces = []
for yaw in (0.0, 27.0, 55.0, 88.0, 122.0, 156.0):
add_irregular_leaf_card(vertices, faces, (0.0, 0.0, 0.0), 170.0, 132.0, yaw, 10.0, 0.24)
for center, rx, ry, rz in (
((-38.0, 18.0, 72.0), 88.0, 62.0, 45.0),
((45.0, -22.0, 68.0), 82.0, 66.0, 42.0),
((0.0, 45.0, 62.0), 72.0, 48.0, 37.0),
((18.0, -56.0, 58.0), 62.0, 44.0, 32.0),
):
add_low_poly_ellipsoid(vertices, faces, center, rx, ry, rz, 9)
return vertices, faces
def dry_grass_clump_mesh():
vertices = []
faces = []
for index, yaw in enumerate((0.0, 16.0, 31.0, 49.0, 73.0, 97.0, 121.0, 148.0, 172.0)):
width = 18.0 + (index % 3) * 4.0
height = 95.0 + (index % 4) * 14.0
add_irregular_leaf_card(vertices, faces, (0.0, 0.0, 0.0), width, height, yaw, -8.0 + (index % 3) * 6.0, 0.08)
return vertices, faces
def import_static_mesh_obj(obj_path, destination_folder, asset_name):
task = unreal.AssetImportTask()
task.set_editor_property("filename", str(obj_path))
task.set_editor_property("destination_path", destination_folder)
task.set_editor_property("destination_name", asset_name)
task.set_editor_property("automated", True)
task.set_editor_property("replace_existing", True)
task.set_editor_property("save", True)
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])
asset_path = f"{destination_folder}/{asset_name}"
asset = unreal.EditorAssetLibrary.load_asset(asset_path)
if not asset:
raise RuntimeError(f"Could not import vegetation mesh asset: {asset_path}")
return asset
def ensure_ground_zero_vegetation_meshes():
if not unreal.EditorAssetLibrary.does_directory_exist(VEGETATION_MESH_FOLDER):
unreal.EditorAssetLibrary.make_directory(VEGETATION_MESH_FOLDER)
mesh_builders = {
"SM_AGR_GZ_CoastalOak_Proxy": coastal_oak_mesh,
"SM_AGR_GZ_CoyoteBrush_Proxy": coyote_brush_mesh,
"SM_AGR_GZ_DryGrassClump_Proxy": dry_grass_clump_mesh,
}
for asset_name, builder in mesh_builders.items():
vertices, faces = builder()
obj_path = VEGETATION_SOURCE_FOLDER / f"{asset_name}.obj"
write_obj_mesh(obj_path, vertices, faces)
import_static_mesh_obj(obj_path, VEGETATION_MESH_FOLDER, asset_name)
unreal.log(f"Created or refreshed Ground Zero vegetation mesh: {VEGETATION_MESH_FOLDER}/{asset_name}")
def ensure_environment_materials():
if not unreal.EditorAssetLibrary.does_directory_exist(MATERIAL_FOLDER):
unreal.EditorAssetLibrary.make_directory(MATERIAL_FOLDER)
@@ -735,11 +1027,166 @@ def ensure_environment_materials():
unreal.MaterialEditingLibrary.recompile_material(material)
unreal.EditorAssetLibrary.save_asset(spec["path"])
unreal.log(f"Created Ground Zero environment material: {spec['path']}")
else:
base_color = unreal.MaterialEditingLibrary.get_material_property_input_node(
material, unreal.MaterialProperty.MP_BASE_COLOR
)
if base_color and hasattr(base_color, "set_editor_property"):
try:
base_color.set_editor_property("constant", spec["color"])
except Exception:
pass
roughness = unreal.MaterialEditingLibrary.get_material_property_input_node(
material, unreal.MaterialProperty.MP_ROUGHNESS
)
if roughness and hasattr(roughness, "set_editor_property"):
try:
roughness.set_editor_property("r", spec["roughness"])
except Exception:
pass
unreal.MaterialEditingLibrary.recompile_material(material)
unreal.EditorAssetLibrary.save_asset(spec["path"], only_if_is_dirty=False)
if spec.get("used_with_instanced_static_meshes"):
material.set_editor_property("used_with_instanced_static_meshes", True)
unreal.MaterialEditingLibrary.recompile_material(material)
unreal.EditorAssetLibrary.save_asset(spec["path"], only_if_is_dirty=False)
if key == "terrain":
rebuild_ground_zero_terrain_material(material, spec)
if key in {"tree", "shrub", "grass"}:
rebuild_ground_zero_foliage_material(material, spec)
created_or_loaded[key] = material
return created_or_loaded
def set_expression_property(expression, property_name, value):
try:
expression.set_editor_property(property_name, value)
return True
except Exception:
return False
def connect_expression(source, source_output, target, target_input):
try:
unreal.MaterialEditingLibrary.connect_material_expressions(source, source_output, target, target_input)
return True
except Exception as exc:
unreal.log_warning(f"Could not connect material expression input {target_input}: {exc}")
return False
def create_constant_color(material, color, x, y):
expression = unreal.MaterialEditingLibrary.create_material_expression(
material, unreal.MaterialExpressionConstant3Vector, x, y
)
expression.set_editor_property("constant", color)
return expression
def create_constant_scalar(material, value, x, y):
expression = unreal.MaterialEditingLibrary.create_material_expression(
material, unreal.MaterialExpressionConstant, x, y
)
expression.set_editor_property("r", value)
return expression
def rebuild_ground_zero_terrain_material(material, spec):
"""Build a simple procedural material so the landscape no longer reads as a flat color."""
if hasattr(unreal.MaterialEditingLibrary, "delete_all_material_expressions"):
unreal.MaterialEditingLibrary.delete_all_material_expressions(material)
dry_soil = create_constant_color(material, spec["dry_soil_color"], -900, -260)
scrub_green = create_constant_color(material, spec["scrub_green_color"], -900, -80)
sandy_path = create_constant_color(material, spec["sandy_path_color"], -900, 100)
broad_noise = unreal.MaterialEditingLibrary.create_material_expression(
material, unreal.MaterialExpressionNoise, -560, -160
)
set_expression_property(broad_noise, "scale", spec.get("noise_scale", 42.0))
set_expression_property(broad_noise, "quality", 3)
set_expression_property(broad_noise, "levels", 5)
fine_noise = unreal.MaterialEditingLibrary.create_material_expression(
material, unreal.MaterialExpressionNoise, -560, 100
)
set_expression_property(fine_noise, "scale", 145.0)
set_expression_property(fine_noise, "quality", 2)
set_expression_property(fine_noise, "levels", 3)
scrub_blend = unreal.MaterialEditingLibrary.create_material_expression(
material, unreal.MaterialExpressionLinearInterpolate, -260, -180
)
connect_expression(dry_soil, "", scrub_blend, "A")
connect_expression(scrub_green, "", scrub_blend, "B")
connect_expression(broad_noise, "", scrub_blend, "Alpha")
final_blend = unreal.MaterialEditingLibrary.create_material_expression(
material, unreal.MaterialExpressionLinearInterpolate, 40, -70
)
connect_expression(scrub_blend, "", final_blend, "A")
connect_expression(sandy_path, "", final_blend, "B")
connect_expression(fine_noise, "", final_blend, "Alpha")
unreal.MaterialEditingLibrary.connect_material_property(
final_blend, "", unreal.MaterialProperty.MP_BASE_COLOR
)
roughness = create_constant_scalar(material, spec["roughness"], -260, 160)
unreal.MaterialEditingLibrary.connect_material_property(
roughness, "", unreal.MaterialProperty.MP_ROUGHNESS
)
specular = create_constant_scalar(material, 0.18, -260, 260)
unreal.MaterialEditingLibrary.connect_material_property(
specular, "", unreal.MaterialProperty.MP_SPECULAR
)
material.set_editor_property("use_material_attributes", False)
unreal.MaterialEditingLibrary.recompile_material(material)
unreal.EditorAssetLibrary.save_asset(spec["path"], only_if_is_dirty=False)
unreal.log("Rebuilt Ground Zero terrain material with coastal scrub color variation and procedural noise.")
def rebuild_ground_zero_foliage_material(material, spec):
"""Use per-instance color variation so repeated foliage clumps do not tile visually."""
if hasattr(unreal.MaterialEditingLibrary, "delete_all_material_expressions"):
unreal.MaterialEditingLibrary.delete_all_material_expressions(material)
color_a = create_constant_color(material, spec["color"], -720, -160)
color_b = create_constant_color(material, spec["variation_color"], -720, 20)
blend = unreal.MaterialEditingLibrary.create_material_expression(
material, unreal.MaterialExpressionLinearInterpolate, -360, -80
)
connect_expression(color_a, "", blend, "A")
connect_expression(color_b, "", blend, "B")
random_expression_class = getattr(unreal, "MaterialExpressionPerInstanceRandom", None)
if random_expression_class:
random_expression = unreal.MaterialEditingLibrary.create_material_expression(
material, random_expression_class, -600, 170
)
connect_expression(random_expression, "", blend, "Alpha")
unreal.MaterialEditingLibrary.connect_material_property(
blend, "", unreal.MaterialProperty.MP_BASE_COLOR
)
roughness = create_constant_scalar(material, spec["roughness"], -360, 140)
unreal.MaterialEditingLibrary.connect_material_property(
roughness, "", unreal.MaterialProperty.MP_ROUGHNESS
)
material.set_editor_property("use_material_attributes", False)
material.set_editor_property("used_with_instanced_static_meshes", True)
if spec.get("two_sided"):
material.set_editor_property("two_sided", True)
unreal.MaterialEditingLibrary.recompile_material(material)
unreal.EditorAssetLibrary.save_asset(spec["path"], only_if_is_dirty=False)
def apply_material_to_actor_meshes(actor, material):
applied_count = 0
for component in actor.get_components_by_class(unreal.StaticMeshComponent):
@@ -1166,6 +1613,7 @@ def main():
raise RuntimeError(f"Could not load map: {MAP_PATH}")
ensure_native_placeholder_meshes()
ensure_ground_zero_vegetation_meshes()
labels = {spec["label"] for spec in DEMO_ACTORS}
labels.update(LEGACY_DEMO_LIGHTING_LABELS)
@@ -0,0 +1,78 @@
#!/usr/bin/env python3
"""Verify 0.1 is complete and 0.2 readiness notes are present."""
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
ROADMAP = ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md"
REVIEW = ROOT / "Docs" / "CodebaseReadinessReview.md"
REQUIRED_ROADMAP_SNIPPETS = [
"# Version 0.2 - Persistent Homesteading",
"Engineering posture for 0.2:",
"Define claim data schema with claim ID",
"Define crop data asset schema",
"Define animal data asset schema",
"Define storage data schema",
"Define structure piece data asset schema",
"Define transaction record schema",
"Convert the 0.1.R knowledge foundation into first data records",
"## 0.2.I 0.1 Hardening And Technical Debt",
"Split `AgrarianEditorAutomationLibrary.cpp`",
"Create a grouped verifier runner",
"Separate admin/dev console command authority",
"## 0.2.J Homesteading Visual And Biome Realism Pass",
"Replace tan/flat terrain presentation",
"Add investor visual QA screenshots",
]
REQUIRED_REVIEW_SNIPPETS = [
"# Codebase Readiness Review",
"All `0.1.A` through `0.1.R` roadmap checkboxes are complete.",
"AgrarianEditorAutomationLibrary.cpp` is doing too much",
"Low-Risk Cleanup Applied",
"Removed duplicate `SavingAndQuit` branch logic",
"0.2 Engineering Priorities",
]
def get_0_1_text(roadmap: str) -> str:
start = roadmap.find("## 0.1.A ")
end = roadmap.find("# Version 0.2")
if start == -1 or end == -1 or end <= start:
raise SystemExit("FAILED: could not isolate 0.1 roadmap section")
return roadmap[start:end]
def main() -> None:
roadmap = ROADMAP.read_text(encoding="utf-8")
review = REVIEW.read_text(encoding="utf-8")
failures: list[str] = []
section_0_1 = get_0_1_text(roadmap)
unchecked_0_1 = [
line.strip()
for line in section_0_1.splitlines()
if line.strip().startswith("- [ ]")
]
if unchecked_0_1:
failures.append("0.1 has unchecked items: " + "; ".join(unchecked_0_1[:5]))
for snippet in REQUIRED_ROADMAP_SNIPPETS:
if snippet not in roadmap:
failures.append(f"roadmap missing {snippet!r}")
for snippet in REQUIRED_REVIEW_SNIPPETS:
if snippet not in review:
failures.append(f"readiness review missing {snippet!r}")
if failures:
raise SystemExit("FAILED: " + "; ".join(failures))
print("OK: 0.1 completion and 0.2 readiness are documented.")
if __name__ == "__main__":
main()
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env python3
"""Fail if tracked Agrarian assets include paid or unclear costs."""
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
REGISTER = ROOT / "Docs" / "Art" / "AssetLicenses.md"
ALLOWED_COSTS = {"free", "$0", "0", "n/a", "na"}
BLOCKED_WORDS = {
"paid",
"purchased",
"purchase",
"subscription",
"unknown",
"unclear",
"tbd",
"marketplace bundle",
}
def parse_markdown_table_rows(text: str) -> list[list[str]]:
rows: list[list[str]] = []
for line in text.splitlines():
stripped = line.strip()
if not stripped.startswith("|") or "---" in stripped:
continue
cells = [cell.strip() for cell in stripped.strip("|").split("|")]
if cells and cells[0] != "Asset":
rows.append(cells)
return rows
def main() -> None:
if not REGISTER.exists():
raise SystemExit(f"Missing asset license register: {REGISTER}")
rows = parse_markdown_table_rows(REGISTER.read_text(encoding="utf-8"))
failures: list[str] = []
for row in rows:
if len(row) < 5:
failures.append(f"Malformed asset license row: {' | '.join(row)}")
continue
asset = row[0]
cost = row[4].strip().lower()
full_row = " ".join(row).lower()
if not cost:
failures.append(f"{asset}: cost is blank")
elif cost not in ALLOWED_COSTS:
failures.append(f"{asset}: cost {row[4]!r} is not free-only")
for blocked in BLOCKED_WORDS:
if blocked in full_row:
failures.append(f"{asset}: blocked free-only word found: {blocked!r}")
if failures:
raise SystemExit("\n".join(failures))
print(f"Asset license free-only verification complete: {len(rows)} entries.")
if __name__ == "__main__":
main()
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env python3
"""Verify Agrarian asset pipeline policy docs are present and explicit."""
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
REQUIRED = {
"Docs/Art/AssetLicenses.md": [
"Fab assets explicitly marked free",
"Quixel/Megascans",
"CC0 or public-domain",
"/home/nathan/AssetStaging/Agrarian",
"Do not import random internet images",
"Free-Only Acquisition Gate",
"Paid Fab assets are blocked",
"Native Ground Zero proxy vegetation",
],
"Docs/Art/AgrarianAssetPipeline.md": [
"realistic modern post-collapse frontier survival",
"Old abandoned equipment",
"Do not scrape random internet images or models",
"Free-Only Lockdown",
"Do not click purchase",
"LicenseEvidence",
"Run visual and placeholder verifiers",
"Water should be handled as a shader/system problem",
],
"AGRARIAN_DEVELOPMENT_ROADMAP.md": [
"Asset acquisition and ingest pipeline",
"Fab/free, Quixel, CC0/public-domain",
"old abandoned equipment",
],
}
def main() -> None:
missing: list[str] = []
for relative_path, snippets in REQUIRED.items():
path = ROOT / relative_path
if not path.exists():
missing.append(f"missing file: {relative_path}")
continue
text = path.read_text(encoding="utf-8")
for snippet in snippets:
if snippet not in text:
missing.append(f"{relative_path}: missing snippet {snippet!r}")
if missing:
raise SystemExit("\n".join(missing))
print("Asset pipeline policy verification complete.")
if __name__ == "__main__":
main()
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env python3
"""Verify deeper-question timing rules are documented."""
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
DOC = ROOT / "Docs" / "KnowledgeAndSkillFoundation.md"
ROADMAP = ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md"
REQUIRED = {
DOC: [
"## When Deeper Questions Matter",
"Quality improvements:",
"Safer work:",
"Complex crafting:",
"Teaching others:",
"Advanced branches:",
"Depth rule:",
],
ROADMAP: [
"[x] Define when deeper questions should matter: quality improvements, safer work, complex crafting, teaching others, and advanced branches.",
],
}
def main() -> None:
missing: list[str] = []
for path, snippets in REQUIRED.items():
text = path.read_text(encoding="utf-8")
for snippet in snippets:
if snippet not in text:
missing.append(f"{path.relative_to(ROOT)} missing {snippet!r}")
if missing:
raise SystemExit("FAILED: " + "; ".join(missing))
print("OK: deeper-question timing rules are documented.")
if __name__ == "__main__":
main()
@@ -0,0 +1,41 @@
#!/usr/bin/env python3
"""Verify the MVP elementary survival question bank is documented."""
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
DOC = ROOT / "Docs" / "KnowledgeAndSkillFoundation.md"
ROADMAP = ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md"
REQUIRED = {
DOC: [
"## MVP Elementary Survival Question Bank",
"id: fire.clearance.001",
"id: water.potable.001",
"id: exposure.cold.001",
"id: shelter.drainage.001",
"id: injury.bleeding.001",
"id: resource.fiber.001",
"Question-bank rule:",
],
ROADMAP: [
"[x] Add a small MVP question bank for elementary survival knowledge.",
],
}
def main() -> None:
missing: list[str] = []
for path, snippets in REQUIRED.items():
text = path.read_text(encoding="utf-8")
for snippet in snippets:
if snippet not in text:
missing.append(f"{path.relative_to(ROOT)} missing {snippet!r}")
if missing:
raise SystemExit("FAILED: " + "; ".join(missing))
print("OK: elementary survival question bank is documented.")
if __name__ == "__main__":
main()
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env python3
"""Verify the Ground Zero visual asset acquisition queue stays actionable."""
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
QUEUE = ROOT / "Docs" / "Art" / "GroundZeroAssetAcquisitionQueue.md"
BIOME_PLAN = ROOT / "Docs" / "World" / "BiomeAndNaturalResourceGenerationPlan.md"
ROADMAP = ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md"
REQUIRED_QUEUE = [
"Free Shrubs Pack (Ultra Realistic Wind)",
"Mediterranean Vegetation: Plant Pack I",
"temperate Vegetation: optimized Grass Library",
"Soul: Cave",
"Modular Rural House & Pine Forest Environment",
"/home/nathan/AssetStaging/Agrarian/Incoming",
"verify_asset_license_free_only.py",
"persistent world-resource record",
]
REQUIRED_BIOME_PLAN = [
"weighted macro/regional/local biome blends",
"Persistent Natural Resources",
"removed/depleted timestamp",
"Player-removed trees and shrubs stay removed",
"realistic seasonal timelines",
"asset sets by biome weight",
]
REQUIRED_ROADMAP = [
"Ground Zero asset acquisition queue",
"Tile Biome And Natural Resource Foundation",
"persistent removable natural resource records",
]
def require_snippets(path: Path, snippets: list[str]) -> list[str]:
if not path.exists():
return [f"missing file: {path.relative_to(ROOT)}"]
text = path.read_text(encoding="utf-8")
return [
f"{path.relative_to(ROOT)} missing {snippet!r}"
for snippet in snippets
if snippet not in text
]
def main() -> None:
failures: list[str] = []
failures.extend(require_snippets(QUEUE, REQUIRED_QUEUE))
failures.extend(require_snippets(BIOME_PLAN, REQUIRED_BIOME_PLAN))
failures.extend(require_snippets(ROADMAP, REQUIRED_ROADMAP))
if failures:
raise SystemExit("\n".join(failures))
print("Ground Zero asset queue and biome/resource plan verification complete.")
if __name__ == "__main__":
main()
@@ -7,9 +7,9 @@ import unreal
MAP_PATH = "/Game/Agrarian/Maps/L_GroundZeroTerrain_Test"
FOLIAGE_LABEL = "AGR_GroundZeroFoliage_FirstPass"
EXPECTED_FOLIAGE_COUNTS = {
"trees": 64,
"shrubs": 148,
"grass": 260,
"trees": 96,
"shrubs": 220,
"grass": 420,
}
CRITICAL_CLEARANCE_CM = {
"trees": 5200.0,
+3 -3
View File
@@ -4,9 +4,9 @@ import unreal
MAP_PATH = "/Game/Agrarian/Maps/L_GroundZeroTerrain_Test"
FOLIAGE_LABEL = "AGR_GroundZeroFoliage_FirstPass"
EXPECTED_COUNTS = {
"trees": 42,
"shrubs": 96,
"grass": 180,
"trees": 96,
"shrubs": 220,
"grass": 420,
}
@@ -15,10 +15,24 @@ MATERIALS = {
"fresh_water": "/Game/Agrarian/Materials/M_AGR_GZ_FreshWater",
}
EXPECTED_FOLIAGE_COUNTS = {
"trees": 64,
"shrubs": 148,
"grass": 260,
"trees": 96,
"shrubs": 220,
"grass": 420,
}
EXPECTED_FOLIAGE_MESHES = {
"tree_instances": "/Game/Agrarian/Environment/Vegetation/SM_AGR_GZ_CoastalOak_Proxy",
"shrub_instances": "/Game/Agrarian/Environment/Vegetation/SM_AGR_GZ_CoyoteBrush_Proxy",
"grass_instances": "/Game/Agrarian/Environment/Vegetation/SM_AGR_GZ_DryGrassClump_Proxy",
}
EXPECTED_FOLIAGE_CULL_DISTANCES = {
"tree_instances": (65000, 95000),
"shrub_instances": (28000, 52000),
"grass_instances": (9000, 22000),
}
FORBIDDEN_FOLIAGE_MESH_PREFIXES = (
"/Engine/BasicShapes/",
"/Game/LevelPrototyping/",
)
RESOURCE_MATERIALS = {
"AGR_GZ_Wood": "wood_resource",
"AGR_GZ_Fiber": "fiber_resource",
@@ -29,8 +43,13 @@ RESOURCE_MATERIALS = {
"AGR_GZ_FreshWaterSource": "fresh_water",
}
VARIATION_PREFIX = "AGR_GZ_EnvVar_"
EXPECTED_VARIATION_COUNT = 23
EXPECTED_VARIATION_COUNT = 31
EXPECTED_VARIATION_MATERIALS = {
"AGR_GZ_EnvVar_FirstLook_OakCanopy": "tree",
"AGR_GZ_EnvVar_FirstLook_OakTrunk": "wood_resource",
"AGR_GZ_EnvVar_FirstLook_Brush": "shrub",
"AGR_GZ_EnvVar_FirstLook_GrassMat": "grass",
"AGR_GZ_EnvVar_FirstLook_Rock": "stone_resource",
"AGR_GZ_EnvVar_Tree_Canopy": "tree",
"AGR_GZ_EnvVar_Tree_Trunk": "wood_resource",
"AGR_GZ_EnvVar_Bush": "shrub",
@@ -56,6 +75,29 @@ def material_path(material):
return material.get_path_name().split(".", 1)[0]
def asset_path(asset):
if not asset:
return ""
return asset.get_path_name().split(".", 1)[0]
def get_component_property(component, property_name, default=None):
try:
return component.get_editor_property(property_name)
except Exception:
return getattr(component, property_name, default)
def static_mesh_vertex_count(mesh):
library = getattr(unreal, "EditorStaticMeshLibrary", None)
if not mesh or not library:
return -1
try:
return library.get_number_verts(mesh, 0)
except Exception:
return -1
def material_key_for_label(label):
for prefix, material_key in RESOURCE_MATERIALS.items():
if label.startswith(prefix):
@@ -77,6 +119,44 @@ def assert_asset(path):
return asset
def verify_terrain_material_is_not_flat(material, failures):
project_root = unreal.Paths.convert_relative_path_to_full(unreal.Paths.project_dir())
material_package = project_root + "Content/Agrarian/Materials/M_AGR_GZ_Terrain_CoastalScrub.uasset"
try:
with open(material_package, "rb") as handle:
package_bytes = handle.read()
except Exception as exc:
failures.append(f"could not inspect terrain material package: {exc}")
return
noise_count = package_bytes.count(b"MaterialExpressionNoise")
lerp_count = package_bytes.count(b"MaterialExpressionLinearInterpolate")
color_count = package_bytes.count(b"MaterialExpressionConstant3Vector")
if noise_count < 1:
failures.append("terrain material should include a noise expression for color breakup")
if lerp_count < 1:
failures.append("terrain material should include a blend expression instead of a flat base color")
if color_count < 1:
failures.append("terrain material should include coastal scrub color vector expressions")
def verify_foliage_material_has_variation(material_path_name, failures):
project_root = unreal.Paths.convert_relative_path_to_full(unreal.Paths.project_dir())
package_path = project_root + "Content" + material_path_name.replace("/Game", "") + ".uasset"
try:
with open(package_path, "rb") as handle:
package_bytes = handle.read()
except Exception as exc:
failures.append(f"could not inspect foliage material package {material_path_name}: {exc}")
return
if package_bytes.count(b"MaterialExpressionLinearInterpolate") < 1:
failures.append(f"{material_path_name} should blend foliage color variation")
if package_bytes.count(b"MaterialExpressionConstant3Vector") < 1:
failures.append(f"{material_path_name} should include foliage color vectors")
def main():
if not unreal.EditorLevelLibrary.load_level(MAP_PATH):
raise RuntimeError(f"Could not load map: {MAP_PATH}")
@@ -93,6 +173,7 @@ def main():
expected = MATERIALS["terrain"]
if assigned != expected:
failures.append(f"landscape material expected {expected}, got {assigned}")
verify_terrain_material_is_not_flat(materials["terrain"], failures)
foliage_actors = [actor for actor in actors if get_actor_label(actor) == FOLIAGE_LABEL]
if len(foliage_actors) != 1:
@@ -115,10 +196,31 @@ def main():
}
for property_name, material_key in component_expectations.items():
component = foliage.get_editor_property(property_name)
static_mesh = component.get_editor_property("static_mesh")
mesh_path = asset_path(static_mesh)
expected_mesh = EXPECTED_FOLIAGE_MESHES[property_name]
if mesh_path != expected_mesh:
failures.append(f"{property_name} mesh expected {expected_mesh}, got {mesh_path}")
if mesh_path.startswith(FORBIDDEN_FOLIAGE_MESH_PREFIXES):
failures.append(f"{property_name} still uses placeholder/basic mesh {mesh_path}")
vertex_count = static_mesh_vertex_count(static_mesh)
if vertex_count <= 0:
failures.append(f"{property_name} mesh {mesh_path} has no renderable vertices")
assigned = material_path(component.get_material(0))
expected = MATERIALS[material_key]
if assigned != expected:
failures.append(f"{property_name} material expected {expected}, got {assigned}")
verify_foliage_material_has_variation(expected, failures)
start_cull, end_cull = EXPECTED_FOLIAGE_CULL_DISTANCES[property_name]
actual_start = int(get_component_property(component, "instance_start_cull_distance", -1))
actual_end = int(get_component_property(component, "instance_end_cull_distance", -1))
if actual_start != start_cull or actual_end != end_cull:
failures.append(
f"{property_name} cull distances expected {start_cull}/{end_cull}, "
f"got {actual_start}/{actual_end}"
)
checked_resource_actors = 0
for actor in actors:
@@ -182,8 +284,12 @@ def main():
roadmap = unreal.Paths.convert_relative_path_to_full(unreal.Paths.project_dir()) + "AGRARIAN_DEVELOPMENT_ROADMAP.md"
for path, snippet in [
(docs, "asset variation"),
(docs, "procedural coastal scrub terrain material"),
(docs, "native low-poly coastal scrub vegetation meshes"),
(roadmap, "[x] Replace grey-box environment presentation with an MVP natural environment pass"),
(roadmap, "[x] Add first-pass environment asset variation"),
(roadmap, "[x] Replace or upgrade the terrain material first so Ground Zero no longer reads as flat tan placeholder ground."),
(roadmap, "[x] Replace or upgrade grasses, shrubs, and trees with believable coastal-scrub vegetation assets"),
]:
with open(path, "r", encoding="utf-8") as handle:
text = handle.read()
@@ -0,0 +1,44 @@
#!/usr/bin/env python3
"""Verify knowledge and skill persistence requirements are documented."""
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
DOC = ROOT / "Docs" / "KnowledgeAndSkillFoundation.md"
ROADMAP = ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md"
REQUIRED = {
DOC: [
"## Knowledge Persistence Requirements",
"Stable knowledge profile ID",
"Learned concepts by stable concept ID.",
"Practical skill experience by taxonomy domain.",
"Failed attempts that produced useful feedback",
"Tutorial and contextual prompt state",
"Optional knowledge checks answered, skipped, failed, or recently displayed.",
"Teaching, observation, and shared-work learning events",
"Schema version, migration marker",
"The server is authoritative for writes in multiplayer.",
"Persistence rule:",
],
ROADMAP: [
"[x] Add persistence requirements for knowledge, skill experience, learned concepts, failed attempts, and tutorial state.",
],
}
def main() -> None:
missing: list[str] = []
for path, snippets in REQUIRED.items():
text = path.read_text(encoding="utf-8")
for snippet in snippets:
if snippet not in text:
missing.append(f"{path.relative_to(ROOT)} missing {snippet!r}")
if missing:
raise SystemExit("FAILED: " + "; ".join(missing))
print("OK: knowledge persistence requirements are documented.")
if __name__ == "__main__":
main()
@@ -0,0 +1,40 @@
#!/usr/bin/env python3
"""Verify exploit-farming and rote-memorization guardrails are documented."""
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
DOC = ROOT / "Docs" / "KnowledgeAndSkillFoundation.md"
ROADMAP = ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md"
REQUIRED = {
DOC: [
"## Exploit And Rote-Memorization Guardrails",
"Exploit farming risks:",
"Guardrails:",
"diminishing returns",
"meaningful context",
"varied wording",
"Anti-exploit rule:",
],
ROADMAP: [
"[x] Add design notes for avoiding exploit farming and rote memorization.",
],
}
def main() -> None:
missing: list[str] = []
for path, snippets in REQUIRED.items():
text = path.read_text(encoding="utf-8")
for snippet in snippets:
if snippet not in text:
missing.append(f"{path.relative_to(ROOT)} missing {snippet!r}")
if missing:
raise SystemExit("FAILED: " + "; ".join(missing))
print("OK: learning exploit guardrails are documented.")
if __name__ == "__main__":
main()
@@ -0,0 +1,46 @@
#!/usr/bin/env python3
"""Verify multiplayer learning rules are documented."""
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
DOC = ROOT / "Docs" / "KnowledgeAndSkillFoundation.md"
ROADMAP = ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md"
REQUIRED = {
DOC: [
"## Multiplayer Learning Rules",
"Teaching:",
"The server validates teaching credit.",
"Observation:",
"Passive observation grants less credit than direct practice.",
"Shared work:",
"Group tasks can grant role-specific experience to active contributors.",
"Group skill benefits:",
"Benefits should be capped",
"No global aura:",
"Network rules:",
"Server authority validates distance, visibility, participation, role,",
"Multiplayer rule:",
],
ROADMAP: [
"[x] Add multiplayer rules for teaching, observation, shared work, and group skill benefits.",
],
}
def main() -> None:
missing: list[str] = []
for path, snippets in REQUIRED.items():
text = path.read_text(encoding="utf-8")
for snippet in snippets:
if snippet not in text:
missing.append(f"{path.relative_to(ROOT)} missing {snippet!r}")
if missing:
raise SystemExit("FAILED: " + "; ".join(missing))
print("OK: multiplayer learning rules are documented.")
if __name__ == "__main__":
main()
+5 -4
View File
@@ -12,8 +12,6 @@ CONFIG = ROOT / "Config" / "DefaultGame.ini"
DOC = ROOT / "Docs" / "Characters" / "MvpCharacterProxies.md"
SETUP = ROOT / "Scripts" / "setup_mvp_character_proxies.py"
ROADMAP = ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md"
MALE_MATERIAL = ROOT / "Content" / "Agrarian" / "Characters" / "Materials" / "M_AGR_CharacterProxy_Workwear_Male.uasset"
FEMALE_MATERIAL = ROOT / "Content" / "Agrarian" / "Characters" / "Materials" / "M_AGR_CharacterProxy_Workwear_Female.uasset"
def require(condition: bool, message: str) -> None:
@@ -43,6 +41,11 @@ def main() -> None:
"M_AGR_CharacterProxy_Workwear_Female",
"SetSkeletalMesh",
"SetMaterial",
"MvpWorkwearTorsoProxy",
"MvpWorkwearBackpackProxy",
"MvpWorkwearBedrollProxy",
"MvpWorkwearBootsProxy",
"EnsureMvpPresentationComponent",
"SelectedMvpCharacterProxyId = TEXT(\"male\")",
"SelectedMvpCharacterProxyId = TEXT(\"female\")",
"ApplyMvpCharacterProxyToPawn();",
@@ -82,8 +85,6 @@ def main() -> None:
"- [x] Add first realistic playable character proxies for the selected young adult male and female archetypes" in roadmap,
"0.1.O character proxy roadmap item is not checked off",
)
require(MALE_MATERIAL.exists(), "male character proxy material asset is missing")
require(FEMALE_MATERIAL.exists(), "female character proxy material asset is missing")
print("OK: MVP male/female playable character proxy flow is wired and documented.")
+2 -3
View File
@@ -37,9 +37,8 @@ def main() -> None:
"UButton::StaticClass()",
"UTextBlock::StaticClass()",
"OnClicked.AddDynamic",
"OnHovered.AddDynamic",
"ButtonStyle.Hovered.TintColor",
"ButtonStyle.Pressed.TintColor",
"SetBackgroundColor",
"DeferFrontendAction",
"SetIsFocusable(true)",
"SetKeyboardFocus()",
"BackFromActiveScreen()",
+36 -3
View File
@@ -13,23 +13,42 @@ EXPECTED = {
"AgrarianMvpFrontendWidget.h": [
"ConfirmActiveScreen",
"BackFromActiveScreen",
"SaveGame",
"SaveAndQuit",
"QuitWithoutSaving",
"Settings",
"GameSaved",
],
"AgrarianMvpFrontendWidget.cpp": [
"UButton::StaticClass()",
"HandleMaleCharacterClicked",
"HandleFemaleCharacterClicked",
"OnClicked.AddDynamic",
"Save & Quit",
"Save Game",
"Settings",
"Save & Exit",
"Quit Without Saving",
"Saving World",
"Game Saved",
"Player Options",
"HandleSaveGameClicked",
"HandleSettingsClicked",
"HandleQuitWithoutSavingClicked",
"ExecuteSaveGame",
"ExecuteQuitWithoutSaving",
"ConsoleCommand(TEXT(\"AgrarianSaveWorld\"))",
"ConsoleCommand(TEXT(\"quit\"))",
"PlayerController->SetIgnoreMoveInput(false)",
"PlayerController->SetIgnoreLookInput(false)",
"AAgrarianGamePlayerController* AgrarianPlayerController",
"AgrarianPlayerController->AgrarianSelectCharacter",
"AgrarianPlayerController->AgrarianCompleteFrontend",
"PlayerController->ResetIgnoreMoveInput()",
"PlayerController->ResetIgnoreLookInput()",
],
"AgrarianGamePlayerController.h": [
"ShowMvpPauseMenu",
"HandleMvpEscapeInput",
"RestoreGameplayControlState",
"AgrarianRepairGameplayInput",
],
"AgrarianGamePlayerController.cpp": [
"SetIgnoreMoveInput(true)",
@@ -40,10 +59,24 @@ EXPECTED = {
"InputComponent->BindKey(EKeys::Escape",
"MvpFrontendWidget->IsInViewport()",
"ShowMvpPauseMenu();",
"ResetIgnoreMoveInput();",
"ResetIgnoreLookInput();",
"ApplyDefaultInputMappingContexts();",
"void AAgrarianGamePlayerController::RestoreGameplayControlState()",
"ControlledPawn->SetActorHiddenInGame(false)",
"ControlledPawn->SetActorEnableCollision(true)",
"MovementComponent->SetMovementMode(MOVE_Walking)",
"void AAgrarianGamePlayerController::AgrarianRepairGameplayInput()",
],
}
FORBIDDEN = {
"AgrarianMvpFrontendWidget.cpp": [
"ConsoleCommand(TEXT(\"AgrarianSelectCharacter",
"ConsoleCommand(TEXT(\"AgrarianCompleteFrontend\"))",
"PlayerController->SetIgnoreMoveInput(false)",
"PlayerController->SetIgnoreLookInput(false)",
],
"AgrarianGamePlayerController.cpp": [
"BindKey(EKeys::LeftMouseButton",
],
@@ -23,8 +23,12 @@ def main() -> None:
roadmap = ROADMAP.read_text(encoding="utf-8")
for token in (
"Settings",
"GameSaved",
"SavingAndQuit",
"SaveGame",
"ExecuteSaveAndQuit",
"QuitWithoutSaving",
):
require(token in header, f"missing segmented flow declaration: {token}")
@@ -34,11 +38,20 @@ def main() -> None:
"Loading Segment",
"Pause Menu",
"Gameplay is paused while this menu is active.",
"Save Game",
"Settings",
"Quit Without Saving",
"Game Saved",
"Player Options",
"Saving World",
"Writing the current world state",
"SetActiveScreen(EAgrarianMvpFrontendScreen::GameSaved)",
"SetActiveScreen(EAgrarianMvpFrontendScreen::Settings)",
"SetActiveScreen(EAgrarianMvpFrontendScreen::SavingAndQuit)",
"GetTimerManager().SetTimer",
"ExecuteSaveGame",
"ExecuteSaveAndQuit",
"ExecuteQuitWithoutSaving",
"ConsoleCommand(TEXT(\"AgrarianSaveWorld\"))",
"ConsoleCommand(TEXT(\"quit\"))",
):
@@ -52,8 +65,9 @@ def main() -> None:
"SetIgnoreMoveInput(true)",
"SetIgnoreLookInput(true)",
"SetInputMode(FInputModeGameOnly())",
"SetIgnoreMoveInput(false)",
"SetIgnoreLookInput(false)",
"ResetIgnoreMoveInput()",
"ResetIgnoreLookInput()",
"RestoreGameplayControlState",
"saving",
):
require(token in controller + frontend, f"missing modal input or debug token: {token}")
+10 -2
View File
@@ -6,6 +6,7 @@ import unreal
MAP_PATH = "/Game/Agrarian/Maps/L_GroundZeroTerrain_Test"
PROJECT_ROOT = Path(unreal.Paths.convert_relative_path_to_full(unreal.Paths.project_dir()))
PLACEHOLDER_MESH_FOLDER = "/Game/Agrarian/Environment/PlaceholderMeshes"
VEGETATION_MESH_FOLDER = "/Game/Agrarian/Environment/Vegetation"
PLACEHOLDER_MESHES = {
"SM_AGR_Placeholder_Cube",
"SM_AGR_Placeholder_ChamferCube",
@@ -47,6 +48,13 @@ def assert_native_mesh(path, failures):
failures.append(f"template mesh reference remains: {path}")
def assert_agrarian_environment_mesh(path, failures):
if not path.startswith((PLACEHOLDER_MESH_FOLDER, VEGETATION_MESH_FOLDER)):
failures.append(f"expected native Agrarian environment mesh, got {path}")
if "LevelPrototyping" in path or path.startswith("/Engine/BasicShapes/"):
failures.append(f"template/basic mesh reference remains: {path}")
def main():
failures = []
@@ -79,7 +87,7 @@ def main():
else:
for property_name in ("tree_instances", "shrub_instances", "grass_instances"):
component = foliage_actors[0].get_editor_property(property_name)
assert_native_mesh(asset_path(component.get_editor_property("static_mesh")), failures)
assert_agrarian_environment_mesh(asset_path(component.get_editor_property("static_mesh")), failures)
for actor in actors:
label = get_actor_label(actor)
@@ -90,7 +98,7 @@ def main():
if not mesh_components:
failures.append(f"{label} has no static mesh component")
continue
assert_native_mesh(asset_path(mesh_components[0].get_editor_property("static_mesh")), failures)
assert_agrarian_environment_mesh(asset_path(mesh_components[0].get_editor_property("static_mesh")), failures)
for path, snippet in DOC_SNIPPETS:
text = path.read_text(encoding="utf-8")
+22 -2
View File
@@ -6,16 +6,27 @@ ROOT = Path(__file__).resolve().parents[1]
REQUIRED = {
ROOT / "Source" / "AgrarianGame" / "AgrarianDemoNoticeActor.h": [
"float NoticeDurationSeconds = 24.0f;",
"bool bAutoShowNotice = false;",
"Investor Demo v0.1.N - Build 2026.05.18",
],
ROOT / "Source" / "AgrarianGame" / "AgrarianDemoNoticeWidget.h": [
"virtual void NativeConstruct() override;",
"NativeOnKeyDown",
"NativeOnMouseButtonDown",
"EAgrarianStartupPresentationSegment",
"DrawSplash",
"DrawStory",
"DrawCinematicCredits",
"DrawCreditIllustration",
"CreditsStartTimeSeconds",
"SegmentStartTimeSeconds",
"Investor Demo v0.1.N - Build 2026.05.18",
],
ROOT / "Source" / "AgrarianGame" / "AgrarianDemoNoticeWidget.cpp": [
"The lights did not go out at once.",
"Agrarian begins at Ground Zero.",
"Press any key to skip to credits",
"RequestSkipSegment",
"AgrarianSkipStartupPresentation",
"Nathan Slaven",
"Lead Developer",
"Hunter Slaven",
@@ -33,10 +44,19 @@ REQUIRED = {
"Agrarian Startup Credits",
],
ROOT / "Source" / "AgrarianGame" / "AgrarianGamePlayerController.h": [
"MvpFrontendStartupDelaySeconds = 24.25f",
"StartupSplashSeconds = 4.0f",
"StartupStorySeconds = 60.0f",
"StartupCreditsSeconds = 20.75f",
"StartupPresentationWidget",
"AgrarianSkipStartupPresentation",
"ShowMvpFrontend",
],
ROOT / "Source" / "AgrarianGame" / "AgrarianGamePlayerController.cpp": [
"StartStartupPresentation",
"ShowStartupPresentationSegment(EAgrarianStartupPresentationSegment::Splash",
"ShowStartupPresentationSegment(EAgrarianStartupPresentationSegment::Story",
"ShowStartupPresentationSegment(EAgrarianStartupPresentationSegment::Credits",
"FinishStartupPresentation",
"GetWorldTimerManager().SetTimer",
"ShowMvpFrontend",
"FInputModeUIOnly",
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env python3
"""Verify the first subject content format is documented."""
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
DOC = ROOT / "Docs" / "KnowledgeAndSkillFoundation.md"
ROADMAP = ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md"
REQUIRED = {
DOC: [
"## Subject Content Format",
"`topic`",
"`concepts`",
"`difficulty_tier`",
"`prerequisite_concepts`",
"`in_game_effect`",
"`practice_action`",
"`source_note`",
"topic: fire.clearance",
"Format rule:",
],
ROADMAP: [
"[x] Define the first subject content format: topic, concepts, difficulty tier, prerequisite concepts, in-game effect, practice action, and source note.",
],
}
def main() -> None:
missing: list[str] = []
for path, snippets in REQUIRED.items():
text = path.read_text(encoding="utf-8")
for snippet in snippets:
if snippet not in text:
missing.append(f"{path.relative_to(ROOT)} missing {snippet!r}")
if missing:
raise SystemExit("FAILED: " + "; ".join(missing))
print("OK: subject content format is documented.")
if __name__ == "__main__":
main()
@@ -17,6 +17,11 @@ void AAgrarianDemoNoticeActor::BeginPlay()
{
Super::BeginPlay();
if (!bAutoShowNotice)
{
return;
}
APlayerController* PlayerController = GetWorld() ? GetWorld()->GetFirstPlayerController() : nullptr;
if (!PlayerController || !NoticeWidgetClass)
{
@@ -44,4 +49,3 @@ void AAgrarianDemoNoticeActor::RemoveNotice()
ActiveNoticeWidget = nullptr;
}
}
@@ -22,6 +22,9 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Demo", meta = (ClampMin = "1.0"))
float NoticeDurationSeconds = 24.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Demo")
bool bAutoShowNotice = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Demo")
FText VersionLabel = FText::FromString(TEXT("Investor Demo v0.1.N - Build 2026.05.18"));
@@ -2,7 +2,10 @@
#include "AgrarianDemoNoticeWidget.h"
#include "AgrarianGamePlayerController.h"
#include "Engine/World.h"
#include "Input/Reply.h"
#include "InputCoreTypes.h"
#include "Rendering/DrawElements.h"
#include "Styling/CoreStyle.h"
@@ -46,12 +49,76 @@ float GetCreditsSequenceDurationSeconds()
}
return Duration;
}
struct FAgrarianStoryBeat
{
const TCHAR* Title;
const TCHAR* Body;
};
const FAgrarianStoryBeat StoryBeats[] = {
{ TEXT("The lights did not go out at once."), TEXT("They failed by region, by habit, by trust. Cities still glowed in places, but the systems beneath them no longer answered together.") },
{ TEXT("People carried what they could."), TEXT("A few tools. A little seed. Names, debts, grief, and the stubborn memory of how life used to work.") },
{ TEXT("The old world became material."), TEXT("Roads became paths. Machines became shelter. Stores became ruins. Every useful thing had to be understood again before it could be used.") },
{ TEXT("The land kept its own calendar."), TEXT("Rain returned when it returned. Trees grew in years, not minutes. Hunger did not wait for plans, and winter did not care who was ready.") },
{ TEXT("Knowledge became inheritance."), TEXT("A fire tended well could save a night. A field understood well could save a season. A lesson taught well could save a generation.") },
{ TEXT("Agrarian begins at Ground Zero."), TEXT("One person steps forward. What they build, waste, protect, plant, teach, and remember becomes the first line of a new history.") },
};
}
void UAgrarianDemoNoticeWidget::NativeConstruct()
{
Super::NativeConstruct();
CreditsStartTimeSeconds = GetWorld() ? GetWorld()->GetTimeSeconds() : 0.0f;
SetIsFocusable(true);
SegmentStartTimeSeconds = GetWorld() ? GetWorld()->GetTimeSeconds() : 0.0f;
SetKeyboardFocus();
}
FReply UAgrarianDemoNoticeWidget::NativeOnKeyDown(const FGeometry& InGeometry, const FKeyEvent& InKeyEvent)
{
if (PresentationSegment == EAgrarianStartupPresentationSegment::Splash
|| PresentationSegment == EAgrarianStartupPresentationSegment::Story
|| PresentationSegment == EAgrarianStartupPresentationSegment::Credits)
{
RequestSkipSegment();
return FReply::Handled();
}
return Super::NativeOnKeyDown(InGeometry, InKeyEvent);
}
FReply UAgrarianDemoNoticeWidget::NativeOnMouseButtonDown(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent)
{
if (PresentationSegment == EAgrarianStartupPresentationSegment::Splash
|| PresentationSegment == EAgrarianStartupPresentationSegment::Story
|| PresentationSegment == EAgrarianStartupPresentationSegment::Credits)
{
RequestSkipSegment();
return FReply::Handled();
}
return Super::NativeOnMouseButtonDown(InGeometry, InMouseEvent);
}
void UAgrarianDemoNoticeWidget::SetPresentationSegment(EAgrarianStartupPresentationSegment NewSegment)
{
PresentationSegment = NewSegment;
SegmentStartTimeSeconds = GetWorld() ? GetWorld()->GetTimeSeconds() : 0.0f;
SetKeyboardFocus();
}
float UAgrarianDemoNoticeWidget::GetSegmentElapsedSeconds() const
{
const UWorld* World = GetWorld();
return World ? World->GetTimeSeconds() - SegmentStartTimeSeconds : 0.0f;
}
void UAgrarianDemoNoticeWidget::RequestSkipSegment() const
{
if (AAgrarianGamePlayerController* AgrarianController = Cast<AAgrarianGamePlayerController>(GetOwningPlayer()))
{
AgrarianController->AgrarianSkipStartupPresentation();
}
}
int32 UAgrarianDemoNoticeWidget::NativePaint(
@@ -66,9 +133,19 @@ int32 UAgrarianDemoNoticeWidget::NativePaint(
LayerId = Super::NativePaint(Args, AllottedGeometry, MyCullingRect, OutDrawElements, LayerId, InWidgetStyle, bParentEnabled);
const FVector2D Size = AllottedGeometry.GetLocalSize();
const UWorld* World = GetWorld();
const float Elapsed = World ? World->GetTimeSeconds() - CreditsStartTimeSeconds : 0.0f;
if (Elapsed <= GetCreditsSequenceDurationSeconds() + 1.0f)
if (PresentationSegment == EAgrarianStartupPresentationSegment::Splash)
{
DrawSplash(OutDrawElements, LayerId, AllottedGeometry);
return LayerId;
}
if (PresentationSegment == EAgrarianStartupPresentationSegment::Story)
{
DrawStory(OutDrawElements, LayerId, AllottedGeometry);
return LayerId;
}
if (PresentationSegment == EAgrarianStartupPresentationSegment::Credits)
{
FSlateDrawElement::MakeBox(
OutDrawElements,
@@ -120,6 +197,104 @@ int32 UAgrarianDemoNoticeWidget::NativePaint(
return LayerId;
}
void UAgrarianDemoNoticeWidget::DrawSplash(
FSlateWindowElementList& OutDrawElements,
int32& LayerId,
const FGeometry& AllottedGeometry) const
{
const FVector2D Size = AllottedGeometry.GetLocalSize();
const float Elapsed = GetSegmentElapsedSeconds();
const float Pulse = 0.5f + (0.5f * FMath::Sin(Elapsed * 2.1f));
FSlateDrawElement::MakeBox(
OutDrawElements,
++LayerId,
AllottedGeometry.ToPaintGeometry(FVector2f(Size), FSlateLayoutTransform(FVector2f::ZeroVector)),
FCoreStyle::Get().GetBrush(TEXT("WhiteBrush")),
ESlateDrawEffect::None,
FLinearColor(0.005f, 0.007f, 0.006f, 1.0f));
const FVector2D MarkSize(148.0f, 148.0f);
const FVector2D MarkPosition((Size.X - MarkSize.X) * 0.5f, (Size.Y * 0.5f) - 185.0f);
FSlateDrawElement::MakeBox(
OutDrawElements,
++LayerId,
AllottedGeometry.ToPaintGeometry(FVector2f(MarkSize), FSlateLayoutTransform(FVector2f(MarkPosition))),
FCoreStyle::Get().GetBrush(TEXT("WhiteBrush")),
ESlateDrawEffect::None,
FLinearColor(0.10f, 0.16f, 0.095f, 0.78f));
FSlateDrawElement::MakeBox(
OutDrawElements,
++LayerId,
AllottedGeometry.ToPaintGeometry(FVector2f(92.0f, 12.0f), FSlateLayoutTransform(FVector2f(MarkPosition.X + 28.0f, MarkPosition.Y + 96.0f))),
FCoreStyle::Get().GetBrush(TEXT("WhiteBrush")),
ESlateDrawEffect::None,
FLinearColor(0.68f, 0.86f, 0.44f, 0.85f + (0.15f * Pulse)));
const FSlateFontInfo TitleFont = FCoreStyle::GetDefaultFontStyle("Bold", 72);
const FSlateFontInfo StudioFont = FCoreStyle::GetDefaultFontStyle("Regular", 24);
const FSlateFontInfo NoticeFont = FCoreStyle::GetDefaultFontStyle("Regular", 16);
DrawCenteredText(OutDrawElements, LayerId, AllottedGeometry, FText::FromString(TEXT("AGRARIAN")), Size.Y * 0.5f - 12.0f, TitleFont, FLinearColor(0.92f, 0.98f, 0.84f, 1.0f));
DrawCenteredText(OutDrawElements, LayerId, AllottedGeometry, FText::FromString(TEXT("Agrarian Studio")), Size.Y * 0.5f + 78.0f, StudioFont, FLinearColor(0.70f, 0.82f, 0.60f, 1.0f));
DrawCenteredText(OutDrawElements, LayerId, AllottedGeometry, CopyrightNotice, Size.Y - 86.0f, NoticeFont, FLinearColor(0.58f, 0.64f, 0.54f, 1.0f));
DrawCenteredText(OutDrawElements, LayerId, AllottedGeometry, FText::FromString(TEXT("Press any key to continue")), Size.Y - 56.0f, NoticeFont, FLinearColor(0.72f, 0.80f, 0.66f, 0.65f + (0.35f * Pulse)));
}
void UAgrarianDemoNoticeWidget::DrawStory(
FSlateWindowElementList& OutDrawElements,
int32& LayerId,
const FGeometry& AllottedGeometry) const
{
const FVector2D Size = AllottedGeometry.GetLocalSize();
const float Elapsed = FMath::Clamp(GetSegmentElapsedSeconds(), 0.0f, 59.99f);
const int32 BeatIndex = FMath::Clamp(FMath::FloorToInt(Elapsed / 10.0f), 0, UE_ARRAY_COUNT(StoryBeats) - 1);
const float BeatTime = FMath::Fmod(Elapsed, 10.0f);
const float FadeIn = SmoothStep(BeatTime / 1.35f);
const float FadeOut = 1.0f - SmoothStep((BeatTime - 8.25f) / 1.25f);
const float Alpha = FMath::Clamp(FadeIn * FadeOut, 0.0f, 1.0f);
const float Drift = (BeatTime - 5.0f) * 8.0f;
FSlateDrawElement::MakeBox(
OutDrawElements,
++LayerId,
AllottedGeometry.ToPaintGeometry(FVector2f(Size), FSlateLayoutTransform(FVector2f::ZeroVector)),
FCoreStyle::Get().GetBrush(TEXT("WhiteBrush")),
ESlateDrawEffect::None,
FLinearColor(0.008f, 0.010f, 0.008f, 1.0f));
for (int32 BandIndex = 0; BandIndex < 5; ++BandIndex)
{
const float BandY = Size.Y * (0.18f + (0.15f * BandIndex)) + (FMath::Sin(Elapsed * 0.22f + BandIndex) * 16.0f);
const float BandWidth = Size.X * (0.42f + (0.08f * BandIndex));
const float BandX = FMath::Fmod((Elapsed * (18.0f + BandIndex * 7.0f)) + BandIndex * 220.0f, Size.X + BandWidth) - BandWidth;
FSlateDrawElement::MakeBox(
OutDrawElements,
++LayerId,
AllottedGeometry.ToPaintGeometry(FVector2f(BandWidth, 3.0f + BandIndex), FSlateLayoutTransform(FVector2f(BandX, BandY))),
FCoreStyle::Get().GetBrush(TEXT("WhiteBrush")),
ESlateDrawEffect::None,
FLinearColor(0.28f, 0.44f, 0.26f, 0.14f));
}
const FVector2D PanelSize(FMath::Min(980.0f, Size.X - 140.0f), 360.0f);
const FVector2D PanelPosition((Size.X - PanelSize.X) * 0.5f + Drift, (Size.Y - PanelSize.Y) * 0.5f);
FSlateDrawElement::MakeBox(
OutDrawElements,
++LayerId,
AllottedGeometry.ToPaintGeometry(FVector2f(PanelSize), FSlateLayoutTransform(FVector2f(PanelPosition))),
FCoreStyle::Get().GetBrush(TEXT("WhiteBrush")),
ESlateDrawEffect::None,
FLinearColor(0.016f, 0.020f, 0.015f, 0.82f * Alpha));
const FSlateFontInfo TitleFont = FCoreStyle::GetDefaultFontStyle("Bold", 40);
const FSlateFontInfo BodyFont = FCoreStyle::GetDefaultFontStyle("Regular", 24);
const FSlateFontInfo LabelFont = FCoreStyle::GetDefaultFontStyle("Regular", 15);
DrawTextAt(OutDrawElements, LayerId, AllottedGeometry, FText::FromString(StoryBeats[BeatIndex].Title), PanelPosition + FVector2D(46.0f, 58.0f), PanelSize.X - 92.0f, TitleFont, FLinearColor(0.90f, 0.96f, 0.82f, Alpha));
DrawTextAt(OutDrawElements, LayerId, AllottedGeometry, FText::FromString(StoryBeats[BeatIndex].Body), PanelPosition + FVector2D(50.0f, 142.0f), PanelSize.X - 100.0f, BodyFont, FLinearColor(0.74f, 0.82f, 0.68f, Alpha));
DrawCenteredText(OutDrawElements, LayerId, AllottedGeometry, FText::FromString(TEXT("Press any key to skip to credits")), Size.Y - 54.0f, LabelFont, FLinearColor(0.68f, 0.76f, 0.62f, 0.78f));
}
void UAgrarianDemoNoticeWidget::DrawCenteredText(
FSlateWindowElementList& OutDrawElements,
int32& LayerId,
@@ -176,7 +351,7 @@ void UAgrarianDemoNoticeWidget::DrawCinematicCredits(
}
const FVector2D Size = AllottedGeometry.GetLocalSize();
const float Elapsed = World->GetTimeSeconds() - CreditsStartTimeSeconds;
const float Elapsed = GetSegmentElapsedSeconds();
const float IntroDelay = 0.55f;
const float SlamSeconds = 0.28f;
const float ExitSeconds = 0.48f;
+28 -1
View File
@@ -6,6 +6,15 @@
#include "Blueprint/UserWidget.h"
#include "AgrarianDemoNoticeWidget.generated.h"
UENUM(BlueprintType)
enum class EAgrarianStartupPresentationSegment : uint8
{
Splash,
Story,
Credits,
DemoNotice
};
UCLASS()
class AGRARIANGAME_API UAgrarianDemoNoticeWidget : public UUserWidget
{
@@ -13,6 +22,10 @@ class AGRARIANGAME_API UAgrarianDemoNoticeWidget : public UUserWidget
public:
virtual void NativeConstruct() override;
virtual FReply NativeOnKeyDown(const FGeometry& InGeometry, const FKeyEvent& InKeyEvent) override;
virtual FReply NativeOnMouseButtonDown(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) override;
void SetPresentationSegment(EAgrarianStartupPresentationSegment NewSegment);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Demo")
FText Motto = FText::FromString(TEXT("What survives after you are gone?"));
@@ -37,7 +50,21 @@ protected:
bool bParentEnabled) const override;
private:
float CreditsStartTimeSeconds = 0.0f;
EAgrarianStartupPresentationSegment PresentationSegment = EAgrarianStartupPresentationSegment::DemoNotice;
float SegmentStartTimeSeconds = 0.0f;
float GetSegmentElapsedSeconds() const;
void RequestSkipSegment() const;
void DrawSplash(
FSlateWindowElementList& OutDrawElements,
int32& LayerId,
const FGeometry& AllottedGeometry) const;
void DrawStory(
FSlateWindowElementList& OutDrawElements,
int32& LayerId,
const FGeometry& AllottedGeometry) const;
void DrawCenteredText(
FSlateWindowElementList& OutDrawElements,
+13 -8
View File
@@ -9,7 +9,12 @@
namespace
{
void ConfigureFoliageComponent(UHierarchicalInstancedStaticMeshComponent* Component, const FName CollisionProfileName)
void ConfigureFoliageComponent(
UHierarchicalInstancedStaticMeshComponent* Component,
const FName CollisionProfileName,
const int32 StartCullDistance,
const int32 EndCullDistance,
const bool bCastShadows)
{
if (!Component)
{
@@ -19,10 +24,10 @@ void ConfigureFoliageComponent(UHierarchicalInstancedStaticMeshComponent* Compon
Component->SetMobility(EComponentMobility::Static);
Component->SetCollisionProfileName(CollisionProfileName);
Component->SetGenerateOverlapEvents(false);
Component->bCastDynamicShadow = true;
Component->bCastStaticShadow = true;
Component->InstanceStartCullDistance = 120000;
Component->InstanceEndCullDistance = 180000;
Component->bCastDynamicShadow = bCastShadows;
Component->bCastStaticShadow = bCastShadows;
Component->InstanceStartCullDistance = StartCullDistance;
Component->InstanceEndCullDistance = EndCullDistance;
}
}
@@ -36,15 +41,15 @@ AAgrarianFoliagePatch::AAgrarianFoliagePatch()
TreeInstances = CreateDefaultSubobject<UHierarchicalInstancedStaticMeshComponent>(TEXT("TreeInstances"));
TreeInstances->SetupAttachment(SceneRoot);
ConfigureFoliageComponent(TreeInstances, TEXT("BlockAll"));
ConfigureFoliageComponent(TreeInstances, TEXT("BlockAll"), 65000, 95000, true);
ShrubInstances = CreateDefaultSubobject<UHierarchicalInstancedStaticMeshComponent>(TEXT("ShrubInstances"));
ShrubInstances->SetupAttachment(SceneRoot);
ConfigureFoliageComponent(ShrubInstances, TEXT("NoCollision"));
ConfigureFoliageComponent(ShrubInstances, TEXT("NoCollision"), 28000, 52000, true);
GrassInstances = CreateDefaultSubobject<UHierarchicalInstancedStaticMeshComponent>(TEXT("GrassInstances"));
GrassInstances->SetupAttachment(SceneRoot);
ConfigureFoliageComponent(GrassInstances, TEXT("NoCollision"));
ConfigureFoliageComponent(GrassInstances, TEXT("NoCollision"), 9000, 22000, false);
}
void AAgrarianFoliagePatch::ClearFoliage()
@@ -5,6 +5,7 @@
#include "AgrarianCampfire.h"
#include "AgrarianCraftingComponent.h"
#include "AgrarianDebugHUD.h"
#include "AgrarianDemoNoticeWidget.h"
#include "AgrarianGameCharacter.h"
#include "AgrarianInventoryComponent.h"
#include "AgrarianItemPickup.h"
@@ -12,6 +13,7 @@
#include "AgrarianPersistenceSubsystem.h"
#include "AgrarianShelterActor.h"
#include "AgrarianSurvivalComponent.h"
#include "Camera/CameraActor.h"
#include "Components/SkeletalMeshComponent.h"
#include "EnhancedInputSubsystems.h"
#include "Engine/LocalPlayer.h"
@@ -92,6 +94,7 @@ namespace
? TEXT("/Game/Agrarian/Characters/Materials/M_AGR_CharacterProxy_Workwear_Female.M_AGR_CharacterProxy_Workwear_Female")
: TEXT("/Game/Agrarian/Characters/Materials/M_AGR_CharacterProxy_Workwear_Male.M_AGR_CharacterProxy_Workwear_Male");
}
}
void AAgrarianGamePlayerController::BeginPlay()
@@ -100,23 +103,7 @@ void AAgrarianGamePlayerController::BeginPlay()
if (IsLocalPlayerController())
{
if (MvpFrontendStartupDelaySeconds > 0.0f)
{
SetIgnoreMoveInput(true);
SetIgnoreLookInput(true);
SetInputMode(FInputModeUIOnly());
bShowMouseCursor = false;
GetWorldTimerManager().SetTimer(
MvpFrontendStartupTimerHandle,
this,
&AAgrarianGamePlayerController::ShowMvpFrontend,
MvpFrontendStartupDelaySeconds,
false);
}
else
{
ShowMvpFrontend();
}
StartStartupPresentation();
}
// only spawn touch controls on local player controllers
@@ -139,6 +126,26 @@ void AAgrarianGamePlayerController::BeginPlay()
}
}
void AAgrarianGamePlayerController::AcknowledgePossession(APawn* P)
{
Super::AcknowledgePossession(P);
if (!IsLocalPlayerController())
{
return;
}
ApplyMvpCharacterProxyToPawn();
if (bMvpFrontendPresentationActive)
{
SetMvpFrontendPresentationActive(true);
}
else
{
RestoreGameplayControlState();
}
}
void AAgrarianGamePlayerController::SetupInputComponent()
{
Super::SetupInputComponent();
@@ -151,23 +158,7 @@ void AAgrarianGamePlayerController::SetupInputComponent()
// only add IMCs for local player controllers
if (IsLocalPlayerController())
{
// Add Input Mapping Contexts
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(GetLocalPlayer()))
{
for (UInputMappingContext* CurrentContext : DefaultMappingContexts)
{
Subsystem->AddMappingContext(CurrentContext, 0);
}
// only add these IMCs if we're not using mobile touch input
if (!ShouldUseTouchControls())
{
for (UInputMappingContext* CurrentContext : MobileExcludedMappingContexts)
{
Subsystem->AddMappingContext(CurrentContext, 0);
}
}
}
ApplyDefaultInputMappingContexts();
}
}
@@ -204,10 +195,83 @@ void AAgrarianGamePlayerController::ShowMvpFrontend()
{
MvpFrontendWidget->AddToPlayerScreen(10);
}
SetMvpFrontendPresentationActive(true);
SetInputMode(FInputModeUIOnly().SetWidgetToFocus(MvpFrontendWidget->TakeWidget()).SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock));
bShowMouseCursor = true;
SetIgnoreMoveInput(true);
SetIgnoreLookInput(true);
}
void AAgrarianGamePlayerController::StartStartupPresentation()
{
if (!IsLocalPlayerController())
{
return;
}
SetMvpFrontendPresentationActive(true);
if (!StartupPresentationWidget)
{
StartupPresentationWidget = CreateWidget<UAgrarianDemoNoticeWidget>(this, UAgrarianDemoNoticeWidget::StaticClass());
}
if (StartupPresentationWidget && !StartupPresentationWidget->IsInViewport())
{
StartupPresentationWidget->AddToPlayerScreen(100);
}
ShowStartupPresentationSegment(EAgrarianStartupPresentationSegment::Splash, StartupSplashSeconds);
}
void AAgrarianGamePlayerController::ShowStartupPresentationSegment(EAgrarianStartupPresentationSegment Segment, float DurationSeconds)
{
StartupPresentationSegment = Segment;
if (StartupPresentationWidget)
{
StartupPresentationWidget->SetPresentationSegment(Segment);
SetInputMode(FInputModeUIOnly().SetWidgetToFocus(StartupPresentationWidget->TakeWidget()).SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock));
}
else
{
SetInputMode(FInputModeUIOnly());
}
bShowMouseCursor = false;
GetWorldTimerManager().ClearTimer(StartupPresentationTimerHandle);
GetWorldTimerManager().SetTimer(
StartupPresentationTimerHandle,
this,
&AAgrarianGamePlayerController::AdvanceStartupPresentation,
FMath::Max(0.1f, DurationSeconds),
false);
}
void AAgrarianGamePlayerController::AdvanceStartupPresentation()
{
if (StartupPresentationSegment == EAgrarianStartupPresentationSegment::Splash)
{
ShowStartupPresentationSegment(EAgrarianStartupPresentationSegment::Story, StartupStorySeconds);
return;
}
if (StartupPresentationSegment == EAgrarianStartupPresentationSegment::Story)
{
ShowStartupPresentationSegment(EAgrarianStartupPresentationSegment::Credits, StartupCreditsSeconds);
return;
}
FinishStartupPresentation();
}
void AAgrarianGamePlayerController::FinishStartupPresentation()
{
GetWorldTimerManager().ClearTimer(StartupPresentationTimerHandle);
if (StartupPresentationWidget)
{
StartupPresentationWidget->RemoveFromParent();
StartupPresentationWidget = nullptr;
}
ShowMvpFrontend();
}
void AAgrarianGamePlayerController::ShowMvpPauseMenu()
@@ -241,10 +305,9 @@ void AAgrarianGamePlayerController::ShowMvpPauseMenu()
}
MvpFrontendWidget->SetActiveScreen(EAgrarianMvpFrontendScreen::MainMenu);
SetMvpFrontendPresentationActive(true);
SetInputMode(FInputModeUIOnly().SetWidgetToFocus(MvpFrontendWidget->TakeWidget()).SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock));
bShowMouseCursor = true;
SetIgnoreMoveInput(true);
SetIgnoreLookInput(true);
}
void AAgrarianGamePlayerController::HandleMvpConfirmInput()
@@ -275,6 +338,217 @@ void AAgrarianGamePlayerController::HandleMvpEscapeInput()
ShowMvpPauseMenu();
}
void AAgrarianGamePlayerController::SetMvpFrontendPresentationActive(bool bNewActive)
{
const bool bWasActive = bMvpFrontendPresentationActive;
bMvpFrontendPresentationActive = bNewActive;
if (bNewActive)
{
if (!bWasActive)
{
SetIgnoreMoveInput(true);
SetIgnoreLookInput(true);
}
}
else
{
RestoreGameplayControlState();
}
APawn* ControlledPawn = GetPawn();
if (ControlledPawn)
{
ControlledPawn->SetActorHiddenInGame(bNewActive);
ControlledPawn->SetActorEnableCollision(!bNewActive);
if (ACharacter* ControlledCharacter = Cast<ACharacter>(ControlledPawn))
{
if (UCharacterMovementComponent* MovementComponent = ControlledCharacter->GetCharacterMovement())
{
if (bNewActive)
{
MovementComponent->DisableMovement();
}
else
{
MovementComponent->SetMovementMode(MOVE_Walking);
}
}
}
}
CacheAndApplyMvpHudSuppression(bNewActive);
if (bNewActive)
{
CreateOrUpdateMvpFrontendCamera();
if (MvpFrontendCameraActor)
{
SetViewTarget(MvpFrontendCameraActor);
}
return;
}
if (ControlledPawn)
{
SetViewTarget(ControlledPawn);
}
}
void AAgrarianGamePlayerController::RestoreGameplayControlState()
{
if (!IsLocalPlayerController())
{
return;
}
ResetIgnoreMoveInput();
ResetIgnoreLookInput();
SetInputMode(FInputModeGameOnly());
bShowMouseCursor = false;
ApplyDefaultInputMappingContexts();
APawn* ControlledPawn = GetPawn();
if (!ControlledPawn)
{
return;
}
ControlledPawn->SetActorHiddenInGame(false);
ControlledPawn->SetActorEnableCollision(true);
if (ACharacter* ControlledCharacter = Cast<ACharacter>(ControlledPawn))
{
if (UCharacterMovementComponent* MovementComponent = ControlledCharacter->GetCharacterMovement())
{
MovementComponent->SetMovementMode(MOVE_Walking);
}
}
SetViewTarget(ControlledPawn);
}
void AAgrarianGamePlayerController::ApplyDefaultInputMappingContexts()
{
if (!IsLocalPlayerController())
{
return;
}
ULocalPlayer* LocalPlayer = GetLocalPlayer();
if (!LocalPlayer)
{
return;
}
UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(LocalPlayer);
if (!Subsystem)
{
return;
}
for (UInputMappingContext* CurrentContext : DefaultMappingContexts)
{
if (CurrentContext)
{
Subsystem->RemoveMappingContext(CurrentContext);
Subsystem->AddMappingContext(CurrentContext, 0);
}
}
if (!ShouldUseTouchControls())
{
for (UInputMappingContext* CurrentContext : MobileExcludedMappingContexts)
{
if (CurrentContext)
{
Subsystem->RemoveMappingContext(CurrentContext);
Subsystem->AddMappingContext(CurrentContext, 0);
}
}
}
}
void AAgrarianGamePlayerController::CreateOrUpdateMvpFrontendCamera()
{
UWorld* World = GetWorld();
if (!World)
{
return;
}
const APawn* ControlledPawn = GetPawn();
const FVector TargetLocation = ControlledPawn
? ControlledPawn->GetActorLocation() + FVector(0.0f, 0.0f, 95.0f)
: GroundZeroDeveloperTravelHomeLocation;
const FVector CameraLocation = TargetLocation + FVector(-620.0f, -520.0f, 260.0f);
const FRotator CameraRotation = (TargetLocation - CameraLocation).Rotation();
if (!MvpFrontendCameraActor)
{
FActorSpawnParameters SpawnParameters;
SpawnParameters.Owner = this;
SpawnParameters.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
MvpFrontendCameraActor = World->SpawnActor<ACameraActor>(ACameraActor::StaticClass(), CameraLocation, CameraRotation, SpawnParameters);
if (MvpFrontendCameraActor)
{
MvpFrontendCameraActor->SetActorHiddenInGame(true);
}
return;
}
MvpFrontendCameraActor->SetActorLocationAndRotation(CameraLocation, CameraRotation);
}
void AAgrarianGamePlayerController::CacheAndApplyMvpHudSuppression(bool bSuppress)
{
AAgrarianDebugHUD* AgrarianHUD = GetHUD<AAgrarianDebugHUD>();
if (!AgrarianHUD)
{
return;
}
if (bSuppress)
{
if (!bCachedMvpHudState)
{
bCachedShowDebugHUD = AgrarianHUD->bShowDebugHUD;
bCachedShowMvpHudFrame = AgrarianHUD->bShowMvpHudFrame;
bCachedShowCriticalStatsHUD = AgrarianHUD->bShowCriticalStatsHUD;
bCachedShowInventoryHUD = AgrarianHUD->bShowInventoryHUD;
bCachedShowCraftingHUD = AgrarianHUD->bShowCraftingHUD;
bCachedShowInteractionPrompt = AgrarianHUD->bShowInteractionPrompt;
bCachedShowDeathRespawnUI = AgrarianHUD->bShowDeathRespawnUI;
bCachedShowDebugDevMenu = AgrarianHUD->bShowDebugDevMenu;
bCachedMvpHudState = true;
}
AgrarianHUD->bShowDebugHUD = false;
AgrarianHUD->bShowMvpHudFrame = false;
AgrarianHUD->bShowCriticalStatsHUD = false;
AgrarianHUD->bShowInventoryHUD = false;
AgrarianHUD->bShowCraftingHUD = false;
AgrarianHUD->bShowInteractionPrompt = false;
AgrarianHUD->bShowDeathRespawnUI = false;
AgrarianHUD->bShowDebugDevMenu = false;
return;
}
if (bCachedMvpHudState)
{
AgrarianHUD->bShowDebugHUD = bCachedShowDebugHUD;
AgrarianHUD->bShowMvpHudFrame = bCachedShowMvpHudFrame;
AgrarianHUD->bShowCriticalStatsHUD = bCachedShowCriticalStatsHUD;
AgrarianHUD->bShowInventoryHUD = bCachedShowInventoryHUD;
AgrarianHUD->bShowCraftingHUD = bCachedShowCraftingHUD;
AgrarianHUD->bShowInteractionPrompt = bCachedShowInteractionPrompt;
AgrarianHUD->bShowDeathRespawnUI = bCachedShowDeathRespawnUI;
AgrarianHUD->bShowDebugDevMenu = bCachedShowDebugDevMenu;
bCachedMvpHudState = false;
}
}
void AAgrarianGamePlayerController::ApplyMvpCharacterProxyToPawn()
{
AAgrarianGameCharacter* AgrarianCharacter = GetPawn<AAgrarianGameCharacter>();
@@ -297,6 +571,8 @@ void AAgrarianGamePlayerController::ApplyMvpCharacterProxyToPawn()
MeshComponent->SetMaterial(MaterialIndex, ProxyMaterial);
}
}
// Blockout clothing and pack geometry was removed because it read as broken placeholder art in the investor build.
}
void AAgrarianGamePlayerController::AgrarianGrantItem(FName ItemId, int32 Quantity)
@@ -558,6 +834,41 @@ void AAgrarianGamePlayerController::AgrarianSelectCharacter(FName Archetype)
ClientMessage(TEXT("Usage: AgrarianSelectCharacter male|female"));
}
void AAgrarianGamePlayerController::AgrarianCompleteFrontend()
{
ApplyMvpCharacterProxyToPawn();
SetMvpFrontendPresentationActive(false);
RestoreGameplayControlState();
if (const APawn* ControlledPawn = GetPawn())
{
SetControlRotation(FRotator(-8.0f, ControlledPawn->GetActorRotation().Yaw, 0.0f));
}
}
void AAgrarianGamePlayerController::AgrarianRepairGameplayInput()
{
bMvpFrontendPresentationActive = false;
GetWorldTimerManager().ClearTimer(StartupPresentationTimerHandle);
if (StartupPresentationWidget)
{
StartupPresentationWidget->RemoveFromParent();
StartupPresentationWidget = nullptr;
}
RestoreGameplayControlState();
ClientMessage(TEXT("Agrarian gameplay input repaired."));
}
void AAgrarianGamePlayerController::AgrarianSkipStartupPresentation()
{
if (!StartupPresentationWidget)
{
return;
}
AdvanceStartupPresentation();
}
void AAgrarianGamePlayerController::AgrarianShowMvpScreen(FName ScreenName)
{
if (!MvpFrontendWidget)
@@ -604,6 +915,78 @@ void AAgrarianGamePlayerController::AgrarianShowMvpScreen(FName ScreenName)
ClientMessage(TEXT("Usage: AgrarianShowMvpScreen main|character|join|loading|saving"));
}
void AAgrarianGamePlayerController::AgrarianInvestorSmokeTest(float CaptureDelaySeconds, float QuitDelaySeconds)
{
if (!IsLocalPlayerController())
{
return;
}
const float ClampedCaptureDelaySeconds = FMath::Max(2.0f, CaptureDelaySeconds);
const float ClampedQuitDelaySeconds = FMath::Max(ClampedCaptureDelaySeconds + 4.0f, QuitDelaySeconds);
FTimerDelegate EnterWorldDelegate;
EnterWorldDelegate.BindWeakLambda(this, [this]()
{
if (!MvpFrontendWidget)
{
ShowMvpFrontend();
}
SelectedMvpCharacterProxyId = TEXT("female");
if (MvpFrontendWidget)
{
MvpFrontendWidget->SetSelectedCharacterArchetype(EAgrarianMvpCharacterArchetype::YoungAdultFemale);
}
AgrarianCompleteFrontend();
if (AAgrarianDebugHUD* AgrarianHUD = GetHUD<AAgrarianDebugHUD>())
{
AgrarianHUD->bShowDebugHUD = false;
AgrarianHUD->bShowCriticalStatsHUD = false;
AgrarianHUD->bShowInventoryHUD = false;
AgrarianHUD->bShowCraftingHUD = false;
}
if (APawn* ControlledPawn = GetPawn())
{
const FRotator InvestorViewRotation(-4.0f, 42.0f, 0.0f);
ControlledPawn->SetActorRotation(FRotator(0.0f, InvestorViewRotation.Yaw, 0.0f));
SetControlRotation(InvestorViewRotation);
}
ClientMessage(TEXT("Investor smoke test entered Ground Zero as the female MVP character proxy."));
});
FTimerHandle EnterWorldTimerHandle;
GetWorldTimerManager().SetTimer(EnterWorldTimerHandle, EnterWorldDelegate, 0.75f, false);
FTimerDelegate ScreenshotDelegate;
ScreenshotDelegate.BindWeakLambda(this, [this]()
{
ClientMessage(TEXT("Investor smoke test capturing gameplay screenshot."));
ConsoleCommand(TEXT("HighResShot 1"));
});
FTimerHandle ScreenshotTimerHandle;
GetWorldTimerManager().SetTimer(ScreenshotTimerHandle, ScreenshotDelegate, ClampedCaptureDelaySeconds, false);
FTimerDelegate QuitDelegate;
QuitDelegate.BindWeakLambda(this, [this]()
{
ClientMessage(TEXT("Investor smoke test completed; quitting packaged client."));
ConsoleCommand(TEXT("quit"));
});
FTimerHandle QuitTimerHandle;
GetWorldTimerManager().SetTimer(QuitTimerHandle, QuitDelegate, ClampedQuitDelaySeconds, false);
ClientMessage(FString::Printf(
TEXT("Investor smoke test scheduled: screenshot in %.1fs, quit in %.1fs."),
ClampedCaptureDelaySeconds,
ClampedQuitDelaySeconds));
}
void AAgrarianGamePlayerController::AgrarianTravel(float X, float Y, float Z)
{
ServerAgrarianTravel(FVector(X, Y, Z));
@@ -3,13 +3,16 @@
#pragma once
#include "CoreMinimal.h"
#include "AgrarianDemoNoticeWidget.h"
#include "GameFramework/PlayerController.h"
#include "AgrarianGamePlayerController.generated.h"
class UInputMappingContext;
class UUserWidget;
class UAgrarianMvpFrontendWidget;
class UAgrarianDemoNoticeWidget;
class AAgrarianShelterActor;
class ACameraActor;
/**
* Basic PlayerController class for a third person game
@@ -45,9 +48,19 @@ protected:
TObjectPtr<UAgrarianMvpFrontendWidget> MvpFrontendWidget;
UPROPERTY(EditAnywhere, Category = "Agrarian|MVP UI", meta = (ClampMin = "0.0"))
float MvpFrontendStartupDelaySeconds = 24.25f;
float MvpFrontendStartupDelaySeconds = 0.0f;
UPROPERTY(EditAnywhere, Category = "Agrarian|Startup", meta = (ClampMin = "1.0"))
float StartupSplashSeconds = 4.0f;
UPROPERTY(EditAnywhere, Category = "Agrarian|Startup", meta = (ClampMin = "10.0"))
float StartupStorySeconds = 60.0f;
UPROPERTY(EditAnywhere, Category = "Agrarian|Startup", meta = (ClampMin = "5.0"))
float StartupCreditsSeconds = 20.75f;
FTimerHandle MvpFrontendStartupTimerHandle;
FTimerHandle StartupPresentationTimerHandle;
/** If true, the player will use UMG touch controls even if not playing on mobile platforms */
UPROPERTY(EditAnywhere, Config, Category = "Input|Touch Controls")
@@ -62,14 +75,42 @@ protected:
/** Returns true if the player should use UMG touch controls */
bool ShouldUseTouchControls() const;
void ShowMvpFrontend();
void StartStartupPresentation();
void AdvanceStartupPresentation();
void FinishStartupPresentation();
void ShowStartupPresentationSegment(EAgrarianStartupPresentationSegment Segment, float DurationSeconds);
void ShowMvpPauseMenu();
void HandleMvpConfirmInput();
void HandleMvpBackInput();
void HandleMvpEscapeInput();
void ApplyMvpCharacterProxyToPawn();
void SetMvpFrontendPresentationActive(bool bNewActive);
void ApplyDefaultInputMappingContexts();
void RestoreGameplayControlState();
void CreateOrUpdateMvpFrontendCamera();
void CacheAndApplyMvpHudSuppression(bool bSuppress);
virtual void AcknowledgePossession(APawn* P) override;
FName SelectedMvpCharacterProxyId = TEXT("male");
UPROPERTY()
TObjectPtr<ACameraActor> MvpFrontendCameraActor;
UPROPERTY()
TObjectPtr<UAgrarianDemoNoticeWidget> StartupPresentationWidget;
bool bMvpFrontendPresentationActive = false;
EAgrarianStartupPresentationSegment StartupPresentationSegment = EAgrarianStartupPresentationSegment::Splash;
bool bCachedMvpHudState = false;
bool bCachedShowDebugHUD = true;
bool bCachedShowMvpHudFrame = true;
bool bCachedShowCriticalStatsHUD = true;
bool bCachedShowInventoryHUD = true;
bool bCachedShowCraftingHUD = true;
bool bCachedShowInteractionPrompt = true;
bool bCachedShowDeathRespawnUI = true;
bool bCachedShowDebugDevMenu = false;
public:
UFUNCTION(Exec)
void AgrarianGrantItem(FName ItemId, int32 Quantity);
@@ -128,9 +169,21 @@ public:
UFUNCTION(Exec)
void AgrarianSelectCharacter(FName Archetype);
UFUNCTION(Exec)
void AgrarianCompleteFrontend();
UFUNCTION(Exec)
void AgrarianRepairGameplayInput();
UFUNCTION(Exec)
void AgrarianSkipStartupPresentation();
UFUNCTION(Exec)
void AgrarianShowMvpScreen(FName ScreenName);
UFUNCTION(Exec)
void AgrarianInvestorSmokeTest(float CaptureDelaySeconds = 6.0f, float QuitDelaySeconds = 18.0f);
UFUNCTION(Exec)
void AgrarianTravel(float X, float Y, float Z);
@@ -15,7 +15,7 @@ AAgrarianMapBoundaryVolume::AAgrarianMapBoundaryVolume()
RootComponent = BoundaryVolume;
BoundaryVolume->SetBoxExtent(FVector(50000.0f, 50000.0f, 25000.0f));
BoundaryVolume->SetCollisionEnabled(ECollisionEnabled::NoCollision);
BoundaryVolume->SetHiddenInGame(false);
BoundaryVolume->SetHiddenInGame(true);
BoundaryVolume->ShapeColor = FColor::Yellow;
}
+188 -159
View File
@@ -2,6 +2,7 @@
#include "AgrarianMvpFrontendWidget.h"
#include "AgrarianGamePlayerController.h"
#include "Blueprint/WidgetTree.h"
#include "Components/Border.h"
#include "Components/Button.h"
@@ -9,32 +10,14 @@
#include "Components/HorizontalBox.h"
#include "Components/HorizontalBoxSlot.h"
#include "Components/SizeBox.h"
#include "Components/Slider.h"
#include "Components/TextBlock.h"
#include "Components/VerticalBox.h"
#include "Components/VerticalBoxSlot.h"
#include "GameFramework/PlayerController.h"
#include "InputCoreTypes.h"
#include "Kismet/GameplayStatics.h"
#include "Styling/CoreStyle.h"
#include "TimerManager.h"
namespace
{
FButtonStyle MakeAgrarianButtonStyle(const FLinearColor& NormalColor, const FLinearColor& HoveredColor)
{
FButtonStyle ButtonStyle = FCoreStyle::Get().GetWidgetStyle<FButtonStyle>(TEXT("Button"));
ButtonStyle.Normal.TintColor = FSlateColor(NormalColor);
ButtonStyle.Hovered.TintColor = FSlateColor(HoveredColor);
ButtonStyle.Pressed.TintColor = FSlateColor(FLinearColor(
FMath::Min(HoveredColor.R + 0.12f, 1.0f),
FMath::Min(HoveredColor.G + 0.12f, 1.0f),
FMath::Min(HoveredColor.B + 0.12f, 1.0f),
HoveredColor.A));
return ButtonStyle;
}
}
void UAgrarianMvpFrontendWidget::NativeConstruct()
{
Super::NativeConstruct();
@@ -50,21 +33,18 @@ FReply UAgrarianMvpFrontendWidget::NativeOnKeyDown(const FGeometry& InGeometry,
const FKey Key = InKeyEvent.GetKey();
if (Key == EKeys::Left || Key == EKeys::A)
{
PlayUiSound(UiSelectionSound);
SetSelectedCharacterArchetype(EAgrarianMvpCharacterArchetype::YoungAdultMale);
return FReply::Handled();
}
if (Key == EKeys::Right || Key == EKeys::D)
{
PlayUiSound(UiSelectionSound);
SetSelectedCharacterArchetype(EAgrarianMvpCharacterArchetype::YoungAdultFemale);
return FReply::Handled();
}
if (Key == EKeys::Enter || Key == EKeys::SpaceBar)
{
PlayUiSound(UiConfirmSound);
ConfirmActiveScreen();
return FReply::Handled();
}
@@ -74,14 +54,12 @@ FReply UAgrarianMvpFrontendWidget::NativeOnKeyDown(const FGeometry& InGeometry,
const FKey Key = InKeyEvent.GetKey();
if (Key == EKeys::Enter || Key == EKeys::SpaceBar)
{
PlayUiSound(UiConfirmSound);
ConfirmActiveScreen();
return FReply::Handled();
}
if (Key == EKeys::BackSpace || Key == EKeys::Escape)
{
PlayUiSound(UiBackSound);
BackFromActiveScreen();
return FReply::Handled();
}
@@ -91,7 +69,6 @@ FReply UAgrarianMvpFrontendWidget::NativeOnKeyDown(const FGeometry& InGeometry,
const FKey Key = InKeyEvent.GetKey();
if (Key == EKeys::Enter || Key == EKeys::SpaceBar)
{
PlayUiSound(UiConfirmSound);
ConfirmActiveScreen();
return FReply::Handled();
}
@@ -101,17 +78,42 @@ FReply UAgrarianMvpFrontendWidget::NativeOnKeyDown(const FGeometry& InGeometry,
const FKey Key = InKeyEvent.GetKey();
if (Key == EKeys::Enter || Key == EKeys::SpaceBar || Key == EKeys::Escape)
{
PlayUiSound(UiConfirmSound);
ConfirmActiveScreen();
return FReply::Handled();
}
if (Key == EKeys::Q)
{
PlayUiSound(UiSaveQuitSound);
SaveAndQuit();
return FReply::Handled();
}
if (Key == EKeys::S)
{
SaveGame();
return FReply::Handled();
}
if (Key == EKeys::O)
{
SetActiveScreen(EAgrarianMvpFrontendScreen::Settings);
return FReply::Handled();
}
if (Key == EKeys::X)
{
QuitWithoutSaving();
return FReply::Handled();
}
}
else if (ActiveScreen == EAgrarianMvpFrontendScreen::Settings || ActiveScreen == EAgrarianMvpFrontendScreen::GameSaved)
{
const FKey Key = InKeyEvent.GetKey();
if (Key == EKeys::Escape || Key == EKeys::BackSpace || Key == EKeys::Enter || Key == EKeys::SpaceBar)
{
SetActiveScreen(EAgrarianMvpFrontendScreen::MainMenu);
return FReply::Handled();
}
}
else if (ActiveScreen == EAgrarianMvpFrontendScreen::SavingAndQuit)
{
@@ -172,6 +174,17 @@ void UAgrarianMvpFrontendWidget::SaveAndQuit()
ExecuteSaveAndQuit();
}
void UAgrarianMvpFrontendWidget::SaveGame()
{
ExecuteSaveGame();
SetActiveScreen(EAgrarianMvpFrontendScreen::GameSaved);
}
void UAgrarianMvpFrontendWidget::QuitWithoutSaving()
{
ExecuteQuitWithoutSaving();
}
void UAgrarianMvpFrontendWidget::ExecuteSaveAndQuit()
{
if (APlayerController* PlayerController = GetOwningPlayer())
@@ -181,11 +194,27 @@ void UAgrarianMvpFrontendWidget::ExecuteSaveAndQuit()
}
}
void UAgrarianMvpFrontendWidget::ExecuteSaveGame()
{
if (APlayerController* PlayerController = GetOwningPlayer())
{
PlayerController->ConsoleCommand(TEXT("AgrarianSaveWorld"));
}
}
void UAgrarianMvpFrontendWidget::ExecuteQuitWithoutSaving()
{
if (APlayerController* PlayerController = GetOwningPlayer())
{
PlayerController->ConsoleCommand(TEXT("quit"));
}
}
void UAgrarianMvpFrontendWidget::ContinueFromActiveScreen()
{
if (ActiveScreen == EAgrarianMvpFrontendScreen::CharacterSelection)
{
SetActiveScreen(EAgrarianMvpFrontendScreen::JoinServer);
CompleteFrontendFlow();
return;
}
@@ -206,14 +235,15 @@ void UAgrarianMvpFrontendWidget::ContinueFromActiveScreen()
return;
}
if (ActiveScreen == EAgrarianMvpFrontendScreen::MainMenu)
if (ActiveScreen == EAgrarianMvpFrontendScreen::Settings || ActiveScreen == EAgrarianMvpFrontendScreen::GameSaved)
{
CompleteFrontendFlow();
SetActiveScreen(EAgrarianMvpFrontendScreen::MainMenu);
return;
}
if (ActiveScreen == EAgrarianMvpFrontendScreen::SavingAndQuit)
if (ActiveScreen == EAgrarianMvpFrontendScreen::MainMenu)
{
CompleteFrontendFlow();
return;
}
}
@@ -230,23 +260,35 @@ void UAgrarianMvpFrontendWidget::ReturnFromActiveScreen()
{
CompleteFrontendFlow();
}
if (ActiveScreen == EAgrarianMvpFrontendScreen::Settings || ActiveScreen == EAgrarianMvpFrontendScreen::GameSaved)
{
SetActiveScreen(EAgrarianMvpFrontendScreen::MainMenu);
}
}
void UAgrarianMvpFrontendWidget::CompleteFrontendFlow()
{
if (APlayerController* PlayerController = GetOwningPlayer())
{
if (ActiveScreen == EAgrarianMvpFrontendScreen::Loading)
if (AAgrarianGamePlayerController* AgrarianPlayerController = Cast<AAgrarianGamePlayerController>(PlayerController))
{
PlayerController->ConsoleCommand(SelectedCharacterArchetype == EAgrarianMvpCharacterArchetype::YoungAdultFemale
? TEXT("AgrarianSelectCharacter female")
: TEXT("AgrarianSelectCharacter male"));
}
if (ActiveScreen == EAgrarianMvpFrontendScreen::CharacterSelection || ActiveScreen == EAgrarianMvpFrontendScreen::Loading)
{
AgrarianPlayerController->AgrarianSelectCharacter(SelectedCharacterArchetype == EAgrarianMvpCharacterArchetype::YoungAdultFemale
? TEXT("female")
: TEXT("male"));
}
PlayerController->SetInputMode(FInputModeGameOnly());
PlayerController->bShowMouseCursor = false;
PlayerController->SetIgnoreMoveInput(false);
PlayerController->SetIgnoreLookInput(false);
AgrarianPlayerController->AgrarianCompleteFrontend();
}
else
{
PlayerController->SetInputMode(FInputModeGameOnly());
PlayerController->bShowMouseCursor = false;
PlayerController->ResetIgnoreMoveInput();
PlayerController->ResetIgnoreLookInput();
}
}
RemoveFromParent();
@@ -297,39 +339,23 @@ void UAgrarianMvpFrontendWidget::RebuildFrontendTree()
{
AddText(Panel, FText::FromString(TEXT("Pause Menu")), FMath::RoundToInt(20.0f * Scale), true, AccentColor, 6.0f * Scale);
AddText(Panel, MainMenuTitle, FMath::RoundToInt(54.0f * Scale), true, TextColor, 6.0f * Scale);
AddText(Panel, FText::FromString(TEXT("Gameplay is paused while this menu is active.")), FMath::RoundToInt(22.0f * Scale), false, MutedTextColor, 72.0f * Scale);
AddText(Panel, FText::FromString(TEXT("Gameplay is paused while this menu is active.")), FMath::RoundToInt(22.0f * Scale), false, MutedTextColor, 34.0f * Scale);
PrimaryFocusButton = AddButton(Panel, FText::FromString(TEXT("Resume")), ButtonColor, ButtonHoverColor, 16.0f * Scale);
PrimaryFocusButton->OnClicked.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandlePrimaryActionClicked);
PrimaryFocusButton->OnHovered.AddDynamic(this, &UAgrarianMvpFrontendWidget::FocusPrimaryButton);
UButton* QuitButton = AddButton(Panel, FText::FromString(TEXT("Save & Quit")), QuitButtonColor, FLinearColor(0.58f, 0.28f, 0.22f, 1.0f), 34.0f * Scale);
UButton* SaveButton = AddButton(Panel, FText::FromString(TEXT("Save Game")), SecondaryButtonColor, ButtonHoverColor, 12.0f * Scale);
SaveButton->OnClicked.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandleSaveGameClicked);
UButton* SettingsButton = AddButton(Panel, FText::FromString(TEXT("Settings")), SecondaryButtonColor, ButtonHoverColor, 12.0f * Scale);
SettingsButton->OnClicked.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandleSettingsClicked);
UButton* QuitButton = AddButton(Panel, FText::FromString(TEXT("Save & Exit")), QuitButtonColor, FLinearColor(0.58f, 0.28f, 0.22f, 1.0f), 12.0f * Scale);
QuitButton->OnClicked.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandleSaveAndQuitClicked);
AddText(Panel, FText::FromString(TEXT("Audio")), FMath::RoundToInt(18.0f * Scale), true, AccentColor, 8.0f * Scale);
if (USlider* Slider = AddVolumeSlider(Panel, FText::FromString(TEXT("Master")), MasterVolume, 8.0f * Scale))
{
Slider->OnValueChanged.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandleMasterVolumeChanged);
}
if (USlider* Slider = AddVolumeSlider(Panel, FText::FromString(TEXT("Ambient")), AmbientVolume, 8.0f * Scale))
{
Slider->OnValueChanged.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandleAmbientVolumeChanged);
}
if (USlider* Slider = AddVolumeSlider(Panel, FText::FromString(TEXT("Weather")), WeatherVolume, 8.0f * Scale))
{
Slider->OnValueChanged.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandleWeatherVolumeChanged);
}
if (USlider* Slider = AddVolumeSlider(Panel, FText::FromString(TEXT("Effects")), EffectsVolume, 8.0f * Scale))
{
Slider->OnValueChanged.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandleEffectsVolumeChanged);
}
if (USlider* Slider = AddVolumeSlider(Panel, FText::FromString(TEXT("Wildlife")), WildlifeVolume, 8.0f * Scale))
{
Slider->OnValueChanged.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandleWildlifeVolumeChanged);
}
if (USlider* Slider = AddVolumeSlider(Panel, FText::FromString(TEXT("UI")), UiVolume, 24.0f * Scale))
{
Slider->OnValueChanged.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandleUiVolumeChanged);
}
AddText(Panel, FText::FromString(TEXT("Escape opens this menu. Save & Quit writes the current world save before closing.")), FMath::RoundToInt(16.0f * Scale), false, MutedTextColor, 0.0f);
UButton* QuitWithoutSavingButton = AddButton(Panel, FText::FromString(TEXT("Quit Without Saving")), SecondaryButtonColor, FLinearColor(0.46f, 0.20f, 0.16f, 1.0f), 28.0f * Scale);
QuitWithoutSavingButton->OnClicked.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandleQuitWithoutSavingClicked);
AddText(Panel, FText::FromString(TEXT("Esc resumes. S saves. O opens settings. Q saves and exits. X exits without saving.")), FMath::RoundToInt(16.0f * Scale), false, MutedTextColor, 0.0f);
return;
}
@@ -348,7 +374,7 @@ void UAgrarianMvpFrontendWidget::RebuildFrontendTree()
const bool bMaleSelected = SelectedCharacterArchetype == EAgrarianMvpCharacterArchetype::YoungAdultMale;
const bool bFemaleSelected = SelectedCharacterArchetype == EAgrarianMvpCharacterArchetype::YoungAdultFemale;
UButton* MaleButton = WidgetTree->ConstructWidget<UButton>(UButton::StaticClass(), TEXT("MalePioneerButton"));
MaleButton->SetStyle(MakeAgrarianButtonStyle(bMaleSelected ? ButtonHoverColor : SecondaryButtonColor, ButtonHoverColor));
MaleButton->SetBackgroundColor(bMaleSelected ? ButtonHoverColor : SecondaryButtonColor);
MaleButton->OnClicked.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandleMaleCharacterClicked);
UVerticalBox* MaleStack = WidgetTree->ConstructWidget<UVerticalBox>(UVerticalBox::StaticClass(), TEXT("MalePioneerStack"));
MaleButton->SetContent(MaleStack);
@@ -362,7 +388,7 @@ void UAgrarianMvpFrontendWidget::RebuildFrontendTree()
}
UButton* FemaleButton = WidgetTree->ConstructWidget<UButton>(UButton::StaticClass(), TEXT("FemalePioneerButton"));
FemaleButton->SetStyle(MakeAgrarianButtonStyle(bFemaleSelected ? ButtonHoverColor : SecondaryButtonColor, ButtonHoverColor));
FemaleButton->SetBackgroundColor(bFemaleSelected ? ButtonHoverColor : SecondaryButtonColor);
FemaleButton->OnClicked.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandleFemaleCharacterClicked);
UVerticalBox* FemaleStack = WidgetTree->ConstructWidget<UVerticalBox>(UVerticalBox::StaticClass(), TEXT("FemalePioneerStack"));
FemaleButton->SetContent(FemaleStack);
@@ -375,9 +401,8 @@ void UAgrarianMvpFrontendWidget::RebuildFrontendTree()
FemaleSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill));
}
PrimaryFocusButton = AddButton(Panel, FText::FromString(TEXT("Continue")), ButtonColor, ButtonHoverColor, 10.0f * Scale);
PrimaryFocusButton = AddButton(Panel, FText::FromString(TEXT("Enter Ground Zero")), ButtonColor, ButtonHoverColor, 10.0f * Scale);
PrimaryFocusButton->OnClicked.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandlePrimaryActionClicked);
PrimaryFocusButton->OnHovered.AddDynamic(this, &UAgrarianMvpFrontendWidget::FocusPrimaryButton);
AddText(Panel, FText::Format(FText::FromString(TEXT("Selected {0}: {1}. Click a card or use Left/Right, then continue.")), GetSelectedRoleLabel(), GetSelectedCharacterLabel()), FMath::RoundToInt(15.0f * Scale), false, MutedTextColor, 0.0f);
return;
}
@@ -397,9 +422,8 @@ void UAgrarianMvpFrontendWidget::RebuildFrontendTree()
}
PrimaryFocusButton = WidgetTree->ConstructWidget<UButton>(UButton::StaticClass(), TEXT("ContinueToLoadingButton"));
PrimaryFocusButton->SetStyle(MakeAgrarianButtonStyle(ButtonColor, ButtonHoverColor));
PrimaryFocusButton->SetBackgroundColor(ButtonColor);
PrimaryFocusButton->OnClicked.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandlePrimaryActionClicked);
PrimaryFocusButton->OnHovered.AddDynamic(this, &UAgrarianMvpFrontendWidget::FocusPrimaryButton);
UTextBlock* ContinueLabel = AddText(nullptr, FText::FromString(TEXT("Continue to loading")), FMath::RoundToInt(20.0f * Scale), true, TextColor, 0.0f);
PrimaryFocusButton->SetContent(ContinueLabel);
if (UButtonSlot* ContinueLabelSlot = Cast<UButtonSlot>(ContinueLabel->Slot))
@@ -414,7 +438,7 @@ void UAgrarianMvpFrontendWidget::RebuildFrontendTree()
}
UButton* BackButton = WidgetTree->ConstructWidget<UButton>(UButton::StaticClass(), TEXT("BackButton"));
BackButton->SetStyle(MakeAgrarianButtonStyle(SecondaryButtonColor, ButtonHoverColor));
BackButton->SetBackgroundColor(SecondaryButtonColor);
BackButton->OnClicked.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandleBackClicked);
UTextBlock* BackLabel = AddText(nullptr, FText::FromString(TEXT("Back")), FMath::RoundToInt(20.0f * Scale), true, TextColor, 0.0f);
BackButton->SetContent(BackLabel);
@@ -441,13 +465,32 @@ void UAgrarianMvpFrontendWidget::RebuildFrontendTree()
return;
}
if (ActiveScreen == EAgrarianMvpFrontendScreen::GameSaved)
{
AddText(Panel, FText::FromString(TEXT("Game Saved")), FMath::RoundToInt(20.0f * Scale), true, AccentColor, 6.0f * Scale);
AddText(Panel, FText::FromString(TEXT("World state saved")), FMath::RoundToInt(34.0f * Scale), true, TextColor, 8.0f * Scale);
AddText(Panel, FText::FromString(TEXT("Return to the pause menu before resuming or exiting.")), FMath::RoundToInt(18.0f * Scale), false, MutedTextColor, 28.0f * Scale);
PrimaryFocusButton = AddButton(Panel, FText::FromString(TEXT("Back to Pause Menu")), ButtonColor, ButtonHoverColor, 0.0f);
PrimaryFocusButton->OnClicked.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandleBackClicked);
return;
}
if (ActiveScreen == EAgrarianMvpFrontendScreen::Settings)
{
AddText(Panel, FText::FromString(TEXT("Settings")), FMath::RoundToInt(20.0f * Scale), true, AccentColor, 6.0f * Scale);
AddText(Panel, FText::FromString(TEXT("Player Options")), FMath::RoundToInt(34.0f * Scale), true, TextColor, 8.0f * Scale);
AddText(Panel, FText::FromString(TEXT("Interface scale and high contrast are available now through tester commands. Units, controls, hardware, audio, and accessibility options are queued for the settings roadmap.")), FMath::RoundToInt(18.0f * Scale), false, MutedTextColor, 28.0f * Scale);
PrimaryFocusButton = AddButton(Panel, FText::FromString(TEXT("Back to Pause Menu")), ButtonColor, ButtonHoverColor, 0.0f);
PrimaryFocusButton->OnClicked.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandleBackClicked);
return;
}
AddText(Panel, FText::FromString(TEXT("Loading Segment")), FMath::RoundToInt(18.0f * Scale), true, AccentColor, 6.0f * Scale);
AddText(Panel, FText::FromString(TEXT("Preparing Ground Zero")), FMath::RoundToInt(34.0f * Scale), true, TextColor, 8.0f * Scale);
AddText(Panel, FText::FromString(TEXT("Loading terrain, weather, survival state, and server session data.")), FMath::RoundToInt(18.0f * Scale), false, MutedTextColor, 70.0f * Scale);
AddText(Panel, FText::Format(FText::FromString(TEXT("{0}: {1} | Server: {2}")), GetSelectedRoleLabel(), GetSelectedCharacterLabel(), JoinServerAddress), FMath::RoundToInt(18.0f * Scale), false, TextColor, 34.0f * Scale);
PrimaryFocusButton = AddButton(Panel, FText::FromString(TEXT("Enter Ground Zero")), ButtonColor, ButtonHoverColor, 14.0f * Scale);
PrimaryFocusButton->OnClicked.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandlePrimaryActionClicked);
PrimaryFocusButton->OnHovered.AddDynamic(this, &UAgrarianMvpFrontendWidget::FocusPrimaryButton);
AddText(Panel, FText::FromString(TEXT("Click or press Enter to close the MVP menu and begin testing.")), FMath::RoundToInt(15.0f * Scale), false, MutedTextColor, 0.0f);
}
@@ -474,7 +517,7 @@ UTextBlock* UAgrarianMvpFrontendWidget::AddText(UVerticalBox* Parent, const FTex
UButton* UAgrarianMvpFrontendWidget::AddButton(UVerticalBox* Parent, const FText& Text, const FLinearColor& NormalColor, const FLinearColor& HoveredColor, float BottomPadding)
{
UButton* Button = WidgetTree->ConstructWidget<UButton>(UButton::StaticClass());
Button->SetStyle(MakeAgrarianButtonStyle(NormalColor, HoveredColor));
Button->SetBackgroundColor(NormalColor);
Button->SetClickMethod(EButtonClickMethod::MouseDown);
Button->SetTouchMethod(EButtonTouchMethod::Down);
@@ -501,78 +544,6 @@ UButton* UAgrarianMvpFrontendWidget::AddButton(UVerticalBox* Parent, const FText
return Button;
}
void UAgrarianMvpFrontendWidget::PlayUiSound(USoundBase* Sound) const
{
if (Sound)
{
UGameplayStatics::PlaySound2D(this, Sound, FMath::Clamp(MasterVolume * UiVolume, 0.0f, 1.0f));
}
}
USlider* UAgrarianMvpFrontendWidget::AddVolumeSlider(UVerticalBox* Parent, const FText& Label, float Value, float BottomPadding)
{
if (!Parent || !WidgetTree)
{
return nullptr;
}
UHorizontalBox* Row = WidgetTree->ConstructWidget<UHorizontalBox>(UHorizontalBox::StaticClass());
if (UVerticalBoxSlot* RowSlot = Parent->AddChildToVerticalBox(Row))
{
RowSlot->SetPadding(FMargin(0.0f, 0.0f, 0.0f, BottomPadding));
RowSlot->SetHorizontalAlignment(HAlign_Fill);
}
UTextBlock* LabelText = AddText(nullptr, Label, 15, false, FLinearColor(0.82f, 0.90f, 0.76f, 1.0f), 0.0f);
if (UHorizontalBoxSlot* LabelSlot = Row->AddChildToHorizontalBox(LabelText))
{
LabelSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill));
LabelSlot->SetHorizontalAlignment(HAlign_Left);
LabelSlot->SetVerticalAlignment(VAlign_Center);
}
USlider* Slider = WidgetTree->ConstructWidget<USlider>(USlider::StaticClass());
Slider->SetValue(FMath::Clamp(Value, 0.0f, 1.0f));
if (UHorizontalBoxSlot* SliderSlot = Row->AddChildToHorizontalBox(Slider))
{
SliderSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill));
SliderSlot->SetHorizontalAlignment(HAlign_Fill);
SliderSlot->SetVerticalAlignment(VAlign_Center);
}
return Slider;
}
void UAgrarianMvpFrontendWidget::HandleMasterVolumeChanged(float Value)
{
MasterVolume = FMath::Clamp(Value, 0.0f, 1.0f);
}
void UAgrarianMvpFrontendWidget::HandleAmbientVolumeChanged(float Value)
{
AmbientVolume = FMath::Clamp(Value, 0.0f, 1.0f);
}
void UAgrarianMvpFrontendWidget::HandleWeatherVolumeChanged(float Value)
{
WeatherVolume = FMath::Clamp(Value, 0.0f, 1.0f);
}
void UAgrarianMvpFrontendWidget::HandleEffectsVolumeChanged(float Value)
{
EffectsVolume = FMath::Clamp(Value, 0.0f, 1.0f);
}
void UAgrarianMvpFrontendWidget::HandleWildlifeVolumeChanged(float Value)
{
WildlifeVolume = FMath::Clamp(Value, 0.0f, 1.0f);
}
void UAgrarianMvpFrontendWidget::HandleUiVolumeChanged(float Value)
{
UiVolume = FMath::Clamp(Value, 0.0f, 1.0f);
}
void UAgrarianMvpFrontendWidget::FocusPrimaryButton()
{
if (PrimaryFocusButton)
@@ -587,32 +558,90 @@ void UAgrarianMvpFrontendWidget::FocusPrimaryButton()
void UAgrarianMvpFrontendWidget::HandlePrimaryActionClicked()
{
PlayUiSound(UiConfirmSound);
ConfirmActiveScreen();
DeferFrontendAction([this]()
{
ConfirmActiveScreen();
});
}
void UAgrarianMvpFrontendWidget::HandleBackClicked()
{
PlayUiSound(UiBackSound);
BackFromActiveScreen();
DeferFrontendAction([this]()
{
BackFromActiveScreen();
});
}
void UAgrarianMvpFrontendWidget::HandleSaveAndQuitClicked()
{
PlayUiSound(UiSaveQuitSound);
SaveAndQuit();
DeferFrontendAction([this]()
{
SaveAndQuit();
});
}
void UAgrarianMvpFrontendWidget::HandleSaveGameClicked()
{
DeferFrontendAction([this]()
{
SaveGame();
});
}
void UAgrarianMvpFrontendWidget::HandleSettingsClicked()
{
DeferFrontendAction([this]()
{
SetActiveScreen(EAgrarianMvpFrontendScreen::Settings);
});
}
void UAgrarianMvpFrontendWidget::HandleQuitWithoutSavingClicked()
{
DeferFrontendAction([this]()
{
QuitWithoutSaving();
});
}
void UAgrarianMvpFrontendWidget::HandleMaleCharacterClicked()
{
PlayUiSound(UiSelectionSound);
SetSelectedCharacterArchetype(EAgrarianMvpCharacterArchetype::YoungAdultMale);
DeferFrontendAction([this]()
{
SetSelectedCharacterArchetype(EAgrarianMvpCharacterArchetype::YoungAdultMale);
});
}
void UAgrarianMvpFrontendWidget::HandleFemaleCharacterClicked()
{
PlayUiSound(UiSelectionSound);
SetSelectedCharacterArchetype(EAgrarianMvpCharacterArchetype::YoungAdultFemale);
DeferFrontendAction([this]()
{
SetSelectedCharacterArchetype(EAgrarianMvpCharacterArchetype::YoungAdultFemale);
});
}
void UAgrarianMvpFrontendWidget::DeferFrontendAction(TFunction<void()> Action)
{
if (!Action)
{
return;
}
if (UWorld* World = GetWorld())
{
FTimerDelegate DeferredAction;
DeferredAction.BindLambda([WeakThis = TWeakObjectPtr<UAgrarianMvpFrontendWidget>(this), Action = MoveTemp(Action)]() mutable
{
if (WeakThis.IsValid())
{
Action();
}
});
World->GetTimerManager().SetTimerForNextTick(DeferredAction);
return;
}
Action();
}
FText UAgrarianMvpFrontendWidget::GetSelectedCharacterLabel() const
+21 -52
View File
@@ -7,8 +7,6 @@
#include "AgrarianMvpFrontendWidget.generated.h"
class UButton;
class USlider;
class USoundBase;
class UTextBlock;
class UVerticalBox;
@@ -19,6 +17,8 @@ enum class EAgrarianMvpFrontendScreen : uint8
CharacterSelection,
JoinServer,
Loading,
Settings,
GameSaved,
SavingAndQuit
};
@@ -59,36 +59,6 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|MVP UI")
bool bUseHighContrast = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|MVP UI|Audio")
TObjectPtr<USoundBase> UiConfirmSound;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|MVP UI|Audio")
TObjectPtr<USoundBase> UiBackSound;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|MVP UI|Audio")
TObjectPtr<USoundBase> UiSelectionSound;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|MVP UI|Audio")
TObjectPtr<USoundBase> UiSaveQuitSound;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|MVP UI|Audio", meta = (ClampMin = "0", ClampMax = "1"))
float MasterVolume = 1.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|MVP UI|Audio", meta = (ClampMin = "0", ClampMax = "1"))
float AmbientVolume = 0.70f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|MVP UI|Audio", meta = (ClampMin = "0", ClampMax = "1"))
float WeatherVolume = 0.75f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|MVP UI|Audio", meta = (ClampMin = "0", ClampMax = "1"))
float EffectsVolume = 0.80f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|MVP UI|Audio", meta = (ClampMin = "0", ClampMax = "1"))
float WildlifeVolume = 0.70f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|MVP UI|Audio", meta = (ClampMin = "0", ClampMax = "1"))
float UiVolume = 0.65f;
UFUNCTION(BlueprintCallable, Category = "Agrarian|MVP UI")
void SetActiveScreen(EAgrarianMvpFrontendScreen NewScreen);
@@ -107,9 +77,15 @@ public:
UFUNCTION(BlueprintCallable, Category = "Agrarian|MVP UI")
void BackFromActiveScreen();
UFUNCTION(BlueprintCallable, Category = "Agrarian|MVP UI")
void SaveGame();
UFUNCTION(BlueprintCallable, Category = "Agrarian|MVP UI")
void SaveAndQuit();
UFUNCTION(BlueprintCallable, Category = "Agrarian|MVP UI")
void QuitWithoutSaving();
protected:
virtual void NativeConstruct() override;
@@ -122,32 +98,14 @@ private:
void RebuildFrontendTree();
UTextBlock* AddText(UVerticalBox* Parent, const FText& Text, int32 FontSize, bool bBold, const FLinearColor& Color, float BottomPadding);
UButton* AddButton(UVerticalBox* Parent, const FText& Text, const FLinearColor& NormalColor, const FLinearColor& HoveredColor, float BottomPadding);
void PlayUiSound(USoundBase* Sound) const;
USlider* AddVolumeSlider(UVerticalBox* Parent, const FText& Label, float Value, float BottomPadding);
UFUNCTION()
void HandleMasterVolumeChanged(float Value);
UFUNCTION()
void HandleAmbientVolumeChanged(float Value);
UFUNCTION()
void HandleWeatherVolumeChanged(float Value);
UFUNCTION()
void HandleEffectsVolumeChanged(float Value);
UFUNCTION()
void HandleWildlifeVolumeChanged(float Value);
UFUNCTION()
void HandleUiVolumeChanged(float Value);
UFUNCTION()
void FocusPrimaryButton();
UFUNCTION()
void ExecuteSaveAndQuit();
void ExecuteSaveGame();
void ExecuteQuitWithoutSaving();
UFUNCTION()
void HandlePrimaryActionClicked();
@@ -158,12 +116,23 @@ private:
UFUNCTION()
void HandleSaveAndQuitClicked();
UFUNCTION()
void HandleSaveGameClicked();
UFUNCTION()
void HandleSettingsClicked();
UFUNCTION()
void HandleQuitWithoutSavingClicked();
UFUNCTION()
void HandleMaleCharacterClicked();
UFUNCTION()
void HandleFemaleCharacterClicked();
void DeferFrontendAction(TFunction<void()> Action);
UPROPERTY()
TObjectPtr<UButton> PrimaryFocusButton;
@@ -7,6 +7,7 @@
#include "Components/DirectionalLightComponent.h"
#include "Components/ExponentialHeightFogComponent.h"
#include "Components/SceneComponent.h"
#include "Components/SkyAtmosphereComponent.h"
#include "Components/SkyLightComponent.h"
#include "Engine/World.h"
#include "ProfilingDebugging/CpuProfilerTrace.h"
@@ -30,6 +31,10 @@ AAgrarianSkyLightingController::AAgrarianSkyLightingController()
SkyLight->SetIntensity(ClearSkyLightIntensity);
SkyLight->SetMobility(EComponentMobility::Movable);
SkyAtmosphere = CreateDefaultSubobject<USkyAtmosphereComponent>(TEXT("SkyAtmosphere"));
SkyAtmosphere->SetupAttachment(SceneRoot);
SkyAtmosphere->SetMobility(EComponentMobility::Movable);
HeightFog = CreateDefaultSubobject<UExponentialHeightFogComponent>(TEXT("HeightFog"));
HeightFog->SetupAttachment(SceneRoot);
HeightFog->SetFogDensity(ClearFogDensity);
@@ -10,6 +10,7 @@
class UDirectionalLightComponent;
class UExponentialHeightFogComponent;
class USceneComponent;
class USkyAtmosphereComponent;
class USkyLightComponent;
UCLASS(Blueprintable)
@@ -32,6 +33,9 @@ public:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Sky")
TObjectPtr<USkyLightComponent> SkyLight;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Sky")
TObjectPtr<USkyAtmosphereComponent> SkyAtmosphere;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Sky")
TObjectPtr<UExponentialHeightFogComponent> HeightFog;