Compare commits
23 Commits
4e17cede2d
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| e28945a076 | |||
| 63490e044f | |||
| 90c15fdf84 | |||
| 2d3e0454cd | |||
| e82045a7f9 | |||
| a7c50f651a | |||
| 6d7cc534c4 | |||
| 507f7ad2f1 | |||
| c77708ee80 | |||
| 810a92372b | |||
| 6eb262acc3 | |||
| e7bd783309 | |||
| 5cd0c9c6d5 | |||
| fd1a8ce477 | |||
| fc74a7b129 | |||
| f0713c6c46 | |||
| 3f27be7f88 | |||
| 03dbcbc5f8 | |||
| 40f7b7e814 | |||
| dd3d247539 | |||
| 98ab61a7a4 | |||
| 13e931eb04 | |||
| 106b3bd01b |
+206
-15
@@ -36,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.
|
- [ ] 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.
|
- [ ] 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
|
## Time And Progression Philosophy
|
||||||
|
|
||||||
Baseline rule:
|
Baseline rule:
|
||||||
@@ -59,37 +94,46 @@ Design intent:
|
|||||||
Primary development repository:
|
Primary development repository:
|
||||||
|
|
||||||
```text
|
```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
|
```text
|
||||||
/mnt/projects/AgrarianGameBulid
|
/home/nathan/UnrealProjects/AgrarianGame
|
||||||
```
|
```
|
||||||
|
|
||||||
Ubuntu-Codex host:
|
Primary Unreal build host:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
192.168.5.10
|
unreal-engine / 192.168.5.20
|
||||||
```
|
```
|
||||||
|
|
||||||
Unraid project share:
|
Primary engine install:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
\\DevBox\projects\AgrarianGameBulid
|
/opt/UnrealEngine-5.7
|
||||||
```
|
```
|
||||||
|
|
||||||
Windows build VM:
|
Current engine source tag:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Windows-Builder / 192.168.5.12
|
5.7.4-release
|
||||||
```
|
```
|
||||||
|
|
||||||
Codex headless editor build command:
|
Headless Linux editor build command:
|
||||||
|
|
||||||
```text
|
```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:
|
Important tracked project root files/folders:
|
||||||
@@ -138,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
|
labels such as `0.1.A`, `0.1.B`, and `0.1.C` so they do not look like
|
||||||
separate versions.
|
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.
|
Status: completed.
|
||||||
|
|
||||||
@@ -898,9 +969,56 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe
|
|||||||
|
|
||||||
# Version 0.2 - Persistent Homesteading
|
# 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.
|
- [ ] Design land claim philosophy.
|
||||||
- [ ] Define claim size limits.
|
- [ ] Define claim size limits.
|
||||||
@@ -913,7 +1031,7 @@ Goal: Transition from temporary survival into lasting settlement and land stewar
|
|||||||
- [ ] Add claim conflict rules.
|
- [ ] Add claim conflict rules.
|
||||||
- [ ] Add abandoned claim decay rules.
|
- [ ] Add abandoned claim decay rules.
|
||||||
|
|
||||||
## 0.2.B Farming
|
## 0.2.C Farming
|
||||||
|
|
||||||
- [ ] Design soil model.
|
- [ ] Design soil model.
|
||||||
- [ ] Add basic soil quality.
|
- [ ] Add basic soil quality.
|
||||||
@@ -977,13 +1095,38 @@ Goal: Transition from temporary survival into lasting settlement and land stewar
|
|||||||
|
|
||||||
- [ ] Add barter container.
|
- [ ] Add barter container.
|
||||||
- [ ] Add simple trade UI.
|
- [ ] 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 ownership transfer.
|
||||||
- [ ] Add local price notes if needed.
|
- [ ] Add local price notes if needed.
|
||||||
- [x] Add AGR placeholder integration planning.
|
- [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 transaction logging.
|
||||||
- [ ] Add early business knowledge for bookkeeping, inventory, profit/loss, fair trade, basic credit, risk, and customer trust.
|
- [ ] 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.
|
- [ ] 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
|
## 0.2.G Homesteading Knowledge Progression
|
||||||
|
|
||||||
- [ ] Define early profession paths: farmer, herder, carpenter, mason, cook, medic, hunter, fisher, trapper, trader, and scout.
|
- [ ] Define early profession paths: farmer, herder, carpenter, mason, cook, medic, hunter, fisher, trapper, trader, and scout.
|
||||||
@@ -1018,6 +1161,11 @@ Goal: Let player communities form organically through trade, trust, conflict, la
|
|||||||
- [ ] Add positive reputation events.
|
- [ ] Add positive reputation events.
|
||||||
- [ ] Add negative reputation events.
|
- [ ] Add negative reputation events.
|
||||||
- [ ] Add reputation decay or locality rules.
|
- [ ] 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
|
## 0.3.B Trade And Contracts
|
||||||
|
|
||||||
@@ -1025,11 +1173,31 @@ Goal: Let player communities form organically through trade, trust, conflict, la
|
|||||||
- [ ] Add secure trade window.
|
- [ ] Add secure trade window.
|
||||||
- [ ] Add barter offer records.
|
- [ ] Add barter offer records.
|
||||||
- [ ] Add basic contract data model.
|
- [ ] 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 delivery contract.
|
||||||
- [ ] Add labor contract.
|
- [ ] Add labor contract.
|
||||||
- [ ] Add rental contract placeholder.
|
- [ ] Add rental contract placeholder.
|
||||||
- [ ] Add contract completion rules.
|
- [ ] Add contract completion rules.
|
||||||
- [ ] Add dispute placeholder.
|
- [ ] 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
|
## 0.3.C Crime And Consequences
|
||||||
|
|
||||||
@@ -1213,10 +1381,14 @@ Goal: Enable cities, citizenship, taxation, diplomacy, organized law, warfare, a
|
|||||||
|
|
||||||
- [ ] Add treasury model.
|
- [ ] Add treasury model.
|
||||||
- [ ] Add tax rules.
|
- [ ] 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 public storage.
|
||||||
- [ ] Add road funding.
|
- [ ] Add road funding.
|
||||||
- [ ] Add public building funding.
|
- [ ] Add public building funding.
|
||||||
- [ ] Add treasury audit logs.
|
- [ ] 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
|
## 0.5.D Diplomacy
|
||||||
|
|
||||||
@@ -1651,6 +1823,25 @@ These tracks run across all phases and must not be left as afterthoughts.
|
|||||||
- [x] Define market transaction logs.
|
- [x] Define market transaction logs.
|
||||||
- [x] Define bridge between web wallet and game account.
|
- [x] Define bridge between web wallet and game account.
|
||||||
- [x] Define legal/compliance review points.
|
- [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
|
## 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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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.
|
||||||
|
|
||||||
@@ -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."
|
||||||
|
```
|
||||||
|
|
||||||
@@ -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.
|
||||||
|
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -1,13 +1,40 @@
|
|||||||
# Codebase Readiness Review
|
# Codebase Readiness Review
|
||||||
|
|
||||||
Date: 2026-05-19
|
Date: 2026-05-21
|
||||||
|
|
||||||
Scope: source code, build scripts, verification scripts, config files, and the
|
Scope: source code, build scripts, verification scripts, config files, and the
|
||||||
roadmap after completion of `0.1.R`.
|
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
|
## 0.1 Completion Check
|
||||||
|
|
||||||
All `0.1.A` through `0.1.R` roadmap checkboxes are complete. The remaining
|
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
|
unchecked roadmap items before `Version 0.2` are North Star and philosophy
|
||||||
statements, not incomplete `0.1` implementation tasks.
|
statements, not incomplete `0.1` implementation tasks.
|
||||||
|
|
||||||
@@ -27,6 +54,9 @@ statements, not incomplete `0.1` implementation tasks.
|
|||||||
moves into larger systems.
|
moves into larger systems.
|
||||||
- The Windows build pipeline, package script, visual QA gate, and Linux server
|
- The Windows build pipeline, package script, visual QA gate, and Linux server
|
||||||
target are established enough to keep milestone work shippable.
|
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
|
## Cleanup Findings
|
||||||
|
|
||||||
@@ -50,6 +80,11 @@ statements, not incomplete `0.1` implementation tasks.
|
|||||||
audit boundary so tester tools do not become gameplay exploits.
|
audit boundary so tester tools do not become gameplay exploits.
|
||||||
- Verifier scripts are useful but numerous. 0.2 should add a grouped verifier
|
- Verifier scripts are useful but numerous. 0.2 should add a grouped verifier
|
||||||
runner so milestone verification is one command with explicit categories.
|
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
|
## Low-Risk Cleanup Applied
|
||||||
|
|
||||||
@@ -58,6 +93,11 @@ statements, not incomplete `0.1` implementation tasks.
|
|||||||
|
|
||||||
## 0.2 Engineering Priorities
|
## 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
|
- Keep land claims, farming, storage, household tasks, and future economy state
|
||||||
server-authoritative from the first implementation pass.
|
server-authoritative from the first implementation pass.
|
||||||
- Add schema/version fields when introducing new persistent records.
|
- Add schema/version fields when introducing new persistent records.
|
||||||
|
|||||||
+9975
File diff suppressed because it is too large
Load Diff
@@ -6,9 +6,14 @@ space.
|
|||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
|
|
||||||
- Terrain receives a warm coastal scrub ground material.
|
- Terrain receives a procedural coastal scrub terrain material that blends
|
||||||
- Foliage patch instances keep the current prototype meshes but use distinct
|
dry soil, scrub green, and sandy path color families with broad and fine
|
||||||
tree, shrub, and dry grass materials.
|
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
|
- Wood, fiber, stone, and freshwater actors receive distinct first-pass
|
||||||
materials.
|
materials.
|
||||||
- Investor-facing asset variation actors add additional tree canopies/trunks,
|
- 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
|
`Scripts/verify_ground_zero_natural_environment_pass.py` checks that the
|
||||||
materials exist, the landscape uses the terrain material, the foliage actor has
|
materials exist, the landscape uses the terrain material, the foliage actor has
|
||||||
the expected investor-facing instance counts and material assignments, and
|
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,
|
layer: twenty-three labeled variation actors, at least four mesh silhouettes,
|
||||||
unique scale profiles, and coverage across tree, bush, grass, rock, and water
|
unique scale profiles, and coverage across tree, bush, grass, rock, and water
|
||||||
visual families. `Scripts/verify_native_placeholder_meshes.py` checks that playable
|
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.
|
||||||
Executable
+33
@@ -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."
|
||||||
Executable
+74
@@ -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
|
||||||
@@ -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": ""
|
||||||
|
}
|
||||||
Executable
+40
@@ -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"
|
||||||
|
|
||||||
Executable
+96
@@ -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"
|
||||||
|
|
||||||
Executable
+295
@@ -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."
|
||||||
Executable
+175
@@ -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
|
||||||
|
|
||||||
@@ -18,6 +18,8 @@ LANDSCAPE_MIN_XY = -50000.0
|
|||||||
FOLIAGE_LABEL = "AGR_GroundZeroFoliage_FirstPass"
|
FOLIAGE_LABEL = "AGR_GroundZeroFoliage_FirstPass"
|
||||||
FOLIAGE_RANDOM_SEED = 4160544
|
FOLIAGE_RANDOM_SEED = 4160544
|
||||||
PLACEHOLDER_MESH_FOLDER = "/Game/Agrarian/Environment/PlaceholderMeshes"
|
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 = {
|
PLACEHOLDER_MESH_SOURCES = {
|
||||||
"SM_AGR_Placeholder_Cube": "/Game/LevelPrototyping/Meshes/SM_Cube",
|
"SM_AGR_Placeholder_Cube": "/Game/LevelPrototyping/Meshes/SM_Cube",
|
||||||
"SM_AGR_Placeholder_ChamferCube": "/Game/LevelPrototyping/Meshes/SM_ChamferCube",
|
"SM_AGR_Placeholder_ChamferCube": "/Game/LevelPrototyping/Meshes/SM_ChamferCube",
|
||||||
@@ -30,9 +32,9 @@ PLACEHOLDER_MESHES = {
|
|||||||
for name in PLACEHOLDER_MESH_SOURCES
|
for name in PLACEHOLDER_MESH_SOURCES
|
||||||
}
|
}
|
||||||
FOLIAGE_MESHES = {
|
FOLIAGE_MESHES = {
|
||||||
"tree": "/Engine/BasicShapes/Cone",
|
"tree": f"{VEGETATION_MESH_FOLDER}/SM_AGR_GZ_CoastalOak_Proxy",
|
||||||
"shrub": "/Engine/BasicShapes/Sphere",
|
"shrub": f"{VEGETATION_MESH_FOLDER}/SM_AGR_GZ_CoyoteBrush_Proxy",
|
||||||
"grass": "/Engine/BasicShapes/Plane",
|
"grass": f"{VEGETATION_MESH_FOLDER}/SM_AGR_GZ_DryGrassClump_Proxy",
|
||||||
}
|
}
|
||||||
VARIATION_MESHES = {
|
VARIATION_MESHES = {
|
||||||
"cube": PLACEHOLDER_MESHES["SM_AGR_Placeholder_Cube"],
|
"cube": PLACEHOLDER_MESHES["SM_AGR_Placeholder_Cube"],
|
||||||
@@ -40,33 +42,43 @@ VARIATION_MESHES = {
|
|||||||
"cylinder": PLACEHOLDER_MESHES["SM_AGR_Placeholder_Cylinder"],
|
"cylinder": PLACEHOLDER_MESHES["SM_AGR_Placeholder_Cylinder"],
|
||||||
"quarter_cylinder": PLACEHOLDER_MESHES["SM_AGR_Placeholder_QuarterCylinder"],
|
"quarter_cylinder": PLACEHOLDER_MESHES["SM_AGR_Placeholder_QuarterCylinder"],
|
||||||
"plane": PLACEHOLDER_MESHES["SM_AGR_Placeholder_Plane"],
|
"plane": PLACEHOLDER_MESHES["SM_AGR_Placeholder_Plane"],
|
||||||
"sphere": "/Engine/BasicShapes/Sphere",
|
"coastal_oak": FOLIAGE_MESHES["tree"],
|
||||||
"cone": "/Engine/BasicShapes/Cone",
|
"coyote_brush": FOLIAGE_MESHES["shrub"],
|
||||||
|
"dry_grass_clump": FOLIAGE_MESHES["grass"],
|
||||||
}
|
}
|
||||||
MATERIAL_FOLDER = "/Game/Agrarian/Materials"
|
MATERIAL_FOLDER = "/Game/Agrarian/Materials"
|
||||||
ENVIRONMENT_MATERIALS = {
|
ENVIRONMENT_MATERIALS = {
|
||||||
"terrain": {
|
"terrain": {
|
||||||
"path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Terrain_CoastalScrub",
|
"path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Terrain_CoastalScrub",
|
||||||
"color": unreal.LinearColor(0.16, 0.23, 0.12, 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,
|
"roughness": 0.92,
|
||||||
},
|
},
|
||||||
"tree": {
|
"tree": {
|
||||||
"path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Tree_CoastalOak",
|
"path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Tree_CoastalOak",
|
||||||
"color": unreal.LinearColor(0.07, 0.18, 0.06, 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,
|
"roughness": 0.88,
|
||||||
"used_with_instanced_static_meshes": True,
|
"used_with_instanced_static_meshes": True,
|
||||||
},
|
},
|
||||||
"shrub": {
|
"shrub": {
|
||||||
"path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Shrub_CoyoteBrush",
|
"path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Shrub_CoyoteBrush",
|
||||||
"color": unreal.LinearColor(0.15, 0.28, 0.10, 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,
|
"roughness": 0.9,
|
||||||
"used_with_instanced_static_meshes": True,
|
"used_with_instanced_static_meshes": True,
|
||||||
|
"two_sided": True,
|
||||||
},
|
},
|
||||||
"grass": {
|
"grass": {
|
||||||
"path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Grass_DryCoastal",
|
"path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Grass_DryCoastal",
|
||||||
"color": unreal.LinearColor(0.32, 0.34, 0.13, 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,
|
"roughness": 0.95,
|
||||||
"used_with_instanced_static_meshes": True,
|
"used_with_instanced_static_meshes": True,
|
||||||
|
"two_sided": True,
|
||||||
},
|
},
|
||||||
"wood_resource": {
|
"wood_resource": {
|
||||||
"path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Wood_Resource",
|
"path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Wood_Resource",
|
||||||
@@ -351,7 +363,7 @@ WEATHER_EXPOSURE_ZONES = [
|
|||||||
ENVIRONMENT_VARIATION_ACTORS = [
|
ENVIRONMENT_VARIATION_ACTORS = [
|
||||||
{
|
{
|
||||||
"label": "AGR_GZ_EnvVar_Tree_Canopy_01",
|
"label": "AGR_GZ_EnvVar_Tree_Canopy_01",
|
||||||
"mesh_key": "sphere",
|
"mesh_key": "coastal_oak",
|
||||||
"material_key": "tree",
|
"material_key": "tree",
|
||||||
"location_xy": unreal.Vector(-27500.0, 6900.0, 0.0),
|
"location_xy": unreal.Vector(-27500.0, 6900.0, 0.0),
|
||||||
"z_offset": 390.0,
|
"z_offset": 390.0,
|
||||||
@@ -369,7 +381,7 @@ ENVIRONMENT_VARIATION_ACTORS = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "AGR_GZ_EnvVar_Tree_Canopy_02",
|
"label": "AGR_GZ_EnvVar_Tree_Canopy_02",
|
||||||
"mesh_key": "sphere",
|
"mesh_key": "coastal_oak",
|
||||||
"material_key": "tree",
|
"material_key": "tree",
|
||||||
"location_xy": unreal.Vector(17600.0, 31800.0, 0.0),
|
"location_xy": unreal.Vector(17600.0, 31800.0, 0.0),
|
||||||
"z_offset": 430.0,
|
"z_offset": 430.0,
|
||||||
@@ -387,7 +399,7 @@ ENVIRONMENT_VARIATION_ACTORS = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "AGR_GZ_EnvVar_Bush_Rounded_01",
|
"label": "AGR_GZ_EnvVar_Bush_Rounded_01",
|
||||||
"mesh_key": "sphere",
|
"mesh_key": "coyote_brush",
|
||||||
"material_key": "shrub",
|
"material_key": "shrub",
|
||||||
"location_xy": unreal.Vector(-33400.0, -15200.0, 0.0),
|
"location_xy": unreal.Vector(-33400.0, -15200.0, 0.0),
|
||||||
"z_offset": 70.0,
|
"z_offset": 70.0,
|
||||||
@@ -396,7 +408,7 @@ ENVIRONMENT_VARIATION_ACTORS = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "AGR_GZ_EnvVar_Bush_Rounded_02",
|
"label": "AGR_GZ_EnvVar_Bush_Rounded_02",
|
||||||
"mesh_key": "sphere",
|
"mesh_key": "coyote_brush",
|
||||||
"material_key": "shrub",
|
"material_key": "shrub",
|
||||||
"location_xy": unreal.Vector(30400.0, -3900.0, 0.0),
|
"location_xy": unreal.Vector(30400.0, -3900.0, 0.0),
|
||||||
"z_offset": 75.0,
|
"z_offset": 75.0,
|
||||||
@@ -779,6 +791,209 @@ def ensure_native_placeholder_meshes():
|
|||||||
unreal.log(f"Created native placeholder mesh: {destination_path}")
|
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():
|
def ensure_environment_materials():
|
||||||
if not unreal.EditorAssetLibrary.does_directory_exist(MATERIAL_FOLDER):
|
if not unreal.EditorAssetLibrary.does_directory_exist(MATERIAL_FOLDER):
|
||||||
unreal.EditorAssetLibrary.make_directory(MATERIAL_FOLDER)
|
unreal.EditorAssetLibrary.make_directory(MATERIAL_FOLDER)
|
||||||
@@ -837,11 +1052,141 @@ def ensure_environment_materials():
|
|||||||
material.set_editor_property("used_with_instanced_static_meshes", True)
|
material.set_editor_property("used_with_instanced_static_meshes", True)
|
||||||
unreal.MaterialEditingLibrary.recompile_material(material)
|
unreal.MaterialEditingLibrary.recompile_material(material)
|
||||||
unreal.EditorAssetLibrary.save_asset(spec["path"], only_if_is_dirty=False)
|
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
|
created_or_loaded[key] = material
|
||||||
|
|
||||||
return created_or_loaded
|
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):
|
def apply_material_to_actor_meshes(actor, material):
|
||||||
applied_count = 0
|
applied_count = 0
|
||||||
for component in actor.get_components_by_class(unreal.StaticMeshComponent):
|
for component in actor.get_components_by_class(unreal.StaticMeshComponent):
|
||||||
@@ -1268,6 +1613,7 @@ def main():
|
|||||||
raise RuntimeError(f"Could not load map: {MAP_PATH}")
|
raise RuntimeError(f"Could not load map: {MAP_PATH}")
|
||||||
|
|
||||||
ensure_native_placeholder_meshes()
|
ensure_native_placeholder_meshes()
|
||||||
|
ensure_ground_zero_vegetation_meshes()
|
||||||
|
|
||||||
labels = {spec["label"] for spec in DEMO_ACTORS}
|
labels = {spec["label"] for spec in DEMO_ACTORS}
|
||||||
labels.update(LEGACY_DEMO_LIGHTING_LABELS)
|
labels.update(LEGACY_DEMO_LIGHTING_LABELS)
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -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"
|
MAP_PATH = "/Game/Agrarian/Maps/L_GroundZeroTerrain_Test"
|
||||||
FOLIAGE_LABEL = "AGR_GroundZeroFoliage_FirstPass"
|
FOLIAGE_LABEL = "AGR_GroundZeroFoliage_FirstPass"
|
||||||
EXPECTED_FOLIAGE_COUNTS = {
|
EXPECTED_FOLIAGE_COUNTS = {
|
||||||
"trees": 64,
|
"trees": 96,
|
||||||
"shrubs": 148,
|
"shrubs": 220,
|
||||||
"grass": 260,
|
"grass": 420,
|
||||||
}
|
}
|
||||||
CRITICAL_CLEARANCE_CM = {
|
CRITICAL_CLEARANCE_CM = {
|
||||||
"trees": 5200.0,
|
"trees": 5200.0,
|
||||||
|
|||||||
@@ -19,6 +19,20 @@ EXPECTED_FOLIAGE_COUNTS = {
|
|||||||
"shrubs": 220,
|
"shrubs": 220,
|
||||||
"grass": 420,
|
"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 = {
|
RESOURCE_MATERIALS = {
|
||||||
"AGR_GZ_Wood": "wood_resource",
|
"AGR_GZ_Wood": "wood_resource",
|
||||||
"AGR_GZ_Fiber": "fiber_resource",
|
"AGR_GZ_Fiber": "fiber_resource",
|
||||||
@@ -61,6 +75,29 @@ def material_path(material):
|
|||||||
return material.get_path_name().split(".", 1)[0]
|
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):
|
def material_key_for_label(label):
|
||||||
for prefix, material_key in RESOURCE_MATERIALS.items():
|
for prefix, material_key in RESOURCE_MATERIALS.items():
|
||||||
if label.startswith(prefix):
|
if label.startswith(prefix):
|
||||||
@@ -82,6 +119,44 @@ def assert_asset(path):
|
|||||||
return asset
|
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():
|
def main():
|
||||||
if not unreal.EditorLevelLibrary.load_level(MAP_PATH):
|
if not unreal.EditorLevelLibrary.load_level(MAP_PATH):
|
||||||
raise RuntimeError(f"Could not load map: {MAP_PATH}")
|
raise RuntimeError(f"Could not load map: {MAP_PATH}")
|
||||||
@@ -98,6 +173,7 @@ def main():
|
|||||||
expected = MATERIALS["terrain"]
|
expected = MATERIALS["terrain"]
|
||||||
if assigned != expected:
|
if assigned != expected:
|
||||||
failures.append(f"landscape material expected {expected}, got {assigned}")
|
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]
|
foliage_actors = [actor for actor in actors if get_actor_label(actor) == FOLIAGE_LABEL]
|
||||||
if len(foliage_actors) != 1:
|
if len(foliage_actors) != 1:
|
||||||
@@ -120,10 +196,31 @@ def main():
|
|||||||
}
|
}
|
||||||
for property_name, material_key in component_expectations.items():
|
for property_name, material_key in component_expectations.items():
|
||||||
component = foliage.get_editor_property(property_name)
|
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))
|
assigned = material_path(component.get_material(0))
|
||||||
expected = MATERIALS[material_key]
|
expected = MATERIALS[material_key]
|
||||||
if assigned != expected:
|
if assigned != expected:
|
||||||
failures.append(f"{property_name} material expected {expected}, got {assigned}")
|
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
|
checked_resource_actors = 0
|
||||||
for actor in actors:
|
for actor in actors:
|
||||||
@@ -187,8 +284,12 @@ def main():
|
|||||||
roadmap = unreal.Paths.convert_relative_path_to_full(unreal.Paths.project_dir()) + "AGRARIAN_DEVELOPMENT_ROADMAP.md"
|
roadmap = unreal.Paths.convert_relative_path_to_full(unreal.Paths.project_dir()) + "AGRARIAN_DEVELOPMENT_ROADMAP.md"
|
||||||
for path, snippet in [
|
for path, snippet in [
|
||||||
(docs, "asset variation"),
|
(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] Replace grey-box environment presentation with an MVP natural environment pass"),
|
||||||
(roadmap, "[x] Add first-pass environment asset variation"),
|
(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:
|
with open(path, "r", encoding="utf-8") as handle:
|
||||||
text = handle.read()
|
text = handle.read()
|
||||||
|
|||||||
@@ -13,23 +13,42 @@ EXPECTED = {
|
|||||||
"AgrarianMvpFrontendWidget.h": [
|
"AgrarianMvpFrontendWidget.h": [
|
||||||
"ConfirmActiveScreen",
|
"ConfirmActiveScreen",
|
||||||
"BackFromActiveScreen",
|
"BackFromActiveScreen",
|
||||||
|
"SaveGame",
|
||||||
"SaveAndQuit",
|
"SaveAndQuit",
|
||||||
|
"QuitWithoutSaving",
|
||||||
|
"Settings",
|
||||||
|
"GameSaved",
|
||||||
],
|
],
|
||||||
"AgrarianMvpFrontendWidget.cpp": [
|
"AgrarianMvpFrontendWidget.cpp": [
|
||||||
"UButton::StaticClass()",
|
"UButton::StaticClass()",
|
||||||
"HandleMaleCharacterClicked",
|
"HandleMaleCharacterClicked",
|
||||||
"HandleFemaleCharacterClicked",
|
"HandleFemaleCharacterClicked",
|
||||||
"OnClicked.AddDynamic",
|
"OnClicked.AddDynamic",
|
||||||
"Save & Quit",
|
"Save Game",
|
||||||
|
"Settings",
|
||||||
|
"Save & Exit",
|
||||||
|
"Quit Without Saving",
|
||||||
"Saving World",
|
"Saving World",
|
||||||
|
"Game Saved",
|
||||||
|
"Player Options",
|
||||||
|
"HandleSaveGameClicked",
|
||||||
|
"HandleSettingsClicked",
|
||||||
|
"HandleQuitWithoutSavingClicked",
|
||||||
|
"ExecuteSaveGame",
|
||||||
|
"ExecuteQuitWithoutSaving",
|
||||||
"ConsoleCommand(TEXT(\"AgrarianSaveWorld\"))",
|
"ConsoleCommand(TEXT(\"AgrarianSaveWorld\"))",
|
||||||
"ConsoleCommand(TEXT(\"quit\"))",
|
"ConsoleCommand(TEXT(\"quit\"))",
|
||||||
"PlayerController->SetIgnoreMoveInput(false)",
|
"AAgrarianGamePlayerController* AgrarianPlayerController",
|
||||||
"PlayerController->SetIgnoreLookInput(false)",
|
"AgrarianPlayerController->AgrarianSelectCharacter",
|
||||||
|
"AgrarianPlayerController->AgrarianCompleteFrontend",
|
||||||
|
"PlayerController->ResetIgnoreMoveInput()",
|
||||||
|
"PlayerController->ResetIgnoreLookInput()",
|
||||||
],
|
],
|
||||||
"AgrarianGamePlayerController.h": [
|
"AgrarianGamePlayerController.h": [
|
||||||
"ShowMvpPauseMenu",
|
"ShowMvpPauseMenu",
|
||||||
"HandleMvpEscapeInput",
|
"HandleMvpEscapeInput",
|
||||||
|
"RestoreGameplayControlState",
|
||||||
|
"AgrarianRepairGameplayInput",
|
||||||
],
|
],
|
||||||
"AgrarianGamePlayerController.cpp": [
|
"AgrarianGamePlayerController.cpp": [
|
||||||
"SetIgnoreMoveInput(true)",
|
"SetIgnoreMoveInput(true)",
|
||||||
@@ -40,10 +59,24 @@ EXPECTED = {
|
|||||||
"InputComponent->BindKey(EKeys::Escape",
|
"InputComponent->BindKey(EKeys::Escape",
|
||||||
"MvpFrontendWidget->IsInViewport()",
|
"MvpFrontendWidget->IsInViewport()",
|
||||||
"ShowMvpPauseMenu();",
|
"ShowMvpPauseMenu();",
|
||||||
|
"ResetIgnoreMoveInput();",
|
||||||
|
"ResetIgnoreLookInput();",
|
||||||
|
"ApplyDefaultInputMappingContexts();",
|
||||||
|
"void AAgrarianGamePlayerController::RestoreGameplayControlState()",
|
||||||
|
"ControlledPawn->SetActorHiddenInGame(false)",
|
||||||
|
"ControlledPawn->SetActorEnableCollision(true)",
|
||||||
|
"MovementComponent->SetMovementMode(MOVE_Walking)",
|
||||||
|
"void AAgrarianGamePlayerController::AgrarianRepairGameplayInput()",
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
FORBIDDEN = {
|
FORBIDDEN = {
|
||||||
|
"AgrarianMvpFrontendWidget.cpp": [
|
||||||
|
"ConsoleCommand(TEXT(\"AgrarianSelectCharacter",
|
||||||
|
"ConsoleCommand(TEXT(\"AgrarianCompleteFrontend\"))",
|
||||||
|
"PlayerController->SetIgnoreMoveInput(false)",
|
||||||
|
"PlayerController->SetIgnoreLookInput(false)",
|
||||||
|
],
|
||||||
"AgrarianGamePlayerController.cpp": [
|
"AgrarianGamePlayerController.cpp": [
|
||||||
"BindKey(EKeys::LeftMouseButton",
|
"BindKey(EKeys::LeftMouseButton",
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -23,8 +23,12 @@ def main() -> None:
|
|||||||
roadmap = ROADMAP.read_text(encoding="utf-8")
|
roadmap = ROADMAP.read_text(encoding="utf-8")
|
||||||
|
|
||||||
for token in (
|
for token in (
|
||||||
|
"Settings",
|
||||||
|
"GameSaved",
|
||||||
"SavingAndQuit",
|
"SavingAndQuit",
|
||||||
|
"SaveGame",
|
||||||
"ExecuteSaveAndQuit",
|
"ExecuteSaveAndQuit",
|
||||||
|
"QuitWithoutSaving",
|
||||||
):
|
):
|
||||||
require(token in header, f"missing segmented flow declaration: {token}")
|
require(token in header, f"missing segmented flow declaration: {token}")
|
||||||
|
|
||||||
@@ -34,11 +38,20 @@ def main() -> None:
|
|||||||
"Loading Segment",
|
"Loading Segment",
|
||||||
"Pause Menu",
|
"Pause Menu",
|
||||||
"Gameplay is paused while this menu is active.",
|
"Gameplay is paused while this menu is active.",
|
||||||
|
"Save Game",
|
||||||
|
"Settings",
|
||||||
|
"Quit Without Saving",
|
||||||
|
"Game Saved",
|
||||||
|
"Player Options",
|
||||||
"Saving World",
|
"Saving World",
|
||||||
"Writing the current world state",
|
"Writing the current world state",
|
||||||
|
"SetActiveScreen(EAgrarianMvpFrontendScreen::GameSaved)",
|
||||||
|
"SetActiveScreen(EAgrarianMvpFrontendScreen::Settings)",
|
||||||
"SetActiveScreen(EAgrarianMvpFrontendScreen::SavingAndQuit)",
|
"SetActiveScreen(EAgrarianMvpFrontendScreen::SavingAndQuit)",
|
||||||
"GetTimerManager().SetTimer",
|
"GetTimerManager().SetTimer",
|
||||||
|
"ExecuteSaveGame",
|
||||||
"ExecuteSaveAndQuit",
|
"ExecuteSaveAndQuit",
|
||||||
|
"ExecuteQuitWithoutSaving",
|
||||||
"ConsoleCommand(TEXT(\"AgrarianSaveWorld\"))",
|
"ConsoleCommand(TEXT(\"AgrarianSaveWorld\"))",
|
||||||
"ConsoleCommand(TEXT(\"quit\"))",
|
"ConsoleCommand(TEXT(\"quit\"))",
|
||||||
):
|
):
|
||||||
@@ -52,8 +65,9 @@ def main() -> None:
|
|||||||
"SetIgnoreMoveInput(true)",
|
"SetIgnoreMoveInput(true)",
|
||||||
"SetIgnoreLookInput(true)",
|
"SetIgnoreLookInput(true)",
|
||||||
"SetInputMode(FInputModeGameOnly())",
|
"SetInputMode(FInputModeGameOnly())",
|
||||||
"SetIgnoreMoveInput(false)",
|
"ResetIgnoreMoveInput()",
|
||||||
"SetIgnoreLookInput(false)",
|
"ResetIgnoreLookInput()",
|
||||||
|
"RestoreGameplayControlState",
|
||||||
"saving",
|
"saving",
|
||||||
):
|
):
|
||||||
require(token in controller + frontend, f"missing modal input or debug token: {token}")
|
require(token in controller + frontend, f"missing modal input or debug token: {token}")
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import unreal
|
|||||||
MAP_PATH = "/Game/Agrarian/Maps/L_GroundZeroTerrain_Test"
|
MAP_PATH = "/Game/Agrarian/Maps/L_GroundZeroTerrain_Test"
|
||||||
PROJECT_ROOT = Path(unreal.Paths.convert_relative_path_to_full(unreal.Paths.project_dir()))
|
PROJECT_ROOT = Path(unreal.Paths.convert_relative_path_to_full(unreal.Paths.project_dir()))
|
||||||
PLACEHOLDER_MESH_FOLDER = "/Game/Agrarian/Environment/PlaceholderMeshes"
|
PLACEHOLDER_MESH_FOLDER = "/Game/Agrarian/Environment/PlaceholderMeshes"
|
||||||
|
VEGETATION_MESH_FOLDER = "/Game/Agrarian/Environment/Vegetation"
|
||||||
PLACEHOLDER_MESHES = {
|
PLACEHOLDER_MESHES = {
|
||||||
"SM_AGR_Placeholder_Cube",
|
"SM_AGR_Placeholder_Cube",
|
||||||
"SM_AGR_Placeholder_ChamferCube",
|
"SM_AGR_Placeholder_ChamferCube",
|
||||||
@@ -47,6 +48,13 @@ def assert_native_mesh(path, failures):
|
|||||||
failures.append(f"template mesh reference remains: {path}")
|
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():
|
def main():
|
||||||
failures = []
|
failures = []
|
||||||
|
|
||||||
@@ -79,7 +87,7 @@ def main():
|
|||||||
else:
|
else:
|
||||||
for property_name in ("tree_instances", "shrub_instances", "grass_instances"):
|
for property_name in ("tree_instances", "shrub_instances", "grass_instances"):
|
||||||
component = foliage_actors[0].get_editor_property(property_name)
|
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:
|
for actor in actors:
|
||||||
label = get_actor_label(actor)
|
label = get_actor_label(actor)
|
||||||
@@ -90,7 +98,7 @@ def main():
|
|||||||
if not mesh_components:
|
if not mesh_components:
|
||||||
failures.append(f"{label} has no static mesh component")
|
failures.append(f"{label} has no static mesh component")
|
||||||
continue
|
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:
|
for path, snippet in DOC_SNIPPETS:
|
||||||
text = path.read_text(encoding="utf-8")
|
text = path.read_text(encoding="utf-8")
|
||||||
|
|||||||
@@ -6,16 +6,27 @@ ROOT = Path(__file__).resolve().parents[1]
|
|||||||
REQUIRED = {
|
REQUIRED = {
|
||||||
ROOT / "Source" / "AgrarianGame" / "AgrarianDemoNoticeActor.h": [
|
ROOT / "Source" / "AgrarianGame" / "AgrarianDemoNoticeActor.h": [
|
||||||
"float NoticeDurationSeconds = 24.0f;",
|
"float NoticeDurationSeconds = 24.0f;",
|
||||||
|
"bool bAutoShowNotice = false;",
|
||||||
"Investor Demo v0.1.N - Build 2026.05.18",
|
"Investor Demo v0.1.N - Build 2026.05.18",
|
||||||
],
|
],
|
||||||
ROOT / "Source" / "AgrarianGame" / "AgrarianDemoNoticeWidget.h": [
|
ROOT / "Source" / "AgrarianGame" / "AgrarianDemoNoticeWidget.h": [
|
||||||
"virtual void NativeConstruct() override;",
|
"virtual void NativeConstruct() override;",
|
||||||
|
"NativeOnKeyDown",
|
||||||
|
"NativeOnMouseButtonDown",
|
||||||
|
"EAgrarianStartupPresentationSegment",
|
||||||
|
"DrawSplash",
|
||||||
|
"DrawStory",
|
||||||
"DrawCinematicCredits",
|
"DrawCinematicCredits",
|
||||||
"DrawCreditIllustration",
|
"DrawCreditIllustration",
|
||||||
"CreditsStartTimeSeconds",
|
"SegmentStartTimeSeconds",
|
||||||
"Investor Demo v0.1.N - Build 2026.05.18",
|
"Investor Demo v0.1.N - Build 2026.05.18",
|
||||||
],
|
],
|
||||||
ROOT / "Source" / "AgrarianGame" / "AgrarianDemoNoticeWidget.cpp": [
|
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",
|
"Nathan Slaven",
|
||||||
"Lead Developer",
|
"Lead Developer",
|
||||||
"Hunter Slaven",
|
"Hunter Slaven",
|
||||||
@@ -33,10 +44,19 @@ REQUIRED = {
|
|||||||
"Agrarian Startup Credits",
|
"Agrarian Startup Credits",
|
||||||
],
|
],
|
||||||
ROOT / "Source" / "AgrarianGame" / "AgrarianGamePlayerController.h": [
|
ROOT / "Source" / "AgrarianGame" / "AgrarianGamePlayerController.h": [
|
||||||
"MvpFrontendStartupDelaySeconds = 24.25f",
|
"StartupSplashSeconds = 4.0f",
|
||||||
|
"StartupStorySeconds = 60.0f",
|
||||||
|
"StartupCreditsSeconds = 20.75f",
|
||||||
|
"StartupPresentationWidget",
|
||||||
|
"AgrarianSkipStartupPresentation",
|
||||||
"ShowMvpFrontend",
|
"ShowMvpFrontend",
|
||||||
],
|
],
|
||||||
ROOT / "Source" / "AgrarianGame" / "AgrarianGamePlayerController.cpp": [
|
ROOT / "Source" / "AgrarianGame" / "AgrarianGamePlayerController.cpp": [
|
||||||
|
"StartStartupPresentation",
|
||||||
|
"ShowStartupPresentationSegment(EAgrarianStartupPresentationSegment::Splash",
|
||||||
|
"ShowStartupPresentationSegment(EAgrarianStartupPresentationSegment::Story",
|
||||||
|
"ShowStartupPresentationSegment(EAgrarianStartupPresentationSegment::Credits",
|
||||||
|
"FinishStartupPresentation",
|
||||||
"GetWorldTimerManager().SetTimer",
|
"GetWorldTimerManager().SetTimer",
|
||||||
"ShowMvpFrontend",
|
"ShowMvpFrontend",
|
||||||
"FInputModeUIOnly",
|
"FInputModeUIOnly",
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ void AAgrarianDemoNoticeActor::BeginPlay()
|
|||||||
{
|
{
|
||||||
Super::BeginPlay();
|
Super::BeginPlay();
|
||||||
|
|
||||||
|
if (!bAutoShowNotice)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
APlayerController* PlayerController = GetWorld() ? GetWorld()->GetFirstPlayerController() : nullptr;
|
APlayerController* PlayerController = GetWorld() ? GetWorld()->GetFirstPlayerController() : nullptr;
|
||||||
if (!PlayerController || !NoticeWidgetClass)
|
if (!PlayerController || !NoticeWidgetClass)
|
||||||
{
|
{
|
||||||
@@ -44,4 +49,3 @@ void AAgrarianDemoNoticeActor::RemoveNotice()
|
|||||||
ActiveNoticeWidget = nullptr;
|
ActiveNoticeWidget = nullptr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Demo", meta = (ClampMin = "1.0"))
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Demo", meta = (ClampMin = "1.0"))
|
||||||
float NoticeDurationSeconds = 24.0f;
|
float NoticeDurationSeconds = 24.0f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Demo")
|
||||||
|
bool bAutoShowNotice = false;
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Demo")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Demo")
|
||||||
FText VersionLabel = FText::FromString(TEXT("Investor Demo v0.1.N - Build 2026.05.18"));
|
FText VersionLabel = FText::FromString(TEXT("Investor Demo v0.1.N - Build 2026.05.18"));
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,10 @@
|
|||||||
|
|
||||||
#include "AgrarianDemoNoticeWidget.h"
|
#include "AgrarianDemoNoticeWidget.h"
|
||||||
|
|
||||||
|
#include "AgrarianGamePlayerController.h"
|
||||||
#include "Engine/World.h"
|
#include "Engine/World.h"
|
||||||
|
#include "Input/Reply.h"
|
||||||
|
#include "InputCoreTypes.h"
|
||||||
#include "Rendering/DrawElements.h"
|
#include "Rendering/DrawElements.h"
|
||||||
#include "Styling/CoreStyle.h"
|
#include "Styling/CoreStyle.h"
|
||||||
|
|
||||||
@@ -46,12 +49,76 @@ float GetCreditsSequenceDurationSeconds()
|
|||||||
}
|
}
|
||||||
return Duration;
|
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()
|
void UAgrarianDemoNoticeWidget::NativeConstruct()
|
||||||
{
|
{
|
||||||
Super::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(
|
int32 UAgrarianDemoNoticeWidget::NativePaint(
|
||||||
@@ -66,9 +133,19 @@ int32 UAgrarianDemoNoticeWidget::NativePaint(
|
|||||||
LayerId = Super::NativePaint(Args, AllottedGeometry, MyCullingRect, OutDrawElements, LayerId, InWidgetStyle, bParentEnabled);
|
LayerId = Super::NativePaint(Args, AllottedGeometry, MyCullingRect, OutDrawElements, LayerId, InWidgetStyle, bParentEnabled);
|
||||||
|
|
||||||
const FVector2D Size = AllottedGeometry.GetLocalSize();
|
const FVector2D Size = AllottedGeometry.GetLocalSize();
|
||||||
const UWorld* World = GetWorld();
|
if (PresentationSegment == EAgrarianStartupPresentationSegment::Splash)
|
||||||
const float Elapsed = World ? World->GetTimeSeconds() - CreditsStartTimeSeconds : 0.0f;
|
{
|
||||||
if (Elapsed <= GetCreditsSequenceDurationSeconds() + 1.0f)
|
DrawSplash(OutDrawElements, LayerId, AllottedGeometry);
|
||||||
|
return LayerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PresentationSegment == EAgrarianStartupPresentationSegment::Story)
|
||||||
|
{
|
||||||
|
DrawStory(OutDrawElements, LayerId, AllottedGeometry);
|
||||||
|
return LayerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PresentationSegment == EAgrarianStartupPresentationSegment::Credits)
|
||||||
{
|
{
|
||||||
FSlateDrawElement::MakeBox(
|
FSlateDrawElement::MakeBox(
|
||||||
OutDrawElements,
|
OutDrawElements,
|
||||||
@@ -120,6 +197,104 @@ int32 UAgrarianDemoNoticeWidget::NativePaint(
|
|||||||
return LayerId;
|
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(
|
void UAgrarianDemoNoticeWidget::DrawCenteredText(
|
||||||
FSlateWindowElementList& OutDrawElements,
|
FSlateWindowElementList& OutDrawElements,
|
||||||
int32& LayerId,
|
int32& LayerId,
|
||||||
@@ -176,7 +351,7 @@ void UAgrarianDemoNoticeWidget::DrawCinematicCredits(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const FVector2D Size = AllottedGeometry.GetLocalSize();
|
const FVector2D Size = AllottedGeometry.GetLocalSize();
|
||||||
const float Elapsed = World->GetTimeSeconds() - CreditsStartTimeSeconds;
|
const float Elapsed = GetSegmentElapsedSeconds();
|
||||||
const float IntroDelay = 0.55f;
|
const float IntroDelay = 0.55f;
|
||||||
const float SlamSeconds = 0.28f;
|
const float SlamSeconds = 0.28f;
|
||||||
const float ExitSeconds = 0.48f;
|
const float ExitSeconds = 0.48f;
|
||||||
|
|||||||
@@ -6,6 +6,15 @@
|
|||||||
#include "Blueprint/UserWidget.h"
|
#include "Blueprint/UserWidget.h"
|
||||||
#include "AgrarianDemoNoticeWidget.generated.h"
|
#include "AgrarianDemoNoticeWidget.generated.h"
|
||||||
|
|
||||||
|
UENUM(BlueprintType)
|
||||||
|
enum class EAgrarianStartupPresentationSegment : uint8
|
||||||
|
{
|
||||||
|
Splash,
|
||||||
|
Story,
|
||||||
|
Credits,
|
||||||
|
DemoNotice
|
||||||
|
};
|
||||||
|
|
||||||
UCLASS()
|
UCLASS()
|
||||||
class AGRARIANGAME_API UAgrarianDemoNoticeWidget : public UUserWidget
|
class AGRARIANGAME_API UAgrarianDemoNoticeWidget : public UUserWidget
|
||||||
{
|
{
|
||||||
@@ -13,6 +22,10 @@ class AGRARIANGAME_API UAgrarianDemoNoticeWidget : public UUserWidget
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
virtual void NativeConstruct() override;
|
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")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Demo")
|
||||||
FText Motto = FText::FromString(TEXT("What survives after you are gone?"));
|
FText Motto = FText::FromString(TEXT("What survives after you are gone?"));
|
||||||
@@ -37,7 +50,21 @@ protected:
|
|||||||
bool bParentEnabled) const override;
|
bool bParentEnabled) const override;
|
||||||
|
|
||||||
private:
|
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(
|
void DrawCenteredText(
|
||||||
FSlateWindowElementList& OutDrawElements,
|
FSlateWindowElementList& OutDrawElements,
|
||||||
|
|||||||
@@ -9,7 +9,12 @@
|
|||||||
|
|
||||||
namespace
|
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)
|
if (!Component)
|
||||||
{
|
{
|
||||||
@@ -19,10 +24,10 @@ void ConfigureFoliageComponent(UHierarchicalInstancedStaticMeshComponent* Compon
|
|||||||
Component->SetMobility(EComponentMobility::Static);
|
Component->SetMobility(EComponentMobility::Static);
|
||||||
Component->SetCollisionProfileName(CollisionProfileName);
|
Component->SetCollisionProfileName(CollisionProfileName);
|
||||||
Component->SetGenerateOverlapEvents(false);
|
Component->SetGenerateOverlapEvents(false);
|
||||||
Component->bCastDynamicShadow = true;
|
Component->bCastDynamicShadow = bCastShadows;
|
||||||
Component->bCastStaticShadow = true;
|
Component->bCastStaticShadow = bCastShadows;
|
||||||
Component->InstanceStartCullDistance = 120000;
|
Component->InstanceStartCullDistance = StartCullDistance;
|
||||||
Component->InstanceEndCullDistance = 180000;
|
Component->InstanceEndCullDistance = EndCullDistance;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,15 +41,15 @@ AAgrarianFoliagePatch::AAgrarianFoliagePatch()
|
|||||||
|
|
||||||
TreeInstances = CreateDefaultSubobject<UHierarchicalInstancedStaticMeshComponent>(TEXT("TreeInstances"));
|
TreeInstances = CreateDefaultSubobject<UHierarchicalInstancedStaticMeshComponent>(TEXT("TreeInstances"));
|
||||||
TreeInstances->SetupAttachment(SceneRoot);
|
TreeInstances->SetupAttachment(SceneRoot);
|
||||||
ConfigureFoliageComponent(TreeInstances, TEXT("BlockAll"));
|
ConfigureFoliageComponent(TreeInstances, TEXT("BlockAll"), 65000, 95000, true);
|
||||||
|
|
||||||
ShrubInstances = CreateDefaultSubobject<UHierarchicalInstancedStaticMeshComponent>(TEXT("ShrubInstances"));
|
ShrubInstances = CreateDefaultSubobject<UHierarchicalInstancedStaticMeshComponent>(TEXT("ShrubInstances"));
|
||||||
ShrubInstances->SetupAttachment(SceneRoot);
|
ShrubInstances->SetupAttachment(SceneRoot);
|
||||||
ConfigureFoliageComponent(ShrubInstances, TEXT("NoCollision"));
|
ConfigureFoliageComponent(ShrubInstances, TEXT("NoCollision"), 28000, 52000, true);
|
||||||
|
|
||||||
GrassInstances = CreateDefaultSubobject<UHierarchicalInstancedStaticMeshComponent>(TEXT("GrassInstances"));
|
GrassInstances = CreateDefaultSubobject<UHierarchicalInstancedStaticMeshComponent>(TEXT("GrassInstances"));
|
||||||
GrassInstances->SetupAttachment(SceneRoot);
|
GrassInstances->SetupAttachment(SceneRoot);
|
||||||
ConfigureFoliageComponent(GrassInstances, TEXT("NoCollision"));
|
ConfigureFoliageComponent(GrassInstances, TEXT("NoCollision"), 9000, 22000, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void AAgrarianFoliagePatch::ClearFoliage()
|
void AAgrarianFoliagePatch::ClearFoliage()
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include "AgrarianCampfire.h"
|
#include "AgrarianCampfire.h"
|
||||||
#include "AgrarianCraftingComponent.h"
|
#include "AgrarianCraftingComponent.h"
|
||||||
#include "AgrarianDebugHUD.h"
|
#include "AgrarianDebugHUD.h"
|
||||||
|
#include "AgrarianDemoNoticeWidget.h"
|
||||||
#include "AgrarianGameCharacter.h"
|
#include "AgrarianGameCharacter.h"
|
||||||
#include "AgrarianInventoryComponent.h"
|
#include "AgrarianInventoryComponent.h"
|
||||||
#include "AgrarianItemPickup.h"
|
#include "AgrarianItemPickup.h"
|
||||||
@@ -102,22 +103,7 @@ void AAgrarianGamePlayerController::BeginPlay()
|
|||||||
|
|
||||||
if (IsLocalPlayerController())
|
if (IsLocalPlayerController())
|
||||||
{
|
{
|
||||||
if (MvpFrontendStartupDelaySeconds > 0.0f)
|
StartStartupPresentation();
|
||||||
{
|
|
||||||
SetMvpFrontendPresentationActive(true);
|
|
||||||
SetInputMode(FInputModeUIOnly());
|
|
||||||
bShowMouseCursor = false;
|
|
||||||
GetWorldTimerManager().SetTimer(
|
|
||||||
MvpFrontendStartupTimerHandle,
|
|
||||||
this,
|
|
||||||
&AAgrarianGamePlayerController::ShowMvpFrontend,
|
|
||||||
MvpFrontendStartupDelaySeconds,
|
|
||||||
false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ShowMvpFrontend();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// only spawn touch controls on local player controllers
|
// only spawn touch controls on local player controllers
|
||||||
@@ -154,6 +140,10 @@ void AAgrarianGamePlayerController::AcknowledgePossession(APawn* P)
|
|||||||
{
|
{
|
||||||
SetMvpFrontendPresentationActive(true);
|
SetMvpFrontendPresentationActive(true);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
RestoreGameplayControlState();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void AAgrarianGamePlayerController::SetupInputComponent()
|
void AAgrarianGamePlayerController::SetupInputComponent()
|
||||||
@@ -168,23 +158,7 @@ void AAgrarianGamePlayerController::SetupInputComponent()
|
|||||||
// only add IMCs for local player controllers
|
// only add IMCs for local player controllers
|
||||||
if (IsLocalPlayerController())
|
if (IsLocalPlayerController())
|
||||||
{
|
{
|
||||||
// Add Input Mapping Contexts
|
ApplyDefaultInputMappingContexts();
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,8 +198,80 @@ void AAgrarianGamePlayerController::ShowMvpFrontend()
|
|||||||
SetMvpFrontendPresentationActive(true);
|
SetMvpFrontendPresentationActive(true);
|
||||||
SetInputMode(FInputModeUIOnly().SetWidgetToFocus(MvpFrontendWidget->TakeWidget()).SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock));
|
SetInputMode(FInputModeUIOnly().SetWidgetToFocus(MvpFrontendWidget->TakeWidget()).SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock));
|
||||||
bShowMouseCursor = true;
|
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()
|
void AAgrarianGamePlayerController::ShowMvpPauseMenu()
|
||||||
@@ -262,8 +308,6 @@ void AAgrarianGamePlayerController::ShowMvpPauseMenu()
|
|||||||
SetMvpFrontendPresentationActive(true);
|
SetMvpFrontendPresentationActive(true);
|
||||||
SetInputMode(FInputModeUIOnly().SetWidgetToFocus(MvpFrontendWidget->TakeWidget()).SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock));
|
SetInputMode(FInputModeUIOnly().SetWidgetToFocus(MvpFrontendWidget->TakeWidget()).SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock));
|
||||||
bShowMouseCursor = true;
|
bShowMouseCursor = true;
|
||||||
SetIgnoreMoveInput(true);
|
|
||||||
SetIgnoreLookInput(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void AAgrarianGamePlayerController::HandleMvpConfirmInput()
|
void AAgrarianGamePlayerController::HandleMvpConfirmInput()
|
||||||
@@ -296,10 +340,21 @@ void AAgrarianGamePlayerController::HandleMvpEscapeInput()
|
|||||||
|
|
||||||
void AAgrarianGamePlayerController::SetMvpFrontendPresentationActive(bool bNewActive)
|
void AAgrarianGamePlayerController::SetMvpFrontendPresentationActive(bool bNewActive)
|
||||||
{
|
{
|
||||||
|
const bool bWasActive = bMvpFrontendPresentationActive;
|
||||||
bMvpFrontendPresentationActive = bNewActive;
|
bMvpFrontendPresentationActive = bNewActive;
|
||||||
|
|
||||||
SetIgnoreMoveInput(bNewActive);
|
if (bNewActive)
|
||||||
SetIgnoreLookInput(bNewActive);
|
{
|
||||||
|
if (!bWasActive)
|
||||||
|
{
|
||||||
|
SetIgnoreMoveInput(true);
|
||||||
|
SetIgnoreLookInput(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
RestoreGameplayControlState();
|
||||||
|
}
|
||||||
|
|
||||||
APawn* ControlledPawn = GetPawn();
|
APawn* ControlledPawn = GetPawn();
|
||||||
if (ControlledPawn)
|
if (ControlledPawn)
|
||||||
@@ -341,6 +396,80 @@ void AAgrarianGamePlayerController::SetMvpFrontendPresentationActive(bool bNewAc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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()
|
void AAgrarianGamePlayerController::CreateOrUpdateMvpFrontendCamera()
|
||||||
{
|
{
|
||||||
UWorld* World = GetWorld();
|
UWorld* World = GetWorld();
|
||||||
@@ -709,8 +838,7 @@ void AAgrarianGamePlayerController::AgrarianCompleteFrontend()
|
|||||||
{
|
{
|
||||||
ApplyMvpCharacterProxyToPawn();
|
ApplyMvpCharacterProxyToPawn();
|
||||||
SetMvpFrontendPresentationActive(false);
|
SetMvpFrontendPresentationActive(false);
|
||||||
SetInputMode(FInputModeGameOnly());
|
RestoreGameplayControlState();
|
||||||
bShowMouseCursor = false;
|
|
||||||
|
|
||||||
if (const APawn* ControlledPawn = GetPawn())
|
if (const APawn* ControlledPawn = GetPawn())
|
||||||
{
|
{
|
||||||
@@ -718,6 +846,29 @@ void AAgrarianGamePlayerController::AgrarianCompleteFrontend()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
void AAgrarianGamePlayerController::AgrarianShowMvpScreen(FName ScreenName)
|
||||||
{
|
{
|
||||||
if (!MvpFrontendWidget)
|
if (!MvpFrontendWidget)
|
||||||
|
|||||||
@@ -3,12 +3,14 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "CoreMinimal.h"
|
#include "CoreMinimal.h"
|
||||||
|
#include "AgrarianDemoNoticeWidget.h"
|
||||||
#include "GameFramework/PlayerController.h"
|
#include "GameFramework/PlayerController.h"
|
||||||
#include "AgrarianGamePlayerController.generated.h"
|
#include "AgrarianGamePlayerController.generated.h"
|
||||||
|
|
||||||
class UInputMappingContext;
|
class UInputMappingContext;
|
||||||
class UUserWidget;
|
class UUserWidget;
|
||||||
class UAgrarianMvpFrontendWidget;
|
class UAgrarianMvpFrontendWidget;
|
||||||
|
class UAgrarianDemoNoticeWidget;
|
||||||
class AAgrarianShelterActor;
|
class AAgrarianShelterActor;
|
||||||
class ACameraActor;
|
class ACameraActor;
|
||||||
|
|
||||||
@@ -46,9 +48,19 @@ protected:
|
|||||||
TObjectPtr<UAgrarianMvpFrontendWidget> MvpFrontendWidget;
|
TObjectPtr<UAgrarianMvpFrontendWidget> MvpFrontendWidget;
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, Category = "Agrarian|MVP UI", meta = (ClampMin = "0.0"))
|
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 MvpFrontendStartupTimerHandle;
|
||||||
|
FTimerHandle StartupPresentationTimerHandle;
|
||||||
|
|
||||||
/** If true, the player will use UMG touch controls even if not playing on mobile platforms */
|
/** If true, the player will use UMG touch controls even if not playing on mobile platforms */
|
||||||
UPROPERTY(EditAnywhere, Config, Category = "Input|Touch Controls")
|
UPROPERTY(EditAnywhere, Config, Category = "Input|Touch Controls")
|
||||||
@@ -63,12 +75,18 @@ protected:
|
|||||||
/** Returns true if the player should use UMG touch controls */
|
/** Returns true if the player should use UMG touch controls */
|
||||||
bool ShouldUseTouchControls() const;
|
bool ShouldUseTouchControls() const;
|
||||||
void ShowMvpFrontend();
|
void ShowMvpFrontend();
|
||||||
|
void StartStartupPresentation();
|
||||||
|
void AdvanceStartupPresentation();
|
||||||
|
void FinishStartupPresentation();
|
||||||
|
void ShowStartupPresentationSegment(EAgrarianStartupPresentationSegment Segment, float DurationSeconds);
|
||||||
void ShowMvpPauseMenu();
|
void ShowMvpPauseMenu();
|
||||||
void HandleMvpConfirmInput();
|
void HandleMvpConfirmInput();
|
||||||
void HandleMvpBackInput();
|
void HandleMvpBackInput();
|
||||||
void HandleMvpEscapeInput();
|
void HandleMvpEscapeInput();
|
||||||
void ApplyMvpCharacterProxyToPawn();
|
void ApplyMvpCharacterProxyToPawn();
|
||||||
void SetMvpFrontendPresentationActive(bool bNewActive);
|
void SetMvpFrontendPresentationActive(bool bNewActive);
|
||||||
|
void ApplyDefaultInputMappingContexts();
|
||||||
|
void RestoreGameplayControlState();
|
||||||
void CreateOrUpdateMvpFrontendCamera();
|
void CreateOrUpdateMvpFrontendCamera();
|
||||||
void CacheAndApplyMvpHudSuppression(bool bSuppress);
|
void CacheAndApplyMvpHudSuppression(bool bSuppress);
|
||||||
virtual void AcknowledgePossession(APawn* P) override;
|
virtual void AcknowledgePossession(APawn* P) override;
|
||||||
@@ -78,7 +96,11 @@ protected:
|
|||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
TObjectPtr<ACameraActor> MvpFrontendCameraActor;
|
TObjectPtr<ACameraActor> MvpFrontendCameraActor;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UAgrarianDemoNoticeWidget> StartupPresentationWidget;
|
||||||
|
|
||||||
bool bMvpFrontendPresentationActive = false;
|
bool bMvpFrontendPresentationActive = false;
|
||||||
|
EAgrarianStartupPresentationSegment StartupPresentationSegment = EAgrarianStartupPresentationSegment::Splash;
|
||||||
bool bCachedMvpHudState = false;
|
bool bCachedMvpHudState = false;
|
||||||
bool bCachedShowDebugHUD = true;
|
bool bCachedShowDebugHUD = true;
|
||||||
bool bCachedShowMvpHudFrame = true;
|
bool bCachedShowMvpHudFrame = true;
|
||||||
@@ -150,6 +172,12 @@ public:
|
|||||||
UFUNCTION(Exec)
|
UFUNCTION(Exec)
|
||||||
void AgrarianCompleteFrontend();
|
void AgrarianCompleteFrontend();
|
||||||
|
|
||||||
|
UFUNCTION(Exec)
|
||||||
|
void AgrarianRepairGameplayInput();
|
||||||
|
|
||||||
|
UFUNCTION(Exec)
|
||||||
|
void AgrarianSkipStartupPresentation();
|
||||||
|
|
||||||
UFUNCTION(Exec)
|
UFUNCTION(Exec)
|
||||||
void AgrarianShowMvpScreen(FName ScreenName);
|
void AgrarianShowMvpScreen(FName ScreenName);
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include "AgrarianMvpFrontendWidget.h"
|
#include "AgrarianMvpFrontendWidget.h"
|
||||||
|
|
||||||
|
#include "AgrarianGamePlayerController.h"
|
||||||
#include "Blueprint/WidgetTree.h"
|
#include "Blueprint/WidgetTree.h"
|
||||||
#include "Components/Border.h"
|
#include "Components/Border.h"
|
||||||
#include "Components/Button.h"
|
#include "Components/Button.h"
|
||||||
@@ -86,6 +87,33 @@ FReply UAgrarianMvpFrontendWidget::NativeOnKeyDown(const FGeometry& InGeometry,
|
|||||||
SaveAndQuit();
|
SaveAndQuit();
|
||||||
return FReply::Handled();
|
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)
|
else if (ActiveScreen == EAgrarianMvpFrontendScreen::SavingAndQuit)
|
||||||
{
|
{
|
||||||
@@ -146,6 +174,17 @@ void UAgrarianMvpFrontendWidget::SaveAndQuit()
|
|||||||
ExecuteSaveAndQuit();
|
ExecuteSaveAndQuit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void UAgrarianMvpFrontendWidget::SaveGame()
|
||||||
|
{
|
||||||
|
ExecuteSaveGame();
|
||||||
|
SetActiveScreen(EAgrarianMvpFrontendScreen::GameSaved);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UAgrarianMvpFrontendWidget::QuitWithoutSaving()
|
||||||
|
{
|
||||||
|
ExecuteQuitWithoutSaving();
|
||||||
|
}
|
||||||
|
|
||||||
void UAgrarianMvpFrontendWidget::ExecuteSaveAndQuit()
|
void UAgrarianMvpFrontendWidget::ExecuteSaveAndQuit()
|
||||||
{
|
{
|
||||||
if (APlayerController* PlayerController = GetOwningPlayer())
|
if (APlayerController* PlayerController = GetOwningPlayer())
|
||||||
@@ -155,6 +194,22 @@ 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()
|
void UAgrarianMvpFrontendWidget::ContinueFromActiveScreen()
|
||||||
{
|
{
|
||||||
if (ActiveScreen == EAgrarianMvpFrontendScreen::CharacterSelection)
|
if (ActiveScreen == EAgrarianMvpFrontendScreen::CharacterSelection)
|
||||||
@@ -180,6 +235,12 @@ void UAgrarianMvpFrontendWidget::ContinueFromActiveScreen()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ActiveScreen == EAgrarianMvpFrontendScreen::Settings || ActiveScreen == EAgrarianMvpFrontendScreen::GameSaved)
|
||||||
|
{
|
||||||
|
SetActiveScreen(EAgrarianMvpFrontendScreen::MainMenu);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (ActiveScreen == EAgrarianMvpFrontendScreen::MainMenu)
|
if (ActiveScreen == EAgrarianMvpFrontendScreen::MainMenu)
|
||||||
{
|
{
|
||||||
CompleteFrontendFlow();
|
CompleteFrontendFlow();
|
||||||
@@ -199,24 +260,35 @@ void UAgrarianMvpFrontendWidget::ReturnFromActiveScreen()
|
|||||||
{
|
{
|
||||||
CompleteFrontendFlow();
|
CompleteFrontendFlow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ActiveScreen == EAgrarianMvpFrontendScreen::Settings || ActiveScreen == EAgrarianMvpFrontendScreen::GameSaved)
|
||||||
|
{
|
||||||
|
SetActiveScreen(EAgrarianMvpFrontendScreen::MainMenu);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void UAgrarianMvpFrontendWidget::CompleteFrontendFlow()
|
void UAgrarianMvpFrontendWidget::CompleteFrontendFlow()
|
||||||
{
|
{
|
||||||
if (APlayerController* PlayerController = GetOwningPlayer())
|
if (APlayerController* PlayerController = GetOwningPlayer())
|
||||||
{
|
{
|
||||||
if (ActiveScreen == EAgrarianMvpFrontendScreen::CharacterSelection || ActiveScreen == EAgrarianMvpFrontendScreen::Loading)
|
if (AAgrarianGamePlayerController* AgrarianPlayerController = Cast<AAgrarianGamePlayerController>(PlayerController))
|
||||||
{
|
{
|
||||||
PlayerController->ConsoleCommand(SelectedCharacterArchetype == EAgrarianMvpCharacterArchetype::YoungAdultFemale
|
if (ActiveScreen == EAgrarianMvpFrontendScreen::CharacterSelection || ActiveScreen == EAgrarianMvpFrontendScreen::Loading)
|
||||||
? TEXT("AgrarianSelectCharacter female")
|
{
|
||||||
: TEXT("AgrarianSelectCharacter male"));
|
AgrarianPlayerController->AgrarianSelectCharacter(SelectedCharacterArchetype == EAgrarianMvpCharacterArchetype::YoungAdultFemale
|
||||||
}
|
? TEXT("female")
|
||||||
|
: TEXT("male"));
|
||||||
|
}
|
||||||
|
|
||||||
PlayerController->ConsoleCommand(TEXT("AgrarianCompleteFrontend"));
|
AgrarianPlayerController->AgrarianCompleteFrontend();
|
||||||
PlayerController->SetInputMode(FInputModeGameOnly());
|
}
|
||||||
PlayerController->bShowMouseCursor = false;
|
else
|
||||||
PlayerController->SetIgnoreMoveInput(false);
|
{
|
||||||
PlayerController->SetIgnoreLookInput(false);
|
PlayerController->SetInputMode(FInputModeGameOnly());
|
||||||
|
PlayerController->bShowMouseCursor = false;
|
||||||
|
PlayerController->ResetIgnoreMoveInput();
|
||||||
|
PlayerController->ResetIgnoreLookInput();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RemoveFromParent();
|
RemoveFromParent();
|
||||||
@@ -267,13 +339,23 @@ void UAgrarianMvpFrontendWidget::RebuildFrontendTree()
|
|||||||
{
|
{
|
||||||
AddText(Panel, FText::FromString(TEXT("Pause Menu")), FMath::RoundToInt(20.0f * Scale), true, AccentColor, 6.0f * Scale);
|
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, 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 = AddButton(Panel, FText::FromString(TEXT("Resume")), ButtonColor, ButtonHoverColor, 16.0f * Scale);
|
||||||
PrimaryFocusButton->OnClicked.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandlePrimaryActionClicked);
|
PrimaryFocusButton->OnClicked.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandlePrimaryActionClicked);
|
||||||
|
|
||||||
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);
|
QuitButton->OnClicked.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandleSaveAndQuitClicked);
|
||||||
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,6 +465,26 @@ void UAgrarianMvpFrontendWidget::RebuildFrontendTree()
|
|||||||
return;
|
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("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("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::FromString(TEXT("Loading terrain, weather, survival state, and server session data.")), FMath::RoundToInt(18.0f * Scale), false, MutedTextColor, 70.0f * Scale);
|
||||||
@@ -478,6 +580,30 @@ void UAgrarianMvpFrontendWidget::HandleSaveAndQuitClicked()
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void UAgrarianMvpFrontendWidget::HandleSaveGameClicked()
|
||||||
|
{
|
||||||
|
DeferFrontendAction([this]()
|
||||||
|
{
|
||||||
|
SaveGame();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void UAgrarianMvpFrontendWidget::HandleSettingsClicked()
|
||||||
|
{
|
||||||
|
DeferFrontendAction([this]()
|
||||||
|
{
|
||||||
|
SetActiveScreen(EAgrarianMvpFrontendScreen::Settings);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void UAgrarianMvpFrontendWidget::HandleQuitWithoutSavingClicked()
|
||||||
|
{
|
||||||
|
DeferFrontendAction([this]()
|
||||||
|
{
|
||||||
|
QuitWithoutSaving();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void UAgrarianMvpFrontendWidget::HandleMaleCharacterClicked()
|
void UAgrarianMvpFrontendWidget::HandleMaleCharacterClicked()
|
||||||
{
|
{
|
||||||
DeferFrontendAction([this]()
|
DeferFrontendAction([this]()
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ enum class EAgrarianMvpFrontendScreen : uint8
|
|||||||
CharacterSelection,
|
CharacterSelection,
|
||||||
JoinServer,
|
JoinServer,
|
||||||
Loading,
|
Loading,
|
||||||
|
Settings,
|
||||||
|
GameSaved,
|
||||||
SavingAndQuit
|
SavingAndQuit
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -75,9 +77,15 @@ public:
|
|||||||
UFUNCTION(BlueprintCallable, Category = "Agrarian|MVP UI")
|
UFUNCTION(BlueprintCallable, Category = "Agrarian|MVP UI")
|
||||||
void BackFromActiveScreen();
|
void BackFromActiveScreen();
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Agrarian|MVP UI")
|
||||||
|
void SaveGame();
|
||||||
|
|
||||||
UFUNCTION(BlueprintCallable, Category = "Agrarian|MVP UI")
|
UFUNCTION(BlueprintCallable, Category = "Agrarian|MVP UI")
|
||||||
void SaveAndQuit();
|
void SaveAndQuit();
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Agrarian|MVP UI")
|
||||||
|
void QuitWithoutSaving();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual void NativeConstruct() override;
|
virtual void NativeConstruct() override;
|
||||||
|
|
||||||
@@ -96,6 +104,8 @@ private:
|
|||||||
|
|
||||||
UFUNCTION()
|
UFUNCTION()
|
||||||
void ExecuteSaveAndQuit();
|
void ExecuteSaveAndQuit();
|
||||||
|
void ExecuteSaveGame();
|
||||||
|
void ExecuteQuitWithoutSaving();
|
||||||
|
|
||||||
UFUNCTION()
|
UFUNCTION()
|
||||||
void HandlePrimaryActionClicked();
|
void HandlePrimaryActionClicked();
|
||||||
@@ -106,6 +116,15 @@ private:
|
|||||||
UFUNCTION()
|
UFUNCTION()
|
||||||
void HandleSaveAndQuitClicked();
|
void HandleSaveAndQuitClicked();
|
||||||
|
|
||||||
|
UFUNCTION()
|
||||||
|
void HandleSaveGameClicked();
|
||||||
|
|
||||||
|
UFUNCTION()
|
||||||
|
void HandleSettingsClicked();
|
||||||
|
|
||||||
|
UFUNCTION()
|
||||||
|
void HandleQuitWithoutSavingClicked();
|
||||||
|
|
||||||
UFUNCTION()
|
UFUNCTION()
|
||||||
void HandleMaleCharacterClicked();
|
void HandleMaleCharacterClicked();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user