From 8ee1f83b16d721f911faeb2b49101148e55f49bc Mon Sep 17 00:00:00 2001 From: nathan Date: Fri, 15 May 2026 21:41:37 -0700 Subject: [PATCH] Complete early roadmap foundation and calendar helpers --- .github/PULL_REQUEST_TEMPLATE.md | 20 + AGRARIAN_DEVELOPMENT_ROADMAP.md | 374 +++++++++++++----- Config/DefaultEngine.ini | 2 +- Config/DefaultGame.ini | 1 - .../Characters/BP_AgrarianGameMode.uasset | 3 + .../BP_AgrarianPlayerCharacter.uasset | 3 + .../BP_AgrarianPlayerController.uasset | 3 + Content/Input/Actions/IA_Crouch.uasset | Bin 1160 -> 129 bytes Content/Input/Actions/IA_Prone.uasset | Bin 1155 -> 129 bytes Content/Input/Actions/IA_Sprint.uasset | Bin 1160 -> 129 bytes Content/Input/Actions/IA_ToggleCamera.uasset | Bin 1190 -> 129 bytes Content/Input/IMC_Default.uasset | Bin 11831 -> 130 bytes .../Blueprints/BP_ThirdPersonCharacter.uasset | Bin 52121 -> 130 bytes .../Blueprints/BP_ThirdPersonGameMode.uasset | Bin 21794 -> 130 bytes Data/Tiles/tile_growing_zone_metadata.json | 21 + Data/Tiles/tile_registry.schema.json | 80 ++++ Data/Tiles/tile_registry.sql | 24 ++ Data/Tiles/tile_solar_metadata.json | 43 ++ Docs/BackupExpectations.md | 4 +- Docs/BasicAnimationBlueprint.md | 22 ++ Docs/BranchingConventions.md | 10 + Docs/CoreDesignDocument.md | 7 +- Docs/GameplayAbilitySystemDecision.md | 37 ++ Docs/Ops/AgrarianProjectBackupRunbook.md | 4 +- Docs/Ops/AgrarianVmBackupRunbook.md | 3 + Docs/Ops/BackupRestoreTestLog.md | 61 +++ Docs/Ops/BuildLogRetentionPolicy.md | 78 ++++ ...evelopmentInfrastructureRecoveryRunbook.md | 179 +++++++++ .../GitHubBranchProtectionAndReviewRules.md | 86 ++++ Docs/Ops/WindowsBuilderGpuRemoteAccess.md | 62 +++ Docs/Ops/WindowsBuilderNetworkRdpStability.md | 84 ++++ Docs/PersistenceDesignDocument.md | 28 ++ Docs/TechnicalDesignDocument.md | 27 +- Docs/TemplateVariantDecision.md | 26 +- Docs/Terrain/TileRegistrySchema.md | 43 ++ Scripts/PackageWindowsDevelopment.bat | 2 +- .../generate_tile_growing_zone_metadata.py | 73 ++++ Scripts/generate_tile_solar_metadata.py | 130 ++++++ Scripts/prune_build_logs.sh | 101 +++++ Scripts/setup_agrarian_player_blueprints.py | 67 ++++ Scripts/setup_camera_toggle_input.py | 2 +- Scripts/setup_interact_input.py | 2 +- Scripts/setup_interaction_prompt.py | 2 +- Scripts/setup_movement_baseline.py | 2 +- Scripts/setup_sprint_input.py | 2 +- Scripts/setup_stance_input.py | 2 +- Scripts/setup_test_map_placements.py | 2 +- Scripts/verify_agrarian_calendar_helpers.py | 86 ++++ Scripts/verify_agrarian_player_blueprints.py | 66 ++++ Scripts/verify_basic_animation_blueprint.py | 72 ++++ Scripts/verify_camera_toggle_input.py | 4 +- Scripts/verify_care_history_fields.py | 59 +++ Scripts/verify_critical_stats_hud.py | 50 +++ Scripts/verify_exhaustion_stat.py | 65 +++ Scripts/verify_interact_input.py | 4 +- Scripts/verify_interaction_prompt.py | 4 +- Scripts/verify_movement_baseline.py | 2 +- Scripts/verify_playable_loop_smoke.py | 4 +- Scripts/verify_player_core_replication.py | 55 +++ Scripts/verify_player_debug_overlay.py | 49 +++ Scripts/verify_sickness_placeholder.py | 61 +++ Scripts/verify_sprint_input.py | 4 +- Scripts/verify_stance_input.py | 6 +- Scripts/verify_stat_save_load_support.py | 70 ++++ Scripts/verify_test_map_placements.py | 2 +- Scripts/verify_tile_solar_time.py | 84 ++++ Scripts/verify_wildlife_loop.py | 4 +- Source/AgrarianGame/AgrarianDebugHUD.cpp | 119 +++++- Source/AgrarianGame/AgrarianDebugHUD.h | 9 + Source/AgrarianGame/AgrarianGameCharacter.cpp | 39 +- Source/AgrarianGame/AgrarianGameCharacter.h | 32 +- .../AgrarianGamePlayerController.cpp | 11 +- Source/AgrarianGame/AgrarianGameState.cpp | 326 ++++++++++++++- Source/AgrarianGame/AgrarianGameState.h | 77 ++++ .../AgrarianPersistenceSubsystem.cpp | 125 ++++++ .../AgrarianPersistenceSubsystem.h | 9 + Source/AgrarianGame/AgrarianSaveGame.h | 3 + .../AgrarianSurvivalComponent.cpp | 109 ++++- .../AgrarianGame/AgrarianSurvivalComponent.h | 37 ++ Source/AgrarianGame/AgrarianTypes.h | 142 +++++++ 80 files changed, 3354 insertions(+), 157 deletions(-) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 Content/Agrarian/Blueprints/Characters/BP_AgrarianGameMode.uasset create mode 100644 Content/Agrarian/Blueprints/Characters/BP_AgrarianPlayerCharacter.uasset create mode 100644 Content/Agrarian/Blueprints/Characters/BP_AgrarianPlayerController.uasset create mode 100644 Data/Tiles/tile_growing_zone_metadata.json create mode 100644 Data/Tiles/tile_solar_metadata.json create mode 100644 Docs/BasicAnimationBlueprint.md create mode 100644 Docs/GameplayAbilitySystemDecision.md create mode 100644 Docs/Ops/BackupRestoreTestLog.md create mode 100644 Docs/Ops/BuildLogRetentionPolicy.md create mode 100644 Docs/Ops/DevelopmentInfrastructureRecoveryRunbook.md create mode 100644 Docs/Ops/GitHubBranchProtectionAndReviewRules.md create mode 100644 Docs/Ops/WindowsBuilderGpuRemoteAccess.md create mode 100644 Docs/Ops/WindowsBuilderNetworkRdpStability.md create mode 100644 Scripts/generate_tile_growing_zone_metadata.py create mode 100644 Scripts/generate_tile_solar_metadata.py create mode 100644 Scripts/prune_build_logs.sh create mode 100644 Scripts/setup_agrarian_player_blueprints.py create mode 100644 Scripts/verify_agrarian_calendar_helpers.py create mode 100644 Scripts/verify_agrarian_player_blueprints.py create mode 100644 Scripts/verify_basic_animation_blueprint.py create mode 100644 Scripts/verify_care_history_fields.py create mode 100644 Scripts/verify_critical_stats_hud.py create mode 100644 Scripts/verify_exhaustion_stat.py create mode 100644 Scripts/verify_player_core_replication.py create mode 100644 Scripts/verify_player_debug_overlay.py create mode 100644 Scripts/verify_sickness_placeholder.py create mode 100644 Scripts/verify_stat_save_load_support.py create mode 100644 Scripts/verify_tile_solar_time.py diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..63d8019 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,20 @@ +# Summary + +Roadmap item: + +What changed: + +## Validation + +- [ ] `git diff --check` +- [ ] Build/test command run, if applicable: +- [ ] Unreal editor or packaged build checked, if applicable: +- [ ] Backup/deploy dry-run checked, if applicable: + +## Review Notes + +- [ ] No secrets or credentials committed. +- [ ] No generated builds, raw terrain datasets, DerivedDataCache, Intermediate, Saved, Binaries, or large source-art archives committed. +- [ ] Risky Unreal binary asset changes are described. +- [ ] Follow-up roadmap items were added if new work was discovered. + diff --git a/AGRARIAN_DEVELOPMENT_ROADMAP.md b/AGRARIAN_DEVELOPMENT_ROADMAP.md index 4598177..7515de0 100644 --- a/AGRARIAN_DEVELOPMENT_ROADMAP.md +++ b/AGRARIAN_DEVELOPMENT_ROADMAP.md @@ -29,6 +29,10 @@ Core commitments: - [ ] Treat terrain, bathymetry, biomes, resources, rivers, and mountains as data-driven long-term infrastructure, not one-off maps. - [ ] Keep base world time grounded; the world should progress without feeling artificially sped up. - [ ] Make player skill, tools, infrastructure, cooperation, and knowledge improve efficiency, yield, reliability, quality, and capacity rather than breaking natural biological time. +- [ ] Treat learning as a core play loop: knowledge unlocks understanding, practice builds competence, and infrastructure makes advanced work possible. +- [ ] Use real-world learning checks carefully; they should teach and confirm understanding without turning the game into mandatory homework. +- [ ] 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. ## Time And Progression Philosophy @@ -123,14 +127,14 @@ Use these markers as the project progresses: Roadmap order is now: 1. Current milestone status and decision log. -2. Phase 0 foundation and guardrails. -3. Phase 1 foundational survival MVP. -4. Later gameplay, civilization, technical, release, and community tracks. +2. Version 0.01 foundation and guardrails. +3. Version 0.1 foundational survival MVP. +4. Later version tracks for gameplay, civilization, technical, release, and community work. 5. Calendar, MVP definition of done, and earliest next actions. -Phase subsection numbers such as `0.1`, `0.2`, and `0.7` are workstream -sections, not release versions. Release versions are tracked separately as -milestones such as `0.01`, `0.1`, `0.6`, and `1.0`. +Roadmap headings now use release-style milestone numbers. Subsections use lettered +labels such as `0.1.A`, `0.1.B`, and `0.1.C` so they do not look like +separate versions. ## Active Milestone - Version 0.01 Foundation Baseline @@ -182,11 +186,11 @@ Remaining version 0.01 cleanup before moving deeper into new gameplay: - [x] Launch near-term MVP map-tile serving cloud VM. - [x] Define MVP day/night length, survival pressure target, success loop, failure conditions, and closed-test readiness criteria. -# Phase 0 - Project Foundation And Guardrails +# Version 0.01 - Project Foundation And Guardrails Goal: Prepare the project so all future development is controlled, recoverable, documented, and aligned with the long-term vision. -## 0.1 Repository And Source Control +## 0.01.A Repository And Source Control - [x] Decide where the Unreal project repository will live. - [x] Create or confirm GitHub repository for the Unreal project. @@ -202,11 +206,11 @@ Goal: Prepare the project so all future development is controlled, recoverable, - [x] Define backup expectations for NAS and repo. - [x] Confirm this roadmap file is committed or otherwise backed up. -## 0.2 Engine And Tooling Decisions +## 0.01.B Engine And Tooling Decisions - [x] Confirm Unreal Engine version. - [x] Decide Blueprint-first, C++-first, or hybrid approach. -- [?] Decide whether Gameplay Ability System is needed now or later. +- [x] Decide whether Gameplay Ability System is needed now or later. Decision: defer GAS until after the version 0.1 MVP survival foundation; keep current explicit replicated components for survival, inventory, crafting, interaction, movement, time, weather, resources, and building. Revisit when skills, combat, injuries, medicine, equipment effects, professions, and long-term status modifiers create enough repeated ability/effect logic to justify GAS. - [x] Decide networking model for MVP. - [x] Decide dedicated server target platform. - [x] Decide local development platforms. @@ -223,6 +227,7 @@ Current tooling decisions: - Unreal Engine version: `5.7`. - Development approach: hybrid C++ foundation with Blueprint/content assembly in the editor. +- Gameplay Ability System: deferred until after version 0.1. Current MVP gameplay remains in explicit replicated components, with a revisit planned when ability/effect complexity grows. - Source control and daily coding: Ubuntu-Codex working against the Unraid `projects` share. - Editor/build host: Windows-Builder VM with GPU passthrough. - Headless build path for Codex: Unraid QEMU guest agent into Windows-Builder. @@ -240,7 +245,7 @@ Current tooling decisions: versions like `1`, build and smoke-test a fresh Windows packaged executable for investor/demo review. -## 0.3 Design Documentation +## 0.01.C Design Documentation - [x] Create game design document. - [x] Create technical design document. @@ -259,7 +264,7 @@ Current tooling decisions: - [x] Create data asset standards. - [x] Create save/persistence standards. -## 0.4 Project Structure +## 0.01.D Project Structure - [x] Organize `Content/Agrarian/` root folder. - [x] Create folders for Characters. @@ -275,9 +280,9 @@ Current tooling decisions: - [x] Create folders for Developer-only test assets. - [x] Create naming convention for assets. - [x] Remove unused Combat, Platforming, and SideScrolling starter variant content. -- [~] Move any starter content into clear prototype folders. Remaining ThirdPerson and LevelPrototyping assets are still referenced by current player Blueprints and automation. +- [x] Move any starter content into clear prototype folders. Decision: close the version 0.01 structure item by retaining the narrow `ThirdPerson` and `LevelPrototyping` dependencies in place until replacement work lands in version 0.1. Moving those assets now would break active player Blueprints, package maps, setup scripts, and automation. Remaining cleanup is tracked as explicit version 0.1 replacement work. -## 0.5 MVP Definition +## 0.01.E MVP Definition - [x] Define what qualifies as the 6-month MVP. - [x] Define what will not be included in MVP. @@ -295,7 +300,7 @@ Current tooling decisions: - [x] Define first playable internal milestone. - [x] Define closed test readiness criteria. -## 0.6 Operational Infrastructure +## 0.01.F Operational Infrastructure - [x] Stand up Unraid `DevBox` as shared project storage. - [x] Create and expose the `projects` SMB share. @@ -309,18 +314,18 @@ Current tooling decisions: - [x] Install VS 2022 Build Tools for Unreal 5.7 compiler compatibility. - [x] Confirm Codex can launch Windows headless build commands without RDP. - [x] Confirm headless editor target build succeeds. -- [ ] Set up Parsec or equivalent real-GPU remote access for occasional Codex visual inspection. -- [ ] Stabilize Windows-Builder network/RDP behavior under GPU passthrough. +- [x] Set up Parsec or equivalent real-GPU remote access for occasional Codex visual inspection. Sunshine is installed on Windows-Builder, paired with the GTX 1660 SUPER and a virtual 1920x1080 display for Moonlight-based LAN inspection; documented in `Docs/Ops/WindowsBuilderGpuRemoteAccess.md`. +- [x] Stabilize Windows-Builder network/RDP behavior under GPU passthrough. Confirmed fixed bridged VirtIO NIC on `br0` with MAC `52:54:00:17:ec:5d`, set Windows network profile to Private, made `TermService` automatic, confirmed RDP firewall/listener access, disabled sleep/hibernate, and documented recovery checks in `Docs/Ops/WindowsBuilderNetworkRdpStability.md`. - [x] Decide and document VM snapshot cadence before major engine/tool changes. - [x] Define Unraid share backup policy. - [x] Implement Linastorage incremental project backup job with deleted-file retention. - [x] Implement quiesced VM backup job for Windows-Builder and Ubuntu-Codex. -- [ ] Add recurring restore-test log for project and VM backups. -- [ ] Define GitHub branch protection and review rules. -- [ ] Add a build log retention policy. -- [ ] Add a simple operational runbook for rebooting/recovering Windows-Builder, Ubuntu-Codex, and DevBox. +- [x] Add recurring restore-test log for project and VM backups. Added `Docs/Ops/BackupRestoreTestLog.md`, linked it from the backup runbooks, and recorded initial blocked baseline entries for the project restic repository and VM snapshot directory so the first successful tests can be appended after snapshots are available. +- [x] Define GitHub branch protection and review rules. Documented the current free-tier/private-repo limitation, main-branch policy, PR review expectations, required validation, and future protected-main settings in `Docs/Ops/GitHubBranchProtectionAndReviewRules.md`; added `.github/PULL_REQUEST_TEMPLATE.md` for repeatable review checks. +- [x] Add a build log retention policy. Documented generated log locations, default retention windows, preserve-before-pruning rules, cadence, and dry-run/apply cleanup flow in `Docs/Ops/BuildLogRetentionPolicy.md`; added `Scripts/prune_build_logs.sh`. +- [x] Add a simple operational runbook for rebooting/recovering Windows-Builder, Ubuntu-Codex, and DevBox. Added `Docs/Ops/DevelopmentInfrastructureRecoveryRunbook.md` with triage checks, safe reboot order, Windows-Builder recovery, Ubuntu-Codex recovery, DevBox/SMB recovery, and post-recovery validation. -## 0.7 Earth-Scale Terrain Architecture +## 0.01.G Earth-Scale Terrain Architecture Long-term goal: Agrarian can eventually expand toward an Earth-scale world made from real terrain and ocean data. The intended unit is a 1 km x 1 km tile. At @@ -357,13 +362,13 @@ redownloaded when a player returns to a region. - [x] Build an MVP tile registry table for the Ground Zero tile and immediate neighbors. - [x] Keep World Partition compatibility as a hard requirement for all terrain decisions. -# Phase 1 - Foundational Survival MVP +# Version 0.1 - Foundational Survival MVP Goal: Build a small playable multiplayer prototype that proves survival, weather, inventory, gathering, crafting, shelter, and basic persistence can work together. Target deliverable: A small group can join a server, spawn into one biome, gather resources, survive weather, craft primitive tools, build simple shelter, manage hunger/thirst/injury, and return later to find basic world state preserved. -## 1.1 Core Player Foundation +## 0.1.A Core Player Foundation - [x] Create base player character class. - [x] Create player controller. @@ -379,12 +384,13 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe - [x] Implement jumping if needed. - [x] Implement interaction trace. - [x] Implement interact prompt. Implemented as a local trace-backed HUD prompt using each interactable's `GetInteractionText`, rendered as `[E] ` through the Agrarian HUD. -- [ ] Implement basic animation blueprint. +- [x] Implement basic animation blueprint. Verified `BP_AgrarianPlayerCharacter` uses `SKM_Quinn_Simple` with `/Game/Characters/Mannequins/Anims/Unarmed/ABP_Unarmed` for first-pass idle, locomotion, jump, fall, and landing presentation. - [x] Implement placeholder character mesh. -- [~] Add replication for player movement and core state. -- [~] Add developer debug overlay for player status. +- [x] Replace `ThirdPerson` template player Blueprint/game mode paths with Agrarian-native player, controller, and game mode assets once the MVP character selection flow is ready. Added `BP_AgrarianPlayerCharacter`, `BP_AgrarianPlayerController`, and `BP_AgrarianGameMode`; updated default game mode, packaging, automation, and docs to use the Agrarian-owned paths. +- [x] Add replication for player movement and core state. Explicitly enabled replicated character movement, replicated sprint/prone intent, replicated age/condition/strength/endurance/terrain movement state, OnRep speed refreshes, and server-authoritative stance/sprint/terrain update paths. +- [x] Add developer debug overlay for player status. Expanded `AgrarianDebugHUD` to show authority/local-control state, location, horizontal speed, movement mode, stance, sprint state, camera mode, movement multiplier, carry weight, age, condition, strength, endurance, and terrain movement modifier. -## 1.2 Character Stats +## 0.1.B Character Stats - [x] Create stat component. - [x] Add health. @@ -392,25 +398,25 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe - [x] Add hunger. - [x] Add thirst. - [x] Add body temperature. -- [ ] Add exhaustion. +- [x] Add exhaustion. Added replicated `Exhaustion` to the survival snapshot, low-stamina exhaustion gain, gradual recovery when fed/hydrated/warm, sprint-related exhaustion gain, movement/sprint penalties at high exhaustion, debug HUD display, admin survival output, and admin heal recovery. - [x] Add injury state. -- [ ] Reserve long-term care history fields for nutrition, illness, injury, sleep, shelter, stress, workload, and treatment quality. -- [ ] Add infection or sickness placeholder. +- [x] Reserve long-term care history fields for nutrition, illness, injury, sleep, shelter, stress, workload, and treatment quality. Added replicated/save-ready `FAgrarianCareHistorySnapshot`, clamping, debug HUD visibility, save-game field reservation, and persistence documentation. +- [x] Add infection or sickness placeholder. Added replicated `SicknessSeverity`, server-authoritative add/reduce hooks, sickness recovery/damage rates, exhaustion and care-history illness burden effects, movement penalty, debug HUD/admin output, admin heal recovery, persistence documentation, and verification coverage. - [x] Add stat regeneration rules. - [x] Add stat decay rules. - [x] Add stat replication. -- [ ] Add stat save/load support. +- [x] Add stat save/load support. `SaveCurrentWorld` now captures live Agrarian player transform, survival snapshot, care history snapshot, and inventory; load commands restore matching player stats/care history/inventory through `RestorePlayers` before restoring world actors. - [x] Add debug commands for modifying stats. -- [~] Add HUD display for critical stats. +- [x] Add HUD display for critical stats. Added an always-on compact survival status panel for health, stamina, food, water, temperature, exhaustion, injury, and sickness, with warning/critical coloring separate from the developer debug overlay. -## 1.3 Time, Weather, And Environment Pressure +## 0.1.C Time, Weather, And Environment Pressure - [x] Create world time system. - [x] Add day/night cycle. - [x] Add configurable time scale. - [x] Set default server time scale to `4 real hours = 1 in-game day`. -- [ ] Add real local time-zone and sunrise/sunset lookup for Ground Zero by latitude/longitude. -- [ ] Add Agrarian calendar conversion helpers for days, seasons, crop cycles, livestock maturity, spoilage, and long-running tasks. +- [x] Add real local time-zone and sunrise/sunset lookup for Ground Zero by latitude/longitude. Added tile-aware solar metadata to `AAgrarianGameState`, Ground Zero Pacifica defaults, local sunrise/sunset/solar-noon/day-length approximation, replicated solar state, `IsNight` based on active tile solar bounds, and a repeatable registry-driven solar metadata generator that skips placeholder/unknown tiles. +- [x] Add Agrarian calendar conversion helpers for days, seasons, crop cycles, livestock maturity, spoilage, and long-running tasks. Added replicated year/day and absolute-day helpers, season/day-of-season lookup, `4 real hours = 1 in-game day` conversion helpers, long-task progress calculation, active tile growing-zone profile, crop maturity fit checks against frost-free/growing-season windows, and a repeatable growing-zone metadata generator that skips placeholder/unknown tiles. - [ ] Add temperature curve by time of day. - [x] Add simple weather states. - [x] Add clear weather. @@ -432,7 +438,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe - [ ] Add first-pass sky and lighting. - [ ] Add audio cues for weather. -## 1.4 Single Biome MVP Map +## 0.1.D Single Biome MVP Map - [x] Choose Ground Zero real-world 1 km x 1 km MVP tile. - [x] Choose MVP biome based on Ground Zero location. @@ -450,15 +456,18 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe - [~] Add resource nodes. - [x] Add biome-appropriate natural resources based on Ground Zero. - [x] Add water source. +- [ ] Replace grey-box environment presentation with an MVP natural environment pass: terrain material, grass, shrubs, bushes, trees, water-source visuals, and clearer Ground Zero biome dressing. +- [ ] Add first-pass environment asset variation so trees, bushes, grass, resource nodes, and water do not read as repeated placeholders. +- [ ] Replace `LevelPrototyping` cube/cylinder mesh dependencies in Agrarian setup scripts and prototype Blueprints with Agrarian-native placeholder environment meshes. - [ ] Add weather exposure zones if needed. - [ ] Add landmark or ruin placeholder. -- [ ] Add spawn area. +- [ ] Add spawn area with validation that the player spawns above sea level, above terrain by a safe offset, away from water, away from steep slopes, away from dense resource clusters, and with a known safe fallback coordinate. - [ ] Add performance profiling markers. - [ ] Add navigation support for wildlife. - [ ] Add map boundaries or soft limits. - [ ] Add developer travel command. -## 1.5 Inventory System +## 0.1.E Inventory System - [ ] Design inventory data model. - [x] Create item definition data asset. @@ -475,7 +484,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe - [ ] Add persistence for inventory. - [x] Add debug item spawn command. -## 1.6 Gathering And Resources +## 0.1.F Gathering And Resources - [x] Create resource node base class. - [x] Add wood resource. @@ -490,7 +499,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe - [ ] Add resource node persistence. - [x] Add replicated gathering feedback. -## 1.7 Primitive Crafting +## 0.1.G Primitive Crafting - [x] Design recipe data model. - [x] Create recipe data assets. @@ -506,7 +515,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe - [x] Add multiplayer authority checks. - [~] Add crafting debug tools. -## 1.8 Fire System +## 0.1.H Fire System - [x] Create fire actor. - [x] Add fuel amount. @@ -521,7 +530,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe - [x] Connect fire to body temperature. - [ ] Connect rain/weather to fire behavior. -## 1.9 Shelter Building +## 0.1.I Shelter Building - [ ] Decide MVP building style. - [x] Create build placement mode. @@ -538,7 +547,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe - [x] Add shelter replication. - [ ] Add deconstruction or damage placeholder. -## 1.10 Injury And Basic Survival Consequences +## 0.1.J Injury And Basic Survival Consequences - [x] Add generic injury state. - [ ] Add bleeding placeholder. @@ -552,7 +561,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe - [ ] Add corpse/backpack placeholder if needed. - [ ] Add replicated death feedback. -## 1.11 Wildlife Prototype +## 0.1.K Wildlife Prototype - [x] Choose MVP wildlife species. - [x] Create wildlife base pawn. @@ -567,7 +576,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe - [x] Add replication. - [ ] Add performance limits. -## 1.12 Basic Multiplayer +## 0.1.L Basic Multiplayer - [x] Confirm listen server vs dedicated server for MVP. Decision: listen server is acceptable for quick internal testing, but dedicated server is the preferred closed-test target. - [ ] Create dedicated server build target if needed. @@ -586,7 +595,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe - [ ] Add basic latency testing. - [ ] Add disconnect/reconnect handling. -## 1.13 Persistence MVP +## 0.1.M Persistence MVP - [x] Decide MVP persistence scope. - [x] Decide what tile metadata is stored in save data vs external tile registry. @@ -607,7 +616,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe - [ ] Add recovery plan for corrupted save. - [ ] Document persistence limitations. -## 1.14 MVP UI And UX +## 0.1.N MVP UI And UX - [ ] Add main menu placeholder. - [ ] After splash/startup screens, land on an MVP character selection landing page. @@ -623,7 +632,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe - [ ] Add accessibility basics. - [ ] Ensure UI scales on common resolutions. -## 1.15 MVP Audio And Atmosphere +## 0.1.O MVP Audio And Atmosphere - [ ] Add ambient biome audio. - [ ] Add footstep placeholders. @@ -635,7 +644,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe - [ ] Add mix settings. - [ ] Add volume sliders. -## 1.16 MVP QA Gates +## 0.1.P MVP QA Gates - [ ] Can launch packaged client. - [ ] Can launch server. @@ -650,13 +659,30 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe - [ ] No critical log spam during 30-minute test. - [ ] Server remains stable with target test player count. +## 0.1.Q Knowledge And Skill Foundation + +- [ ] Define the MVP separation between knowledge, practical experience, physical stats, tools, and infrastructure. +- [ ] Add a first-pass skill taxonomy for survival, gathering, tool use, crafting, fire, shelter, navigation, first aid, food safety, and weather awareness. +- [ ] Define how knowledge affects survival actions: fewer mistakes, safer attempts, better yields, lower injury risk, and more reliable outcomes. +- [ ] Define how practical experience grows through use, repetition, mistakes, and recovery from failure. +- [ ] Add first contextual learning prompts for fire safety, potable water, exposure, shelter placement, injury care, and resource identification. +- [ ] Design optional knowledge checks that appear when relevant to the action instead of interrupting basic play. +- [ ] Add player-facing feedback that explains why an action failed or produced poor results. +- [ ] Define accessibility rules for the learning system: hints, retries, readable wording, no hard lockout from basic survival, and non-punitive practice paths. +- [ ] Define the first subject content format: topic, concepts, difficulty tier, prerequisite concepts, in-game effect, practice action, and source note. +- [ ] Add a small MVP question bank for elementary survival knowledge. +- [ ] Define when deeper questions should matter: quality improvements, safer work, complex crafting, teaching others, and advanced branches. +- [ ] Add design notes for avoiding exploit farming and rote memorization. +- [ ] Add persistence requirements for knowledge, skill experience, learned concepts, failed attempts, and tutorial state. +- [ ] Add multiplayer rules for teaching, observation, shared work, and group skill benefits. + --- -# Phase 2 - Persistent Homesteading +# Version 0.2 - Persistent Homesteading Goal: Transition from temporary survival into lasting settlement and land stewardship. -## 2.1 Land And Claiming +## 0.2.A Land And Claiming - [ ] Design land claim philosophy. - [ ] Define claim size limits. @@ -669,7 +695,7 @@ Goal: Transition from temporary survival into lasting settlement and land stewar - [ ] Add claim conflict rules. - [ ] Add abandoned claim decay rules. -## 2.2 Farming +## 0.2.B Farming - [ ] Design soil model. - [ ] Add basic soil quality. @@ -683,8 +709,12 @@ Goal: Transition from temporary survival into lasting settlement and land stewar - [ ] Add crop failure rules. - [ ] Add seed recovery. - [ ] Add crop persistence. +- [ ] Add farming knowledge topics for soil texture, soil fertility, composting, crop rotation, planting depth, spacing, irrigation, frost risk, and harvest timing. +- [ ] Add seed selection and plant breeding foundation concepts. +- [ ] Add crop disease, pest pressure, and weed management knowledge hooks. +- [ ] Add weather and seasonal planning effects on planting and yield reliability. -## 2.3 Domestication +## 0.2.C Domestication - [ ] Choose first domestic animal. - [ ] Add taming concept. @@ -695,8 +725,11 @@ Goal: Transition from temporary survival into lasting settlement and land stewar - [ ] Add animal products. - [ ] Add ownership. - [ ] Add persistence. +- [ ] Add animal handling knowledge for approach, containment, stress, feeding, hydration, shelter, and humane care. +- [ ] Add livestock reproduction knowledge for breeding windows, gestation, offspring care, genetic traits, and recordkeeping. +- [ ] Add animal disease, sanitation, quarantine, parasite, and nutrition knowledge hooks. -## 2.4 Storage And Preservation +## 0.2.D Storage And Preservation - [ ] Add storage containers. - [ ] Add container permissions. @@ -705,8 +738,10 @@ Goal: Transition from temporary survival into lasting settlement and land stewar - [ ] Add drying/smoking/salting placeholder. - [ ] Add storage persistence. - [ ] Add storage UI. +- [ ] Add food preservation knowledge for drying, smoking, salting, fermenting, canning later, cold storage, spoilage, pests, and contamination risk. +- [ ] Add material storage knowledge for moisture, rot, rust, pests, fire risk, and inventory rotation. -## 2.5 Permanent Structures +## 0.2.E Permanent Structures - [ ] Expand building pieces. - [ ] Add foundation. @@ -720,7 +755,7 @@ Goal: Transition from temporary survival into lasting settlement and land stewar - [ ] Add permissions. - [ ] Add persistence optimizations. -## 2.6 Simple Economy +## 0.2.F Simple Economy - [ ] Add barter container. - [ ] Add simple trade UI. @@ -728,14 +763,25 @@ Goal: Transition from temporary survival into lasting settlement and land stewar - [ ] Add local price notes if needed. - [x] Add AGR placeholder integration planning. - [ ] Add transaction logging. +- [ ] Add early business knowledge for bookkeeping, inventory, profit/loss, fair trade, basic credit, risk, and customer trust. +- [ ] Add simple workshop/business ownership rules for homestead-scale production. + +## 0.2.G Homesteading Knowledge Progression + +- [ ] Define early profession paths: farmer, herder, carpenter, mason, cook, medic, hunter, fisher, trapper, trader, and scout. +- [ ] Add learning-by-doing bonuses for routine homestead work without letting repetition replace core understanding. +- [ ] Add simple manuals, oral tips, and settlement notes as discoverable teaching objects. +- [ ] Add household teaching rules for passing practical knowledge to family or trusted settlement members. +- [ ] Define how poor knowledge creates believable outcomes: spoiled food, sick animals, failed crops, weak structures, wasted materials, and avoidable injury. +- [ ] Define how good knowledge improves resilience without making nature instant or effortless. --- -# Phase 3 - Social Civilization Systems +# Version 0.3 - Social Civilization Systems Goal: Let player communities form organically through trade, trust, conflict, law, and local coordination. -## 3.1 Identity And Reputation +## 0.3.A Identity And Reputation - [ ] Add player display identity. - [ ] Add household/family name placeholder. @@ -746,7 +792,7 @@ Goal: Let player communities form organically through trade, trust, conflict, la - [ ] Add negative reputation events. - [ ] Add reputation decay or locality rules. -## 3.2 Trade And Contracts +## 0.3.B Trade And Contracts - [ ] Design direct trade flow. - [ ] Add secure trade window. @@ -758,7 +804,7 @@ Goal: Let player communities form organically through trade, trust, conflict, la - [ ] Add contract completion rules. - [ ] Add dispute placeholder. -## 3.3 Crime And Consequences +## 0.3.C Crime And Consequences - [ ] Define theft rules. - [ ] Define trespassing rules. @@ -769,7 +815,7 @@ Goal: Let player communities form organically through trade, trust, conflict, la - [ ] Add punishment placeholder. - [ ] Add moderation/admin review tools. -## 3.4 Local Governance +## 0.3.D Local Governance - [ ] Add group/settlement entity. - [ ] Add membership. @@ -780,7 +826,7 @@ Goal: Let player communities form organically through trade, trust, conflict, la - [ ] Add shared storage. - [ ] Add settlement notice board. -## 3.5 Migration And Frontier +## 0.3.E Migration And Frontier - [ ] Design migration pressure model. - [ ] Add spawn region variation. @@ -789,13 +835,24 @@ Goal: Let player communities form organically through trade, trust, conflict, la - [ ] Add map hints for settlement density. - [ ] Add travel risk/reward systems. +## 0.3.F Apprenticeships, Schools, And Local Expertise + +- [ ] Add apprenticeship agreements between players, families, guilds, or settlements. +- [ ] Add mentor effectiveness based on knowledge depth, practical experience, communication skill, reputation, and available tools. +- [ ] Add local expert roles for healers, builders, farmers, smiths, navigators, teachers, and administrators. +- [ ] Add settlement knowledge records for who can teach which topics. +- [ ] Add skill reputation and proof-of-work records for contracts and leadership roles. +- [ ] Add basic literacy, numeracy, recordkeeping, and measurement knowledge as civilization multipliers. +- [ ] Add trade school and guild hall design concepts. +- [ ] Add rules for misinformation, poor teaching, outdated methods, and dangerous shortcuts. + --- -# Phase 4 - Generational Gameplay +# Version 0.4 - Generational Gameplay Goal: Introduce mortality, inheritance, lineage, family knowledge, and the emotional heart of Agrarian. -## 4.1 Aging And Lifespan +## 0.4.A Aging And Lifespan - [ ] Define time scale for aging. - [ ] Add character age. @@ -812,7 +869,7 @@ Goal: Introduce mortality, inheritance, lineage, family knowledge, and the emoti - [ ] Add death by old age. - [ ] Add UI for age and legacy. -## 4.2 Family And Relationships +## 0.4.B Family And Relationships - [ ] Add relationship model. - [ ] Add household model. @@ -822,7 +879,7 @@ Goal: Introduce mortality, inheritance, lineage, family knowledge, and the emoti - [ ] Add family tree UI. - [ ] Add family persistence. -## 4.3 Knowledge Inheritance +## 0.4.C Knowledge Inheritance - [ ] Define knowledge categories. - [ ] Add knowledge acquisition. @@ -832,8 +889,20 @@ Goal: Introduce mortality, inheritance, lineage, family knowledge, and the emoti - [ ] Add knowledge loss if lineage collapses. - [ ] Add written records or books. - [ ] Add library/archives concept. +- [ ] Define knowledge levels by subject: awareness, basic literacy, working competence, advanced practice, expert, master, and research frontier. +- [ ] Define knowledge proof types: contextual question checks, observed practice, completed projects, mentorship endorsements, written exams, and institutional certification. +- [ ] Define how knowledge affects unlocks, error rates, safety, quality, design options, diagnosis, planning, teaching, and research speed. +- [ ] Define how knowledge decay works when characters do not practice or preserve records. +- [ ] Add children and descendant learning model if childhood systems become playable. +- [ ] Add intergenerational teaching bonuses for stable families, apprenticeships, schools, libraries, and preserved notebooks. +- [ ] Add knowledge inheritance limits so descendants receive a head start, not a full replacement for study and practice. +- [ ] Add knowledge catastrophe rules for burned libraries, dead specialists, migration, war, famine, or settlement collapse. +- [ ] Add player-authored notes, manuals, diagrams, recipes, blueprints, and lesson plans. +- [ ] Add content review rules for real-world educational questions and source quality. +- [ ] Add difficulty tiers from elementary concepts through advanced professional and research-level material. +- [ ] Define how players can opt into deeper learning while casual survival players remain viable at lower technology levels. -## 4.4 Inheritance +## 0.4.D Inheritance - [ ] Add estate model. - [ ] Add property inheritance rules. @@ -843,7 +912,7 @@ Goal: Introduce mortality, inheritance, lineage, family knowledge, and the emoti - [ ] Add will/testament placeholder. - [ ] Add no-heir fallback rules. -## 4.5 Character Visual Aging And Condition +## 0.4.E Character Visual Aging And Condition - [ ] Define visual age stages for child, young adult, adult, mature adult, elder, and late elder if those stages are playable. - [ ] Define skin aging rules: wrinkles, texture changes, sun/weather exposure, scars, illness marks, and complexion changes. @@ -858,11 +927,11 @@ Goal: Introduce mortality, inheritance, lineage, family knowledge, and the emoti --- -# Phase 5 - Governments And Civilization +# Version 0.5 - Governments And Civilization Goal: Enable cities, citizenship, taxation, diplomacy, organized law, warfare, and territorial control. -## 5.1 Settlements To Cities +## 0.5.A Settlements To Cities - [ ] Add settlement progression metrics. - [ ] Add population tracking. @@ -871,7 +940,7 @@ Goal: Enable cities, citizenship, taxation, diplomacy, organized law, warfare, a - [ ] Add city designation. - [ ] Add city management UI. -## 5.2 Citizenship And Law +## 0.5.B Citizenship And Law - [ ] Add citizenship model. - [ ] Add legal code records. @@ -880,7 +949,7 @@ Goal: Enable cities, citizenship, taxation, diplomacy, organized law, warfare, a - [ ] Add court/dispute placeholder. - [ ] Add fines or penalties. -## 5.3 Taxation And Public Works +## 0.5.C Taxation And Public Works - [ ] Add treasury model. - [ ] Add tax rules. @@ -889,7 +958,7 @@ Goal: Enable cities, citizenship, taxation, diplomacy, organized law, warfare, a - [ ] Add public building funding. - [ ] Add treasury audit logs. -## 5.4 Diplomacy +## 0.5.D Diplomacy - [ ] Add settlement relations. - [ ] Add alliances. @@ -898,7 +967,7 @@ Goal: Enable cities, citizenship, taxation, diplomacy, organized law, warfare, a - [ ] Add war declaration. - [ ] Add treaty records. -## 5.5 Warfare +## 0.5.E Warfare - [ ] Define combat scale. - [ ] Add weapon progression. @@ -908,13 +977,31 @@ Goal: Enable cities, citizenship, taxation, diplomacy, organized law, warfare, a - [ ] Add war consequences. - [ ] Add anti-griefing protections. +## 0.5.F Public Education, Professions, And Certification + +- [ ] Add public schools, academies, guilds, universities, libraries, laboratories, hospitals, workshops, and proving grounds as settlement institutions. +- [ ] Add licensing/certification systems for high-risk roles such as doctor, engineer, pilot, heavy equipment operator, power operator, explosives handler, and public official. +- [ ] Add standardized curricula controlled by settlements or governments. +- [ ] Add professional ethics, malpractice, negligence, and public safety consequences. +- [ ] Add civil engineering knowledge for roads, bridges, water systems, sanitation, drainage, public buildings, and urban planning. +- [ ] Add administration knowledge for law, tax records, logistics, auditing, diplomacy, emergency management, and public works. +- [ ] Add political science and civic knowledge for constitutions, representation, elections, legitimacy, civil rights, bureaucracy, corruption control, and peaceful transfer of power. +- [ ] Add legal studies knowledge for property law, contracts, criminal law, courts, evidence, due process, appeals, and enforcement limits. +- [ ] Add economics knowledge for scarcity, labor, markets, money, credit, taxation, inflation, public goods, trade policy, and resource allocation. +- [ ] Add business administration knowledge for accounting, payroll, purchasing, contracts, operations, pricing, management, insurance, and risk controls. +- [ ] Add finance knowledge for banking, loans, collateral, investment, bonds, equity, reserves, audits, bankruptcy, and financial regulation. +- [ ] Add sociology and anthropology knowledge for culture, social trust, institutions, migration, inequality, conflict, education, and community resilience. +- [ ] Add ethics and philosophy knowledge for public risk, medical consent, animal welfare, war conduct, research limits, stewardship, and governance legitimacy. +- [ ] Add military training knowledge for tactics, logistics, fortification, command, communications, and rules of engagement. +- [ ] Add public research funding, grants, patents or open knowledge policies, and institutional prestige. + --- -# Phase 6 - Dynamic Civilization Cycles +# Version 0.6 - Dynamic Civilization Cycles Goal: Keep the world alive for future generations through change, decay, scarcity, collapse, and renewal. -## 6.1 Environmental Sustainability +## 0.6.A Environmental Sustainability - [ ] Add soil exhaustion. - [ ] Add crop rotation benefits. @@ -924,7 +1011,7 @@ Goal: Keep the world alive for future generations through change, decay, scarcit - [ ] Add resource depletion. - [ ] Add environmental recovery. -## 6.2 Disease And Hardship +## 0.6.B Disease And Hardship - [ ] Add disease model. - [ ] Add spread rules. @@ -933,7 +1020,7 @@ Goal: Keep the world alive for future generations through change, decay, scarcit - [ ] Add famine conditions. - [ ] Add disaster events. -## 6.3 Infrastructure Decay +## 0.6.C Infrastructure Decay - [ ] Add road decay. - [ ] Add building decay. @@ -942,7 +1029,7 @@ Goal: Keep the world alive for future generations through change, decay, scarcit - [ ] Add ruin state. - [ ] Add rediscovery value for ruins. -## 6.4 Collapse And Frontier Renewal +## 0.6.D Collapse And Frontier Renewal - [ ] Add settlement instability metrics. - [ ] Add migration waves. @@ -950,13 +1037,24 @@ Goal: Keep the world alive for future generations through change, decay, scarcit - [ ] Add frontier emergence rules. - [ ] Add new player frontier assignment. +## 0.6.E Medicine, Ecology, And Resilience Knowledge + +- [ ] Add public health knowledge for sanitation, clean water, waste handling, quarantine, nutrition, vaccination if era-appropriate, and disease surveillance. +- [ ] Add medical science progression from first aid and herbal care through anatomy, physiology, pathology, clinical diagnosis, surgery support, pharmacology, epidemiology, mental health, rehabilitation, and hospital operations. +- [ ] Add nursing, emergency medicine, veterinary medicine, dentistry, maternal care, pediatrics, geriatrics, occupational health, and medical logistics knowledge. +- [ ] Add ecology knowledge for soil recovery, watershed health, forest management, habitat restoration, carrying capacity, and sustainable harvesting. +- [ ] Add disaster preparedness knowledge for fire, flood, drought, storm, famine, disease outbreak, infrastructure failure, and evacuation planning. +- [ ] Add failure analysis knowledge so settlements can diagnose why crops, animals, machines, structures, or public systems failed. +- [ ] Add archive recovery and rediscovery mechanics for collapsed settlements and ruins. +- [ ] Add resilience planning bonuses for settlements that preserve specialists, records, redundant systems, and training pipelines. + --- -# Phase 7 - Earth-Scale Expansion +# Version 0.7 - Earth-Scale Expansion Goal: Expand from a small test world toward a huge, regionally diverse, persistent world that can eventually approach Earth scale through staged 1 km x 1 km real-terrain tiles. -## 7.1 Terrain And World Partition +## 0.7.A Terrain And World Partition - [ ] Evaluate Unreal World Partition. - [ ] Define real terrain generation pipeline. @@ -969,15 +1067,15 @@ Goal: Expand from a small test world toward a huge, regionally diverse, persiste - [ ] Define river, lake, coastline, and wetland generation rules. - [ ] Define missing/low-quality source data fallback rules. - [ ] Add streaming tests. -- [~] Add server-delivered tile streaming prototype. Near-term MVP proof is tracked in Phase 0.7. -- [~] Add client local tile cache prototype. Near-term MVP proof is tracked in Phase 0.7. +- [~] Add server-delivered tile streaming prototype. Near-term MVP proof is tracked in version 0.01.G. +- [~] Add client local tile cache prototype. Near-term MVP proof is tracked in version 0.01.G. - [ ] Add stale tile scrubber prototype. - [ ] Add tile redownload and version update prototype. - [ ] Add biome boundaries. - [ ] Add region metadata. - [ ] Add server scaling plan. -## 7.2 Biome Diversity +## 0.7.B Biome Diversity - [ ] Derive biome candidates from real-world land-cover, climate, elevation, and water data. - [ ] Add forest biome. @@ -990,7 +1088,7 @@ Goal: Expand from a small test world toward a huge, regionally diverse, persiste - [ ] Map natural resources to likely real-world geology, flora, water, and climate. - [ ] Add biome-specific survival pressure. -## 7.3 Logistics And Transportation +## 0.7.C Logistics And Transportation - [ ] Keep walking and running speeds close to real-world human pace. - [ ] Keep animal travel speeds close to real-world animal pace. @@ -1005,8 +1103,11 @@ Goal: Expand from a small test world toward a huge, regionally diverse, persiste - [ ] Add shipping routes. - [ ] Add regional trade value. - [ ] Add distance-based economy pressure. +- [ ] Add practical navigation knowledge for landmarks, maps, compass use, celestial navigation, terrain reading, river travel, sea travel, and weather routing. +- [ ] Add vehicle operation knowledge for carts, animal teams, bicycles if used, boats, cars, trucks, tractors, trains, aircraft, and heavy equipment. +- [ ] Add maintenance knowledge for wheels, axles, harnesses, engines, hulls, tires, fuel systems, batteries, brakes, and spare parts. -## 7.4 Continental Economy +## 0.7.D Continental Economy - [ ] Add regional scarcity. - [ ] Add market hubs. @@ -1014,8 +1115,12 @@ Goal: Expand from a small test world toward a huge, regionally diverse, persiste - [ ] Add tariffs or taxes. - [ ] Add price history. - [ ] Add economic dashboards. +- [ ] Add interregional and global trade knowledge for comparative advantage, shipping routes, ports, rail hubs, customs, tariffs, sanctions, trade agreements, currency exchange, and supply-chain disruption. +- [ ] Add commercial logistics knowledge for warehousing, freight forwarding, fleet management, cold chains, bulk commodities, manifests, insurance, and customs documents. +- [ ] Add business scaling rules for sole proprietors, partnerships, cooperatives, corporations, franchises, guild enterprises, and state-owned enterprises. +- [ ] Add market infrastructure for commodity exchanges, long-term contracts, futures/forward agreements if appropriate, credit risk, and price discovery. -## 7.5 Tile Operations At Scale +## 0.7.E Tile Operations At Scale - [ ] Define tile generation queue. - [ ] Define tile publish queue. @@ -1027,13 +1132,24 @@ Goal: Expand from a small test world toward a huge, regionally diverse, persiste - [ ] Define cost controls for serving and regenerating terrain content. - [ ] Define long-term archival strategy for superseded tile versions. +## 0.7.F Earth-Scale Field Science And Resource Knowledge + +- [ ] Add surveying and cartography knowledge for accurate coordinates, tile boundaries, elevation, slope, hydrology, and route planning. +- [ ] Add geology knowledge for likely mineral resources, soil parent material, rock types, groundwater, caves, hazards, and mining prospects. +- [ ] Add hydrology knowledge for rivers, wetlands, aquifers, floodplains, irrigation, erosion, and drinking water risk. +- [ ] Add climatology and meteorology knowledge for regional weather, seasons, wind, storms, drought, snowpack, and agricultural planning. +- [ ] Add botany, forestry, and ecology knowledge for biome-specific plants, timber, wild foods, medicinal plants, invasive pressure, and regeneration. +- [ ] Add zoology and wildlife management knowledge for animal behavior, migration, hunting pressure, domestication candidates, and ecosystem balance. +- [ ] Add GIS/data-literacy progression for interpreting real-world terrain, climate, land-cover, and resource datasets. +- [ ] Add in-game field notebooks and sample collection to connect player exploration with world knowledge. + --- -# Phase 8 - Industrial And Automation Era +# Version 0.8 - Industrial And Automation Era Goal: Let player civilizations advance into mechanization, energy, automation, and large-scale production. -## 8.1 Industrial Materials +## 0.8.A Industrial Materials - [ ] Add mining progression. - [ ] Add ore processing. @@ -1041,8 +1157,12 @@ Goal: Let player civilizations advance into mechanization, energy, automation, a - [ ] Add machine parts. - [ ] Add fuel types. - [ ] Add industrial storage. +- [ ] Add mining, geology, metallurgy, chemistry, ceramics, glass, concrete, polymers, and materials testing knowledge. +- [ ] Add iron and steel progression for charcoal iron, coke, blast furnaces, basic oxygen or electric arc steelmaking, alloying, heat treatment, rolling, casting, forging, and quality testing. +- [ ] Add industrial material standards for grades, tolerances, traceability, certification, failure reports, and safety-critical use. +- [ ] Add workplace safety knowledge for mines, furnaces, pressure vessels, chemicals, dust, noise, burns, crush risk, and confined spaces. -## 8.2 Power Systems +## 0.8.B Power Systems - [ ] Add mechanical power. - [ ] Add water power. @@ -1050,8 +1170,10 @@ Goal: Let player civilizations advance into mechanization, energy, automation, a - [ ] Add electrical power. - [ ] Add power grid rules. - [ ] Add maintenance/failure. +- [ ] Add power generation knowledge for mechanical advantage, water wheels, wind, steam, combustion, generators, batteries, transformers, grid stability, and load management. +- [ ] Add electrical safety knowledge for grounding, insulation, overcurrent protection, arc risk, maintenance lockout, and public distribution hazards. -## 8.3 Production Chains +## 0.8.C Production Chains - [ ] Add workshops. - [ ] Add factories. @@ -1059,30 +1181,49 @@ Goal: Let player civilizations advance into mechanization, energy, automation, a - [ ] Add labor requirements. - [ ] Add logistics bottlenecks. - [ ] Add quality tiers. +- [ ] Add manufacturing knowledge for measurement, tolerances, machining, welding, casting, quality control, process flow, and supply-chain planning. +- [ ] Add chemistry/process knowledge for fuels, fertilizers, medicines, solvents, plastics, explosives where appropriate, and pollution control. +- [ ] Add industrialism knowledge for division of labor, interchangeable parts, standardization, assembly lines, factory layout, worker training, shift planning, maintenance schedules, and industrial relations. +- [ ] Add manufacturing business systems for procurement, bills of materials, production planning, inventory turns, cost accounting, quality audits, recalls, warranties, and supplier qualification. +- [ ] Add heavy industry chains for steel, cement, glass, chemicals, machine tools, engines, electrical equipment, vehicles, ships, rail, and construction equipment. +- [ ] Add industrial environmental controls for emissions, waste treatment, water use, occupational exposure, cleanup liability, and community impact. -## 8.4 Automation And Robotics +## 0.8.D Automation And Robotics - [ ] Add automation control concept. - [ ] Add basic machine workers. - [ ] Add robotics progression. - [ ] Add AI-assisted labor. - [ ] Add safety/failure risks. +- [ ] Add control systems knowledge for sensors, actuators, feedback loops, programmable logic, robotics, telemetry, fail-safes, and maintenance diagnostics. +- [ ] Add computing and communications knowledge for basic electronics, radio, wired networks, data storage, software, cybersecurity, and automation monitoring. + +## 0.8.E Advanced Engineering And Energy Education + +- [ ] Add engineering education paths for mechanical, civil, electrical, chemical, industrial, environmental, agricultural, biomedical, nuclear, aerospace, software, and systems engineering. +- [ ] Add design-review mechanics for complex machines, structures, power plants, factories, medical systems, transportation networks, and public infrastructure. +- [ ] Add failure investigation loops where players learn from breakdowns, accidents, low yield, overload, corrosion, fatigue, contamination, and operator error. +- [ ] Add team competency requirements for high-risk builds so major infrastructure depends on multiple trained characters, not one unlocked recipe. +- [ ] Add early nuclear science literacy as a long-term branch: atoms, radiation, shielding, fission basics, reactor safety, waste handling, and regulatory controls. --- -# Phase 9 - Orbital Foundations +# Version 0.9 - Orbital Foundations Goal: Make space feel earned by centuries of civilization, science, industry, and infrastructure. -## 9.1 Astronomy And Knowledge +## 0.9.A Astronomy And Knowledge - [ ] Add astronomy observations. - [ ] Add observatory building. - [ ] Add star charts. - [ ] Add orbital mechanics knowledge. - [ ] Add education requirements. +- [ ] Add mathematics progression for algebra, geometry, trigonometry, calculus concepts, statistics, and numerical modeling as prerequisites for advanced engineering. +- [ ] Add physics progression for mechanics, thermodynamics, electromagnetism, fluids, materials, radiation, and high-energy safety. +- [ ] Add astronomy and space science learning for celestial observation, timekeeping, navigation, orbital periods, launch windows, and planetary environments. -## 9.2 Launch Infrastructure +## 0.9.B Launch Infrastructure - [ ] Add advanced materials. - [ ] Add fuel production. @@ -1090,8 +1231,12 @@ Goal: Make space feel earned by centuries of civilization, science, industry, an - [ ] Add rocket components. - [ ] Add launch preparation. - [ ] Add launch failure risk. +- [ ] Add rocketry knowledge for propulsion, staging, thrust-to-weight, mass fraction, aerodynamics, guidance, navigation, control, telemetry, and range safety. +- [ ] Add aerospace manufacturing knowledge for precision machining, composites, avionics, cryogenics, pressure systems, clean handling, and test procedures. +- [ ] Add nuclear and advanced energy knowledge for reactors, radioisotope systems, shielding, radiation monitoring, emergency response, and long-term waste stewardship. +- [ ] Add pilot, mission controller, engineer, and safety officer training paths. -## 9.3 Orbital Gameplay +## 0.9.C Orbital Gameplay - [ ] Add orbital object model. - [ ] Add satellites. @@ -1099,13 +1244,21 @@ Goal: Make space feel earned by centuries of civilization, science, industry, an - [ ] Add navigation benefits. - [ ] Add orbital maintenance. +## 0.9.D Research Institutions And Advanced Science + +- [ ] Add observatories, universities, laboratories, proving grounds, launch ranges, wind tunnels, reactors or simulator facilities, and mission control centers. +- [ ] Add research programs that require funding, specialists, instruments, materials, power, safety approvals, tests, failures, and published findings. +- [ ] Add advanced question banks and practical exams for orbital mechanics, rocket science, nuclear physics, materials science, avionics, life support, and mission planning. +- [ ] Add simulation and prototype testing so players can learn before risking major resources or lives. +- [ ] Add ethical and safety governance for dangerous research, weapons-adjacent technology, radiation, launch debris, and public risk. + --- -# Phase 10 - Interplanetary Frontier +# Version 1.0 - Interplanetary Frontier Goal: Expand civilization beyond Earth while keeping Earth emotionally and mechanically central. -## 10.1 Interplanetary Travel +## 1.0.A Interplanetary Travel - [ ] Add spacecraft design. - [ ] Add fuel logistics. @@ -1114,7 +1267,7 @@ Goal: Expand civilization beyond Earth while keeping Earth emotionally and mecha - [ ] Add travel windows. - [ ] Add travel risk. -## 10.2 Colony Logistics +## 1.0.B Colony Logistics - [ ] Add colony founding. - [ ] Add supply chains. @@ -1123,7 +1276,7 @@ Goal: Expand civilization beyond Earth while keeping Earth emotionally and mecha - [ ] Add communication delay if desired. - [ ] Add political connection to Earth. -## 10.3 Multi-World Persistence +## 1.0.C Multi-World Persistence - [ ] Add planetary world records. - [ ] Add orbital economy. @@ -1131,6 +1284,18 @@ Goal: Expand civilization beyond Earth while keeping Earth emotionally and mecha - [ ] Add settlement history across worlds. - [ ] Add long-term expansion governance. +## 1.0.D Offworld Knowledge And Colonization Science + +- [ ] Add life support knowledge for atmosphere, pressure, oxygen, carbon dioxide removal, water recycling, food loops, waste processing, and emergency repair. +- [ ] Add space medicine knowledge for radiation, low gravity, isolation, injury care, nutrition, infection control, and long-duration health. +- [ ] Add closed-loop agriculture knowledge for hydroponics, aeroponics, soil substitutes, lighting, pollination, nutrient balance, and genetic diversity. +- [ ] Add planetary geology and resource utilization knowledge for prospecting, mining, refining, construction materials, ice extraction, and in-situ fuel production. +- [ ] Add offworld construction knowledge for habitats, shielding, pressure vessels, thermal control, foundations, dust management, and maintenance. +- [ ] Add interplanetary navigation knowledge for transfer windows, orbital insertion, fuel margins, communication delay, rescue planning, and mission risk. +- [ ] Add colony governance and culture knowledge for law, education, medical standards, emergency authority, succession, archives, and Earth relationship. +- [ ] Add long-term knowledge preservation systems for isolated colonies, including redundant archives, local schools, apprenticeships, and disaster recovery plans. +- [ ] Add late-game research frontiers for terraforming studies, advanced propulsion, planetary-scale ecology, deep-space industry, and multi-generation settlement planning. + --- # Cross-Cutting Technical Tracks @@ -1169,6 +1334,9 @@ These tracks run across all phases and must not be left as afterthoughts. - [ ] Add automated save validation. - [ ] Add schema/version tracking. - [ ] Add admin restore workflow. +- [ ] Define knowledge database schema for subjects, topics, concepts, prerequisites, question banks, practice records, certifications, teaching records, source notes, and localization. +- [ ] Define audit/version rules for educational content updates so question changes do not corrupt character progression. +- [ ] Define privacy-safe analytics for which concepts confuse players or create excessive friction. ## C. Accounts And Authentication @@ -1217,6 +1385,8 @@ These tracks run across all phases and must not be left as afterthoughts. - [ ] Add log retention policy. - [ ] Add account recovery policy. - [ ] Add economy abuse monitoring. +- [ ] Add anti-cheat rules for knowledge checks, certifications, teaching records, and externally-assisted answer farming. +- [ ] Add moderation workflow for player-authored notes, books, schools, and public educational content. ## G. Performance @@ -1274,6 +1444,8 @@ These tracks run across all phases and must not be left as afterthoughts. - [ ] Add crash reporting plan. - [ ] Add automated build validation. - [ ] Add test server reset process. +- [ ] Add educational content QA checklist for accuracy, clarity, difficulty tiering, accessibility, and source review. +- [ ] Add balance tests to ensure knowledge checks improve depth without pushing players out of the survival loop. ## K. Build And Release Pipeline diff --git a/Config/DefaultEngine.ini b/Config/DefaultEngine.ini index acfe9cb..fa653c7 100644 --- a/Config/DefaultEngine.ini +++ b/Config/DefaultEngine.ini @@ -1,7 +1,7 @@ [/Script/EngineSettings.GameMapsSettings] GameDefaultMap=/Game/Agrarian/Maps/L_GroundZeroTerrain_Test.L_GroundZeroTerrain_Test EditorStartupMap=/Game/Agrarian/Maps/L_GroundZeroTerrain_Test.L_GroundZeroTerrain_Test -GlobalDefaultGameMode=/Game/ThirdPerson/Blueprints/BP_ThirdPersonGameMode.BP_ThirdPersonGameMode_C +GlobalDefaultGameMode=/Game/Agrarian/Blueprints/Characters/BP_AgrarianGameMode.BP_AgrarianGameMode_C [/Script/Engine.RendererSettings] r.Mobile.ShadingPath=1 diff --git a/Config/DefaultGame.ini b/Config/DefaultGame.ini index 32e250d..6963a97 100644 --- a/Config/DefaultGame.ini +++ b/Config/DefaultGame.ini @@ -30,4 +30,3 @@ IncludePrerequisites=True IncludeAppLocalPrerequisites=False Build=IfProjectHasCode +MapsToCook=(FilePath="/Game/Agrarian/Maps/L_GroundZeroTerrain_Test") -+MapsToCook=(FilePath="/Game/ThirdPerson/Lvl_ThirdPerson") diff --git a/Content/Agrarian/Blueprints/Characters/BP_AgrarianGameMode.uasset b/Content/Agrarian/Blueprints/Characters/BP_AgrarianGameMode.uasset new file mode 100644 index 0000000..fd3d706 --- /dev/null +++ b/Content/Agrarian/Blueprints/Characters/BP_AgrarianGameMode.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fe56d6dc0704594d8a5da2c385fe0ee53a76336aafc66ae78a59d318279f980f +size 20790 diff --git a/Content/Agrarian/Blueprints/Characters/BP_AgrarianPlayerCharacter.uasset b/Content/Agrarian/Blueprints/Characters/BP_AgrarianPlayerCharacter.uasset new file mode 100644 index 0000000..2ce7503 --- /dev/null +++ b/Content/Agrarian/Blueprints/Characters/BP_AgrarianPlayerCharacter.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8158faf669bcfe01895de54e8e148244aef0124cf37b1c29942d252d4de457a1 +size 46695 diff --git a/Content/Agrarian/Blueprints/Characters/BP_AgrarianPlayerController.uasset b/Content/Agrarian/Blueprints/Characters/BP_AgrarianPlayerController.uasset new file mode 100644 index 0000000..7f09919 --- /dev/null +++ b/Content/Agrarian/Blueprints/Characters/BP_AgrarianPlayerController.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af8bd6139b8354627f1480126a642ee7c489f89236f7cc891ba22c5823a2026d +size 20651 diff --git a/Content/Input/Actions/IA_Crouch.uasset b/Content/Input/Actions/IA_Crouch.uasset index 641ce6cac86ff86b1b7d89adcab12dd8d8236efe..e8f279cfe82b534ae0dec9adeba6e8a14762fba4 100644 GIT binary patch literal 129 zcmWm3O%B2!5J2HQr{DsX!oUE%O@|*LQE43P!qZzpl)CqMdsJSo zwht|7JQW`$sZ)#5v*flLhg-&?3s4~N_7BS5MR3U!sxI$qvcX;(P4zJY_+aCnqDCYH N0OtkwZ;@K#Dt^j_Cfxu4 literal 1160 zcmX@utTpfZ|Ns9Jm>C$jm>3v-0cjBU^YD-7%z3A?Zmf2=|NfJ~V;fDcbaxg821cOV z+l&oQUOG+r!hOqdk>hSRJq@6k91!cfC+4Q=d*&6CmgqYsmt^MW73+IC#yc0~mnLTb zka-{sauh2A zI|B=Y+W|plAs|m%JJi)wJ18|LHL*BV*Hq6OEYHC3KISSDc6p#CFv$3je;>r?0NNV@ z3@&5<4CVizAOtx*0!fY)$oI?7OJ(@Ne3Vxf$O%YH&Q45E^~q08%*m`u1O=oYFl37h z5|dLIgi0zq2nV*J}ee5cmfSGIj_Pg@maCSqcM4Aqo>>00lXm17v_a1_Inb z%nN2QFo2>27l0)ZkUY9s$a+9Y2p0gEfgWI}K^g}1jycp7Al0a{N81fU0&RVQesI@lp$i8-aIA(aKG44}Yv%FhR8iac-{hD8EU m7!@1@mMPP}Ep5=VK5`M{E*Vs5hO&DWfh?!?YZ^esLFxhUoa+hz diff --git a/Content/Input/Actions/IA_Prone.uasset b/Content/Input/Actions/IA_Prone.uasset index 974126180ce1cd80e34b1106d00fc23de2801da9..1488c49cb7a2795c7be270c1eb9b673f4e55f3a3 100644 GIT binary patch literal 129 zcmWN_K@x&63_#I+Pr(JGBqi2zo3u@&Gb)TJEIhre{+oZw*V^_O*4}qpynnWSd#R0A z*CUT*dwbiZ_!HdL>R>A)-4{v98Dbz6;GmK)ATe+y)pG5e9sy%Qoa3CKvLIEA9Cd;z M0+6%zli>onKhFUs^Z)<= literal 1155 zcmX@utTpfZ|Ns9Jm>C$jm>3v-0cjBE6Sw$qXr1Uhb)Iu4>m@hLj`sXl@5I8uzzCFk zo3Y`^OQ$JcxNjLQa@_5vrvVg`1!8^o#N1SU&%A=t5`D+ylFaz{1VY$iUFaz{J4C)z}rT7^Lqu zP!kX_bb|;W@P@h;M1u^3@%@2(5RQk+gVb8w=c|0b^p)@43FpPi*@S+8%mZPNqgWZ( z8CV$H4hS*}0eRZmp{}OdL8&>ZiN&e9rh4XJc?O2}F;|(e%L6rm!CNJ6br7QkWIr&p zkO44||ARsh0}q zU;sr3E&xj)AbE7NkoAB<2p0gEfgWHeAsP(yjSN8*w+KgsuMI*oq-EzKv-fC$jm>3v-0cj9uiZ<{Ju4?7A)Aav*Ab66A#my^M-B}nI7=dzc zGd4VV=``gF_btOkj=SCTG=O4qK&yhCUDh1b$HGf@qL|Fn$n_55h@Md63$ldD#;4;)9(QTvHPE;h$m(G7p47j$&nC zXJBD)J0Qp`1mtOJhq{_-2c_nuCKjjan(CQ@$6RH?E)UcM2Gh#+WPlhQKzl=g z!G#Qfq5K~dgdnF!Ajz=;`F{C%sSID3kMha_IRS~u*@@|?KKaRsIhj?7pn&uPhHP;` zVsa{jP)TJ6;lRE%FSuR=Xq0|%a#3bMiN14wQEI4vQdVkm3BwOhT{|%(IoG_5#JuFx z6mY~bTwQni4I5C_IVZ8Wn4zS+Vj>rihlob)M-9O|AQ4bBLt>dhNFsrU11JV{I76{} zhbWK>4qZ?PgMcCs6O3wDY=MFV1pWboj2*&6Az|u3mcjs1h{A*zKtT@Y02v^UfdDrU z^MY9n44^2%1z<@8B#&+uvK~+p!UaHPpa&RgkcI)hV-9r%NHwag5>NydjVMv2ha3f< zL{K;+pQn{b^GX|^1^&M_axF%q!T MqSs%N#%x;r0gMeMBme*a literal 1190 zcmX@utTpfZ|Ns9Jm>C$jm>3v-0cj98F?Z*JSCxD1Uu=02x=-WPkqGs)Td6Dz42(dz zw;3CrymXrKh5MG_BFEisdKy466(H7kPs~l#_slCOEzx&OF3HT#E7tdPj1S3APtQqp z2FevB0#z|EZ~}2B$RHrd2HL3zqz&CHEKMAZ+>G5UOx=u)9nB1#jhtPK9W7lgT?|~D z&Ebkc`aS?P0TIIj5CH@r$HQolfiQjwP#lCypz_Z zL5^Z&U}s=qa62H#ECl3fYlpg;Y6qp}q$U=p>YD1AgXI|*-p5>J!Y&Wg1O^(5K4^j% z3xM_(0Rs&g0K@t}C?G*jFGG@J1@is!^HLeUFdyZW1#$usld}`kQ+@K26LT`F5<$V~ z2MojFg2d!h2BDJ54q}7+V)NsTB0vN6gOiIg3rh5z^NUhL{gbj%lS>$Wc1}-k>TpP({I>-vd%e)#l;LIPu Nv|{#el>pI#_yGvpC?fy> literal 11831 zcmeHNeQZ=!7C(GeK~_Owskm!V6iT<9Ql=jkMcU~U%F-5@0;aeruhYlO*y+5U34 zQN(B(chz9k<+aHDFu>6qMSnigwTMzIUguV?QQw z+Uzu(M8)gu(2)#CW)rOvx-g)6SH|#iZkI5 zp{uy4*jZjsSWxC(>~gzZ?jo0vhVWHf>X}M3i=S;=2bE0kdqPIm{mjR;#cGGtzwWD_ zmA`vm^?`$bqsM-{ZX4bMI#=*Di85#+RUVo?fk#X^Icwd8IcwCQszlV>!n|V9PIUgB zqvHo|=T3qO+n;(Ag_qdwgglcc?~Hjb0L^7ahOG_bad^CXAZ$3?A-@vxs!akuI@6r;^k6()hn^c`Hyj6eU zmkEt^{%A{M#0YqsDO1VnyvEkNCgAf!=f>V+?T89#O7P=KC)FK1VSiE4YSa44gM}S8 za&HkWmit^<$WXT!R;@xY6xtO$d0--oG3IpOj=OhbMsc}@0iydo;fe6I7`Wq+wzE^L zipL*dD7H;;I^w_o`kaE>rfw{xiQ~_G-o3toYPbnpX)1wkim2=!%5daWun-wZWT%MNfp@*naV? z$t)qjp{d>3L4>P^K6-5R1Asgs%xLM}ce*en5A>HSI+ech{1LpK$Izp;*UP8pFF~$O z5BPi-=*eX*f5Hm7HcZYJ*YAXtb@b|IjZdJop2mNlaT=w1>fC-j|JPiyua9PMMjTaT z4K7`adi^vvvtxbM;8IoWMqTk5^!i`+%)DW6xs}nDFy*$r`}-keR%u!@{qXFX{2^qj zwN{nR|L(#ixMYwwXV+t2}I~o?|%w`f;B%Wvb+t;A7MAe zb`DBvTQ4$Ysn~j+;q9^9Dr5PQG?1K`6c7z3S79j`PR^aAT;%|A@;%F}!q+^Gk|gB5 zV!7Lrkh{QgrAf%0XSpRw$bHFjwb)-LU%dlayPOq+CIga*LCcb0#TgiS-*V!{00UcrLRloKe%v zIc5dv>0mUD8*qUVb(viAKHbvwqs<=rVb0=-B@>we$dHaUtNrfM7MMfXgFete_OSEX z?LC-6dp{(M4{J^0_#Pi&dq_O&<9lv|?IB;Z+k0t*?crc!xA$ra?OjUnANGJm|NS+E z_P$TBhrJ{b&-X^y9+Gc6o*#~|J)~fEd!LW6y~KFwPN6*>%FiOiR$ta-o~o^5nfMH0 z|5g&@&j+>ET8w^)x#B|R$+TA6~31_t3x>~su=I2=Dfm<%8Y**jx{0ws2S5}lC&FF-4kT|t)M zm^aBM9AZF{0gYK;SS0y`brqB)?&eK2_V6UenFM5iN6CXmT#~X;Kt2t^@ko+D=~k;I z)0bPUI@}Gy_BBnE`fCLXQ(2NwhOa)Lo6N^g-yM8uDQzLcksKrkl7^58Zy`vqG7cJP z-6@T<9AL(j#;}P=ff<1#HYVdJK{t<0fzlYdc*J2p9wpe|DNq{2HvD#w+DVG+1(e2+ z#Ul-y_9#V~*kjWhw)onH0(SOMf(|v z@c4BPG^QcDn9?Z2$1Bif$V?o>eds8|S1!<)hO8^4QD*%Lbs4hcKS$z^U!XA^W#6SV z%J4x2bQv-e2XP;IEffD18S9XJlhP>rjRo$Qjs%T&I&VX{ix)wiGP1HTmBse^8W*xXdsgS diff --git a/Content/ThirdPerson/Blueprints/BP_ThirdPersonCharacter.uasset b/Content/ThirdPerson/Blueprints/BP_ThirdPersonCharacter.uasset index 337768a150715a3969c4eb5e76faf0860325c3a0..ead5f41c4f931ff9affe8a9cb8ac45dd68794d09 100644 GIT binary patch literal 130 zcmWN`yAi`63;@vHQ?Nh`5(u!jL3{#tN$>^iBGbp!E8dmAqxFxda~@-t^4aF&RmOT- zPhMkx%W>o=FIaj@V$^_S_kzYMESj87nh^qRCNdlEfYF7LEV4H)05R)3g7YePQVBNV N64Nr;FAG9ISpMHKC#L`a literal 52121 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 diff --git a/Content/ThirdPerson/Blueprints/BP_ThirdPersonGameMode.uasset b/Content/ThirdPerson/Blueprints/BP_ThirdPersonGameMode.uasset index 649362b6417b851b6f7b5135119d0ac739048beb..07793294d751c045a961f36874301df83dfbb6ed 100644 GIT binary patch literal 130 zcmWN?K@x)?3;@8pukZtv5->!6Lm;WlwAE?QLto$SWiRKShvqOrz{IIt=+LmF)HOz3Yh_g}wWhTj?212S_6_5lo5r+t>f~=gClTS$(@d+fw NGFbb{2tF-w_yG|6v#$CIsC(o>`-I}RZY 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 diff --git a/Data/Tiles/tile_growing_zone_metadata.json b/Data/Tiles/tile_growing_zone_metadata.json new file mode 100644 index 0000000..b65c891 --- /dev/null +++ b/Data/Tiles/tile_growing_zone_metadata.json @@ -0,0 +1,21 @@ +{ + "schema_version": 1, + "generated_at_utc": "2026-05-16T04:33:53Z", + "source_registry": "Data/Tiles/ground_zero_tiles.json", + "generation_rule": "Only tiles with real source status and explicit growing-zone data are emitted.", + "tiles": [ + { + "tile_id": "gz_us_ca_pacifica_utm10n_e544_n4160", + "center_latitude": 37.5925, + "center_longitude": -122.4995, + "growing_zone_label": "USDA 10a", + "climate_profile": "coastal_mediterranean_mild", + "growing_season_start_day": 46, + "growing_season_end_day": 350, + "frost_free_days": 305, + "min_average_growing_temp_c": 7.0, + "crop_safety_buffer_days": 14, + "data_basis": "MVP conservative coastal Pacifica profile; replace with authoritative zone/temperature datasets during regional expansion." + } + ] +} diff --git a/Data/Tiles/tile_registry.schema.json b/Data/Tiles/tile_registry.schema.json index 301ee31..8125228 100644 --- a/Data/Tiles/tile_registry.schema.json +++ b/Data/Tiles/tile_registry.schema.json @@ -107,6 +107,12 @@ "$ref": "#/$defs/source" } }, + "solar_metadata": { + "$ref": "#/$defs/solar_metadata" + }, + "growing_season_metadata": { + "$ref": "#/$defs/growing_season_metadata" + }, "notes": { "type": "string" } @@ -161,6 +167,80 @@ } } }, + "solar_metadata": { + "type": "object", + "required": [ + "time_zone_id", + "standard_utc_offset_hours", + "daylight_utc_offset_hours", + "solar_model" + ], + "additionalProperties": false, + "properties": { + "time_zone_id": { + "type": "string" + }, + "standard_utc_offset_hours": { + "type": "number", + "minimum": -12, + "maximum": 14 + }, + "daylight_utc_offset_hours": { + "type": "number", + "minimum": -12, + "maximum": 14 + }, + "solar_model": { + "type": "string" + } + } + }, + "growing_season_metadata": { + "type": "object", + "required": [ + "growing_zone_label", + "climate_profile", + "growing_season_start_day", + "growing_season_end_day", + "frost_free_days", + "crop_safety_buffer_days" + ], + "additionalProperties": false, + "properties": { + "growing_zone_label": { + "type": "string" + }, + "climate_profile": { + "type": "string" + }, + "growing_season_start_day": { + "type": "integer", + "minimum": 1, + "maximum": 366 + }, + "growing_season_end_day": { + "type": "integer", + "minimum": 1, + "maximum": 366 + }, + "frost_free_days": { + "type": "integer", + "minimum": 0, + "maximum": 366 + }, + "min_average_growing_temp_c": { + "type": "number" + }, + "crop_safety_buffer_days": { + "type": "integer", + "minimum": 0, + "maximum": 90 + }, + "data_basis": { + "type": "string" + } + } + }, "source": { "type": "object", "required": ["source_kind", "source_name", "coverage_status"], diff --git a/Data/Tiles/tile_registry.sql b/Data/Tiles/tile_registry.sql index a5f3945..9a6f4ec 100644 --- a/Data/Tiles/tile_registry.sql +++ b/Data/Tiles/tile_registry.sql @@ -40,6 +40,30 @@ ON terrain_tiles (grid_scheme, projection, utm_zone, easting_min_m, northing_min CREATE INDEX IF NOT EXISTS idx_terrain_tiles_status ON terrain_tiles (status); +CREATE TABLE IF NOT EXISTS terrain_tile_solar_metadata ( + tile_id TEXT PRIMARY KEY, + time_zone_id TEXT NOT NULL, + standard_utc_offset_hours REAL NOT NULL CHECK (standard_utc_offset_hours >= -12 AND standard_utc_offset_hours <= 14), + daylight_utc_offset_hours REAL NOT NULL CHECK (daylight_utc_offset_hours >= -12 AND daylight_utc_offset_hours <= 14), + solar_model TEXT NOT NULL DEFAULT 'NOAA approximate sunrise/sunset', + generated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (tile_id) REFERENCES terrain_tiles(tile_id) +); + +CREATE TABLE IF NOT EXISTS terrain_tile_growing_season_metadata ( + tile_id TEXT PRIMARY KEY, + growing_zone_label TEXT NOT NULL, + climate_profile TEXT NOT NULL DEFAULT 'unknown', + growing_season_start_day INTEGER NOT NULL CHECK (growing_season_start_day >= 1 AND growing_season_start_day <= 366), + growing_season_end_day INTEGER NOT NULL CHECK (growing_season_end_day >= 1 AND growing_season_end_day <= 366), + frost_free_days INTEGER NOT NULL CHECK (frost_free_days >= 0 AND frost_free_days <= 366), + min_average_growing_temp_c REAL NOT NULL DEFAULT 7.0, + crop_safety_buffer_days INTEGER NOT NULL DEFAULT 14 CHECK (crop_safety_buffer_days >= 0 AND crop_safety_buffer_days <= 90), + data_basis TEXT NOT NULL DEFAULT '', + generated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (tile_id) REFERENCES terrain_tiles(tile_id) +); + CREATE TABLE IF NOT EXISTS terrain_tile_neighbors ( tile_id TEXT NOT NULL, direction TEXT NOT NULL CHECK (direction IN ('n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw')), diff --git a/Data/Tiles/tile_solar_metadata.json b/Data/Tiles/tile_solar_metadata.json new file mode 100644 index 0000000..9325709 --- /dev/null +++ b/Data/Tiles/tile_solar_metadata.json @@ -0,0 +1,43 @@ +{ + "schema_version": 1, + "generated_at_utc": "2026-05-16T04:14:42Z", + "source_registry": "Data/Tiles/ground_zero_tiles.json", + "generation_rule": "Only tiles with real source status and explicit timezone data are emitted.", + "tiles": [ + { + "tile_id": "gz_us_ca_pacifica_utm10n_e544_n4160", + "center_latitude": 37.5925, + "center_longitude": -122.4995, + "time_zone_id": "America/Los_Angeles", + "standard_utc_offset_hours": -8.0, + "daylight_utc_offset_hours": -7.0, + "solar_model": "NOAA approximate sunrise/sunset", + "sample_solar_hours": { + "march_equinox": { + "sunrise_hour": 7.231, + "sunset_hour": 19.364, + "solar_noon_hour": 13.298, + "day_length_hours": 12.133 + }, + "june_solstice": { + "sunrise_hour": 5.807, + "sunset_hour": 20.571, + "solar_noon_hour": 13.189, + "day_length_hours": 14.764 + }, + "september_equinox": { + "sunrise_hour": 6.956, + "sunset_hour": 19.122, + "solar_noon_hour": 13.039, + "day_length_hours": 12.166 + }, + "december_solstice": { + "sunrise_hour": 8.348, + "sunset_hour": 17.912, + "solar_noon_hour": 13.13, + "day_length_hours": 9.564 + } + } + } + ] +} diff --git a/Docs/BackupExpectations.md b/Docs/BackupExpectations.md index 8049ed9..a145b9f 100644 --- a/Docs/BackupExpectations.md +++ b/Docs/BackupExpectations.md @@ -147,7 +147,9 @@ Minimum restore tests: - Quarterly: perform a fuller restore drill of the project folder plus one VM definition/disk to a non-production test path. -Record restore tests in the handoff log with snapshot path, date, and result. +Record restore tests in `Docs/Ops/BackupRestoreTestLog.md` with snapshot path, +date, result, evidence, and cleanup status. Handoff notes can summarize the +latest result, but the durable restore-test record belongs in the log file. ## Security diff --git a/Docs/BasicAnimationBlueprint.md b/Docs/BasicAnimationBlueprint.md new file mode 100644 index 0000000..8b14fe4 --- /dev/null +++ b/Docs/BasicAnimationBlueprint.md @@ -0,0 +1,22 @@ +# Basic Animation Blueprint + +The MVP player character uses the Unreal mannequin unarmed animation Blueprint: + +- Character Blueprint: `/Game/Agrarian/Blueprints/Characters/BP_AgrarianPlayerCharacter` +- Skeletal mesh: `/Game/Characters/Mannequins/Meshes/SKM_Quinn_Simple` +- Animation Blueprint: `/Game/Characters/Mannequins/Anims/Unarmed/ABP_Unarmed` + +This provides the first playable animation baseline for idle, walk/run +locomotion, jump, falling, and landing. Crouch and prone currently reuse the +same baseline presentation until the dedicated stance animation pass replaces +them with survival-specific movement. + +Verification script: + +```text +Scripts/verify_basic_animation_blueprint.py +``` + +The script validates that the Agrarian player Blueprint is still wired to the expected +mesh and animation class, and that the required idle, blendspace, jump, fall, +and land assets exist. diff --git a/Docs/BranchingConventions.md b/Docs/BranchingConventions.md index 604b6c7..4352a95 100644 --- a/Docs/BranchingConventions.md +++ b/Docs/BranchingConventions.md @@ -10,6 +10,8 @@ storefront channels require a heavier workflow. - Use short-lived task branches for risky, multi-commit, or parallel work. - Small Codex-only changes may land directly on `main` when the worktree is clean, the change is scoped, and validation is run. +- Branch protection/review expectations are defined in + `Docs/Ops/GitHubBranchProtectionAndReviewRules.md`. ## Branch Names @@ -61,3 +63,11 @@ Create a long-lived staging branch only when one of these becomes true: - Release candidates need stabilization while feature work continues. - Automated CI checks become strict enough that `main` should only receive reviewed, green changes. + +## Protection Enforcement + +The repository is currently private and staying on the free tier. GitHub does +not currently allow branch protection/ruleset enforcement for this private repo +without upgrading or making it public, so version 0.01 uses documented process +rules and a pull request template. Enforced protected `main` remains tracked in +the later build/release pipeline milestone. diff --git a/Docs/CoreDesignDocument.md b/Docs/CoreDesignDocument.md index 6856ba3..be18070 100644 --- a/Docs/CoreDesignDocument.md +++ b/Docs/CoreDesignDocument.md @@ -297,8 +297,11 @@ Mitigation: - The public tile endpoint currently uses `maps.agrariangame.com:18080`. - The first tile server runs inside the dedicated `Agrarian-TileServer` VM. - Unused Unreal starter variants have been removed. -- ThirdPerson and LevelPrototyping content remain temporarily while still - referenced by current player Blueprint and prototype automation. +- ThirdPerson player/game mode Blueprint paths have been replaced with + Agrarian-owned player, controller, and game mode assets under + `/Game/Agrarian/Blueprints/Characters`. +- LevelPrototyping content remains temporarily by decision: it is a narrow + active dependency and its replacement is tracked as version `0.1` work. ## Follow-Up Documents diff --git a/Docs/GameplayAbilitySystemDecision.md b/Docs/GameplayAbilitySystemDecision.md new file mode 100644 index 0000000..2e6174e --- /dev/null +++ b/Docs/GameplayAbilitySystemDecision.md @@ -0,0 +1,37 @@ +# Gameplay Ability System Decision + +Decision: defer Unreal's Gameplay Ability System for the MVP. + +Agrarian should not adopt GAS during version 0.01 or the early version 0.1 +survival foundation. The current project already has focused replicated +components for survival, inventory, crafting, resources, building placement, +time, weather, interaction, and early movement modifiers. Those systems are +small, readable, and directly aligned with the immediate playable MVP. + +GAS is useful for games with many networked abilities, buffs, cooldowns, +attribute effects, prediction needs, tags, and reusable gameplay effects. That +will probably matter later for Agrarian when character skills, injuries, +illnesses, professions, equipment, medicine, combat, aging, and long-term +condition effects become more complex. + +For now, adding GAS would increase setup cost, content complexity, debugging +surface, and onboarding burden before the project has enough gameplay variety +to benefit from it. + +Guideline: + +- Keep MVP survival systems implemented through explicit C++ components and + Blueprint assets. +- Keep future stat/effect code modular enough that it can migrate into GAS if + the need becomes clear. +- Revisit GAS after version 0.1 once combat, skills, injuries, medicine, + profession bonuses, equipment effects, and replicated long-term status + modifiers are better defined. + +Revisit trigger: + +- The project has many recurring timed effects, buffs, debuffs, cooldowns, or + ability activations that are becoming duplicated across systems. +- Multiplayer prediction for ability-style actions becomes important. +- Designers need data-driven effect stacking, gameplay tags, and reusable + effect authoring more than hand-coded component behavior. diff --git a/Docs/Ops/AgrarianProjectBackupRunbook.md b/Docs/Ops/AgrarianProjectBackupRunbook.md index e58b3b8..595ff74 100644 --- a/Docs/Ops/AgrarianProjectBackupRunbook.md +++ b/Docs/Ops/AgrarianProjectBackupRunbook.md @@ -96,4 +96,6 @@ sudo RESTIC_PASSWORD_FILE=/root/.backup-secrets/agrarian-project-restic.password snapshots --tag agrarian-game --tag project ``` -Record restore tests in the handoff log with the snapshot name and result. +Record restore tests in `Docs/Ops/BackupRestoreTestLog.md` with the snapshot +name, restored path, result, and cleanup status. Summarize the latest result in +handoff notes when useful. diff --git a/Docs/Ops/AgrarianVmBackupRunbook.md b/Docs/Ops/AgrarianVmBackupRunbook.md index aa9da42..aba46f4 100644 --- a/Docs/Ops/AgrarianVmBackupRunbook.md +++ b/Docs/Ops/AgrarianVmBackupRunbook.md @@ -89,3 +89,6 @@ qemu-img info vms/Ubuntu-Codex/disks/hdc-vdisk1.img.qcow2 Restore should be done to a new VM or a test path first. Do not overwrite a working VM disk until the backup has been verified and the original disk has been preserved. + +Record VM restore tests in `Docs/Ops/BackupRestoreTestLog.md` with the snapshot +timestamp, checksum result, disk inspection result, and cleanup status. diff --git a/Docs/Ops/BackupRestoreTestLog.md b/Docs/Ops/BackupRestoreTestLog.md new file mode 100644 index 0000000..b0c463c --- /dev/null +++ b/Docs/Ops/BackupRestoreTestLog.md @@ -0,0 +1,61 @@ +# Backup Restore Test Log + +## Purpose + +Backups are only useful if restore paths are tested before an emergency. This log records recurring restore tests for the Agrarian project backup and the development VM backups. + +Use this file for the durable project record. Handoff notes can summarize the latest result, but the evidence and cadence live here. + +## Cadence + +- Weekly: restore one small file from the latest project snapshot to a temporary folder and verify it matches the live file. +- Monthly: restore or inspect enough of the project repository snapshot to confirm Git metadata/history are usable. +- Monthly: verify the latest VM backup snapshot is readable by checking `SHA256SUMS` and inspecting the backed-up disk metadata. +- Quarterly: restore the project folder plus one VM definition/disk to a non-production test path. + +## Evidence Required + +Each entry should include: + +- Date and local time. +- Backup type: project, VM, or full drill. +- Snapshot path, restic snapshot ID, or VM backup timestamp. +- Command or runbook section used. +- Result: Pass, Blocked, or Fail. +- Notes on what was restored, where it was restored, and whether cleanup was completed. + +## Log + +| Date | Type | Snapshot | Result | Evidence / Notes | +| --- | --- | --- | --- | --- | +| 2026-05-15 | Project restore-test log setup | `/mnt/backups/linastorage/backups/agrarian-game/project/restic-repository` | Blocked | Log created. The expected restic repository path was not present on Ubuntu-Codex at setup time, so the first project restore test must run after the Linastorage backup mount/repository is confirmed. | +| 2026-05-15 | VM restore-test log setup | `/mnt/user/backups/agrarian-game/vms/snapshots` | Blocked | Log created. The Unraid snapshot directory exists, but no VM snapshot directories were present at setup time. Run the first VM readability test after the next successful VM backup snapshot. | + +## Project Restore Test Procedure + +Use the project backup runbook for the authoritative paths: + +```bash +mkdir -p /tmp/agrarian-project-restore-test +sudo sh -c 'RESTIC_PASSWORD_FILE=/root/.backup-secrets/agrarian-project-restic.password \ + restic -r /mnt/backups/linastorage/backups/agrarian-game/project/restic-repository \ + dump latest /mnt/projects/AgrarianGameBulid/AGRARIAN_DEVELOPMENT_ROADMAP.md \ + > /tmp/agrarian-project-restore-test/AGRARIAN_DEVELOPMENT_ROADMAP.md' +diff -q /mnt/projects/AgrarianGameBulid/AGRARIAN_DEVELOPMENT_ROADMAP.md \ + /tmp/agrarian-project-restore-test/AGRARIAN_DEVELOPMENT_ROADMAP.md +``` + +Record the restic snapshot ID, result, and cleanup status in the log table. + +## VM Restore Test Procedure + +Use the VM backup runbook for authoritative paths: + +```bash +cd /mnt/user/backups/agrarian-game/vms/snapshots/ +sha256sum -c SHA256SUMS +qemu-img info vms/Ubuntu-Codex/disks/hdc-vdisk1.img.qcow2 +``` + +Record the VM snapshot timestamp, checksum result, disk metadata result, and cleanup status in the log table. + diff --git a/Docs/Ops/BuildLogRetentionPolicy.md b/Docs/Ops/BuildLogRetentionPolicy.md new file mode 100644 index 0000000..ed1476a --- /dev/null +++ b/Docs/Ops/BuildLogRetentionPolicy.md @@ -0,0 +1,78 @@ +# Agrarian Build Log Retention Policy + +This policy covers generated build, package, cook, editor, crash, shader, and +automation logs for the Agrarian Unreal project. + +## Goals + +- Keep enough recent logs to debug build and packaging failures. +- Preserve important release/investor-demo logs long enough to explain what was + shipped. +- Prevent `Saved/`, `Builds/`, and generated automation output from growing + without limit on the shared project drive. +- Keep generated logs out of Git. + +## Log Locations + +- `Saved/BuildLogs/`: Windows editor builds, packaged target builds, and + packaging logs. +- `Saved/Logs/`: Unreal editor, cook, runtime, UnrealPak, World Partition, and + commandlet logs. +- `Saved/Crashes/`: editor/runtime crash context, logs, and dumps. +- `Saved/MaterialStats/`: generated material stat CSV files. +- `Saved/Shaders/` and `Saved/ShaderDebugInfo/`: generated shader diagnostics. +- `Builds/`: packaged local build outputs. These are not retained as source + artifacts unless explicitly published or sent externally. + +## Default Retention + +- Build/package logs in `Saved/BuildLogs/`: keep 90 days. +- Unreal logs in `Saved/Logs/`: keep 30 days. +- Crash reports in `Saved/Crashes/`: keep 90 days. +- Material stats in `Saved/MaterialStats/`: keep 30 days. +- Shader debug output in `Saved/Shaders/` and `Saved/ShaderDebugInfo/`: keep 14 + days. +- Local packaged builds in `Builds/`: keep the latest published investor/demo + package and any active test build; prune older regenerated builds manually. + +## Preserve Before Pruning + +Before deleting logs, preserve any file that is tied to: + +- an investor/demo package; +- a release candidate; +- a crash or build failure still being investigated; +- a bug report sent to another person; +- a store upload or external test distribution. + +Preserved logs should be copied to a dated folder under a local operational +archive or attached to the related issue/release note before cleanup. + +## Cleanup Script + +Use: + +```text +Scripts/prune_build_logs.sh --dry-run +Scripts/prune_build_logs.sh --apply +``` + +The script is dry-run by default. It only removes generated files under known +`Saved/` log/debug paths and empty directories left behind. It does not touch +source, `Content/`, `Config/`, `Source/`, `Docs/`, or Git history. + +## Cadence + +- Run dry-run before major packaging cycles if the shared drive is getting full. +- Run apply monthly during active development. +- Always run dry-run first when preparing an investor or external test build. + +## Git Policy + +Generated logs are not committed. Commit only: + +- build scripts; +- packaging scripts; +- retention policy docs; +- release notes; +- summaries of important failures or fixes. diff --git a/Docs/Ops/DevelopmentInfrastructureRecoveryRunbook.md b/Docs/Ops/DevelopmentInfrastructureRecoveryRunbook.md new file mode 100644 index 0000000..c57bfaa --- /dev/null +++ b/Docs/Ops/DevelopmentInfrastructureRecoveryRunbook.md @@ -0,0 +1,179 @@ +# Development Infrastructure Recovery Runbook + +## Purpose + +This runbook gives a simple recovery path when the Agrarian development machines or shared project storage are unreachable. It covers: + +- Unraid `DevBox` +- `Ubuntu-Codex` +- `Windows-Builder` +- The `projects` SMB share + +Use the least disruptive recovery path first. Do not reboot `DevBox` until VM-level and service-level checks have failed, because it hosts shared storage and the development VMs. + +## Current Baseline + +| System | Role | Address / Name | Notes | +| --- | --- | --- | --- | +| `DevBox` | Unraid host, SMB storage, VM host | `192.168.5.8` / `DevBox` | Hosts `projects` share and VMs. | +| `Ubuntu-Codex` | Source-control and automation VM | `192.168.5.10` expected, current host access may also show `192.168.5.6` or `192.168.5.9` depending on interface | Mounts `//192.168.5.8/projects` at `/mnt/projects`. | +| `Windows-Builder` | Unreal/Visual Studio/GPU build VM | `192.168.5.12` | Uses fixed VirtIO MAC `52:54:00:17:ec:5d`. | +| `projects` share | Shared Unreal project storage | `\\DevBox\projects` / `/mnt/projects` | Repo path is `/mnt/projects/AgrarianGameBulid`. | + +## First Triage + +From Ubuntu-Codex or another LAN machine: + +```bash +ping -c 3 192.168.5.8 +ping -c 3 192.168.5.12 +nc -vz -w 5 192.168.5.12 3389 +mount | rg '/mnt/projects|cifs|smb' +``` + +From `DevBox`: + +```bash +virsh list --all +virsh domiflist Windows-Builder +virsh domiflist Ubuntu-Codex +``` + +Expected VM NIC baselines: + +- `Windows-Builder`: bridge `br0`, model `virtio-net`, MAC `52:54:00:17:ec:5d` +- `Ubuntu-Codex`: bridge `br0`, model `virtio-net`, MAC `52:54:00:a5:cf:63` + +## Safe Reboot Order + +Use this order when multiple systems are unhealthy: + +1. Save or stop active work where possible. +2. Restart only the failing service if the host is reachable. +3. Restart the affected VM from inside the guest if guest access works. +4. Use `virsh shutdown ` from `DevBox` if guest access does not work. +5. Use `virsh reboot ` only when a graceful shutdown is not enough. +6. Use `virsh destroy ` only when the VM is hung and no graceful path works. +7. Reboot `DevBox` only after confirming SMB, libvirt, or host networking cannot be recovered individually. + +Before planned VM shutdowns, consider a manual VM backup if the change is risky: + +```bash +/bin/bash /boot/config/custom/agrarian-vm-backup.sh --shutdown-running --vm Windows-Builder +/bin/bash /boot/config/custom/agrarian-vm-backup.sh --shutdown-running --vm Ubuntu-Codex +``` + +## Windows-Builder Recovery + +Use these in order: + +1. Confirm the VM is running: + + ```bash + virsh domstate Windows-Builder + virsh domiflist Windows-Builder + ``` + +2. Confirm RDP is listening from Ubuntu-Codex: + + ```bash + nc -vz -w 5 192.168.5.12 3389 + ``` + +3. Use the QEMU guest-agent path before relying on RDP when possible. +4. If RDP is down but guest commands work, check: + + ```powershell + Get-Service TermService + Get-NetConnectionProfile + Get-NetFirewallRule -DisplayGroup "Remote Desktop" + ``` + +5. Restart RDP only if it is not listening: + + ```powershell + Restart-Service TermService -Force + ``` + +6. If Unreal visual inspection is needed, use Sunshine/Moonlight instead of RDP. + +Detailed Windows-Builder references: + +- `Docs/Ops/WindowsBuilderNetworkRdpStability.md` +- `Docs/Ops/WindowsBuilderGpuRemoteAccess.md` + +## Ubuntu-Codex Recovery + +Use these in order: + +1. Confirm VM state from `DevBox`: + + ```bash + virsh domstate Ubuntu-Codex + virsh domiflist Ubuntu-Codex + ``` + +2. Confirm SSH or console access. +3. Confirm the project mount: + + ```bash + mount | rg '/mnt/projects' + ls -la /mnt/projects/AgrarianGameBulid + ``` + +4. If `/mnt/projects` is missing, remount the SMB share using the existing system mount configuration rather than creating a new ad hoc mount. +5. Confirm Git access: + + ```bash + git -C /mnt/projects/AgrarianGameBulid status --short + git -C /mnt/projects/AgrarianGameBulid remote -v + ``` + +Do not wipe local changes to recover the VM. Preserve uncommitted work first with a commit, patch, or backup copy outside the repo. + +## DevBox And SMB Recovery + +Use these in order: + +1. Confirm the host is reachable: + + ```bash + ping -c 3 192.168.5.8 + ``` + +2. Confirm Unraid services and VM state through the Unraid UI or SSH. +3. Confirm the `projects` share is visible: + + ```bash + smbclient -L //192.168.5.8 -N + ``` + +4. Confirm Ubuntu-Codex sees the share mounted: + + ```bash + mount | rg '/mnt/projects' + ``` + +5. If DNS name `DevBox` fails but IP works, use the IP temporarily and repair the router/DNS record later. +6. Avoid storing project files directly on the Unraid OS boot filesystem. Project data belongs on the `projects` share or in VMs. + +## When To Stop And Inspect Before Rebooting + +Pause before rebooting if: + +- A build, package, backup, or VM disk copy is running. +- Unreal Editor is open with unsaved assets. +- Git has uncommitted changes that are not understood. +- A backup or restore test is in progress. +- DevBox has disk or array warnings. + +## After Recovery + +After any VM or `DevBox` recovery: + +1. Confirm `/mnt/projects/AgrarianGameBulid` is reachable. +2. Run `git status --short`. +3. Confirm Windows-Builder RDP with `nc -vz -w 5 192.168.5.12 3389`. +4. Confirm Sunshine only if visual inspection is needed. +5. Record unusual recovery steps in the handoff notes or the relevant ops doc. + diff --git a/Docs/Ops/GitHubBranchProtectionAndReviewRules.md b/Docs/Ops/GitHubBranchProtectionAndReviewRules.md new file mode 100644 index 0000000..b614b23 --- /dev/null +++ b/Docs/Ops/GitHubBranchProtectionAndReviewRules.md @@ -0,0 +1,86 @@ +# GitHub Branch Protection And Review Rules + +## Current Repository State + +- Repository: `pacificao/AgrarianGameBuild` +- Visibility: private +- Default branch: `main` +- Current plan constraint: GitHub branch protection and repository rulesets are not enforceable on this private repository without GitHub Pro/Team or making the repository public. + +GitHub API checks on 2026-05-15 returned the upgrade requirement for both branch protection and repository rulesets. To stay on the free tier, version 0.01 uses documented process controls and pull request templates now, with enforced protected `main` deferred to the later release/build pipeline milestone already tracked in the roadmap. + +## Main Branch Policy + +`main` is the only long-lived development branch for now. + +Rules: + +- Keep `main` buildable. +- Prefer short-lived task branches for risky, multi-file, Unreal asset, infrastructure, or parallel work. +- Direct commits to `main` are allowed only for small, scoped changes when validation is run and the working tree is understood. +- Do not commit secrets, raw terrain source datasets, generated builds, `DerivedDataCache`, `Intermediate`, `Saved`, `Binaries`, large source-art archives, or local machine caches. +- Before milestone demos or investor builds, confirm `main` is clean, pushed, and smoke-tested. + +## Pull Request Rules + +Use a pull request when any of these are true: + +- The change touches gameplay systems, replication, persistence, build tooling, backup tooling, deployment, or server infrastructure. +- The change modifies Unreal binary assets that are hard to review in text. +- The change spans multiple roadmap items. +- The change could affect packaged builds, dedicated servers, map tile delivery, or player data. +- Another developer, workstation, or Codex session is working in the same area. + +For PRs: + +- Use the existing branch naming convention in `Docs/BranchingConventions.md`. +- Link the roadmap item being completed. +- Include validation steps actually run. +- Include screenshots or build artifacts only as links or paths, not committed binaries. +- Merge only after the change is reviewed or explicitly accepted by the project owner. + +## Review Expectations + +Minimum review expectations by change type: + +| Change Type | Review Expectation | +| --- | --- | +| Docs-only | Self-review is acceptable if scoped and validated with `git diff --check`. | +| Small code-only fix | Self-review is acceptable when tests/build checks pass and risk is low. | +| Gameplay feature | Project owner review or second-developer review before merge when practical. | +| Unreal binary assets | Record the editor action taken and visual/build validation performed. | +| Build, backup, deployment, server, or GitHub workflow changes | Owner review before merge unless it is an emergency fix. | +| Secrets, credentials, auth, payment, wallet, or production data handling | Do not merge without explicit owner review. | + +## Required Local Checks Before Push + +Run the checks appropriate for the change: + +- Documentation/text changes: `git diff --check` +- C++ changes: editor target build wrapper or Windows headless build command +- Packaged-client changes: packaged client build wrapper and smoke test +- Server changes: dedicated server build wrapper and relevant smoke test +- Backup/tooling changes: dry-run mode first when available + +Record skipped checks in the PR or commit message. + +## Free-Tier Controls Installed Now + +- `Docs/Ops/GitHubBranchProtectionAndReviewRules.md` defines the working policy. +- `.github/PULL_REQUEST_TEMPLATE.md` gives a repeatable review checklist. +- The roadmap keeps paid/private branch protection enforcement in the later release/build pipeline section. + +## Future Enforced Settings + +When a paid GitHub plan is justified or the repository becomes public, enable protected `main` with: + +- Require pull request before merge. +- Require at least one approving review for non-doc changes. +- Dismiss stale approvals when new commits are pushed. +- Require conversation resolution before merge. +- Require status checks once CI exists. +- Restrict force pushes. +- Restrict branch deletion. +- Require linear history if it does not interfere with Unreal asset workflows. +- Allow administrators to bypass only for emergency recovery. + diff --git a/Docs/Ops/WindowsBuilderGpuRemoteAccess.md b/Docs/Ops/WindowsBuilderGpuRemoteAccess.md new file mode 100644 index 0000000..411f633 --- /dev/null +++ b/Docs/Ops/WindowsBuilderGpuRemoteAccess.md @@ -0,0 +1,62 @@ +# Windows-Builder GPU Remote Access + +## Purpose + +Windows-Builder needs two remote access paths: + +- Headless build/control path for normal Codex work through QEMU guest tools and PowerShell. +- Real-GPU visual path for occasional Unreal Editor inspection, startup screen checks, and gameplay verification. + +RDP remains useful for administration, but it can interfere with the display session Unreal and GPU capture tools see. Use Sunshine/Moonlight when the goal is to inspect the GPU-rendered desktop. + +## Current Setup + +- Host VM: `Windows-Builder` +- LAN address: `192.168.5.12` +- GPU: NVIDIA GeForce GTX 1660 SUPER +- GPU driver observed during setup: `32.0.15.9649` +- Streaming host: Sunshine `2025.924.154138` +- Virtual display: Virtual Display Driver by MTT, 1920x1080 at 60 Hz +- Sunshine service: `SunshineService`, automatic startup +- Sunshine web UI: `https://192.168.5.12:47990` +- Login: use the Windows-Builder admin credential stored in the operational handoff, not a separate project password. + +## Firewall Ports + +Windows Firewall has LAN rules for Sunshine GameStream: + +- TCP: `47984`, `47989`, `47990`, `48010` +- UDP: `47998`, `47999`, `48000`, `48002`, `48010` + +Keep these LAN-only. Do not expose Sunshine directly to the public internet. + +## Client Workflow + +1. Install Moonlight on the client machine used for visual inspection. +2. Add host `192.168.5.12`. +3. Pair with Sunshine when prompted. +4. Launch Desktop from Moonlight. +5. Use the GPU-streamed desktop for Unreal visual checks. + +Codex should continue using the headless build/control path for normal code, cook, package, and commandlet work. Use Sunshine only when a visual inspection is actually needed. + +## Verification + +The setup was validated by: + +- Confirming the GTX 1660 SUPER is present and healthy in Windows. +- Confirming Sunshine is listening on its LAN ports. +- Confirming the Sunshine web UI responds with HTTP 401 authentication from Ubuntu-Codex. +- Confirming the virtual display is present as `Virtual Display Driver by MTT`. +- Confirming Sunshine detects a 1920x1080 desktop and creates NVENC H.264/HEVC encoders on the GTX 1660 SUPER. + +The GTX 1660 SUPER does not support AV1 NVENC. Sunshine may log an AV1 encoder failure during startup; this is expected and H.264/HEVC remain available. + +## Troubleshooting + +- If Sunshine is unreachable, check `SunshineService`, Windows Firewall, and the VM IP address. +- If Sunshine logs `failed to query display paths and modes`, verify the virtual display driver is installed and enabled. +- If Moonlight shows a black screen after using RDP, disconnect RDP, restart `SunshineService`, and reconnect through Moonlight. +- If controller input is required later, install and validate the ViGEmBus gamepad driver. It is not required for visual inspection. +- If Unreal rendering differs under RDP, use Moonlight before changing graphics settings. + diff --git a/Docs/Ops/WindowsBuilderNetworkRdpStability.md b/Docs/Ops/WindowsBuilderNetworkRdpStability.md new file mode 100644 index 0000000..5e712ca --- /dev/null +++ b/Docs/Ops/WindowsBuilderNetworkRdpStability.md @@ -0,0 +1,84 @@ +# Windows-Builder Network And RDP Stability + +## Purpose + +Windows-Builder uses GPU passthrough for Unreal Editor and packaged build validation. GPU passthrough can make RDP and display behavior confusing because RDP, Sunshine, the virtual display driver, and the NVIDIA GPU are separate access paths. + +This runbook records the stable baseline for normal administration and recovery. + +## Stable Network Baseline + +- VM: `Windows-Builder` +- LAN IP: `192.168.5.12` +- VM bridge: `br0` +- NIC model: `virtio-net` +- MAC address: `52:54:00:17:ec:5d` +- Windows adapter: `Red Hat VirtIO Ethernet Adapter` +- Windows adapter alias: `Ethernet` +- Windows network profile: `Private` +- DNS observed during stabilization: `192.168.4.1` + +The fixed MAC address is important. Keep the VM NIC on the same bridge/MAC so router DHCP reservations, firewall rules, SMB mappings, RDP bookmarks, and Sunshine/Moonlight pairing continue to resolve to the same machine. + +## Stable RDP Baseline + +- RDP registry switch: enabled through `fDenyTSConnections = 0` +- Service: `TermService` +- Startup type: `Automatic` +- Listener: TCP `3389` +- Windows Firewall group: `Remote Desktop` +- Required firewall rules: + - `Remote Desktop - User Mode (TCP-In)` + - `Remote Desktop - User Mode (UDP-In)` + - `Remote Desktop - Shadow (TCP-In)` + +Use RDP for administration, installs, and recovery. Use Sunshine/Moonlight for real-GPU visual inspection of Unreal because RDP can change the active display path seen by graphics applications. + +## Power Baseline + +The VM should not sleep while it is acting as the build host. + +- Hibernate: off +- AC sleep timeout: disabled +- DC sleep timeout: disabled +- AC monitor timeout: disabled +- DC monitor timeout: disabled + +This avoids the Windows kernel power standby events that can leave the VirtIO NIC disconnected until a manual wake or reconnect. + +## Verification Commands + +From Ubuntu-Codex: + +```bash +nc -vz -w 5 192.168.5.12 3389 +sshpass -p '' ssh root@192.168.5.8 'virsh domiflist Windows-Builder' +``` + +From Windows-Builder PowerShell: + +```powershell +Get-NetAdapter +Get-NetConnectionProfile +Get-Service TermService +Get-NetFirewallRule -DisplayGroup "Remote Desktop" +powercfg /a +``` + +Expected results: + +- `nc` reports that TCP `3389` succeeds. +- `virsh domiflist` shows `bridge br0`, `virtio-net`, and MAC `52:54:00:17:ec:5d`. +- `TermService` is running and automatic. +- Remote Desktop firewall rules are enabled. +- Hibernate is unavailable because it is disabled. + +## Recovery Flow + +1. Use the QEMU guest-agent path first if RDP fails. +2. Confirm the VM still has IP `192.168.5.12`. +3. Confirm the VM XML still has the fixed VirtIO NIC on `br0`. +4. Confirm Windows still sees the network as `Private`. +5. Restart `TermService` only if the service is not listening on TCP `3389`. +6. If Unreal visual inspection is needed, use Sunshine/Moonlight instead of RDP. + diff --git a/Docs/PersistenceDesignDocument.md b/Docs/PersistenceDesignDocument.md index ff4dfd0..38a9e64 100644 --- a/Docs/PersistenceDesignDocument.md +++ b/Docs/PersistenceDesignDocument.md @@ -92,6 +92,7 @@ DisplayName LastKnownTileId LastKnownTransform SurvivalSnapshot +CareHistorySnapshot InventorySnapshot UpdatedAt RecordVersion @@ -107,12 +108,39 @@ Persist the survival state needed to resume a player: - thirst; - body temperature; - injury severity; +- sickness severity; +- exhaustion; - alive/dead state if death persistence is active. Survival rates and tuning values should not be duplicated into the save unless there is a specific compatibility reason. They belong in code/config/data assets. +MVP implementation note: `UAgrarianPersistenceSubsystem::SaveCurrentWorld` +captures live Agrarian player characters into `FAgrarianSavedPlayer`, including +transform, survival snapshot, care history snapshot, and inventory stacks. +`RestorePlayers` reapplies those records to matching live characters before or +alongside world actor restore. + +## Care History Snapshot + +Reserve a long-term care history snapshot beside the immediate survival +snapshot. The MVP records neutral normalized fields only; later lifecycle and +aging systems will decide how aggressively those histories affect health, +stamina, strength, endurance, disease resistance, injury risk, recovery, visual +aging, and old-age decline. + +Reserved care history fields: + +- nutrition quality; +- illness burden; +- injury burden; +- sleep quality; +- shelter quality; +- stress burden; +- workload burden; +- treatment quality. + ## Inventory Snapshot Persist inventory as item definition IDs and stack counts. diff --git a/Docs/TechnicalDesignDocument.md b/Docs/TechnicalDesignDocument.md index 061c9c0..dfefbe8 100644 --- a/Docs/TechnicalDesignDocument.md +++ b/Docs/TechnicalDesignDocument.md @@ -108,12 +108,35 @@ still derive from the represented map tile. Near-term technical work: -- add Ground Zero local time-zone metadata; -- add sunrise/sunset lookup or approximation by latitude/longitude; +- add Ground Zero local time-zone metadata; completed for the current C++ game-state default. +- add sunrise/sunset lookup or approximation by latitude/longitude; completed as a tile-aware NOAA approximation in `AAgrarianGameState`. - map real weather snapshots into internal Agrarian weather states; - cache weather snapshots server-side; - keep deterministic fallback weather when external data is unavailable. +The repeatable solar metadata data path is +`Scripts/generate_tile_solar_metadata.py`. It reads the tile registry and emits +metadata only for source-backed, generated, validated, packaged, or published +tiles with explicit time-zone data. Placeholder/unknown tiles are skipped so the +future Earth-scale registry does not generate or fetch data for theoretical +tiles that do not exist yet. + +Calendar conversion helpers live in `AAgrarianGameState` and keep the MVP target +of `4 real hours = 1 in-game day`. The same game state now exposes replicated +calendar year/day, absolute-day, season, real-hour conversion, long-task +progress, and crop-season fit helpers. Crop checks use the active tile's +growing-zone profile, including frost-free days and a crop safety buffer, so a +long-maturity crop can be rejected or marked marginal in regions with short +seasons. + +The repeatable growing-zone metadata data path is +`Scripts/generate_tile_growing_zone_metadata.py`. It reads the tile registry and +emits metadata only for source-backed, generated, validated, packaged, or +published tiles with explicit growing-zone data. Ground Zero currently uses a +conservative Pacifica coastal profile; later regional expansion should replace +or enrich these overrides with authoritative zone, climate, and temperature +datasets. + ## Terrain And Tile Delivery ### MVP Tile diff --git a/Docs/TemplateVariantDecision.md b/Docs/TemplateVariantDecision.md index ebc8bdb..483d4aa 100644 --- a/Docs/TemplateVariantDecision.md +++ b/Docs/TemplateVariantDecision.md @@ -18,9 +18,12 @@ Removed: Keep temporarily: -- `ThirdPerson` base character/input assets needed by the current player - Blueprint and early automation until Agrarian-specific player assets and the - MVP character selection flow replace them. +- `ThirdPerson` compatibility assets only while references are audited. The + active player, controller, and game mode Blueprint paths now live under + `/Game/Agrarian/Blueprints/Characters`. +- `LevelPrototyping` meshes needed by current setup scripts and prototype + Blueprints until the version 0.1 environment pass replaces them with + Agrarian-native placeholder meshes. - Shared mannequin/animation content only where current Agrarian assets still reference it. @@ -43,15 +46,20 @@ Cleanup performed: 2. Removed matching content folders and external actor/object data. 3. Removed `StateTreeModule` and `GameplayStateTreeModule`. 4. Disabled `StateTree` and `GameplayStateTree` in `AgrarianGame.uproject`. -5. Kept the current `ThirdPerson` character path until the MVP landing page and - male/female character selection flow are implemented. +5. Replaced the current `ThirdPerson` player/game mode paths with Agrarian-owned + player, controller, and game mode Blueprint assets. +6. Kept the current `LevelPrototyping` mesh path until the Ground Zero + environment pass replaces the simple cube/cylinder placeholder dependencies. ## Current State Decision and cleanup are complete for the unused starter variants. Remaining -starter/prototype dependencies are intentionally narrow: +starter/prototype dependencies are intentionally narrow and no longer block +version `0.01` project-structure completion: -- `ThirdPerson` stays until Agrarian-specific player character assets and the - MVP character selection flow replace it. +- `ThirdPerson` active player/game mode paths have been replaced. Remaining + ThirdPerson compatibility assets should be deleted only after redirector and + editor reference checks confirm they are unused. - `LevelPrototyping` stays because current Agrarian setup scripts and prototype - Blueprints still use its simple cube/cylinder meshes. + Blueprints still use its simple cube/cylinder meshes. This is now tracked as + version `0.1` environment replacement work. diff --git a/Docs/Terrain/TileRegistrySchema.md b/Docs/Terrain/TileRegistrySchema.md index e640982..cc5e165 100644 --- a/Docs/Terrain/TileRegistrySchema.md +++ b/Docs/Terrain/TileRegistrySchema.md @@ -69,6 +69,15 @@ Required fields: - `created_at` - `updated_at` +Optional solar/time fields should be stored only for tiles that have real source +work or a published package, not for every theoretical 1 km square. The +Earth-scale registry may eventually contain hundreds of millions of possible +tiles, so solar metadata is generated on demand for existing/active tiles. + +Optional growing-zone fields follow the same rule. Crop viability, frost-free +windows, and growing-season length are generated only for tiles with real source +work or published packages and explicit growing-zone data. + ### `terrain_tile_neighbors` Tracks adjacency for stitching and prefetching. @@ -93,6 +102,40 @@ Required fields: - `source_version` - `coverage_status` +### `terrain_tile_solar_metadata` + +Tracks local time-zone and solar lookup metadata for tiles that actually exist +in the registry as source-backed, generated, validated, packaged, or published +tiles. + +Required fields: + +- `tile_id` +- `time_zone_id` +- `standard_utc_offset_hours` +- `daylight_utc_offset_hours` +- `solar_model` +- `generated_at` + +### `terrain_tile_growing_season_metadata` + +Tracks the active growing zone and season window for source-backed tiles. Crop +systems use this table to decide whether a crop with a given maturity time can +finish before the represented region's growing season closes. + +Required fields: + +- `tile_id` +- `growing_zone_label` +- `climate_profile` +- `growing_season_start_day` +- `growing_season_end_day` +- `frost_free_days` +- `min_average_growing_temp_c` +- `crop_safety_buffer_days` +- `data_basis` +- `generated_at` + ### `terrain_tile_packages` Tracks generated downloadable packages. diff --git a/Scripts/PackageWindowsDevelopment.bat b/Scripts/PackageWindowsDevelopment.bat index d46f2ec..16366ec 100644 --- a/Scripts/PackageWindowsDevelopment.bat +++ b/Scripts/PackageWindowsDevelopment.bat @@ -63,7 +63,7 @@ call "%RUN_UAT%" BuildCookRun ^ -pak ^ -archive ^ -archivedirectory="%ARCHIVE_DIR%" ^ - -map=/Game/Agrarian/Maps/L_GroundZeroTerrain_Test+/Game/ThirdPerson/Lvl_ThirdPerson ^ + -map=/Game/Agrarian/Maps/L_GroundZeroTerrain_Test ^ -prereqs ^ -utf8output ^ -NoUBA > "%LOG_FILE%" 2>&1 diff --git a/Scripts/generate_tile_growing_zone_metadata.py b/Scripts/generate_tile_growing_zone_metadata.py new file mode 100644 index 0000000..c5a880b --- /dev/null +++ b/Scripts/generate_tile_growing_zone_metadata.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +"""Generate growing-zone metadata for existing Agrarian terrain tiles. + +Only source-backed/generated tiles with explicit profiles are emitted. That +keeps the Earth-scale path repeatable without fetching or calculating metadata +for placeholder squares that do not exist in-game yet. +""" + +from __future__ import annotations + +import json +from datetime import datetime, timezone +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +REGISTRY_PATH = ROOT / "Data" / "Tiles" / "ground_zero_tiles.json" +OUTPUT_PATH = ROOT / "Data" / "Tiles" / "tile_growing_zone_metadata.json" +GROWING_READY_STATUSES = {"source_data_found", "generated", "validated", "packaged", "published"} +GROWING_ZONE_OVERRIDES = { + "gz_us_ca_pacifica_utm10n_e544_n4160": { + "growing_zone_label": "USDA 10a", + "climate_profile": "coastal_mediterranean_mild", + "growing_season_start_day": 46, + "growing_season_end_day": 350, + "frost_free_days": 305, + "min_average_growing_temp_c": 7.0, + "crop_safety_buffer_days": 14, + "data_basis": "MVP conservative coastal Pacifica profile; replace with authoritative zone/temperature datasets during regional expansion.", + } +} + + +def main() -> None: + registry = json.loads(REGISTRY_PATH.read_text(encoding="utf-8")) + records = [] + + for tile in registry.get("tiles", []): + tile_id = tile.get("tile_id") + status = tile.get("status") + if status not in GROWING_READY_STATUSES or tile_id not in GROWING_ZONE_OVERRIDES: + continue + + grid = tile.get("grid", {}) + records.append( + { + "tile_id": tile_id, + "center_latitude": grid.get("center_latitude"), + "center_longitude": grid.get("center_longitude"), + **GROWING_ZONE_OVERRIDES[tile_id], + } + ) + + OUTPUT_PATH.write_text( + json.dumps( + { + "schema_version": 1, + "generated_at_utc": datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z"), + "source_registry": str(REGISTRY_PATH.relative_to(ROOT)), + "generation_rule": "Only tiles with real source status and explicit growing-zone data are emitted.", + "tiles": records, + }, + indent=2, + ) + + "\n", + encoding="utf-8", + ) + + print(f"Wrote {len(records)} tile growing-zone metadata record(s) to {OUTPUT_PATH.relative_to(ROOT)}") + + +if __name__ == "__main__": + main() diff --git a/Scripts/generate_tile_solar_metadata.py b/Scripts/generate_tile_solar_metadata.py new file mode 100644 index 0000000..962e863 --- /dev/null +++ b/Scripts/generate_tile_solar_metadata.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +"""Generate solar/time-zone metadata for existing Agrarian terrain tiles. + +The script intentionally skips placeholder/unknown tiles. That keeps the +Earth-scale path cheap: solar metadata is generated only after a tile has real +source work attached. +""" + +from __future__ import annotations + +import json +import math +from datetime import datetime, timezone +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +REGISTRY_PATH = ROOT / "Data" / "Tiles" / "ground_zero_tiles.json" +OUTPUT_PATH = ROOT / "Data" / "Tiles" / "tile_solar_metadata.json" +SOLAR_READY_STATUSES = {"source_data_found", "generated", "validated", "packaged", "published"} +TIMEZONE_OVERRIDES = { + "gz_us_ca_pacifica_utm10n_e544_n4160": { + "time_zone_id": "America/Los_Angeles", + "standard_utc_offset_hours": -8.0, + "daylight_utc_offset_hours": -7.0, + } +} + + +def solar_hours(latitude: float, longitude: float, utc_offset_hours: float, day_of_year: int) -> dict: + gamma = (2.0 * math.pi / 365.0) * (day_of_year - 1) + equation_of_time = 229.18 * ( + 0.000075 + + 0.001868 * math.cos(gamma) + - 0.032077 * math.sin(gamma) + - 0.014615 * math.cos(2.0 * gamma) + - 0.040849 * math.sin(2.0 * gamma) + ) + declination = ( + 0.006918 + - 0.399912 * math.cos(gamma) + + 0.070257 * math.sin(gamma) + - 0.006758 * math.cos(2.0 * gamma) + + 0.000907 * math.sin(2.0 * gamma) + - 0.002697 * math.cos(3.0 * gamma) + + 0.00148 * math.sin(3.0 * gamma) + ) + + lat_rad = math.radians(max(-89.8, min(89.8, latitude))) + zenith_rad = math.radians(90.833) + hour_angle_arg = (math.cos(zenith_rad) / (math.cos(lat_rad) * math.cos(declination))) - ( + math.tan(lat_rad) * math.tan(declination) + ) + + solar_noon = (720.0 - (4.0 * longitude) - equation_of_time + (utc_offset_hours * 60.0)) / 60.0 + solar_noon %= 24.0 + + if hour_angle_arg <= -1.0: + return {"sunrise_hour": 0.0, "sunset_hour": 24.0, "solar_noon_hour": round(solar_noon, 3), "day_length_hours": 24.0} + if hour_angle_arg >= 1.0: + return {"sunrise_hour": round(solar_noon, 3), "sunset_hour": round(solar_noon, 3), "solar_noon_hour": round(solar_noon, 3), "day_length_hours": 0.0} + + hour_angle_deg = math.degrees(math.acos(hour_angle_arg)) + sunrise = ((solar_noon * 60.0) - (hour_angle_deg * 4.0)) / 60.0 + sunset = ((solar_noon * 60.0) + (hour_angle_deg * 4.0)) / 60.0 + + return { + "sunrise_hour": round(sunrise % 24.0, 3), + "sunset_hour": round(sunset % 24.0, 3), + "solar_noon_hour": round(solar_noon, 3), + "day_length_hours": round((sunset - sunrise), 3), + } + + +def main() -> None: + registry = json.loads(REGISTRY_PATH.read_text(encoding="utf-8")) + records = [] + + for tile in registry.get("tiles", []): + status = tile.get("status") + tile_id = tile.get("tile_id") + grid = tile.get("grid", {}) + if status not in SOLAR_READY_STATUSES or not tile_id or tile_id not in TIMEZONE_OVERRIDES: + continue + + timezone_data = TIMEZONE_OVERRIDES[tile_id] + latitude = float(grid["center_latitude"]) + longitude = float(grid["center_longitude"]) + utc_offset = float(timezone_data["daylight_utc_offset_hours"]) + sample_days = { + "march_equinox": 80, + "june_solstice": 172, + "september_equinox": 266, + "december_solstice": 355, + } + + records.append( + { + "tile_id": tile_id, + "center_latitude": latitude, + "center_longitude": longitude, + **timezone_data, + "solar_model": "NOAA approximate sunrise/sunset", + "sample_solar_hours": { + label: solar_hours(latitude, longitude, utc_offset, day) + for label, day in sample_days.items() + }, + } + ) + + OUTPUT_PATH.write_text( + json.dumps( + { + "schema_version": 1, + "generated_at_utc": datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z"), + "source_registry": str(REGISTRY_PATH.relative_to(ROOT)), + "generation_rule": "Only tiles with real source status and explicit timezone data are emitted.", + "tiles": records, + }, + indent=2, + ) + + "\n", + encoding="utf-8", + ) + + print(f"Wrote {len(records)} tile solar metadata record(s) to {OUTPUT_PATH.relative_to(ROOT)}") + + +if __name__ == "__main__": + main() diff --git a/Scripts/prune_build_logs.sh b/Scripts/prune_build_logs.sh new file mode 100644 index 0000000..a6c807c --- /dev/null +++ b/Scripts/prune_build_logs.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash +set -euo pipefail + +PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +MODE="dry-run" + +BUILD_LOG_DAYS="${BUILD_LOG_DAYS:-90}" +UNREAL_LOG_DAYS="${UNREAL_LOG_DAYS:-30}" +CRASH_DAYS="${CRASH_DAYS:-90}" +MATERIAL_STATS_DAYS="${MATERIAL_STATS_DAYS:-30}" +SHADER_DEBUG_DAYS="${SHADER_DEBUG_DAYS:-14}" + +usage() { + cat <&2 + exit 2 + ;; + esac + shift +done + +log() { + printf '[%s] %s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$*" +} + +prune_files() { + local label="$1" + local dir="$2" + local days="$3" + + if [[ ! -d "$dir" ]]; then + log "Skipping $label; directory not found: $dir" + return + fi + + log "$label: files older than $days days in $dir" + if [[ "$MODE" == "dry-run" ]]; then + find "$dir" -type f -mtime +"$days" -print + else + find "$dir" -type f -mtime +"$days" -print -delete + fi +} + +prune_empty_dirs() { + local dir="$1" + if [[ ! -d "$dir" ]]; then + return + fi + + if [[ "$MODE" == "dry-run" ]]; then + find "$dir" -mindepth 1 -type d -empty -print + else + find "$dir" -mindepth 1 -type d -empty -print -delete + fi +} + +log "Agrarian build log retention prune starting in $MODE mode" +log "Project root: $PROJECT_ROOT" + +prune_files "Build/package logs" "$PROJECT_ROOT/Saved/BuildLogs" "$BUILD_LOG_DAYS" +prune_files "Unreal/editor/runtime logs" "$PROJECT_ROOT/Saved/Logs" "$UNREAL_LOG_DAYS" +prune_files "Crash reports" "$PROJECT_ROOT/Saved/Crashes" "$CRASH_DAYS" +prune_files "Material stats" "$PROJECT_ROOT/Saved/MaterialStats" "$MATERIAL_STATS_DAYS" +prune_files "Shader output" "$PROJECT_ROOT/Saved/Shaders" "$SHADER_DEBUG_DAYS" +prune_files "Shader debug info" "$PROJECT_ROOT/Saved/ShaderDebugInfo" "$SHADER_DEBUG_DAYS" + +prune_empty_dirs "$PROJECT_ROOT/Saved/BuildLogs" +prune_empty_dirs "$PROJECT_ROOT/Saved/Logs" +prune_empty_dirs "$PROJECT_ROOT/Saved/Crashes" +prune_empty_dirs "$PROJECT_ROOT/Saved/MaterialStats" +prune_empty_dirs "$PROJECT_ROOT/Saved/Shaders" +prune_empty_dirs "$PROJECT_ROOT/Saved/ShaderDebugInfo" + +log "Agrarian build log retention prune finished in $MODE mode" diff --git a/Scripts/setup_agrarian_player_blueprints.py b/Scripts/setup_agrarian_player_blueprints.py new file mode 100644 index 0000000..035f21e --- /dev/null +++ b/Scripts/setup_agrarian_player_blueprints.py @@ -0,0 +1,67 @@ +import unreal + + +SOURCE_CHARACTER = "/Game/ThirdPerson/Blueprints/BP_ThirdPersonCharacter" +SOURCE_CONTROLLER = "/Game/ThirdPerson/Blueprints/BP_ThirdPersonPlayerController" +SOURCE_GAME_MODE = "/Game/ThirdPerson/Blueprints/BP_ThirdPersonGameMode" + +DEST_ROOT = "/Game/Agrarian/Blueprints/Characters" +DEST_CHARACTER = f"{DEST_ROOT}/BP_AgrarianPlayerCharacter" +DEST_CONTROLLER = f"{DEST_ROOT}/BP_AgrarianPlayerController" +DEST_GAME_MODE = f"{DEST_ROOT}/BP_AgrarianGameMode" + + +def ensure_directory(path): + if not unreal.EditorAssetLibrary.does_directory_exist(path): + if not unreal.EditorAssetLibrary.make_directory(path): + raise RuntimeError(f"Could not create content directory: {path}") + + +def duplicate_if_missing(source_path, destination_path): + if unreal.EditorAssetLibrary.does_asset_exist(destination_path): + unreal.log(f"Keeping existing asset: {destination_path}") + return unreal.EditorAssetLibrary.load_asset(destination_path) + + if not unreal.EditorAssetLibrary.does_asset_exist(source_path): + raise RuntimeError(f"Source asset missing: {source_path}") + + duplicated = unreal.EditorAssetLibrary.duplicate_asset(source_path, destination_path) + if not duplicated: + raise RuntimeError(f"Could not duplicate {source_path} to {destination_path}") + + unreal.log(f"Created {destination_path} from {source_path}") + return duplicated + + +def load_blueprint_class(path): + generated_class = unreal.EditorAssetLibrary.load_blueprint_class(path) + if not generated_class: + raise RuntimeError(f"Could not load generated Blueprint class: {path}") + return generated_class + + +def main(): + ensure_directory(DEST_ROOT) + + character_bp = duplicate_if_missing(SOURCE_CHARACTER, DEST_CHARACTER) + controller_bp = duplicate_if_missing(SOURCE_CONTROLLER, DEST_CONTROLLER) + game_mode_bp = duplicate_if_missing(SOURCE_GAME_MODE, DEST_GAME_MODE) + + character_class = load_blueprint_class(DEST_CHARACTER) + controller_class = load_blueprint_class(DEST_CONTROLLER) + game_mode_cdo = unreal.get_default_object(game_mode_bp.generated_class()) + + game_mode_cdo.set_editor_property("default_pawn_class", character_class) + game_mode_cdo.set_editor_property("player_controller_class", controller_class) + + unreal.EditorAssetLibrary.save_loaded_asset(character_bp) + unreal.EditorAssetLibrary.save_loaded_asset(controller_bp) + unreal.EditorAssetLibrary.save_loaded_asset(game_mode_bp) + + unreal.log( + "Agrarian player Blueprint assets are ready: " + f"{DEST_CHARACTER}, {DEST_CONTROLLER}, {DEST_GAME_MODE}" + ) + + +main() diff --git a/Scripts/setup_camera_toggle_input.py b/Scripts/setup_camera_toggle_input.py index e1007b8..ce3cc87 100644 --- a/Scripts/setup_camera_toggle_input.py +++ b/Scripts/setup_camera_toggle_input.py @@ -77,7 +77,7 @@ def main(): map_key(context, toggle_action, "Gamepad_RightThumbstick") unreal.EditorAssetLibrary.save_loaded_asset(context) - character_bp = load("/Game/ThirdPerson/Blueprints/BP_ThirdPersonCharacter") + character_bp = load("/Game/Agrarian/Blueprints/Characters/BP_AgrarianPlayerCharacter") character_cdo = unreal.get_default_object(character_bp.generated_class()) character_cdo.set_editor_property("ToggleCameraAction", toggle_action) unreal.EditorAssetLibrary.save_loaded_asset(character_bp) diff --git a/Scripts/setup_interact_input.py b/Scripts/setup_interact_input.py index de03bd2..a0c43dd 100644 --- a/Scripts/setup_interact_input.py +++ b/Scripts/setup_interact_input.py @@ -75,7 +75,7 @@ def main(): map_key(context, interact_action, "Gamepad_FaceButton_Left") unreal.EditorAssetLibrary.save_loaded_asset(context) - character_bp = load("/Game/ThirdPerson/Blueprints/BP_ThirdPersonCharacter") + character_bp = load("/Game/Agrarian/Blueprints/Characters/BP_AgrarianPlayerCharacter") character_cdo = unreal.get_default_object(character_bp.generated_class()) character_cdo.set_editor_property("InteractAction", interact_action) unreal.EditorAssetLibrary.save_loaded_asset(character_bp) diff --git a/Scripts/setup_interaction_prompt.py b/Scripts/setup_interaction_prompt.py index 77f9b30..ce97fef 100644 --- a/Scripts/setup_interaction_prompt.py +++ b/Scripts/setup_interaction_prompt.py @@ -9,7 +9,7 @@ def load(path): def main(): - game_mode_bp = load("/Game/ThirdPerson/Blueprints/BP_ThirdPersonGameMode") + game_mode_bp = load("/Game/Agrarian/Blueprints/Characters/BP_AgrarianGameMode") game_mode_cdo = unreal.get_default_object(game_mode_bp.generated_class()) hud_class = unreal.load_class(None, "/Script/AgrarianGame.AgrarianDebugHUD") if not hud_class: diff --git a/Scripts/setup_movement_baseline.py b/Scripts/setup_movement_baseline.py index 4358def..3e92267 100644 --- a/Scripts/setup_movement_baseline.py +++ b/Scripts/setup_movement_baseline.py @@ -24,7 +24,7 @@ def load(path): def main(): - character_bp = load("/Game/ThirdPerson/Blueprints/BP_ThirdPersonCharacter") + character_bp = load("/Game/Agrarian/Blueprints/Characters/BP_AgrarianPlayerCharacter") character_cdo = unreal.get_default_object(character_bp.generated_class()) for property_name, value in MOVEMENT_DEFAULTS.items(): diff --git a/Scripts/setup_sprint_input.py b/Scripts/setup_sprint_input.py index 3bea140..4aed351 100644 --- a/Scripts/setup_sprint_input.py +++ b/Scripts/setup_sprint_input.py @@ -77,7 +77,7 @@ def main(): map_key(context, sprint_action, "Gamepad_LeftThumbstick") unreal.EditorAssetLibrary.save_loaded_asset(context) - character_bp = load("/Game/ThirdPerson/Blueprints/BP_ThirdPersonCharacter") + character_bp = load("/Game/Agrarian/Blueprints/Characters/BP_AgrarianPlayerCharacter") character_cdo = unreal.get_default_object(character_bp.generated_class()) character_cdo.set_editor_property("SprintAction", sprint_action) unreal.EditorAssetLibrary.save_loaded_asset(character_bp) diff --git a/Scripts/setup_stance_input.py b/Scripts/setup_stance_input.py index beb60a3..b8fc249 100644 --- a/Scripts/setup_stance_input.py +++ b/Scripts/setup_stance_input.py @@ -87,7 +87,7 @@ def main(): map_key(context, prone_action, "Gamepad_LeftShoulder") unreal.EditorAssetLibrary.save_loaded_asset(context) - character_bp = load("/Game/ThirdPerson/Blueprints/BP_ThirdPersonCharacter") + character_bp = load("/Game/Agrarian/Blueprints/Characters/BP_AgrarianPlayerCharacter") character_cdo = unreal.get_default_object(character_bp.generated_class()) character_cdo.set_editor_property("CrouchAction", crouch_action) character_cdo.set_editor_property("ProneAction", prone_action) diff --git a/Scripts/setup_test_map_placements.py b/Scripts/setup_test_map_placements.py index 7bd2afc..8bebee0 100644 --- a/Scripts/setup_test_map_placements.py +++ b/Scripts/setup_test_map_placements.py @@ -1,7 +1,7 @@ import unreal -MAP_PATH = "/Game/ThirdPerson/Lvl_ThirdPerson" +MAP_PATH = "/Game/Agrarian/Maps/L_GroundZeroTerrain_Test" PLACEMENTS = [ { diff --git a/Scripts/verify_agrarian_calendar_helpers.py b/Scripts/verify_agrarian_calendar_helpers.py new file mode 100644 index 0000000..bed300e --- /dev/null +++ b/Scripts/verify_agrarian_calendar_helpers.py @@ -0,0 +1,86 @@ +from pathlib import Path +import json + + +ROOT = Path(__file__).resolve().parents[1] +TYPES_H = ROOT / "Source" / "AgrarianGame" / "AgrarianTypes.h" +GAME_STATE_H = ROOT / "Source" / "AgrarianGame" / "AgrarianGameState.h" +GAME_STATE_CPP = ROOT / "Source" / "AgrarianGame" / "AgrarianGameState.cpp" +GENERATOR = ROOT / "Scripts" / "generate_tile_growing_zone_metadata.py" +METADATA = ROOT / "Data" / "Tiles" / "tile_growing_zone_metadata.json" +SCHEMA = ROOT / "Data" / "Tiles" / "tile_registry.schema.json" +SQL = ROOT / "Data" / "Tiles" / "tile_registry.sql" +ROADMAP = ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md" + + +EXPECTED = { + TYPES_H: [ + "FAgrarianCalendarSnapshot", + "AbsoluteDay", + "FAgrarianGrowingSeasonProfile", + "CropSafetyBufferDays", + "FAgrarianCropSeasonAssessment", + "bCanPlantToday", + ], + GAME_STATE_H: [ + "DaysPerAgrarianYear", + "ActiveYear", + "ActiveGrowingSeason", + "ConvertAgrarianDaysToRealHours", + "ConvertRealHoursToAgrarianDays", + "GetLongTaskProgress", + "IsDayInsideActiveGrowingSeason", + "AssessCropForActiveGrowingSeason", + ], + GAME_STATE_CPP: [ + "DOREPLIFETIME(AAgrarianGameState, ActiveGrowingSeason);", + "FAgrarianCalendarSnapshot AAgrarianGameState::GetCalendarSnapshot() const", + "float AAgrarianGameState::ConvertAgrarianDaysToRealHours", + "FAgrarianCropSeasonAssessment AAgrarianGameState::AssessCropForActiveGrowingSeason", + "Crop maturity is too long for this tile's frost-free growing window.", + ], + GENERATOR: [ + "GROWING_READY_STATUSES", + "GROWING_ZONE_OVERRIDES", + "gz_us_ca_pacifica_utm10n_e544_n4160", + "Only source-backed/generated tiles with explicit profiles are emitted.", + ], + SCHEMA: ["growing_season_metadata", "growing_zone_label", "frost_free_days"], + SQL: ["terrain_tile_growing_season_metadata", "crop_safety_buffer_days"], + ROADMAP: ["Add Agrarian calendar conversion helpers"], +} + + +def assert_contains() -> None: + missing = [] + for path, snippets in EXPECTED.items(): + text = path.read_text(encoding="utf-8") + for snippet in snippets: + if snippet not in text: + missing.append(f"{path.relative_to(ROOT)}: {snippet}") + if missing: + raise RuntimeError("Agrarian calendar verification failed: " + "; ".join(missing)) + + +def assert_metadata() -> None: + data = json.loads(METADATA.read_text(encoding="utf-8")) + tiles = data.get("tiles", []) + if len(tiles) != 1: + raise RuntimeError(f"Expected exactly one growing-zone tile, got {len(tiles)}") + tile = tiles[0] + if tile.get("tile_id") != "gz_us_ca_pacifica_utm10n_e544_n4160": + raise RuntimeError(f"Unexpected growing-zone tile id: {tile.get('tile_id')}") + if tile.get("growing_zone_label") != "USDA 10a": + raise RuntimeError("Ground Zero growing zone is missing or incorrect") + if tile.get("frost_free_days", 0) < 250: + raise RuntimeError(f"Ground Zero frost-free window is unexpectedly short: {tile}") + + +def main() -> None: + assert_contains() + assert_metadata() + print("Agrarian calendar and growing-zone helper verification complete.") + + +if __name__ == "__main__": + main() diff --git a/Scripts/verify_agrarian_player_blueprints.py b/Scripts/verify_agrarian_player_blueprints.py new file mode 100644 index 0000000..9e29234 --- /dev/null +++ b/Scripts/verify_agrarian_player_blueprints.py @@ -0,0 +1,66 @@ +import unreal + + +CHARACTER_BLUEPRINT_PATH = "/Game/Agrarian/Blueprints/Characters/BP_AgrarianPlayerCharacter" +CONTROLLER_BLUEPRINT_PATH = "/Game/Agrarian/Blueprints/Characters/BP_AgrarianPlayerController" +GAME_MODE_BLUEPRINT_PATH = "/Game/Agrarian/Blueprints/Characters/BP_AgrarianGameMode" + + +def class_path(value): + if not value: + return "" + return value.get_path_name().split(".")[0] + + +def load_blueprint_class(path): + generated_class = unreal.EditorAssetLibrary.load_blueprint_class(path) + if not generated_class: + raise RuntimeError(f"Could not load generated Blueprint class: {path}") + return generated_class + + +def main(): + failures = [] + + for path in [CHARACTER_BLUEPRINT_PATH, CONTROLLER_BLUEPRINT_PATH, GAME_MODE_BLUEPRINT_PATH]: + if not unreal.EditorAssetLibrary.does_asset_exist(path): + failures.append(f"{path} missing") + + character_class = load_blueprint_class(CHARACTER_BLUEPRINT_PATH) + controller_class = load_blueprint_class(CONTROLLER_BLUEPRINT_PATH) + game_mode_class = load_blueprint_class(GAME_MODE_BLUEPRINT_PATH) + game_mode_cdo = unreal.get_default_object(game_mode_class) + + default_pawn_path = class_path(game_mode_cdo.get_editor_property("default_pawn_class")) + player_controller_path = class_path(game_mode_cdo.get_editor_property("player_controller_class")) + + if default_pawn_path != CHARACTER_BLUEPRINT_PATH: + failures.append( + f"Game mode default pawn expected {CHARACTER_BLUEPRINT_PATH}, got {default_pawn_path}" + ) + + if player_controller_path != CONTROLLER_BLUEPRINT_PATH: + failures.append( + "Game mode player controller expected " + f"{CONTROLLER_BLUEPRINT_PATH}, got {player_controller_path}" + ) + + character_cdo = unreal.get_default_object(character_class) + if not character_cdo: + failures.append(f"{CHARACTER_BLUEPRINT_PATH} CDO missing") + + controller_cdo = unreal.get_default_object(controller_class) + if not controller_cdo: + failures.append(f"{CONTROLLER_BLUEPRINT_PATH} CDO missing") + + if failures: + raise RuntimeError("Agrarian player Blueprint verification failed: " + "; ".join(failures)) + + unreal.log( + "Agrarian player Blueprint verification complete: " + "native Agrarian character, controller, and game mode asset paths load correctly." + ) + + +main() + diff --git a/Scripts/verify_basic_animation_blueprint.py b/Scripts/verify_basic_animation_blueprint.py new file mode 100644 index 0000000..4bab012 --- /dev/null +++ b/Scripts/verify_basic_animation_blueprint.py @@ -0,0 +1,72 @@ +import unreal + + +CHARACTER_BLUEPRINT_PATH = "/Game/Agrarian/Blueprints/Characters/BP_AgrarianPlayerCharacter" +EXPECTED_MESH_PATH = "/Game/Characters/Mannequins/Meshes/SKM_Quinn_Simple" +EXPECTED_ANIM_BLUEPRINT_PATH = "/Game/Characters/Mannequins/Anims/Unarmed/ABP_Unarmed" +REQUIRED_ANIMATION_ASSETS = [ + "/Game/Characters/Mannequins/Anims/Unarmed/BS_Idle_Walk_Run", + "/Game/Characters/Mannequins/Anims/Unarmed/MM_Idle", + "/Game/Characters/Mannequins/Anims/Unarmed/Jump/MM_Jump", + "/Game/Characters/Mannequins/Anims/Unarmed/Jump/MM_Fall_Loop", + "/Game/Characters/Mannequins/Anims/Unarmed/Jump/MM_Land", +] + + +def object_path(value): + if not value: + return "" + return value.get_path_name().split(".")[0] + + +def main(): + failures = [] + + for asset_path in [EXPECTED_MESH_PATH, EXPECTED_ANIM_BLUEPRINT_PATH] + REQUIRED_ANIMATION_ASSETS: + if not unreal.EditorAssetLibrary.does_asset_exist(asset_path): + failures.append(f"{asset_path} missing") + + character_class = unreal.EditorAssetLibrary.load_blueprint_class(CHARACTER_BLUEPRINT_PATH) + if not character_class: + failures.append(f"{CHARACTER_BLUEPRINT_PATH} generated class missing") + else: + character_cdo = unreal.get_default_object(character_class) + mesh_component = character_cdo.get_editor_property("mesh") + if not mesh_component: + failures.append(f"{CHARACTER_BLUEPRINT_PATH} mesh component missing") + else: + skeletal_mesh_path = object_path(mesh_component.get_skeletal_mesh_asset()) + if skeletal_mesh_path != EXPECTED_MESH_PATH: + failures.append( + f"{CHARACTER_BLUEPRINT_PATH} skeletal mesh expected " + f"{EXPECTED_MESH_PATH}, got {skeletal_mesh_path}" + ) + + anim_class = mesh_component.get_editor_property("anim_class") + anim_class_path = anim_class.get_path_name().split(".")[0] if anim_class else "" + if anim_class_path != EXPECTED_ANIM_BLUEPRINT_PATH: + failures.append( + f"{CHARACTER_BLUEPRINT_PATH} anim class expected " + f"{EXPECTED_ANIM_BLUEPRINT_PATH}, got {anim_class_path}" + ) + + anim_blueprint_class = unreal.EditorAssetLibrary.load_blueprint_class(EXPECTED_ANIM_BLUEPRINT_PATH) + if not anim_blueprint_class: + failures.append(f"{EXPECTED_ANIM_BLUEPRINT_PATH} generated class missing") + elif "ABP_Unarmed_C" not in anim_blueprint_class.get_name(): + failures.append( + f"{EXPECTED_ANIM_BLUEPRINT_PATH} generated class should be ABP_Unarmed_C, " + f"got {anim_blueprint_class.get_name()}" + ) + + if failures: + raise RuntimeError("Basic animation Blueprint verification failed: " + "; ".join(failures)) + + unreal.log( + "Basic animation Blueprint verification complete: " + "BP_AgrarianPlayerCharacter uses SKM_Quinn_Simple with ABP_Unarmed and required " + "idle, locomotion, jump, fall, and land assets are present." + ) + + +main() diff --git a/Scripts/verify_camera_toggle_input.py b/Scripts/verify_camera_toggle_input.py index aa6e4b4..c56c29c 100644 --- a/Scripts/verify_camera_toggle_input.py +++ b/Scripts/verify_camera_toggle_input.py @@ -23,7 +23,7 @@ def mapping_found(context, action, key_name): def main(): action = load("/Game/Input/Actions/IA_ToggleCamera") context = load("/Game/Input/IMC_Default") - character_bp = load("/Game/ThirdPerson/Blueprints/BP_ThirdPersonCharacter") + character_bp = load("/Game/Agrarian/Blueprints/Characters/BP_AgrarianPlayerCharacter") character_cdo = unreal.get_default_object(character_bp.generated_class()) missing = [] @@ -33,7 +33,7 @@ def main(): assigned_action = character_cdo.get_editor_property("ToggleCameraAction") if assigned_action != action: - missing.append("BP_ThirdPersonCharacter ToggleCameraAction is not IA_ToggleCamera") + missing.append("BP_AgrarianPlayerCharacter ToggleCameraAction is not IA_ToggleCamera") if missing: raise RuntimeError("Camera toggle input verification failed: " + "; ".join(missing)) diff --git a/Scripts/verify_care_history_fields.py b/Scripts/verify_care_history_fields.py new file mode 100644 index 0000000..f12806a --- /dev/null +++ b/Scripts/verify_care_history_fields.py @@ -0,0 +1,59 @@ +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +FILES = { + "AgrarianTypes.h": ROOT / "Source" / "AgrarianGame" / "AgrarianTypes.h", + "AgrarianSurvivalComponent.h": ROOT / "Source" / "AgrarianGame" / "AgrarianSurvivalComponent.h", + "AgrarianSurvivalComponent.cpp": ROOT / "Source" / "AgrarianGame" / "AgrarianSurvivalComponent.cpp", + "AgrarianSaveGame.h": ROOT / "Source" / "AgrarianGame" / "AgrarianSaveGame.h", + "AgrarianDebugHUD.cpp": ROOT / "Source" / "AgrarianGame" / "AgrarianDebugHUD.cpp", + "PersistenceDesignDocument.md": ROOT / "Docs" / "PersistenceDesignDocument.md", +} + +CARE_FIELDS = [ + "NutritionQuality", + "IllnessBurden", + "InjuryBurden", + "SleepQuality", + "ShelterQuality", + "StressBurden", + "WorkloadBurden", + "TreatmentQuality", +] + +EXPECTED = { + "AgrarianTypes.h": ["struct FAgrarianCareHistorySnapshot", *CARE_FIELDS], + "AgrarianSurvivalComponent.h": [ + "FAgrarianCareHistorySnapshot CareHistory;", + "void OnRep_CareHistory();", + "void ClampCareHistory();", + ], + "AgrarianSurvivalComponent.cpp": [ + "DOREPLIFETIME(UAgrarianSurvivalComponent, CareHistory);", + "void UAgrarianSurvivalComponent::OnRep_CareHistory()", + "void UAgrarianSurvivalComponent::ClampCareHistory()", + *CARE_FIELDS, + ], + "AgrarianSaveGame.h": ["FAgrarianCareHistorySnapshot CareHistory;"], + "AgrarianDebugHUD.cpp": ["Care N/S/T", "Burden I/I/S/W"], + "PersistenceDesignDocument.md": ["CareHistorySnapshot", "Care History Snapshot"], +} + + +def main(): + missing = [] + for label, path in FILES.items(): + text = path.read_text(encoding="utf-8") + for snippet in EXPECTED[label]: + if snippet not in text: + missing.append(f"{label}: {snippet}") + + if missing: + raise RuntimeError("Care history verification failed: " + "; ".join(missing)) + + print("Agrarian care history field verification complete.") + + +if __name__ == "__main__": + main() diff --git a/Scripts/verify_critical_stats_hud.py b/Scripts/verify_critical_stats_hud.py new file mode 100644 index 0000000..38369c3 --- /dev/null +++ b/Scripts/verify_critical_stats_hud.py @@ -0,0 +1,50 @@ +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +FILES = { + "AgrarianDebugHUD.h": ROOT / "Source" / "AgrarianGame" / "AgrarianDebugHUD.h", + "AgrarianDebugHUD.cpp": ROOT / "Source" / "AgrarianGame" / "AgrarianDebugHUD.cpp", +} + +EXPECTED = { + "AgrarianDebugHUD.h": [ + "bool bShowCriticalStatsHUD = true;", + "float CriticalStatsTextScale = 1.0f;", + "void DrawCriticalStats(const UAgrarianSurvivalComponent* SurvivalComponent);", + "void DrawScaledLine(const FString& Text, float X, float& Y, float Scale, const FColor& Color = FColor::White);", + ], + "AgrarianDebugHUD.cpp": [ + "DrawCriticalStats(AgrarianCharacter->GetSurvivalComponent());", + "void AAgrarianDebugHUD::DrawCriticalStats", + "SURVIVAL", + "Health", + "Stamina", + "Food", + "Water", + "Temp", + "Exhaust", + "Injury", + "Sickness", + "StatusColor", + "void AAgrarianDebugHUD::DrawScaledLine", + ], +} + + +def main(): + missing = [] + for label, path in FILES.items(): + text = path.read_text(encoding="utf-8") + for snippet in EXPECTED[label]: + if snippet not in text: + missing.append(f"{label}: {snippet}") + + if missing: + raise RuntimeError("Critical stats HUD verification failed: " + "; ".join(missing)) + + print("Agrarian critical stats HUD verification complete.") + + +if __name__ == "__main__": + main() diff --git a/Scripts/verify_exhaustion_stat.py b/Scripts/verify_exhaustion_stat.py new file mode 100644 index 0000000..043d87b --- /dev/null +++ b/Scripts/verify_exhaustion_stat.py @@ -0,0 +1,65 @@ +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +FILES = { + "AgrarianTypes.h": ROOT / "Source" / "AgrarianGame" / "AgrarianTypes.h", + "AgrarianSurvivalComponent.h": ROOT / "Source" / "AgrarianGame" / "AgrarianSurvivalComponent.h", + "AgrarianSurvivalComponent.cpp": ROOT / "Source" / "AgrarianGame" / "AgrarianSurvivalComponent.cpp", + "AgrarianGameCharacter.cpp": ROOT / "Source" / "AgrarianGame" / "AgrarianGameCharacter.cpp", + "AgrarianDebugHUD.cpp": ROOT / "Source" / "AgrarianGame" / "AgrarianDebugHUD.cpp", + "AgrarianGamePlayerController.cpp": ROOT / "Source" / "AgrarianGame" / "AgrarianGamePlayerController.cpp", +} + +EXPECTED = { + "AgrarianTypes.h": [ + "float Exhaustion = 0.0f;", + ], + "AgrarianSurvivalComponent.h": [ + "float ExhaustionGainPerLowStaminaSecond = 0.35f;", + "float ExhaustionRecoveryPerSecond = 0.08f;", + "float LowStaminaExhaustionThreshold = 20.0f;", + "void AddExhaustion(float Amount);", + "void ReduceExhaustion(float Amount);", + ], + "AgrarianSurvivalComponent.cpp": [ + "Survival.Exhaustion += ExhaustionGainPerLowStaminaSecond * DeltaTime;", + "Survival.Exhaustion -= ExhaustionRecoveryPerSecond * DeltaTime;", + "Survival.Exhaustion += PositiveAmount * 0.05f;", + "void UAgrarianSurvivalComponent::AddExhaustion", + "void UAgrarianSurvivalComponent::ReduceExhaustion", + "Survival.Exhaustion = FMath::Clamp(Survival.Exhaustion, 0.0f, 100.0f);", + ], + "AgrarianGameCharacter.cpp": [ + "SurvivalComponent->Survival.Exhaustion < 85.0f", + "const float ExhaustionMultiplier = FMath::GetMappedRangeValueClamped", + "HungerMultiplier * ThirstMultiplier * InjuryMultiplier * ExhaustionMultiplier", + ], + "AgrarianDebugHUD.cpp": [ + "Exhaust:", + "Survival.Exhaustion", + ], + "AgrarianGamePlayerController.cpp": [ + "Exhaustion %.1f", + "Survival.Exhaustion", + "SurvivalComponent->ReduceExhaustion(100.0f);", + ], +} + + +def main(): + missing = [] + for label, path in FILES.items(): + text = path.read_text(encoding="utf-8") + for snippet in EXPECTED[label]: + if snippet not in text: + missing.append(f"{label}: {snippet}") + + if missing: + raise RuntimeError("Exhaustion stat verification failed: " + "; ".join(missing)) + + print("Agrarian exhaustion stat verification complete.") + + +if __name__ == "__main__": + main() diff --git a/Scripts/verify_interact_input.py b/Scripts/verify_interact_input.py index f8bf3c4..803eda9 100644 --- a/Scripts/verify_interact_input.py +++ b/Scripts/verify_interact_input.py @@ -23,7 +23,7 @@ def mapping_found(context, action, key_name): def main(): action = load("/Game/Input/Actions/IA_Interact") context = load("/Game/Input/IMC_Default") - character_bp = load("/Game/ThirdPerson/Blueprints/BP_ThirdPersonCharacter") + character_bp = load("/Game/Agrarian/Blueprints/Characters/BP_AgrarianPlayerCharacter") character_cdo = unreal.get_default_object(character_bp.generated_class()) missing = [] @@ -33,7 +33,7 @@ def main(): assigned_action = character_cdo.get_editor_property("InteractAction") if assigned_action != action: - missing.append("BP_ThirdPersonCharacter InteractAction is not IA_Interact") + missing.append("BP_AgrarianPlayerCharacter InteractAction is not IA_Interact") if missing: raise RuntimeError("Interact input verification failed: " + "; ".join(missing)) diff --git a/Scripts/verify_interaction_prompt.py b/Scripts/verify_interaction_prompt.py index 2533598..8c71eb9 100644 --- a/Scripts/verify_interaction_prompt.py +++ b/Scripts/verify_interaction_prompt.py @@ -9,7 +9,7 @@ def load(path): def main(): - game_mode_bp = load("/Game/ThirdPerson/Blueprints/BP_ThirdPersonGameMode") + game_mode_bp = load("/Game/Agrarian/Blueprints/Characters/BP_AgrarianGameMode") game_mode_cdo = unreal.get_default_object(game_mode_bp.generated_class()) expected_hud_class = unreal.load_class(None, "/Script/AgrarianGame.AgrarianDebugHUD") character_class = unreal.load_class(None, "/Script/AgrarianGame.AgrarianGameCharacter") @@ -20,7 +20,7 @@ def main(): if not expected_hud_class: missing.append("could not load AgrarianDebugHUD class") elif game_mode_cdo.get_editor_property("hud_class") != expected_hud_class: - missing.append("BP_ThirdPersonGameMode HUD class is not AgrarianDebugHUD") + missing.append("BP_AgrarianGameMode HUD class is not AgrarianDebugHUD") elif hud_cdo: if not bool(hud_cdo.get_editor_property("bShowInteractionPrompt")): missing.append("AgrarianDebugHUD bShowInteractionPrompt is disabled") diff --git a/Scripts/verify_movement_baseline.py b/Scripts/verify_movement_baseline.py index b39199c..d44f700 100644 --- a/Scripts/verify_movement_baseline.py +++ b/Scripts/verify_movement_baseline.py @@ -25,7 +25,7 @@ def load(path): def main(): - character_bp = load("/Game/ThirdPerson/Blueprints/BP_ThirdPersonCharacter") + character_bp = load("/Game/Agrarian/Blueprints/Characters/BP_AgrarianPlayerCharacter") character_cdo = unreal.get_default_object(character_bp.generated_class()) mismatches = [] diff --git a/Scripts/verify_playable_loop_smoke.py b/Scripts/verify_playable_loop_smoke.py index b4997be..fb863ef 100644 --- a/Scripts/verify_playable_loop_smoke.py +++ b/Scripts/verify_playable_loop_smoke.py @@ -1,8 +1,8 @@ import unreal -MAP_PATH = "/Game/ThirdPerson/Lvl_ThirdPerson" -CHARACTER_CLASS_PATH = "/Game/ThirdPerson/Blueprints/BP_ThirdPersonCharacter" +MAP_PATH = "/Game/Agrarian/Maps/L_GroundZeroTerrain_Test" +CHARACTER_CLASS_PATH = "/Game/Agrarian/Blueprints/Characters/BP_AgrarianPlayerCharacter" SHELTER_RECIPE_PATH = "/Game/Agrarian/DataAssets/Recipes/DA_Recipe_PrimitiveShelter" FRAME_RECIPE_PATH = "/Game/Agrarian/DataAssets/Recipes/DA_Recipe_PrimitiveFrame" WALL_PANEL_RECIPE_PATH = "/Game/Agrarian/DataAssets/Recipes/DA_Recipe_PrimitiveWallPanel" diff --git a/Scripts/verify_player_core_replication.py b/Scripts/verify_player_core_replication.py new file mode 100644 index 0000000..2382553 --- /dev/null +++ b/Scripts/verify_player_core_replication.py @@ -0,0 +1,55 @@ +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +HEADER = ROOT / "Source" / "AgrarianGame" / "AgrarianGameCharacter.h" +SOURCE = ROOT / "Source" / "AgrarianGame" / "AgrarianGameCharacter.cpp" + + +EXPECTED_HEADER_SNIPPETS = [ + "ReplicatedUsing = OnRep_SprintState", + "ReplicatedUsing = OnRep_ProneState", + "ReplicatedUsing = OnRep_MovementModifierState", + "void OnRep_MovementModifierState();", + "void ServerSetWantsToSprint(bool bNewWantsToSprint);", + "void ServerSetProne(bool bNewProne);", + "void ServerSetTerrainMovementMultiplier(float NewTerrainMovementMultiplier);", +] + +EXPECTED_SOURCE_SNIPPETS = [ + "bReplicates = true;", + "SetReplicateMovement(true);", + "DOREPLIFETIME(AAgrarianGameCharacter, bWantsToSprint);", + "DOREPLIFETIME(AAgrarianGameCharacter, bIsProne);", + "DOREPLIFETIME(AAgrarianGameCharacter, AgeYears);", + "DOREPLIFETIME(AAgrarianGameCharacter, PhysicalConditionMultiplier);", + "DOREPLIFETIME(AAgrarianGameCharacter, StrengthMultiplier);", + "DOREPLIFETIME(AAgrarianGameCharacter, EnduranceMultiplier);", + "DOREPLIFETIME(AAgrarianGameCharacter, TerrainMovementMultiplier);", + "void AAgrarianGameCharacter::OnRep_SprintState()", + "void AAgrarianGameCharacter::OnRep_ProneState()", + "void AAgrarianGameCharacter::OnRep_MovementModifierState()", + "void AAgrarianGameCharacter::ServerSetWantsToSprint_Implementation", + "void AAgrarianGameCharacter::ServerSetProne_Implementation", + "void AAgrarianGameCharacter::ServerSetTerrainMovementMultiplier_Implementation", +] + + +def assert_contains(label, text, snippets): + missing = [snippet for snippet in snippets if snippet not in text] + if missing: + raise RuntimeError(f"{label} is missing expected replication snippets: {missing}") + + +def main(): + header_text = HEADER.read_text(encoding="utf-8") + source_text = SOURCE.read_text(encoding="utf-8") + + assert_contains("AgrarianGameCharacter.h", header_text, EXPECTED_HEADER_SNIPPETS) + assert_contains("AgrarianGameCharacter.cpp", source_text, EXPECTED_SOURCE_SNIPPETS) + + print("Agrarian player core replication verification complete.") + + +if __name__ == "__main__": + main() diff --git a/Scripts/verify_player_debug_overlay.py b/Scripts/verify_player_debug_overlay.py new file mode 100644 index 0000000..4af9112 --- /dev/null +++ b/Scripts/verify_player_debug_overlay.py @@ -0,0 +1,49 @@ +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +HUD_HEADER = ROOT / "Source" / "AgrarianGame" / "AgrarianDebugHUD.h" +HUD_SOURCE = ROOT / "Source" / "AgrarianGame" / "AgrarianDebugHUD.cpp" +CHARACTER_HEADER = ROOT / "Source" / "AgrarianGame" / "AgrarianGameCharacter.h" + + +EXPECTED_HUD_HEADER_SNIPPETS = [ + "void DrawPlayerStatus(const class AAgrarianGameCharacter* AgrarianCharacter, float X, float& Y);", +] + +EXPECTED_HUD_SOURCE_SNIPPETS = [ + "#include \"GameFramework/CharacterMovementComponent.h\"", + "DrawPlayerStatus(AgrarianCharacter, X, Y);", + "void AAgrarianDebugHUD::DrawPlayerStatus", + "Role:", + "Location:", + "Speed:", + "Stance:", + "Move Mult:", + "Age %.1f | Cond %.2f | Str %.2f | End %.2f | Terrain %.2f", +] + +EXPECTED_CHARACTER_HEADER_SNIPPETS = [ + "float GetAgeYears() const", + "float GetPhysicalConditionMultiplier() const", + "float GetStrengthMultiplier() const", + "float GetEnduranceMultiplier() const", + "float GetTerrainMovementMultiplier() const", +] + + +def assert_contains(label, text, snippets): + missing = [snippet for snippet in snippets if snippet not in text] + if missing: + raise RuntimeError(f"{label} is missing expected debug overlay snippets: {missing}") + + +def main(): + assert_contains("AgrarianDebugHUD.h", HUD_HEADER.read_text(encoding="utf-8"), EXPECTED_HUD_HEADER_SNIPPETS) + assert_contains("AgrarianDebugHUD.cpp", HUD_SOURCE.read_text(encoding="utf-8"), EXPECTED_HUD_SOURCE_SNIPPETS) + assert_contains("AgrarianGameCharacter.h", CHARACTER_HEADER.read_text(encoding="utf-8"), EXPECTED_CHARACTER_HEADER_SNIPPETS) + print("Agrarian player debug overlay verification complete.") + + +if __name__ == "__main__": + main() diff --git a/Scripts/verify_sickness_placeholder.py b/Scripts/verify_sickness_placeholder.py new file mode 100644 index 0000000..41b8306 --- /dev/null +++ b/Scripts/verify_sickness_placeholder.py @@ -0,0 +1,61 @@ +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +FILES = { + "AgrarianTypes.h": ROOT / "Source" / "AgrarianGame" / "AgrarianTypes.h", + "AgrarianSurvivalComponent.h": ROOT / "Source" / "AgrarianGame" / "AgrarianSurvivalComponent.h", + "AgrarianSurvivalComponent.cpp": ROOT / "Source" / "AgrarianGame" / "AgrarianSurvivalComponent.cpp", + "AgrarianGameCharacter.cpp": ROOT / "Source" / "AgrarianGame" / "AgrarianGameCharacter.cpp", + "AgrarianDebugHUD.cpp": ROOT / "Source" / "AgrarianGame" / "AgrarianDebugHUD.cpp", + "AgrarianGamePlayerController.cpp": ROOT / "Source" / "AgrarianGame" / "AgrarianGamePlayerController.cpp", + "PersistenceDesignDocument.md": ROOT / "Docs" / "PersistenceDesignDocument.md", +} + +EXPECTED = { + "AgrarianTypes.h": ["float SicknessSeverity = 0.0f;"], + "AgrarianSurvivalComponent.h": [ + "float SicknessDamagePerMinute = 1.5f;", + "float SicknessRecoveryPerSecond = 0.02f;", + "void AddSickness(float Severity);", + "void ReduceSickness(float Amount);", + ], + "AgrarianSurvivalComponent.cpp": [ + "Survival.SicknessSeverity > 0.0f", + "CareHistory.IllnessBurden", + "SicknessDamagePerMinute", + "void UAgrarianSurvivalComponent::AddSickness", + "void UAgrarianSurvivalComponent::ReduceSickness", + "Survival.SicknessSeverity = FMath::Clamp(Survival.SicknessSeverity, 0.0f, 100.0f);", + ], + "AgrarianGameCharacter.cpp": [ + "const float SicknessMultiplier = FMath::GetMappedRangeValueClamped", + "Survival.SicknessSeverity", + "InjuryMultiplier * SicknessMultiplier * ExhaustionMultiplier", + ], + "AgrarianDebugHUD.cpp": ["Sick:", "Survival.SicknessSeverity"], + "AgrarianGamePlayerController.cpp": [ + "Sickness %.1f", + "Survival.SicknessSeverity", + "SurvivalComponent->ReduceSickness(100.0f);", + ], + "PersistenceDesignDocument.md": ["sickness severity"], +} + + +def main(): + missing = [] + for label, path in FILES.items(): + text = path.read_text(encoding="utf-8") + for snippet in EXPECTED[label]: + if snippet not in text: + missing.append(f"{label}: {snippet}") + + if missing: + raise RuntimeError("Sickness placeholder verification failed: " + "; ".join(missing)) + + print("Agrarian sickness placeholder verification complete.") + + +if __name__ == "__main__": + main() diff --git a/Scripts/verify_sprint_input.py b/Scripts/verify_sprint_input.py index 5aaef7a..f39ffa4 100644 --- a/Scripts/verify_sprint_input.py +++ b/Scripts/verify_sprint_input.py @@ -23,7 +23,7 @@ def mapping_found(context, action, key_name): def main(): action = load("/Game/Input/Actions/IA_Sprint") context = load("/Game/Input/IMC_Default") - character_bp = load("/Game/ThirdPerson/Blueprints/BP_ThirdPersonCharacter") + character_bp = load("/Game/Agrarian/Blueprints/Characters/BP_AgrarianPlayerCharacter") character_cdo = unreal.get_default_object(character_bp.generated_class()) missing = [] @@ -33,7 +33,7 @@ def main(): assigned_action = character_cdo.get_editor_property("SprintAction") if assigned_action != action: - missing.append("BP_ThirdPersonCharacter SprintAction is not IA_Sprint") + missing.append("BP_AgrarianPlayerCharacter SprintAction is not IA_Sprint") if missing: raise RuntimeError("Sprint input verification failed: " + "; ".join(missing)) diff --git a/Scripts/verify_stance_input.py b/Scripts/verify_stance_input.py index a907a49..c295c25 100644 --- a/Scripts/verify_stance_input.py +++ b/Scripts/verify_stance_input.py @@ -31,7 +31,7 @@ def main(): crouch_action = load("/Game/Input/Actions/IA_Crouch") prone_action = load("/Game/Input/Actions/IA_Prone") context = load("/Game/Input/IMC_Default") - character_bp = load("/Game/ThirdPerson/Blueprints/BP_ThirdPersonCharacter") + character_bp = load("/Game/Agrarian/Blueprints/Characters/BP_AgrarianPlayerCharacter") character_cdo = unreal.get_default_object(character_bp.generated_class()) missing = [] @@ -45,9 +45,9 @@ def main(): missing.append(f"missing mapping {action.get_name()} -> {key_name}") if character_cdo.get_editor_property("CrouchAction") != crouch_action: - missing.append("BP_ThirdPersonCharacter CrouchAction is not IA_Crouch") + missing.append("BP_AgrarianPlayerCharacter CrouchAction is not IA_Crouch") if character_cdo.get_editor_property("ProneAction") != prone_action: - missing.append("BP_ThirdPersonCharacter ProneAction is not IA_Prone") + missing.append("BP_AgrarianPlayerCharacter ProneAction is not IA_Prone") for property_name, expected in STANCE_DEFAULTS.items(): actual = float(character_cdo.get_editor_property(property_name)) diff --git a/Scripts/verify_stat_save_load_support.py b/Scripts/verify_stat_save_load_support.py new file mode 100644 index 0000000..78820e8 --- /dev/null +++ b/Scripts/verify_stat_save_load_support.py @@ -0,0 +1,70 @@ +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +FILES = { + "AgrarianSurvivalComponent.h": ROOT / "Source" / "AgrarianGame" / "AgrarianSurvivalComponent.h", + "AgrarianSurvivalComponent.cpp": ROOT / "Source" / "AgrarianGame" / "AgrarianSurvivalComponent.cpp", + "AgrarianPersistenceSubsystem.h": ROOT / "Source" / "AgrarianGame" / "AgrarianPersistenceSubsystem.h", + "AgrarianPersistenceSubsystem.cpp": ROOT / "Source" / "AgrarianGame" / "AgrarianPersistenceSubsystem.cpp", + "AgrarianGamePlayerController.cpp": ROOT / "Source" / "AgrarianGame" / "AgrarianGamePlayerController.cpp", + "PersistenceDesignDocument.md": ROOT / "Docs" / "PersistenceDesignDocument.md", +} + +EXPECTED = { + "AgrarianSurvivalComponent.h": [ + "void ApplySavedState(const FAgrarianSurvivalSnapshot& SavedSurvival, const FAgrarianCareHistorySnapshot& SavedCareHistory);", + ], + "AgrarianSurvivalComponent.cpp": [ + "void UAgrarianSurvivalComponent::ApplySavedState", + "Survival = SavedSurvival;", + "CareHistory = SavedCareHistory;", + "ClampSurvival();", + "ClampCareHistory();", + ], + "AgrarianPersistenceSubsystem.h": [ + "int32 CapturePlayers(UAgrarianSaveGame* SaveGame) const;", + "int32 RestorePlayers(const UAgrarianSaveGame* SaveGame) const;", + "void FindAgrarianPlayers(TArray& OutPlayers) const;", + "FString GetPlayerPersistenceId(const AAgrarianGameCharacter* Character) const;", + ], + "AgrarianPersistenceSubsystem.cpp": [ + "#include \"AgrarianInventoryComponent.h\"", + "int32 UAgrarianPersistenceSubsystem::CapturePlayers", + "SavedPlayer.Survival = SurvivalComponent->Survival;", + "SavedPlayer.CareHistory = SurvivalComponent->CareHistory;", + "SavedPlayer.Inventory = InventoryComponent->Items;", + "int32 UAgrarianPersistenceSubsystem::RestorePlayers", + "SurvivalComponent->ApplySavedState(SavedPlayer->Survival, SavedPlayer->CareHistory);", + "InventoryComponent->Items = SavedPlayer->Inventory;", + "CapturePlayers(SaveGame);", + "void UAgrarianPersistenceSubsystem::FindAgrarianPlayers", + "FString UAgrarianPersistenceSubsystem::GetPlayerPersistenceId", + ], + "AgrarianGamePlayerController.cpp": [ + "const int32 RestoredPlayerCount = Persistence->RestorePlayers(SaveGame);", + "Restored players: %d. Restored actors: %d.", + ], + "PersistenceDesignDocument.md": [ + "captures live Agrarian player characters", + "RestorePlayers", + ], +} + + +def main(): + missing = [] + for label, path in FILES.items(): + text = path.read_text(encoding="utf-8") + for snippet in EXPECTED[label]: + if snippet not in text: + missing.append(f"{label}: {snippet}") + + if missing: + raise RuntimeError("Stat save/load verification failed: " + "; ".join(missing)) + + print("Agrarian stat save/load verification complete.") + + +if __name__ == "__main__": + main() diff --git a/Scripts/verify_test_map_placements.py b/Scripts/verify_test_map_placements.py index 9d96969..a817cc0 100644 --- a/Scripts/verify_test_map_placements.py +++ b/Scripts/verify_test_map_placements.py @@ -1,7 +1,7 @@ import unreal -MAP_PATH = "/Game/ThirdPerson/Lvl_ThirdPerson" +MAP_PATH = "/Game/Agrarian/Maps/L_GroundZeroTerrain_Test" EXPECTED_PLACEMENTS = { "AGR_WoodResourceNode_01": { diff --git a/Scripts/verify_tile_solar_time.py b/Scripts/verify_tile_solar_time.py new file mode 100644 index 0000000..ac56c28 --- /dev/null +++ b/Scripts/verify_tile_solar_time.py @@ -0,0 +1,84 @@ +from pathlib import Path +import json + + +ROOT = Path(__file__).resolve().parents[1] +GAME_STATE_H = ROOT / "Source" / "AgrarianGame" / "AgrarianGameState.h" +GAME_STATE_CPP = ROOT / "Source" / "AgrarianGame" / "AgrarianGameState.cpp" +GENERATOR = ROOT / "Scripts" / "generate_tile_solar_metadata.py" +SOLAR_METADATA = ROOT / "Data" / "Tiles" / "tile_solar_metadata.json" +SCHEMA = ROOT / "Data" / "Tiles" / "tile_registry.schema.json" +SQL = ROOT / "Data" / "Tiles" / "tile_registry.sql" +ROADMAP = ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md" + + +EXPECTED = { + GAME_STATE_H: [ + "ActiveSolarTileId", + "ActiveTileLatitude", + "ActiveTileLongitude", + "ActiveTileTimeZoneId", + "ActiveTileUtcOffsetHours", + "ActiveDayOfYear", + "SunriseHourLocal", + "SunsetHourLocal", + "SolarNoonHourLocal", + "DayLengthHours", + "ConfigureActiveSolarTile", + ], + GAME_STATE_CPP: [ + "UpdateSolarTimes();", + "DOREPLIFETIME(AAgrarianGameState, SunriseHourLocal);", + "return WorldHours < SunriseHourLocal || WorldHours > SunsetHourLocal;", + "bool AAgrarianGameState::ConfigureActiveSolarTile", + "void AAgrarianGameState::UpdateSolarTimes()", + "NOAA", + "EquationOfTimeMinutes", + "SolarDeclinationRadians", + ], + GENERATOR: [ + "SOLAR_READY_STATUSES", + "TIMEZONE_OVERRIDES", + "Only tiles with real source status", + "gz_us_ca_pacifica_utm10n_e544_n4160", + ], + SCHEMA: ["solar_metadata", "standard_utc_offset_hours", "daylight_utc_offset_hours"], + SQL: ["terrain_tile_solar_metadata", "time_zone_id", "solar_model"], + ROADMAP: ["Add real local time-zone and sunrise/sunset lookup for Ground Zero"], +} + + +def assert_contains(): + missing = [] + for path, snippets in EXPECTED.items(): + text = path.read_text(encoding="utf-8") + for snippet in snippets: + if snippet not in text: + missing.append(f"{path.relative_to(ROOT)}: {snippet}") + if missing: + raise RuntimeError("Tile solar time verification failed: " + "; ".join(missing)) + + +def assert_solar_metadata(): + data = json.loads(SOLAR_METADATA.read_text(encoding="utf-8")) + tiles = data.get("tiles", []) + if len(tiles) != 1: + raise RuntimeError(f"Expected exactly one generated solar tile, got {len(tiles)}") + tile = tiles[0] + if tile.get("tile_id") != "gz_us_ca_pacifica_utm10n_e544_n4160": + raise RuntimeError(f"Unexpected solar tile id: {tile.get('tile_id')}") + if tile.get("time_zone_id") != "America/Los_Angeles": + raise RuntimeError("Ground Zero timezone metadata is missing or incorrect") + june = tile.get("sample_solar_hours", {}).get("june_solstice", {}) + if not (5.0 <= june.get("sunrise_hour", -1) <= 6.5 and 20.0 <= june.get("sunset_hour", -1) <= 21.0): + raise RuntimeError(f"Ground Zero June solar sample is outside expected range: {june}") + + +def main(): + assert_contains() + assert_solar_metadata() + print("Agrarian tile solar time verification complete.") + + +if __name__ == "__main__": + main() diff --git a/Scripts/verify_wildlife_loop.py b/Scripts/verify_wildlife_loop.py index 082b580..6b59b90 100644 --- a/Scripts/verify_wildlife_loop.py +++ b/Scripts/verify_wildlife_loop.py @@ -1,8 +1,8 @@ import unreal -MAP_PATH = "/Game/ThirdPerson/Lvl_ThirdPerson" -CHARACTER_CLASS_PATH = "/Game/ThirdPerson/Blueprints/BP_ThirdPersonCharacter" +MAP_PATH = "/Game/Agrarian/Maps/L_GroundZeroTerrain_Test" +CHARACTER_CLASS_PATH = "/Game/Agrarian/Blueprints/Characters/BP_AgrarianPlayerCharacter" RABBIT_LABEL = "AGR_RabbitWildlife_01" diff --git a/Source/AgrarianGame/AgrarianDebugHUD.cpp b/Source/AgrarianGame/AgrarianDebugHUD.cpp index 2a58213..ee0e522 100644 --- a/Source/AgrarianGame/AgrarianDebugHUD.cpp +++ b/Source/AgrarianGame/AgrarianDebugHUD.cpp @@ -5,6 +5,7 @@ #include "AgrarianInventoryComponent.h" #include "AgrarianSurvivalComponent.h" #include "Engine/Canvas.h" +#include "GameFramework/CharacterMovementComponent.h" void AAgrarianDebugHUD::DrawHUD() { @@ -22,6 +23,7 @@ void AAgrarianDebugHUD::DrawHUD() } DrawInteractionPrompt(AgrarianCharacter); + DrawCriticalStats(AgrarianCharacter->GetSurvivalComponent()); if (bShowDebugHUD) { @@ -29,6 +31,7 @@ void AAgrarianDebugHUD::DrawHUD() constexpr float X = 32.0f; DrawLine(TEXT("AGRARIAN DEV HUD"), X, Y, FColor(160, 220, 140)); + DrawPlayerStatus(AgrarianCharacter, X, Y); DrawSurvival(AgrarianCharacter->GetSurvivalComponent(), X, Y); DrawInventory(AgrarianCharacter->GetInventoryComponent(), X, Y); } @@ -51,6 +54,108 @@ void AAgrarianDebugHUD::DrawInteractionPrompt(const AAgrarianGameCharacter* Agra DrawText(Prompt, FColor(245, 245, 225), X, Y, nullptr, PromptTextScale, true); } +void AAgrarianDebugHUD::DrawCriticalStats(const UAgrarianSurvivalComponent* SurvivalComponent) +{ + if (!bShowCriticalStatsHUD || !SurvivalComponent || !Canvas) + { + return; + } + + const FAgrarianSurvivalSnapshot& Survival = SurvivalComponent->Survival; + float Y = FMath::Max(32.0f, Canvas->ClipY - (178.0f * CriticalStatsTextScale)); + constexpr float X = 32.0f; + + const FColor CriticalColor(230, 90, 70); + const FColor WarningColor(245, 190, 80); + const FColor StableColor(225, 235, 220); + const FColor HeaderColor(160, 220, 140); + + auto StatusColor = [CriticalColor, WarningColor, StableColor](float Value, bool bHigherIsWorse = false) + { + if (bHigherIsWorse) + { + if (Value >= 75.0f) + { + return CriticalColor; + } + if (Value >= 40.0f) + { + return WarningColor; + } + return StableColor; + } + + if (Value <= 20.0f) + { + return CriticalColor; + } + if (Value <= 45.0f) + { + return WarningColor; + } + return StableColor; + }; + + DrawScaledLine(TEXT("SURVIVAL"), X, Y, CriticalStatsTextScale, HeaderColor); + DrawScaledLine(FString::Printf(TEXT("Health %3.0f"), Survival.Health), X, Y, CriticalStatsTextScale, StatusColor(Survival.Health)); + DrawScaledLine(FString::Printf(TEXT("Stamina %3.0f"), Survival.Stamina), X, Y, CriticalStatsTextScale, StatusColor(Survival.Stamina)); + DrawScaledLine(FString::Printf(TEXT("Food %3.0f"), Survival.Hunger), X, Y, CriticalStatsTextScale, StatusColor(Survival.Hunger)); + DrawScaledLine(FString::Printf(TEXT("Water %3.0f"), Survival.Thirst), X, Y, CriticalStatsTextScale, StatusColor(Survival.Thirst)); + DrawScaledLine(FString::Printf(TEXT("Temp %4.1f C"), Survival.BodyTemperature), X, Y, CriticalStatsTextScale, Survival.BodyTemperature < 35.0f ? CriticalColor : StableColor); + DrawScaledLine(FString::Printf(TEXT("Exhaust %3.0f"), Survival.Exhaustion), X, Y, CriticalStatsTextScale, StatusColor(Survival.Exhaustion, true)); + DrawScaledLine(FString::Printf(TEXT("Injury %3.0f"), Survival.InjurySeverity), X, Y, CriticalStatsTextScale, StatusColor(Survival.InjurySeverity, true)); + DrawScaledLine(FString::Printf(TEXT("Sickness %3.0f"), Survival.SicknessSeverity), X, Y, CriticalStatsTextScale, StatusColor(Survival.SicknessSeverity, true)); +} + +void AAgrarianDebugHUD::DrawPlayerStatus(const AAgrarianGameCharacter* AgrarianCharacter, float X, float& Y) +{ + if (!AgrarianCharacter) + { + DrawLine(TEXT("Player: unavailable"), X, Y, FColor::Red); + return; + } + + const UCharacterMovementComponent* MovementComponent = AgrarianCharacter->GetCharacterMovement(); + const FVector Location = AgrarianCharacter->GetActorLocation(); + const float HorizontalSpeed = AgrarianCharacter->GetVelocity().Size2D(); + const FString MovementMode = MovementComponent + ? UEnum::GetValueAsString(MovementComponent->MovementMode) + : TEXT("Unavailable"); + + DrawLine(FString::Printf( + TEXT("Role: %s | Local: %s"), + AgrarianCharacter->HasAuthority() ? TEXT("Authority") : TEXT("Simulated/Autonomous"), + AgrarianCharacter->IsLocallyControlled() ? TEXT("yes") : TEXT("no")), + X, + Y, + FColor(160, 220, 140)); + DrawLine(FString::Printf(TEXT("Location: X %.0f Y %.0f Z %.0f"), Location.X, Location.Y, Location.Z), X, Y); + DrawLine(FString::Printf(TEXT("Speed: %.0f uu/s | Mode: %s"), HorizontalSpeed, *MovementMode), X, Y); + DrawLine(FString::Printf( + TEXT("Stance: %s | Sprint: %s | Camera: %s"), + AgrarianCharacter->IsProne() ? TEXT("Prone") : (AgrarianCharacter->bIsCrouched ? TEXT("Crouched") : TEXT("Standing")), + AgrarianCharacter->IsSprinting() ? TEXT("yes") : TEXT("no"), + AgrarianCharacter->IsFirstPersonCamera() ? TEXT("First") : TEXT("Third")), + X, + Y); + DrawLine(FString::Printf( + TEXT("Move Mult: %.2f | Carry: %.1f"), + AgrarianCharacter->GetCurrentMovementSpeedMultiplier(), + AgrarianCharacter->GetCurrentCarryWeight()), + X, + Y); + DrawLine(FString::Printf( + TEXT("Age %.1f | Cond %.2f | Str %.2f | End %.2f | Terrain %.2f"), + AgrarianCharacter->GetAgeYears(), + AgrarianCharacter->GetPhysicalConditionMultiplier(), + AgrarianCharacter->GetStrengthMultiplier(), + AgrarianCharacter->GetEnduranceMultiplier(), + AgrarianCharacter->GetTerrainMovementMultiplier()), + X, + Y); + Y += 10.0f * TextScale; +} + void AAgrarianDebugHUD::DrawSurvival(const UAgrarianSurvivalComponent* SurvivalComponent, float X, float& Y) { if (!SurvivalComponent) @@ -62,10 +167,15 @@ void AAgrarianDebugHUD::DrawSurvival(const UAgrarianSurvivalComponent* SurvivalC const FAgrarianSurvivalSnapshot& Survival = SurvivalComponent->Survival; DrawLine(FString::Printf(TEXT("Health: %.0f"), Survival.Health), X, Y); DrawLine(FString::Printf(TEXT("Stamina: %.0f"), Survival.Stamina), X, Y); + DrawLine(FString::Printf(TEXT("Exhaust: %.0f"), Survival.Exhaustion), X, Y); DrawLine(FString::Printf(TEXT("Hunger: %.0f"), Survival.Hunger), X, Y); DrawLine(FString::Printf(TEXT("Thirst: %.0f"), Survival.Thirst), X, Y); DrawLine(FString::Printf(TEXT("Temp: %.1f C"), Survival.BodyTemperature), X, Y); DrawLine(FString::Printf(TEXT("Injury: %.0f"), Survival.InjurySeverity), X, Y); + DrawLine(FString::Printf(TEXT("Sick: %.0f"), Survival.SicknessSeverity), X, Y); + const FAgrarianCareHistorySnapshot& Care = SurvivalComponent->CareHistory; + DrawLine(FString::Printf(TEXT("Care N/S/T: %.2f %.2f %.2f"), Care.NutritionQuality, Care.SleepQuality, Care.TreatmentQuality), X, Y, FColor::Silver); + DrawLine(FString::Printf(TEXT("Burden I/I/S/W: %.2f %.2f %.2f %.2f"), Care.IllnessBurden, Care.InjuryBurden, Care.StressBurden, Care.WorkloadBurden), X, Y, FColor::Silver); Y += 10.0f * TextScale; } @@ -94,6 +204,11 @@ void AAgrarianDebugHUD::DrawInventory(const UAgrarianInventoryComponent* Invento void AAgrarianDebugHUD::DrawLine(const FString& Text, float X, float& Y, const FColor& Color) { - DrawText(Text, Color, X, Y, nullptr, TextScale, false); - Y += 18.0f * TextScale; + DrawScaledLine(Text, X, Y, TextScale, Color); +} + +void AAgrarianDebugHUD::DrawScaledLine(const FString& Text, float X, float& Y, float Scale, const FColor& Color) +{ + DrawText(Text, Color, X, Y, nullptr, Scale, false); + Y += 18.0f * Scale; } diff --git a/Source/AgrarianGame/AgrarianDebugHUD.h b/Source/AgrarianGame/AgrarianDebugHUD.h index 02da66c..30c1a01 100644 --- a/Source/AgrarianGame/AgrarianDebugHUD.h +++ b/Source/AgrarianGame/AgrarianDebugHUD.h @@ -20,9 +20,15 @@ public: UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|HUD") bool bShowDebugHUD = true; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|HUD") + bool bShowCriticalStatsHUD = true; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|HUD", meta = (ClampMin = "0.25")) float TextScale = 1.0f; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|HUD", meta = (ClampMin = "0.25")) + float CriticalStatsTextScale = 1.0f; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|HUD") bool bShowInteractionPrompt = true; @@ -31,7 +37,10 @@ public: protected: void DrawInteractionPrompt(const class AAgrarianGameCharacter* AgrarianCharacter); + void DrawCriticalStats(const UAgrarianSurvivalComponent* SurvivalComponent); + void DrawPlayerStatus(const class AAgrarianGameCharacter* AgrarianCharacter, float X, float& Y); void DrawSurvival(const UAgrarianSurvivalComponent* SurvivalComponent, float X, float& Y); void DrawInventory(const UAgrarianInventoryComponent* InventoryComponent, float X, float& Y); void DrawLine(const FString& Text, float X, float& Y, const FColor& Color = FColor::White); + void DrawScaledLine(const FString& Text, float X, float& Y, float Scale, const FColor& Color = FColor::White); }; diff --git a/Source/AgrarianGame/AgrarianGameCharacter.cpp b/Source/AgrarianGame/AgrarianGameCharacter.cpp index 6223337..8188c97 100644 --- a/Source/AgrarianGame/AgrarianGameCharacter.cpp +++ b/Source/AgrarianGame/AgrarianGameCharacter.cpp @@ -21,6 +21,10 @@ AAgrarianGameCharacter::AAgrarianGameCharacter() { PrimaryActorTick.bCanEverTick = true; + bReplicates = true; + SetReplicateMovement(true); + SetNetUpdateFrequency(30.0f); + SetMinNetUpdateFrequency(10.0f); // Set size for collision capsule GetCapsuleComponent()->InitCapsuleSize(42.f, 96.0f); @@ -286,6 +290,7 @@ bool AAgrarianGameCharacter::CanSprint() const && SurvivalComponent->IsAlive() && !bIsProne && !bIsCrouched + && SurvivalComponent->Survival.Exhaustion < 85.0f && SurvivalComponent->Survival.Stamina > MinSprintStamina; } @@ -329,7 +334,19 @@ void AAgrarianGameCharacter::SetProne(bool bNewProne) void AAgrarianGameCharacter::SetTerrainMovementMultiplier(float NewTerrainMovementMultiplier) { - TerrainMovementMultiplier = FMath::Clamp(NewTerrainMovementMultiplier, 0.25f, 1.25f); + const float ClampedTerrainMovementMultiplier = FMath::Clamp(NewTerrainMovementMultiplier, 0.25f, 1.25f); + if (!HasAuthority()) + { + ServerSetTerrainMovementMultiplier(ClampedTerrainMovementMultiplier); + return; + } + + if (FMath::IsNearlyEqual(TerrainMovementMultiplier, ClampedTerrainMovementMultiplier)) + { + return; + } + + TerrainMovementMultiplier = ClampedTerrainMovementMultiplier; ApplyMovementSpeed(); } @@ -403,8 +420,16 @@ float AAgrarianGameCharacter::CalculateSurvivalMovementMultiplier() const FVector2D(0.0f, 100.0f), FVector2D(1.0f, 0.5f), Survival.InjurySeverity); + const float SicknessMultiplier = FMath::GetMappedRangeValueClamped( + FVector2D(0.0f, 100.0f), + FVector2D(1.0f, 0.7f), + Survival.SicknessSeverity); + const float ExhaustionMultiplier = FMath::GetMappedRangeValueClamped( + FVector2D(0.0f, 100.0f), + FVector2D(1.0f, 0.55f), + Survival.Exhaustion); - return HungerMultiplier * ThirstMultiplier * InjuryMultiplier; + return HungerMultiplier * ThirstMultiplier * InjuryMultiplier * SicknessMultiplier * ExhaustionMultiplier; } float AAgrarianGameCharacter::CalculateCarryWeightMovementMultiplier() const @@ -460,6 +485,11 @@ void AAgrarianGameCharacter::OnRep_ProneState() ApplyMovementSpeed(); } +void AAgrarianGameCharacter::OnRep_MovementModifierState() +{ + ApplyMovementSpeed(); +} + void AAgrarianGameCharacter::DoMove(float Right, float Forward) { if (GetController() != nullptr) @@ -585,6 +615,11 @@ void AAgrarianGameCharacter::ServerSetProne_Implementation(bool bNewProne) SetProne(bNewProne); } +void AAgrarianGameCharacter::ServerSetTerrainMovementMultiplier_Implementation(float NewTerrainMovementMultiplier) +{ + SetTerrainMovementMultiplier(NewTerrainMovementMultiplier); +} + void AAgrarianGameCharacter::ServerInteract_Implementation(AActor* TargetActor) { if (!TargetActor || !TargetActor->GetClass()->ImplementsInterface(UAgrarianInteractable::StaticClass())) diff --git a/Source/AgrarianGame/AgrarianGameCharacter.h b/Source/AgrarianGame/AgrarianGameCharacter.h index 48938e7..ab45765 100644 --- a/Source/AgrarianGame/AgrarianGameCharacter.h +++ b/Source/AgrarianGame/AgrarianGameCharacter.h @@ -126,19 +126,19 @@ protected: float ProneSpeedMultiplier = 0.25f; /** Age hook used by movement until full lifecycle/aging systems own it. */ - UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category="Agrarian|Movement|Modifiers", meta = (ClampMin = "0")) + UPROPERTY(EditAnywhere, BlueprintReadWrite, ReplicatedUsing = OnRep_MovementModifierState, Category="Agrarian|Movement|Modifiers", meta = (ClampMin = "0")) float AgeYears = 25.0f; /** General physical condition scalar reserved for care, illness, sleep, and long-term state. */ - UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category="Agrarian|Movement|Modifiers", meta = (ClampMin = "0.25", ClampMax = "1.25")) + UPROPERTY(EditAnywhere, BlueprintReadWrite, ReplicatedUsing = OnRep_MovementModifierState, Category="Agrarian|Movement|Modifiers", meta = (ClampMin = "0.25", ClampMax = "1.25")) float PhysicalConditionMultiplier = 1.0f; /** Strength scalar that mainly offsets carried-weight penalties. */ - UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category="Agrarian|Movement|Modifiers", meta = (ClampMin = "0.25", ClampMax = "2.0")) + UPROPERTY(EditAnywhere, BlueprintReadWrite, ReplicatedUsing = OnRep_MovementModifierState, Category="Agrarian|Movement|Modifiers", meta = (ClampMin = "0.25", ClampMax = "2.0")) float StrengthMultiplier = 1.0f; /** Endurance scalar that improves stamina efficiency and movement resilience. */ - UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category="Agrarian|Movement|Modifiers", meta = (ClampMin = "0.25", ClampMax = "2.0")) + UPROPERTY(EditAnywhere, BlueprintReadWrite, ReplicatedUsing = OnRep_MovementModifierState, Category="Agrarian|Movement|Modifiers", meta = (ClampMin = "0.25", ClampMax = "2.0")) float EnduranceMultiplier = 1.0f; /** Comfort carry capacity before speed penalties, in item-weight units. */ @@ -150,7 +150,7 @@ protected: float HeavyCarryWeight = 60.0f; /** Terrain hook for later surface/volume systems. Values below one slow the character. */ - UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category="Agrarian|Movement|Modifiers", meta = (ClampMin = "0.25", ClampMax = "1.25")) + UPROPERTY(EditAnywhere, BlueprintReadWrite, ReplicatedUsing = OnRep_MovementModifierState, Category="Agrarian|Movement|Modifiers", meta = (ClampMin = "0.25", ClampMax = "1.25")) float TerrainMovementMultiplier = 1.0f; /** Third-person spring arm distance used when returning from first person. */ @@ -238,6 +238,9 @@ protected: UFUNCTION() void OnRep_ProneState(); + UFUNCTION() + void OnRep_MovementModifierState(); + public: /** Handles move inputs from either controls or UI interfaces */ @@ -292,6 +295,21 @@ public: UFUNCTION(BlueprintPure, Category="Agrarian|Movement") float GetCurrentMovementSpeedMultiplier() const; + UFUNCTION(BlueprintPure, Category="Agrarian|Movement|Modifiers") + float GetAgeYears() const { return AgeYears; } + + UFUNCTION(BlueprintPure, Category="Agrarian|Movement|Modifiers") + float GetPhysicalConditionMultiplier() const { return PhysicalConditionMultiplier; } + + UFUNCTION(BlueprintPure, Category="Agrarian|Movement|Modifiers") + float GetStrengthMultiplier() const { return StrengthMultiplier; } + + UFUNCTION(BlueprintPure, Category="Agrarian|Movement|Modifiers") + float GetEnduranceMultiplier() const { return EnduranceMultiplier; } + + UFUNCTION(BlueprintPure, Category="Agrarian|Movement|Modifiers") + float GetTerrainMovementMultiplier() const { return TerrainMovementMultiplier; } + UFUNCTION(BlueprintCallable, Category="Agrarian|Movement") void SetTerrainMovementMultiplier(float NewTerrainMovementMultiplier); @@ -307,6 +325,10 @@ public: UFUNCTION(Server, Reliable) void ServerSetProne(bool bNewProne); + /** Server-authoritative terrain movement modifier update. */ + UFUNCTION(Server, Reliable) + void ServerSetTerrainMovementMultiplier(float NewTerrainMovementMultiplier); + /** Refreshes local interactable focus and prompt text. */ void UpdateInteractionPrompt(); diff --git a/Source/AgrarianGame/AgrarianGamePlayerController.cpp b/Source/AgrarianGame/AgrarianGamePlayerController.cpp index b71610a..cd1513a 100644 --- a/Source/AgrarianGame/AgrarianGamePlayerController.cpp +++ b/Source/AgrarianGame/AgrarianGamePlayerController.cpp @@ -104,13 +104,15 @@ void AAgrarianGamePlayerController::AgrarianSurvival() const FAgrarianSurvivalSnapshot& Survival = SurvivalComponent->Survival; ClientMessage(FString::Printf( - TEXT("Health %.1f | Stamina %.1f | Hunger %.1f | Thirst %.1f | Temp %.1fC | Injury %.1f"), + TEXT("Health %.1f | Stamina %.1f | Exhaustion %.1f | Hunger %.1f | Thirst %.1f | Temp %.1fC | Injury %.1f | Sickness %.1f"), Survival.Health, Survival.Stamina, + Survival.Exhaustion, Survival.Hunger, Survival.Thirst, Survival.BodyTemperature, - Survival.InjurySeverity)); + Survival.InjurySeverity, + Survival.SicknessSeverity)); } void AAgrarianGamePlayerController::AgrarianHeal() @@ -167,8 +169,9 @@ void AAgrarianGamePlayerController::ServerAgrarianLoadWorld_Implementation() Persistence->RegisterWorldActorClass(TEXT("primitive_shelter"), AAgrarianShelterActor::StaticClass()); const UAgrarianSaveGame* SaveGame = Persistence->LoadOrCreateSave(); + const int32 RestoredPlayerCount = Persistence->RestorePlayers(SaveGame); const int32 RestoredCount = Persistence->RestoreWorldActors(SaveGame); - ClientMessage(FString::Printf(TEXT("Agrarian world loaded. Restored actors: %d."), RestoredCount)); + ClientMessage(FString::Printf(TEXT("Agrarian world loaded. Restored players: %d. Restored actors: %d."), RestoredPlayerCount, RestoredCount)); } void AAgrarianGamePlayerController::ServerAgrarianHeal_Implementation() @@ -184,6 +187,8 @@ void AAgrarianGamePlayerController::ServerAgrarianHeal_Implementation() SurvivalComponent->RestoreHealth(100.0f); SurvivalComponent->AddFood(100.0f); SurvivalComponent->AddWater(100.0f); + SurvivalComponent->ReduceExhaustion(100.0f); + SurvivalComponent->ReduceSickness(100.0f); SurvivalComponent->AddWarmth(37.0f - SurvivalComponent->Survival.BodyTemperature); ClientMessage(TEXT("Agrarian survival restored.")); } diff --git a/Source/AgrarianGame/AgrarianGameState.cpp b/Source/AgrarianGame/AgrarianGameState.cpp index 29696c9..1b905da 100644 --- a/Source/AgrarianGame/AgrarianGameState.cpp +++ b/Source/AgrarianGame/AgrarianGameState.cpp @@ -3,10 +3,52 @@ #include "AgrarianGameState.h" #include "Net/UnrealNetwork.h" +namespace +{ +int32 NormalizeCalendarDay(int32 DayOfYear, int32 DaysPerYear) +{ + const int32 SafeDaysPerYear = FMath::Max(1, DaysPerYear); + const int32 ZeroBasedDay = ((DayOfYear - 1) % SafeDaysPerYear + SafeDaysPerYear) % SafeDaysPerYear; + return ZeroBasedDay + 1; +} + +bool IsDayInRange(int32 DayOfYear, int32 StartDay, int32 EndDay, int32 DaysPerYear) +{ + const int32 Day = NormalizeCalendarDay(DayOfYear, DaysPerYear); + const int32 Start = NormalizeCalendarDay(StartDay, DaysPerYear); + const int32 End = NormalizeCalendarDay(EndDay, DaysPerYear); + + if (Start <= End) + { + return Day >= Start && Day <= End; + } + + return Day >= Start || Day <= End; +} +} + AAgrarianGameState::AAgrarianGameState() { PrimaryActorTick.bCanEverTick = true; bReplicates = true; + ActiveGrowingSeason.TileId = ActiveSolarTileId; + ActiveGrowingSeason.GrowingZoneLabel = TEXT("USDA 10a"); + ActiveGrowingSeason.GrowingSeasonStartDay = 46; + ActiveGrowingSeason.GrowingSeasonEndDay = 350; + ActiveGrowingSeason.FrostFreeDays = 305; + ActiveGrowingSeason.MinAverageGrowingTempC = 7.0f; + ActiveGrowingSeason.CropSafetyBufferDays = 14; + ActiveGrowingSeason.ClimateProfile = TEXT("coastal_mediterranean_mild"); +} + +void AAgrarianGameState::BeginPlay() +{ + Super::BeginPlay(); + + if (HasAuthority()) + { + UpdateSolarTimes(); + } } void AAgrarianGameState::Tick(float DeltaSeconds) @@ -22,6 +64,13 @@ void AAgrarianGameState::Tick(float DeltaSeconds) while (WorldHours >= 24.0f) { WorldHours -= 24.0f; + const int32 PreviousDayOfYear = ActiveDayOfYear; + ActiveDayOfYear = (ActiveDayOfYear % FMath::Max(1, DaysPerAgrarianYear)) + 1; + if (ActiveDayOfYear < PreviousDayOfYear) + { + ++ActiveYear; + } + UpdateSolarTimes(); } UpdateAmbientTemperature(); @@ -33,10 +82,29 @@ void AAgrarianGameState::GetLifetimeReplicatedProps(TArray& O DOREPLIFETIME(AAgrarianGameState, WorldHours); DOREPLIFETIME(AAgrarianGameState, Weather); DOREPLIFETIME(AAgrarianGameState, AmbientTemperatureC); + DOREPLIFETIME(AAgrarianGameState, DaysPerAgrarianYear); + DOREPLIFETIME(AAgrarianGameState, ActiveSolarTileId); + DOREPLIFETIME(AAgrarianGameState, ActiveTileLatitude); + DOREPLIFETIME(AAgrarianGameState, ActiveTileLongitude); + DOREPLIFETIME(AAgrarianGameState, ActiveTileTimeZoneId); + DOREPLIFETIME(AAgrarianGameState, ActiveTileUtcOffsetHours); + DOREPLIFETIME(AAgrarianGameState, ActiveDayOfYear); + DOREPLIFETIME(AAgrarianGameState, ActiveYear); + DOREPLIFETIME(AAgrarianGameState, ActiveGrowingSeason); + DOREPLIFETIME(AAgrarianGameState, bHasActiveTileSolarData); + DOREPLIFETIME(AAgrarianGameState, SunriseHourLocal); + DOREPLIFETIME(AAgrarianGameState, SunsetHourLocal); + DOREPLIFETIME(AAgrarianGameState, SolarNoonHourLocal); + DOREPLIFETIME(AAgrarianGameState, DayLengthHours); } bool AAgrarianGameState::IsNight() const { + if (bHasActiveTileSolarData) + { + return WorldHours < SunriseHourLocal || WorldHours > SunsetHourLocal; + } + return WorldHours < 6.0f || WorldHours > 20.0f; } @@ -50,14 +118,270 @@ void AAgrarianGameState::SetWeather(EAgrarianWeatherType NewWeather) } } +bool AAgrarianGameState::ConfigureActiveSolarTile(FName TileId, float Latitude, float Longitude, const FString& TimeZoneId, float UtcOffsetHours) +{ + if (!HasAuthority() || TileId == NAME_None) + { + return false; + } + + ActiveSolarTileId = TileId; + ActiveTileLatitude = FMath::Clamp(Latitude, -90.0f, 90.0f); + ActiveTileLongitude = FMath::Clamp(Longitude, -180.0f, 180.0f); + ActiveTileTimeZoneId = TimeZoneId; + ActiveTileUtcOffsetHours = FMath::Clamp(UtcOffsetHours, -12.0f, 14.0f); + ActiveGrowingSeason.TileId = TileId; + UpdateSolarTimes(); + UpdateAmbientTemperature(); + return bHasActiveTileSolarData; +} + +FAgrarianCalendarSnapshot AAgrarianGameState::GetCalendarSnapshot() const +{ + FAgrarianCalendarSnapshot Snapshot; + Snapshot.DayOfYear = NormalizeCalendarDay(ActiveDayOfYear, DaysPerAgrarianYear); + Snapshot.Year = FMath::Max(1, ActiveYear); + Snapshot.AbsoluteDay = GetAbsoluteAgrarianDay(); + Snapshot.DayOfSeason = GetDayOfSeason(Snapshot.DayOfYear); + Snapshot.Season = GetSeasonForDay(Snapshot.DayOfYear); + Snapshot.HourOfDay = FMath::Clamp(WorldHours, 0.0f, 24.0f); + Snapshot.bInsideGrowingSeason = IsDayInsideActiveGrowingSeason(Snapshot.DayOfYear); + Snapshot.GrowingSeasonDaysRemaining = GetDaysRemainingInActiveGrowingSeason(Snapshot.DayOfYear); + return Snapshot; +} + +int32 AAgrarianGameState::GetAbsoluteAgrarianDay() const +{ + const int32 SafeYear = FMath::Max(1, ActiveYear); + const int32 SafeDaysPerYear = FMath::Max(1, DaysPerAgrarianYear); + return ((SafeYear - 1) * SafeDaysPerYear) + NormalizeCalendarDay(ActiveDayOfYear, SafeDaysPerYear); +} + +EAgrarianSeason AAgrarianGameState::GetSeasonForDay(int32 DayOfYear) const +{ + const int32 SafeDaysPerYear = FMath::Max(1, DaysPerAgrarianYear); + const int32 SeasonalDay = ActiveTileLatitude < 0.0f + ? NormalizeCalendarDay(DayOfYear + (SafeDaysPerYear / 2), SafeDaysPerYear) + : NormalizeCalendarDay(DayOfYear, SafeDaysPerYear); + + if (SeasonalDay >= 355 || SeasonalDay < 80) + { + return EAgrarianSeason::Winter; + } + if (SeasonalDay < 172) + { + return EAgrarianSeason::Spring; + } + if (SeasonalDay < 264) + { + return EAgrarianSeason::Summer; + } + return EAgrarianSeason::Autumn; +} + +int32 AAgrarianGameState::GetDayOfSeason(int32 DayOfYear) const +{ + const int32 SafeDaysPerYear = FMath::Max(1, DaysPerAgrarianYear); + const int32 SeasonalDay = ActiveTileLatitude < 0.0f + ? NormalizeCalendarDay(DayOfYear + (SafeDaysPerYear / 2), SafeDaysPerYear) + : NormalizeCalendarDay(DayOfYear, SafeDaysPerYear); + + switch (GetSeasonForDay(DayOfYear)) + { + case EAgrarianSeason::Spring: + return SeasonalDay - 79; + case EAgrarianSeason::Summer: + return SeasonalDay - 171; + case EAgrarianSeason::Autumn: + return SeasonalDay - 263; + default: + return SeasonalDay >= 355 ? SeasonalDay - 354 : SeasonalDay + 12; + } +} + +float AAgrarianGameState::ConvertAgrarianDaysToRealHours(float AgrarianDays) const +{ + const float SafeGameHoursPerRealMinute = FMath::Max(0.001f, GameHoursPerRealMinute); + return FMath::Max(0.0f, AgrarianDays) * (24.0f / SafeGameHoursPerRealMinute) / 60.0f; +} + +float AAgrarianGameState::ConvertRealHoursToAgrarianDays(float RealHours) const +{ + return (FMath::Max(0.0f, RealHours) * 60.0f * FMath::Max(0.001f, GameHoursPerRealMinute)) / 24.0f; +} + +float AAgrarianGameState::GetLongTaskProgress(int32 StartAbsoluteDay, float StartHourOfDay, float DurationAgrarianDays) const +{ + const float SafeDuration = FMath::Max(0.001f, DurationAgrarianDays); + const float CurrentAgrarianDay = static_cast(GetAbsoluteAgrarianDay() - 1) + (WorldHours / 24.0f); + const float StartAgrarianDay = static_cast(FMath::Max(1, StartAbsoluteDay) - 1) + + (FMath::Clamp(StartHourOfDay, 0.0f, 24.0f) / 24.0f); + return FMath::Clamp((CurrentAgrarianDay - StartAgrarianDay) / SafeDuration, 0.0f, 1.0f); +} + +bool AAgrarianGameState::IsDayInsideActiveGrowingSeason(int32 DayOfYear) const +{ + return IsDayInRange( + DayOfYear, + ActiveGrowingSeason.GrowingSeasonStartDay, + ActiveGrowingSeason.GrowingSeasonEndDay, + DaysPerAgrarianYear); +} + +int32 AAgrarianGameState::GetDaysRemainingInActiveGrowingSeason(int32 PlantDayOfYear) const +{ + const int32 SafeDaysPerYear = FMath::Max(1, DaysPerAgrarianYear); + const int32 Day = NormalizeCalendarDay(PlantDayOfYear, SafeDaysPerYear); + const int32 Start = NormalizeCalendarDay(ActiveGrowingSeason.GrowingSeasonStartDay, SafeDaysPerYear); + const int32 End = NormalizeCalendarDay(ActiveGrowingSeason.GrowingSeasonEndDay, SafeDaysPerYear); + + if (Start <= End) + { + if (Day < Start) + { + return End - Start + 1; + } + if (Day > End) + { + return 0; + } + return End - Day + 1; + } + + if (Day >= Start) + { + return (SafeDaysPerYear - Day + 1) + End; + } + if (Day <= End) + { + return End - Day + 1; + } + return 0; +} + +FAgrarianCropSeasonAssessment AAgrarianGameState::AssessCropForActiveGrowingSeason(int32 CropMaturityDays, int32 PlantDayOfYear) const +{ + FAgrarianCropSeasonAssessment Assessment; + Assessment.DaysAvailable = GetDaysRemainingInActiveGrowingSeason(PlantDayOfYear); + Assessment.SafetyBufferDays = FMath::Max(0, ActiveGrowingSeason.CropSafetyBufferDays); + Assessment.DaysRequired = FMath::Max(0, CropMaturityDays) + Assessment.SafetyBufferDays; + Assessment.GrowingZoneLabel = ActiveGrowingSeason.GrowingZoneLabel; + Assessment.bCanPlantToday = false; + + if (CropMaturityDays <= 0) + { + Assessment.Fit = EAgrarianCropSeasonFit::Unknown; + Assessment.Reason = FText::FromString(TEXT("Crop maturity days must be greater than zero.")); + return Assessment; + } + if (!IsDayInsideActiveGrowingSeason(PlantDayOfYear)) + { + Assessment.Fit = EAgrarianCropSeasonFit::OutOfSeason; + Assessment.Reason = FText::FromString(TEXT("Planting day is outside the active tile growing season.")); + return Assessment; + } + if (Assessment.DaysRequired > ActiveGrowingSeason.FrostFreeDays) + { + Assessment.Fit = EAgrarianCropSeasonFit::TooLong; + Assessment.Reason = FText::FromString(TEXT("Crop maturity is too long for this tile's frost-free growing window.")); + return Assessment; + } + if (Assessment.DaysRequired > Assessment.DaysAvailable) + { + Assessment.Fit = EAgrarianCropSeasonFit::Marginal; + Assessment.Reason = FText::FromString(TEXT("Crop can grow in this zone, but this planting date is too late for a reliable harvest.")); + return Assessment; + } + + Assessment.Fit = EAgrarianCropSeasonFit::FitsSeason; + Assessment.bCanPlantToday = true; + Assessment.Reason = FText::FromString(TEXT("Crop maturity fits the active tile growing season.")); + return Assessment; +} + void AAgrarianGameState::OnRep_Weather() { UpdateAmbientTemperature(); } +void AAgrarianGameState::UpdateSolarTimes() +{ + if (ActiveSolarTileId == NAME_None) + { + bHasActiveTileSolarData = false; + SunriseHourLocal = 6.0f; + SunsetHourLocal = 20.0f; + SolarNoonHourLocal = 13.0f; + DayLengthHours = 14.0f; + return; + } + + const float ClampedLatitude = FMath::Clamp(ActiveTileLatitude, -89.8f, 89.8f); + const float ClampedLongitude = FMath::Clamp(ActiveTileLongitude, -180.0f, 180.0f); + const int32 ClampedDayOfYear = FMath::Clamp(ActiveDayOfYear, 1, 366); + // NOAA approximate sunrise/sunset model; good enough for MVP regional light timing. + const double Gamma = (2.0 * PI / 365.0) * (static_cast(ClampedDayOfYear) - 1.0); + const double EquationOfTimeMinutes = 229.18 * ( + 0.000075 + + 0.001868 * FMath::Cos(Gamma) + - 0.032077 * FMath::Sin(Gamma) + - 0.014615 * FMath::Cos(2.0 * Gamma) + - 0.040849 * FMath::Sin(2.0 * Gamma)); + const double SolarDeclinationRadians = + 0.006918 + - 0.399912 * FMath::Cos(Gamma) + + 0.070257 * FMath::Sin(Gamma) + - 0.006758 * FMath::Cos(2.0 * Gamma) + + 0.000907 * FMath::Sin(2.0 * Gamma) + - 0.002697 * FMath::Cos(3.0 * Gamma) + + 0.00148 * FMath::Sin(3.0 * Gamma); + + const double LatitudeRadians = FMath::DegreesToRadians(static_cast(ClampedLatitude)); + const double SolarZenithRadians = FMath::DegreesToRadians(90.833); + const double HourAngleArgument = + (FMath::Cos(SolarZenithRadians) / (FMath::Cos(LatitudeRadians) * FMath::Cos(SolarDeclinationRadians))) + - FMath::Tan(LatitudeRadians) * FMath::Tan(SolarDeclinationRadians); + + if (HourAngleArgument <= -1.0 || HourAngleArgument >= 1.0) + { + bHasActiveTileSolarData = true; + SolarNoonHourLocal = FMath::Fmod(12.0f + ActiveTileUtcOffsetHours - (ClampedLongitude / 15.0f), 24.0f); + if (SolarNoonHourLocal < 0.0f) + { + SolarNoonHourLocal += 24.0f; + } + DayLengthHours = HourAngleArgument <= -1.0 ? 24.0f : 0.0f; + SunriseHourLocal = DayLengthHours >= 24.0f ? 0.0f : SolarNoonHourLocal; + SunsetHourLocal = DayLengthHours >= 24.0f ? 24.0f : SolarNoonHourLocal; + return; + } + + const double HourAngleDegrees = FMath::RadiansToDegrees(FMath::Acos(HourAngleArgument)); + const double SolarNoonMinutes = 720.0 - (4.0 * ClampedLongitude) - EquationOfTimeMinutes + (ActiveTileUtcOffsetHours * 60.0); + const double SunriseMinutes = SolarNoonMinutes - (HourAngleDegrees * 4.0); + const double SunsetMinutes = SolarNoonMinutes + (HourAngleDegrees * 4.0); + + auto NormalizeHour = [](double Minutes) + { + double Hours = FMath::Fmod(Minutes / 60.0, 24.0); + if (Hours < 0.0) + { + Hours += 24.0; + } + return static_cast(Hours); + }; + + bHasActiveTileSolarData = true; + SunriseHourLocal = NormalizeHour(SunriseMinutes); + SunsetHourLocal = NormalizeHour(SunsetMinutes); + SolarNoonHourLocal = NormalizeHour(SolarNoonMinutes); + DayLengthHours = static_cast((SunsetMinutes - SunriseMinutes) / 60.0); +} + void AAgrarianGameState::UpdateAmbientTemperature() { - const float DayWarmth = FMath::Sin((WorldHours / 24.0f) * 2.0f * PI - (PI * 0.5f)) * 8.0f; + const float WarmestHour = bHasActiveTileSolarData ? FMath::Fmod(SolarNoonHourLocal + 3.0f, 24.0f) : 14.0f; + const float DayWarmth = FMath::Sin(((WorldHours - WarmestHour) / 24.0f) * 2.0f * PI + (PI * 0.5f)) * 8.0f; float WeatherModifier = 0.0f; switch (Weather) diff --git a/Source/AgrarianGame/AgrarianGameState.h b/Source/AgrarianGame/AgrarianGameState.h index 4ebe40f..83ace19 100644 --- a/Source/AgrarianGame/AgrarianGameState.h +++ b/Source/AgrarianGame/AgrarianGameState.h @@ -15,6 +15,7 @@ class AAgrarianGameState : public AGameStateBase public: AAgrarianGameState(); + virtual void BeginPlay() override; virtual void Tick(float DeltaSeconds) override; virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; @@ -24,21 +25,97 @@ public: UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|World", meta = (ClampMin = "0.1")) float GameHoursPerRealMinute = 0.1f; + UPROPERTY(EditAnywhere, BlueprintReadOnly, Replicated, Category = "Agrarian|World|Calendar", meta = (ClampMin = "1", ClampMax = "366")) + int32 DaysPerAgrarianYear = 366; + UPROPERTY(EditAnywhere, BlueprintReadOnly, ReplicatedUsing = OnRep_Weather, Category = "Agrarian|World") EAgrarianWeatherType Weather = EAgrarianWeatherType::Clear; UPROPERTY(EditAnywhere, BlueprintReadOnly, Replicated, Category = "Agrarian|World") float AmbientTemperatureC = 12.0f; + UPROPERTY(EditAnywhere, BlueprintReadOnly, Replicated, Category = "Agrarian|World|Tile Solar") + FName ActiveSolarTileId = TEXT("gz_us_ca_pacifica_utm10n_e544_n4160"); + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Replicated, Category = "Agrarian|World|Tile Solar") + float ActiveTileLatitude = 37.5925f; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Replicated, Category = "Agrarian|World|Tile Solar") + float ActiveTileLongitude = -122.4995f; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Replicated, Category = "Agrarian|World|Tile Solar") + FString ActiveTileTimeZoneId = TEXT("America/Los_Angeles"); + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Replicated, Category = "Agrarian|World|Tile Solar") + float ActiveTileUtcOffsetHours = -7.0f; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Replicated, Category = "Agrarian|World|Tile Solar", meta = (ClampMin = "1", ClampMax = "366")) + int32 ActiveDayOfYear = 172; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Replicated, Category = "Agrarian|World|Calendar", meta = (ClampMin = "1")) + int32 ActiveYear = 1; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Replicated, Category = "Agrarian|World|Growing Season") + FAgrarianGrowingSeasonProfile ActiveGrowingSeason; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Replicated, Category = "Agrarian|World|Tile Solar") + bool bHasActiveTileSolarData = false; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Replicated, Category = "Agrarian|World|Tile Solar") + float SunriseHourLocal = 6.0f; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Replicated, Category = "Agrarian|World|Tile Solar") + float SunsetHourLocal = 20.0f; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Replicated, Category = "Agrarian|World|Tile Solar") + float SolarNoonHourLocal = 13.0f; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Replicated, Category = "Agrarian|World|Tile Solar") + float DayLengthHours = 14.0f; + UFUNCTION(BlueprintCallable, Category = "Agrarian|World") bool IsNight() const; UFUNCTION(BlueprintCallable, Category = "Agrarian|World") void SetWeather(EAgrarianWeatherType NewWeather); + UFUNCTION(BlueprintCallable, Category = "Agrarian|World|Tile Solar") + bool ConfigureActiveSolarTile(FName TileId, float Latitude, float Longitude, const FString& TimeZoneId, float UtcOffsetHours); + + UFUNCTION(BlueprintPure, Category = "Agrarian|World|Calendar") + FAgrarianCalendarSnapshot GetCalendarSnapshot() const; + + UFUNCTION(BlueprintPure, Category = "Agrarian|World|Calendar") + int32 GetAbsoluteAgrarianDay() const; + + UFUNCTION(BlueprintPure, Category = "Agrarian|World|Calendar") + EAgrarianSeason GetSeasonForDay(int32 DayOfYear) const; + + UFUNCTION(BlueprintPure, Category = "Agrarian|World|Calendar") + int32 GetDayOfSeason(int32 DayOfYear) const; + + UFUNCTION(BlueprintPure, Category = "Agrarian|World|Calendar") + float ConvertAgrarianDaysToRealHours(float AgrarianDays) const; + + UFUNCTION(BlueprintPure, Category = "Agrarian|World|Calendar") + float ConvertRealHoursToAgrarianDays(float RealHours) const; + + UFUNCTION(BlueprintPure, Category = "Agrarian|World|Calendar") + float GetLongTaskProgress(int32 StartAbsoluteDay, float StartHourOfDay, float DurationAgrarianDays) const; + + UFUNCTION(BlueprintPure, Category = "Agrarian|World|Growing Season") + bool IsDayInsideActiveGrowingSeason(int32 DayOfYear) const; + + UFUNCTION(BlueprintPure, Category = "Agrarian|World|Growing Season") + int32 GetDaysRemainingInActiveGrowingSeason(int32 PlantDayOfYear) const; + + UFUNCTION(BlueprintPure, Category = "Agrarian|World|Growing Season") + FAgrarianCropSeasonAssessment AssessCropForActiveGrowingSeason(int32 CropMaturityDays, int32 PlantDayOfYear) const; + protected: UFUNCTION() void OnRep_Weather(); + void UpdateSolarTimes(); void UpdateAmbientTemperature(); }; diff --git a/Source/AgrarianGame/AgrarianPersistenceSubsystem.cpp b/Source/AgrarianGame/AgrarianPersistenceSubsystem.cpp index 3256f10..5b541f3 100644 --- a/Source/AgrarianGame/AgrarianPersistenceSubsystem.cpp +++ b/Source/AgrarianGame/AgrarianPersistenceSubsystem.cpp @@ -1,10 +1,14 @@ // Copyright Pacificao. All Rights Reserved. #include "AgrarianPersistenceSubsystem.h" +#include "AgrarianGameCharacter.h" +#include "AgrarianInventoryComponent.h" #include "AgrarianPersistentActorComponent.h" #include "AgrarianSaveGame.h" +#include "AgrarianSurvivalComponent.h" #include "EngineUtils.h" #include "Engine/World.h" +#include "GameFramework/PlayerState.h" #include "Kismet/GameplayStatics.h" UAgrarianSaveGame* UAgrarianPersistenceSubsystem::CreateEmptySave() const @@ -116,6 +120,87 @@ int32 UAgrarianPersistenceSubsystem::RestoreWorldActors(const UAgrarianSaveGame* return RestoredCount; } +int32 UAgrarianPersistenceSubsystem::CapturePlayers(UAgrarianSaveGame* SaveGame) const +{ + if (!SaveGame) + { + return 0; + } + + TArray Players; + FindAgrarianPlayers(Players); + + SaveGame->Players.Reset(); + for (const AAgrarianGameCharacter* Character : Players) + { + const UAgrarianSurvivalComponent* SurvivalComponent = Character ? Character->GetSurvivalComponent() : nullptr; + if (!Character || !SurvivalComponent) + { + continue; + } + + FAgrarianSavedPlayer SavedPlayer; + SavedPlayer.PlayerId = GetPlayerPersistenceId(Character); + SavedPlayer.Transform = Character->GetActorTransform(); + SavedPlayer.Survival = SurvivalComponent->Survival; + SavedPlayer.CareHistory = SurvivalComponent->CareHistory; + + if (const UAgrarianInventoryComponent* InventoryComponent = Character->GetInventoryComponent()) + { + SavedPlayer.Inventory = InventoryComponent->Items; + } + + SaveGame->Players.Add(SavedPlayer); + } + + return SaveGame->Players.Num(); +} + +int32 UAgrarianPersistenceSubsystem::RestorePlayers(const UAgrarianSaveGame* SaveGame) const +{ + if (!SaveGame) + { + return 0; + } + + TArray Players; + FindAgrarianPlayers(Players); + + int32 RestoredCount = 0; + for (AAgrarianGameCharacter* Character : Players) + { + UAgrarianSurvivalComponent* SurvivalComponent = Character ? Character->GetSurvivalComponent() : nullptr; + if (!Character || !SurvivalComponent) + { + continue; + } + + const FString PlayerId = GetPlayerPersistenceId(Character); + const FAgrarianSavedPlayer* SavedPlayer = SaveGame->Players.FindByPredicate( + [&PlayerId](const FAgrarianSavedPlayer& Candidate) + { + return Candidate.PlayerId == PlayerId; + }); + + if (!SavedPlayer) + { + continue; + } + + Character->SetActorTransform(SavedPlayer->Transform, false, nullptr, ETeleportType::TeleportPhysics); + SurvivalComponent->ApplySavedState(SavedPlayer->Survival, SavedPlayer->CareHistory); + + if (UAgrarianInventoryComponent* InventoryComponent = Character->GetInventoryComponent()) + { + InventoryComponent->Items = SavedPlayer->Inventory; + } + + RestoredCount++; + } + + return RestoredCount; +} + bool UAgrarianPersistenceSubsystem::SaveCurrentWorld() const { UAgrarianSaveGame* SaveGame = LoadOrCreateSave(); @@ -124,6 +209,7 @@ bool UAgrarianPersistenceSubsystem::SaveCurrentWorld() const return false; } + CapturePlayers(SaveGame); CaptureWorldActors(SaveGame); return WriteSave(SaveGame); } @@ -152,3 +238,42 @@ void UAgrarianPersistenceSubsystem::FindPersistentComponents(TArray& OutPlayers) const +{ + OutPlayers.Reset(); + + UWorld* World = GetWorld(); + if (!World) + { + return; + } + + for (TActorIterator ActorIt(World); ActorIt; ++ActorIt) + { + AAgrarianGameCharacter* Character = *ActorIt; + if (Character && !Character->IsPendingKillPending()) + { + OutPlayers.Add(Character); + } + } +} + +FString UAgrarianPersistenceSubsystem::GetPlayerPersistenceId(const AAgrarianGameCharacter* Character) const +{ + if (!Character) + { + return TEXT("UnknownPlayer"); + } + + if (const APlayerState* PlayerState = Character->GetPlayerState()) + { + const FString PlayerName = PlayerState->GetPlayerName(); + if (!PlayerName.IsEmpty()) + { + return PlayerName; + } + } + + return Character->GetName(); +} diff --git a/Source/AgrarianGame/AgrarianPersistenceSubsystem.h b/Source/AgrarianGame/AgrarianPersistenceSubsystem.h index 1ecaa5d..9a66576 100644 --- a/Source/AgrarianGame/AgrarianPersistenceSubsystem.h +++ b/Source/AgrarianGame/AgrarianPersistenceSubsystem.h @@ -8,6 +8,7 @@ class UAgrarianSaveGame; class UAgrarianPersistentActorComponent; +class AAgrarianGameCharacter; UCLASS() class UAgrarianPersistenceSubsystem : public UGameInstanceSubsystem @@ -45,9 +46,17 @@ public: UFUNCTION(BlueprintCallable, Category = "Agrarian|Persistence") int32 RestoreWorldActors(const UAgrarianSaveGame* SaveGame, bool bClearExistingActors = true) const; + UFUNCTION(BlueprintCallable, Category = "Agrarian|Persistence") + int32 CapturePlayers(UAgrarianSaveGame* SaveGame) const; + + UFUNCTION(BlueprintCallable, Category = "Agrarian|Persistence") + int32 RestorePlayers(const UAgrarianSaveGame* SaveGame) const; + UFUNCTION(BlueprintCallable, Category = "Agrarian|Persistence") bool SaveCurrentWorld() const; protected: void FindPersistentComponents(TArray& OutComponents) const; + void FindAgrarianPlayers(TArray& OutPlayers) const; + FString GetPlayerPersistenceId(const AAgrarianGameCharacter* Character) const; }; diff --git a/Source/AgrarianGame/AgrarianSaveGame.h b/Source/AgrarianGame/AgrarianSaveGame.h index c55b678..a4860d0 100644 --- a/Source/AgrarianGame/AgrarianSaveGame.h +++ b/Source/AgrarianGame/AgrarianSaveGame.h @@ -21,6 +21,9 @@ struct FAgrarianSavedPlayer UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Save") FAgrarianSurvivalSnapshot Survival; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Save") + FAgrarianCareHistorySnapshot CareHistory; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Save") TArray Inventory; }; diff --git a/Source/AgrarianGame/AgrarianSurvivalComponent.cpp b/Source/AgrarianGame/AgrarianSurvivalComponent.cpp index 9e45236..bce7eb6 100644 --- a/Source/AgrarianGame/AgrarianSurvivalComponent.cpp +++ b/Source/AgrarianGame/AgrarianSurvivalComponent.cpp @@ -15,6 +15,7 @@ void UAgrarianSurvivalComponent::BeginPlay() { Super::BeginPlay(); ClampSurvival(); + ClampCareHistory(); BroadcastSurvivalChanged(); } @@ -32,6 +33,26 @@ void UAgrarianSurvivalComponent::TickComponent(float DeltaTime, ELevelTick TickT Survival.Thirst -= ThirstDecayPerMinute * Minutes; Survival.Stamina += StaminaRecoveryPerSecond * DeltaTime; + if (Survival.Stamina <= LowStaminaExhaustionThreshold) + { + Survival.Exhaustion += ExhaustionGainPerLowStaminaSecond * DeltaTime; + } + else if (Survival.Hunger > 10.0f && Survival.Thirst > 10.0f && Survival.BodyTemperature >= 35.0f) + { + Survival.Exhaustion -= ExhaustionRecoveryPerSecond * DeltaTime; + } + + if (Survival.SicknessSeverity > 0.0f) + { + Survival.Exhaustion += (Survival.SicknessSeverity / 100.0f) * 0.08f * DeltaTime; + CareHistory.IllnessBurden += (Survival.SicknessSeverity / 100.0f) * 0.001f * DeltaTime; + + if (Survival.Hunger > 20.0f && Survival.Thirst > 20.0f && Survival.BodyTemperature >= 35.0f) + { + Survival.SicknessSeverity -= SicknessRecoveryPerSecond * DeltaTime * FMath::Max(0.25f, CareHistory.TreatmentQuality); + } + } + if (const UWorld* World = GetWorld()) { if (const AAgrarianGameState* AgrarianGameState = World->GetGameState()) @@ -56,7 +77,13 @@ void UAgrarianSurvivalComponent::TickComponent(float DeltaTime, ELevelTick TickT Survival.Health -= ColdDamagePerMinute * Minutes; } + if (Survival.SicknessSeverity >= 60.0f) + { + Survival.Health -= SicknessDamagePerMinute * (Survival.SicknessSeverity / 100.0f) * Minutes; + } + ClampSurvival(); + ClampCareHistory(); BroadcastSurvivalChanged(); } @@ -64,6 +91,7 @@ void UAgrarianSurvivalComponent::GetLifetimeReplicatedProps(TArrayHasAuthority()) { Survival.InjurySeverity += FMath::Max(0.0f, Severity); + CareHistory.InjuryBurden += FMath::Max(0.0f, Severity) / 100.0f; Survival.Health -= Severity * 5.0f; ClampSurvival(); + ClampCareHistory(); + BroadcastSurvivalChanged(); + } +} + +void UAgrarianSurvivalComponent::AddSickness(float Severity) +{ + if (GetOwner() && GetOwner()->HasAuthority()) + { + const float PositiveSeverity = FMath::Max(0.0f, Severity); + Survival.SicknessSeverity += PositiveSeverity; + CareHistory.IllnessBurden += PositiveSeverity / 100.0f; + ClampSurvival(); + ClampCareHistory(); + BroadcastSurvivalChanged(); + } +} + +void UAgrarianSurvivalComponent::ReduceSickness(float Amount) +{ + if (GetOwner() && GetOwner()->HasAuthority()) + { + Survival.SicknessSeverity -= FMath::Max(0.0f, Amount); + ClampSurvival(); + BroadcastSurvivalChanged(); + } +} + +void UAgrarianSurvivalComponent::ApplySavedState(const FAgrarianSurvivalSnapshot& SavedSurvival, const FAgrarianCareHistorySnapshot& SavedCareHistory) +{ + if (GetOwner() && GetOwner()->HasAuthority()) + { + Survival = SavedSurvival; + CareHistory = SavedCareHistory; + ClampSurvival(); + ClampCareHistory(); BroadcastSurvivalChanged(); } } @@ -136,7 +201,29 @@ void UAgrarianSurvivalComponent::SpendStamina(float Amount) { if (GetOwner() && GetOwner()->HasAuthority()) { - Survival.Stamina -= FMath::Max(0.0f, Amount); + const float PositiveAmount = FMath::Max(0.0f, Amount); + Survival.Stamina -= PositiveAmount; + Survival.Exhaustion += PositiveAmount * 0.05f; + ClampSurvival(); + BroadcastSurvivalChanged(); + } +} + +void UAgrarianSurvivalComponent::AddExhaustion(float Amount) +{ + if (GetOwner() && GetOwner()->HasAuthority()) + { + Survival.Exhaustion += FMath::Max(0.0f, Amount); + ClampSurvival(); + BroadcastSurvivalChanged(); + } +} + +void UAgrarianSurvivalComponent::ReduceExhaustion(float Amount) +{ + if (GetOwner() && GetOwner()->HasAuthority()) + { + Survival.Exhaustion -= FMath::Max(0.0f, Amount); ClampSurvival(); BroadcastSurvivalChanged(); } @@ -147,14 +234,34 @@ void UAgrarianSurvivalComponent::OnRep_Survival() BroadcastSurvivalChanged(); } +void UAgrarianSurvivalComponent::OnRep_CareHistory() +{ + ClampCareHistory(); + BroadcastSurvivalChanged(); +} + void UAgrarianSurvivalComponent::ClampSurvival() { Survival.Health = FMath::Clamp(Survival.Health, 0.0f, 100.0f); Survival.Stamina = FMath::Clamp(Survival.Stamina, 0.0f, 100.0f); + Survival.Exhaustion = FMath::Clamp(Survival.Exhaustion, 0.0f, 100.0f); Survival.Hunger = FMath::Clamp(Survival.Hunger, 0.0f, 100.0f); Survival.Thirst = FMath::Clamp(Survival.Thirst, 0.0f, 100.0f); Survival.BodyTemperature = FMath::Clamp(Survival.BodyTemperature, 30.0f, 42.0f); Survival.InjurySeverity = FMath::Clamp(Survival.InjurySeverity, 0.0f, 100.0f); + Survival.SicknessSeverity = FMath::Clamp(Survival.SicknessSeverity, 0.0f, 100.0f); +} + +void UAgrarianSurvivalComponent::ClampCareHistory() +{ + CareHistory.NutritionQuality = FMath::Clamp(CareHistory.NutritionQuality, 0.0f, 1.0f); + CareHistory.IllnessBurden = FMath::Clamp(CareHistory.IllnessBurden, 0.0f, 1.0f); + CareHistory.InjuryBurden = FMath::Clamp(CareHistory.InjuryBurden, 0.0f, 1.0f); + CareHistory.SleepQuality = FMath::Clamp(CareHistory.SleepQuality, 0.0f, 1.0f); + CareHistory.ShelterQuality = FMath::Clamp(CareHistory.ShelterQuality, 0.0f, 1.0f); + CareHistory.StressBurden = FMath::Clamp(CareHistory.StressBurden, 0.0f, 1.0f); + CareHistory.WorkloadBurden = FMath::Clamp(CareHistory.WorkloadBurden, 0.0f, 1.0f); + CareHistory.TreatmentQuality = FMath::Clamp(CareHistory.TreatmentQuality, 0.0f, 1.0f); } void UAgrarianSurvivalComponent::BroadcastSurvivalChanged() diff --git a/Source/AgrarianGame/AgrarianSurvivalComponent.h b/Source/AgrarianGame/AgrarianSurvivalComponent.h index cca3c0f..df6c528 100644 --- a/Source/AgrarianGame/AgrarianSurvivalComponent.h +++ b/Source/AgrarianGame/AgrarianSurvivalComponent.h @@ -27,6 +27,9 @@ public: UPROPERTY(EditAnywhere, BlueprintReadOnly, ReplicatedUsing = OnRep_Survival, Category = "Agrarian|Survival") FAgrarianSurvivalSnapshot Survival; + UPROPERTY(EditAnywhere, BlueprintReadOnly, ReplicatedUsing = OnRep_CareHistory, Category = "Agrarian|Survival") + FAgrarianCareHistorySnapshot CareHistory; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Survival|Rates", meta = (ClampMin = "0")) float HungerDecayPerMinute = 0.55f; @@ -36,6 +39,15 @@ public: UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Survival|Rates", meta = (ClampMin = "0")) float StaminaRecoveryPerSecond = 14.0f; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Survival|Rates", meta = (ClampMin = "0")) + float ExhaustionGainPerLowStaminaSecond = 0.35f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Survival|Rates", meta = (ClampMin = "0")) + float ExhaustionRecoveryPerSecond = 0.08f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Survival|Rates", meta = (ClampMin = "0", ClampMax = "100")) + float LowStaminaExhaustionThreshold = 20.0f; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Survival|Rates", meta = (ClampMin = "0")) float StarvationDamagePerMinute = 3.0f; @@ -45,6 +57,12 @@ public: UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Survival|Rates", meta = (ClampMin = "0")) float ColdDamagePerMinute = 4.0f; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Survival|Rates", meta = (ClampMin = "0")) + float SicknessDamagePerMinute = 1.5f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Survival|Rates", meta = (ClampMin = "0")) + float SicknessRecoveryPerSecond = 0.02f; + UFUNCTION(BlueprintCallable, Category = "Agrarian|Survival") bool IsAlive() const; @@ -66,13 +84,32 @@ public: UFUNCTION(BlueprintCallable, Category = "Agrarian|Survival") void AddInjury(float Severity); + UFUNCTION(BlueprintCallable, Category = "Agrarian|Survival") + void AddSickness(float Severity); + + UFUNCTION(BlueprintCallable, Category = "Agrarian|Survival") + void ReduceSickness(float Amount); + + UFUNCTION(BlueprintCallable, Category = "Agrarian|Survival") + void ApplySavedState(const FAgrarianSurvivalSnapshot& SavedSurvival, const FAgrarianCareHistorySnapshot& SavedCareHistory); + UFUNCTION(BlueprintCallable, Category = "Agrarian|Survival") void SpendStamina(float Amount); + UFUNCTION(BlueprintCallable, Category = "Agrarian|Survival") + void AddExhaustion(float Amount); + + UFUNCTION(BlueprintCallable, Category = "Agrarian|Survival") + void ReduceExhaustion(float Amount); + protected: UFUNCTION() void OnRep_Survival(); + UFUNCTION() + void OnRep_CareHistory(); + void ClampSurvival(); + void ClampCareHistory(); void BroadcastSurvivalChanged(); }; diff --git a/Source/AgrarianGame/AgrarianTypes.h b/Source/AgrarianGame/AgrarianTypes.h index d738cea..9ed1b29 100644 --- a/Source/AgrarianGame/AgrarianTypes.h +++ b/Source/AgrarianGame/AgrarianTypes.h @@ -14,6 +14,112 @@ enum class EAgrarianWeatherType : uint8 Storm UMETA(DisplayName = "Storm") }; +UENUM(BlueprintType) +enum class EAgrarianSeason : uint8 +{ + Winter UMETA(DisplayName = "Winter"), + Spring UMETA(DisplayName = "Spring"), + Summer UMETA(DisplayName = "Summer"), + Autumn UMETA(DisplayName = "Autumn") +}; + +UENUM(BlueprintType) +enum class EAgrarianCropSeasonFit : uint8 +{ + Unknown UMETA(DisplayName = "Unknown"), + FitsSeason UMETA(DisplayName = "Fits Season"), + Marginal UMETA(DisplayName = "Marginal"), + TooLong UMETA(DisplayName = "Too Long"), + OutOfSeason UMETA(DisplayName = "Out Of Season") +}; + +USTRUCT(BlueprintType) +struct FAgrarianCalendarSnapshot +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Calendar") + int32 DayOfYear = 1; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Calendar") + int32 Year = 1; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Calendar") + int32 AbsoluteDay = 1; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Calendar") + int32 DayOfSeason = 1; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Calendar") + EAgrarianSeason Season = EAgrarianSeason::Winter; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Calendar") + float HourOfDay = 8.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Calendar") + bool bInsideGrowingSeason = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Calendar") + int32 GrowingSeasonDaysRemaining = 0; +}; + +USTRUCT(BlueprintType) +struct FAgrarianGrowingSeasonProfile +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Growing Season") + FName TileId = NAME_None; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Growing Season") + FString GrowingZoneLabel = TEXT("unknown"); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Growing Season", meta = (ClampMin = "1", ClampMax = "366")) + int32 GrowingSeasonStartDay = 80; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Growing Season", meta = (ClampMin = "1", ClampMax = "366")) + int32 GrowingSeasonEndDay = 300; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Growing Season", meta = (ClampMin = "0", ClampMax = "366")) + int32 FrostFreeDays = 220; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Growing Season") + float MinAverageGrowingTempC = 7.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Growing Season", meta = (ClampMin = "0", ClampMax = "90")) + int32 CropSafetyBufferDays = 14; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Growing Season") + FString ClimateProfile = TEXT("unknown"); +}; + +USTRUCT(BlueprintType) +struct FAgrarianCropSeasonAssessment +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Growing Season") + EAgrarianCropSeasonFit Fit = EAgrarianCropSeasonFit::Unknown; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Growing Season") + int32 DaysAvailable = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Growing Season") + int32 DaysRequired = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Growing Season") + int32 SafetyBufferDays = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Growing Season") + bool bCanPlantToday = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Growing Season") + FString GrowingZoneLabel = TEXT("unknown"); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Growing Season") + FText Reason; +}; + USTRUCT(BlueprintType) struct FAgrarianItemStack { @@ -125,6 +231,9 @@ struct FAgrarianSurvivalSnapshot UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Survival") float Stamina = 100.0f; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Survival") + float Exhaustion = 0.0f; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Survival") float Hunger = 100.0f; @@ -136,4 +245,37 @@ struct FAgrarianSurvivalSnapshot UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Survival") float InjurySeverity = 0.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Survival") + float SicknessSeverity = 0.0f; +}; + +USTRUCT(BlueprintType) +struct FAgrarianCareHistorySnapshot +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Care History") + float NutritionQuality = 1.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Care History") + float IllnessBurden = 0.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Care History") + float InjuryBurden = 0.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Care History") + float SleepQuality = 1.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Care History") + float ShelterQuality = 1.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Care History") + float StressBurden = 0.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Care History") + float WorkloadBurden = 0.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Care History") + float TreatmentQuality = 1.0f; };