From 65bd932473c26cec60e19b9337c8c6aa2f6a666c Mon Sep 17 00:00:00 2001 From: koritsa Date: Sun, 31 May 2026 21:54:33 +0300 Subject: [PATCH] Added phone item --- Content/Maps/L_EquipmentPreview.umap | 4 +- .../koritsa/Cylinder_168FE0FA.uasset | 3 + PLAN.md | 13 ++-- README.md | 45 ++++++----- .../Clothing/ClothingItemInstance.h | 3 - .../Inventory/InventorySubsystem.cpp | 77 +++++++++++++++++++ .../Inventory/InventorySubsystem.h | 15 ++++ .../NakedDesire/Phone/PhoneItemDefinition.cpp | 11 +++ .../NakedDesire/Phone/PhoneItemDefinition.h | 22 ++++++ .../NakedDesire/Phone/PhoneItemInstance.cpp | 22 ++++++ Source/NakedDesire/Phone/PhoneItemInstance.h | 35 +++++++++ Source/NakedDesire/SaveGame/SaveSubsystem.cpp | 2 +- .../NakedDesire/SaveGame/StartingSaveData.h | 9 ++- 13 files changed, 229 insertions(+), 32 deletions(-) create mode 100644 Content/Test/Maps/_GENERATED/koritsa/Cylinder_168FE0FA.uasset create mode 100644 Source/NakedDesire/Phone/PhoneItemDefinition.cpp create mode 100644 Source/NakedDesire/Phone/PhoneItemDefinition.h create mode 100644 Source/NakedDesire/Phone/PhoneItemInstance.cpp create mode 100644 Source/NakedDesire/Phone/PhoneItemInstance.h diff --git a/Content/Maps/L_EquipmentPreview.umap b/Content/Maps/L_EquipmentPreview.umap index 36aece2f..9e405cf6 100644 --- a/Content/Maps/L_EquipmentPreview.umap +++ b/Content/Maps/L_EquipmentPreview.umap @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:abcdf01c5244f0599867e2c9062c446afeb462cc3ca5b598a3a5a6b19fc09499 -size 85703 +oid sha256:6af638e1be56189dde54c1977829145a39bdbc39210a23690377762d17761297 +size 85299 diff --git a/Content/Test/Maps/_GENERATED/koritsa/Cylinder_168FE0FA.uasset b/Content/Test/Maps/_GENERATED/koritsa/Cylinder_168FE0FA.uasset new file mode 100644 index 00000000..7dcac08c --- /dev/null +++ b/Content/Test/Maps/_GENERATED/koritsa/Cylinder_168FE0FA.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:84ab88d864efd67bb1920a40f93ea073cab4a60568e88b68112d0b9f91e3f519 +size 14856 diff --git a/PLAN.md b/PLAN.md index 6a2428ed..90c3e286 100644 --- a/PLAN.md +++ b/PLAN.md @@ -80,7 +80,7 @@ State of the C++ module as of the latest pass. File references use `Source/Naked ### 1.1 Implemented -- **Item identity scaffold (GDD §6.1)** — `Items/ItemInstance.h` defines `UItemInstance` as an abstract `UObject` with a stable `FGuid InstanceID`, auto-assigned in `PostInitProperties` and refreshed in `PostDuplicate`. `Clothing/ClothingItemInstance.h` inherits from it and holds per-instance `Condition`, a pointer to the immutable `UClothingItem` definition, and a `StoredItems` array for container slots. +- **Item identity scaffold (GDD §6.1)** — `Items/ItemInstance.h` defines `UItemInstance` as an abstract `UObject` with a stable `FGuid InstanceID`, auto-assigned in `PostInitProperties` and refreshed in `PostDuplicate`. `Clothing/ClothingItemInstance.h` inherits from it and holds per-instance `Condition`, and a pointer to the immutable `UClothingItem` definition. (The legacy per-garment `StoredItems` container array was removed per GDD §27 — only bags hold items; the item list will live on a future bag `UItemInstance` subclass.) - **Definition / instance split (GDD §6.1, §17.4)** — `Clothing/ClothingItem.h` is the immutable `UPrimaryDataAsset` definition. Most fields align with the current GDD: `Coverage`, `CanExpose`, `ContainerSlots` (typed S/M/L via `EGarmentContainerSlotType` + `FGarmentContainerSlot`), `ProgressionPath`, plus rendering data (`SkeletalMesh`, `Materials`, `StaticMesh`). The `IsUnderwear` and `IsRestrictive` fields also exist but **no longer match the locked spec** (§20 #20, §20 #21) — see §1.4 for the removal / migration work. - **Progression path enum (GDD §5)** — `Progression/ProgressionPath.h` defines `EProgressionPath { None, Slut, Exhibitionist, Slave }`. Used on `UClothingItem::ProgressionPath`. - **Body-part enum (GDD §6.3)** — `Clothing/BodyPart.h` defines `EBodyPart { None, Boobs, Ass, Genitals }`. Used on `UClothingItem::CanExpose`. @@ -123,7 +123,10 @@ State of the C++ module as of the latest pass. File references use `Source/Naked - **NPC types (§10.2)** — only one generic `ANPC` class. No Walker / Stalker / Blogger / Snitch / Harasser, no Police, no Wanted-poster mechanic. - **Calendar, rent, sleep (§2.4, §15.2)** — **core landed (Phase 5):** `UTimeOfDaySubsystem` (`Global/TimeOfDaySubsystem`) owns the clock, rolls the calendar at 04:00, fires `OnHourChanged`/`OnDayChanged`/`OnPhaseChanged`, charges `WEEKLY_RENT` every 7th roll with immediate eviction via `OnCampaignEnded(Evicted)`, and exposes `Sleep()`/`SkipToNextMorning()`/`SkipTime()`. Mission refresh moved to `GameMode::HandleDayChanged`. The apartment **bed interactable** (`Interactables/Bed`) is in — interacting calls `USessionLossResolver::ResolveSleepLoss()` (outside-clothing loss) then `UTimeOfDaySubsystem::Sleep()`, with a BP `PlaySleepTransition` fade hook. **Still to do:** rewire the UltraDynamicSky actor to follow `SetCurrentTime` (and stop the old BP clock from advancing independently — otherwise the clock double-advances); the §4.4 cutscenes calling into the new skip API; the ending/eviction screen reacting to `OnCampaignEnded`; daily follower deposit is stubbed (no follower count until Phase 8); phone charge-to-100% on sleep stubbed (Phase 9). `WEEKLY_RENT` is a §21 tuning placeholder. - **Item-world AActor (§6.1)** — no `AItemActor` / `AClothingPickup` base. -- **Bag inventory (§6.4)** — absent. +- **Bag inventory (§6.4)** — absent. Per the locked model (GDD §6.4 / §27): bags are the **only** container, capacity is a flat per-bag **item count** (no S/M/L size classes), and any item type can be stored. The container concept is removed from clothing — the per-garment `UClothingItemInstance::StoredItems` array has been deleted; the item list will live on a future bag `UItemInstance` subclass. +- **Dedicated carry slots (§6.5 / §27)** — `Phone`, `Bag`, `Key` are single-item slots on the player, separate from the 18 clothing/accessory/restraint/toy slots and from the old (removed) hand slot. Deliberately **not** in `EClothingSlotType` (it's clothing-named); each carry slot is modeled independently. + - **Phone slot + phone item — done (data + slot).** `Phone/PhoneItemDefinition` (`UItemDefinition`) + `Phone/PhoneItemInstance` (`UItemInstance`, per-instance `BatteryPercent` in `FPhoneInstanceState`). The slot lives on `UInventorySubsystem`: `EquippedPhone`, `GetEquippedPhone()`, `OnPhoneChanged`, hot-swap on equip (§9.9). `EquipFromWardrobe`/`UnequipToWardrobe` route phones to `EquipPhone`/`UnequipPhone`; the equipped phone persists via the shared `EquippedItems` save bucket and re-hydrates in `EnsureHydrated` (clothing in that bucket is still hydrated by `ClothingManager`). **Follow-ups:** no phone-slot UI widget yet and the wardrobe list (`WardrobeInventoryWidget`) renders clothing only, so a phone is equippable from code / `StartingItems` but not yet via the wardrobe screen; battery drain/charge is Phase 8/9. + - **Bag + key slots — pending** (deferred with the bag system, see above). - **Theft (§6.3.4)** — new chance-based model (per-tick `P_theft` after grace period) not implemented. No theft timer / probability code at all. - **Rip & tear (§6.3.5)** — absent. `UClothingItemInstance::Condition` is read-only in code; no decrement source. - **Expose action (§6.3.6)** — `CanExpose` data exists on the definition; runtime action absent. @@ -345,12 +348,12 @@ Phase estimates are rough and assume one engineer. Adjust as we go. ### Phase 8 — Phone + forum UI + battery + livestream (3–4 weeks) -- Phone as an `AItemActor` (Phase 2 base) with hand / pocket / bag location semantics (§9). +- Phone as an `AItemActor` (Phase 2 base). Usable from the dedicated **phone slot** (§6.5 / §27); placed-in-world streaming exception still applies (§9.1.1). - `PhoneSubsystem` (§17.1): tickable; owns battery %, active app, livestream session lifecycle, charger interaction. - **Battery (§9.8):** - Passive base drain + per-app multiplier (Forum/Bank/Maps/Health Tracker/Gallery ~1×, back-camera ~3×, livestream ~5×). Livestream stacks on top of foreground app. - Apartment charger interactable: place phone on it, recharges at a fixed rate. - - **Portable powerbank**: consumable `UItemInstance` purchasable at Convenience Store (§10.4). Single-use, restores fixed % when consumed. S-size container slot. + - **Portable powerbank**: consumable `UItemInstance` purchasable at Convenience Store (§10.4). Single-use, restores fixed % when consumed. Stored in the equipped bag like any other item (no size classes, §6.4 / §27). - **Hard shutdown at 0%**: all apps lock; livestream ends and deposits earnings-to-date; future tips forfeit. Phone wakes from a consumed powerbank. - **Sleep auto-charges to 100%** (voluntary or energy-zero cutscene). - Battery % is part of the phone's `UItemInstance` state; persists across saves. @@ -385,7 +388,7 @@ Phase estimates are rough and assume one engineer. Adjust as we go. ### Phase 10 — Remaining systems & polish -- Bag inventory (§6.4) — uses Phase 2's `AItemActor` for world placement. +- Bag inventory (§6.4) — uses Phase 2's `AItemActor` for world placement. Bag `UItemInstance` subclass holds a count-limited list of any `UItemInstance` (no size classes); add the `Phone` / `Bag` / `Key` carry slots (§6.5 / §27). - **Food (§6.7):** - Implement the locked vocabulary: 2 instant effects (energy restore, lust decrease) + 4 timed buffs (stamina regen +, max stamina +, embarrassment-gain resistance, lust-gain resistance). - Universal built-in effect: every food restores effective max energy to base max (hunger reset; §7.3 hookpoint from Phase 4). diff --git a/README.md b/README.md index 2341a0cf..7328318f 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,7 @@ Each path has a level (e.g., 1–10). Level gates clothing, missions, and attrib - **Phone** (one active at a time, but the player may own multiple models simultaneously — §9, §9.9) - **Consumables** (food, §6.7) - **Toys** — sex toys for the toy slots (§6.5). Bought at the Adult Shop (§10.4). +- **Keys** — unlock restraints (§10.4.1). Carried in the dedicated key slot (§6.5). There are no other gadget categories. The phone is the only electronic device the player carries. @@ -164,12 +165,9 @@ Attributes per clothing instance: - `condition` — float [0,1]. Starts at 1.0 (new). Decreases via §6.3.4. - `canExpose` — list of body parts this garment can momentarily reveal without unequipping (e.g., coat → boobs, ass). - `restrictions` — list of restriction entries that fire while this item is equipped (e.g., wrist cuffs block phone use and changing top clothing; ankle cuffs block running). Most items have an empty list. See §6.3.7 for the full vocabulary. -- `containerSlots` — optional inventory (e.g., pants pocket for phone). -#### 6.3.1 Container slots -Container slot allows player to carry items there. -- `size`: S, M, L - Size of the container slot -- `count`: Count of the container slots +#### 6.3.1 No clothing inventory +Clothing provides no storage of its own — there are no pockets or per-garment container slots. The only item container in the game is the equipped bag (§6.4). All carriable items go in a bag; the phone, bag, and key have their own dedicated equipment slots (§6.5). #### 6.3.2 Coverage resolution For each body part `b`: @@ -239,14 +237,16 @@ Authored examples: **Commission interaction:** an accepted commission whose objective needs a blocked action (e.g., `PerformAction(masturbate)` with `BlockMasturbate` active) does not auto-fail; it simply cannot progress until the restriction is cleared. The commission's day / week deadline still applies normally. ### 6.4 Bags -- Bag is an equippable item with its own inventory. -- Player can equip **one** bag at a time. -- Bag can be placed in the world (set down) — persists as an AActor. +- A bag is an equippable item with its own inventory — the **only** container in the game. +- Player can equip **one** bag at a time (the bag slot, §6.5). +- **Any** item type can be stored in a bag — clothing, phone, food, toys, keys, powerbanks, etc. +- Capacity is a flat **maximum item count**, authored per bag — different bags hold different numbers of items. There are no item size classes (no S / M / L): one slot holds one item regardless of what it is. +- Bag can be placed in the world (set down) — persists as an AActor with its contents. - Bags follow the same theft rules as clothing (§6.3.4) and the same session-loss rules as other items (§4.4 / §6.6). ### 6.5 Inventory & Equipment -The player has 18 equipment slots in four groups, plus a bag slot, a hand slot, and container slots exposed by what's currently equipped. +The player has 18 equipment slots in four groups, plus three dedicated carry slots — **phone**, **bag**, and **key**. There is no hand slot and no per-garment container slots; general item storage is the equipped bag's inventory only (§6.4). #### Equipment slots @@ -294,19 +294,23 @@ Restraint slots are governed by §6.3.7 (effects) and §10.4.1 (removal). `NeckR Sex toys are bought from the Adult Shop (§10.4) and follow the standard item-identity rules (§6.1). Toys do **not** contribute to coverage (§6.3.2); a butt plug under panties leaves `UnderwearBottom`'s coverage value unchanged and the ass remains "covered" by panties for embarrassment math. Toys can be hidden beneath clothing or worn while otherwise nude — the slot is orthogonal to the body-clothing layering rules. Effects (lust modifier, embarrassment modifier, pulse modifier, audible vibration that NPCs may detect) are per-toy and authored on the toy's `UItemInstance`. -#### Other equipment -- **Bag slot** — 1 (see §6.4). -- **Hand slot** — 1. Holds one carriable item at a time (phone, food, key, etc.). Hand-dependent actions (phone use, item pickup, masturbate, expose action) are disabled when an equipped item declares the corresponding `Block*` restriction (§6.3.7); wrist cuffs are the canonical case. +#### Dedicated carry slots +- **Phone slot** — 1. Holds the active phone (§9, §9.9). The phone is usable directly from this slot; an empty slot means no phone is available. `BlockPhoneUse` (§6.3.7) still disables it while a restraint is on. +- **Bag slot** — 1. Holds one bag (§6.4). The bag's inventory is the only general item storage. +- **Key slot** — 1. Holds the key used for the restraint unlock action (§10.4.1). -#### Container slots -- Each equipped clothing item may expose container slots (e.g., pants pocket). The equipped bag exposes container slots as well. Each container slot has a size class (S / M / L, §6.3.1) that constrains what fits. -- There is no abstract "backpack" inventory. Total carry capacity = sum of container slots provided by currently equipped items. +Each of these slots accepts only its own item type — a phone in the phone slot, a bag in the bag slot, a key in the key slot. Everything else the player carries lives in the equipped bag. Actions that previously implied a free hand (item pickup, masturbate, expose action) are gated only by the relevant `Block*` restriction (§6.3.7); wrist cuffs are the canonical case. + +#### Item storage +- The only item storage is the equipped bag's inventory (§6.4). Clothing exposes no container slots. +- Capacity is the equipped bag's authored item-count limit. With no bag equipped, the player carries only what is worn plus what occupies the phone, bag, and key slots. +- There are no item size classes — one bag slot holds one item of any type. ### 6.6 Item loss summary table | Situation | Outcome | |-------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------| | Session ends safely | All carried / equipped / placed items persist. | -| Session lost, item equipped | Item persists, fully equipped. Loss never forcibly removes worn clothing (§4.4). | +| Session lost, item in a slot on the player (worn, phone, bag, key) | Item persists, fully equipped. Loss never forcibly removes anything held in a slot on the player (§4.4). | | Session lost, item in equipped bag | Item persists (bag is on the player). | | Session lost, bag placed in world | Bag and contents are subject to the same theft chance as clothing (§6.3.4) and to sleep-loss if the player sleeps before retrieving them. | | In-session, clothing past the grace period | Each check is a flat chance of theft (§6.3.4). Picking the item back up resets the grace timer. | @@ -452,7 +456,7 @@ Movement should be implemented as a state machine on the character with clear tr ## 9. Phone -The phone is the player's diegetic UI hub. It can be used if it is in: hand, pocket (pants with phone-sized container slot), or equipped bag. **Exception:** the livestream app (§9.1.1) continues to operate while the phone is placed in the world, so the player can keep streaming after setting the phone down for a back-camera shot. +The phone is the player's diegetic UI hub. It is usable whenever it occupies the player's **phone slot** (§6.5) — that is the active phone (§9.9). **Exception:** the livestream app (§9.1.1) continues to operate while the phone is placed in the world, so the player can keep streaming after setting the phone down for a back-camera shot. The phone is battery-powered (§9.8). A dead phone is unusable until charged — no forum, no camera, no maps. Plan accordingly. @@ -509,7 +513,7 @@ The phone has a finite battery that drains while powered on and recharges at the - Livestream: ~5× — heaviest drain. Livestream is the only app that ticks in the background and stacks its drain on top of whatever app is in the foreground. - **Charging.** - **Apartment charger** — primary source. A charger spot in the apartment recharges the phone at a fixed rate (TBD) while the phone is placed on it. - - **Portable powerbank** — purchasable consumable (Convenience Store, §10.4). Single-use; consuming one restores a fixed percentage of charge (TBD). Occupies one container slot (size S, §6.3.1) in the player's inventory and follows normal item-identity / theft rules. + - **Portable powerbank** — purchasable consumable (Convenience Store, §10.4). Single-use; consuming one restores a fixed percentage of charge (TBD). Stored in the equipped bag like any other item (§6.4) and follows normal item-identity / theft rules. - **Dead battery (0%).** Hard shutdown: - All apps lock. Opening the phone shows a "dead battery" overlay; no input passes through. - An active livestream **ends immediately**. Earnings accrued up to the cut-off are deposited; further viewers / tips that would have arrived are forfeit. @@ -960,11 +964,11 @@ Decisions previously open, now fixed: 8. **Run length:** 90-day campaign. Endless mode unlocked after first completion (rent-eviction disabled, all other systems intact). See §3.3. 9. **Cuff/restraint removal:** Key + timed unlock action only. No Helper NPC, no paid adult-shop service. See §10.4.1. 10. **Voice commands:** Not used. Hotkey-driven only. -11. **Equipment slot list:** 18 slots, locked. Body clothing (8): `Outerwear`, `Top`, `Bottom`, `UnderwearTop`, `UnderwearBottom`, `Bodysuit` (exclusive with the prior four), `Socks`, `Footwear`. Accessories (4): `Head`, `Face`, `Eyes`, `Neck`. Restraints (3, independent): `WristRestraint`, `AnkleRestraint`, `NeckRestraint`. Toys (3, independent): `Nipples`, `Anal`, `Vagina`. Face-cover bypass for recognition is driven by `Face` and `Eyes`. Toys do not contribute to coverage. See §6.5 for the full table. +11. **Equipment slot list:** 18 slots, locked. Body clothing (8): `Outerwear`, `Top`, `Bottom`, `UnderwearTop`, `UnderwearBottom`, `Bodysuit` (exclusive with the prior four), `Socks`, `Footwear`. Accessories (4): `Head`, `Face`, `Eyes`, `Neck`. Restraints (3, independent): `WristRestraint`, `AnkleRestraint`, `NeckRestraint`. Toys (3, independent): `Nipples`, `Anal`, `Vagina`. Face-cover bypass for recognition is driven by `Face` and `Eyes`. Toys do not contribute to coverage. The phone, bag, and key are carried in three separate dedicated slots (§27), not part of the 18. See §6.5 for the full table. 12. **City location list:** locked for launch. Apartment, Convenience Store, Café, Clothing Shops, Gym, Beauty Salon, Adult Shop, Electronics Shop, Streets / Parks / Alleys, **Beach**, **Train Station**, **Casino**. School exterior and hot springs (onsen) were considered and cut. Vertical slice (§18.1) covers the basic shop set; Beach, Train Station, Casino, and Electronics Shop are full-launch additions. See §10.4. 13. **Attribute level-up cost:** XP only. XP is a single shared pool (not per-path); the player chooses which path's attribute pool to spend it on, and may eventually max all three. No money, no separate currency. See §7.10. 14. **Food effect vocabulary:** locked. Two instant effects (energy restore, lust decrease) plus four timed buffs (stamina regen +, max stamina +, embarrassment-gain resistance, lust-gain resistance). Stacking: different types parallel; same type additive up to a per-type cap. No pulse buffs, no caffeine trickle. See §6.7. -15. **Carriable categories:** locked. Clothing, Bags, Phone, Consumables (food), Toys. No other gadget categories beyond the phone. See §6.2. +15. **Carriable categories:** locked. Clothing, Bags, Phone, Consumables (food), Toys, Keys. No other gadget categories beyond the phone. See §6.2. 16. **Casino:** added to the city (§10.4, §10.4.2). Main floor (slots, blackjack, roulette — money only). VIP room — **gated by a per-day entrance fee** (no permanent unlock; pay each day you want in). Permissive dress code; strip-game blackjack and roulette tables that bet clothing. No poker. Mixed pacing: slots instant, table games take in-game minutes. Net EV negative — variance is the appeal. 17. **Police capture, can't pay:** short non-interactive holding-cell cutscene, then time fast-forwards to the next morning. The night served settles the debt; no money owed afterward. Replaces the earlier "skip days proportional to unpaid amount" rule. See §4.4. 18. **Loss precedence during a police chase:** if the player is actively being chased by police (per §10.3) and any other loss condition fires (embarrassment max, energy zero), the loss resolves as **police capture** — the chase wins. The cops catch the player as they collapse. Applies even without a prior `wanted` tag. See §4.4. @@ -976,6 +980,7 @@ Decisions previously open, now fixed: 24. **Hunger via max-energy decay.** Effective max energy decays over time (a hunger rate); eating any food restores effective max to base max as a built-in universal effect (no per-food authoring). Sleep restores current energy but does NOT reset hunger — only eating does. Floors at a TBD fraction of base max so the player can't be starved into a forced game-over. See §7.3 / §6.7. 25. **Follower income cadence — daily, not weekly.** Passive follower income accrues and auto-deposits to the bank **every in-game day** at the 04:00 day-roll, not in a weekly lump. Resolves the prior §7.9 ↔ §9.4 / §13.3 contradiction in favor of §7.9's daily model. Rent remains a separate **weekly** charge (§15.2); the two cadences are intentionally offset. See §7.9 / §9.4 / §13.3 / §15.1. 26. **Masturbation gating — home vs. public.** Home masturbation (in the apartment) is **always available to every player, regardless of path**. Masturbating *during a session* (outside the apartment) is a **Slut-path unlock** (§5.1). The §14.1 quick-action shows the masturbate entry unconditionally at home; in-session it is hidden until the player has Slut-path investment. Non-Slut players therefore cannot reset lust mid-session and rely on lust-decrease food (§6.7) or returning home. Commission `PerformAction(masturbate)` objectives are public and thus Slut-gated by the generator (§13.4). Resolves the prior §5.1 ↔ §7.2 contradiction. See §7.2 / §5.1 / §14.1. +27. **Bags are the only containers; clothing has no pockets.** Per-garment container slots and item size classes (S / M / L) are removed. Only an equipped bag stores items, capacity is a flat per-bag **item count** (no size constraints), and any item type can go in any bag slot. The player also gains three dedicated single-item carry slots — **phone**, **bag**, and **key** — replacing the old generic hand slot; the phone is usable directly from the phone slot. See §6.3.1 / §6.4 / §6.5. ## 21. Open Design Questions diff --git a/Source/NakedDesire/Clothing/ClothingItemInstance.h b/Source/NakedDesire/Clothing/ClothingItemInstance.h index 2c2e485a..69666b86 100644 --- a/Source/NakedDesire/Clothing/ClothingItemInstance.h +++ b/Source/NakedDesire/Clothing/ClothingItemInstance.h @@ -33,7 +33,4 @@ public: protected: virtual void CaptureState(FInstancedStruct& OutState) const override; virtual void ApplyState(const FInstancedStruct& InState) override; - - UPROPERTY(BlueprintReadOnly, Category = "Clothing Item") - TArray> StoredItems; }; \ No newline at end of file diff --git a/Source/NakedDesire/Inventory/InventorySubsystem.cpp b/Source/NakedDesire/Inventory/InventorySubsystem.cpp index 271035f2..4aa6b60b 100644 --- a/Source/NakedDesire/Inventory/InventorySubsystem.cpp +++ b/Source/NakedDesire/Inventory/InventorySubsystem.cpp @@ -7,7 +7,10 @@ #include "NakedDesire/Clothing/ClothingItemDefinition.h" #include "NakedDesire/Clothing/ClothingItemInstance.h" #include "NakedDesire/Clothing/ClothingManager.h" +#include "NakedDesire/Items/ItemDefinition.h" #include "NakedDesire/Items/ItemInstance.h" +#include "NakedDesire/Phone/PhoneItemDefinition.h" +#include "NakedDesire/Phone/PhoneItemInstance.h" #include "NakedDesire/Player/NakedDesireCharacter.h" #include "NakedDesire/SaveGame/GlobalSaveGameData.h" #include "NakedDesire/SaveGame/ItemSaveRecord.h" @@ -19,6 +22,12 @@ const TArray>& UInventorySubsystem::GetWardrobeItems() return WardrobeItems; } +UPhoneItemInstance* UInventorySubsystem::GetEquippedPhone() +{ + EnsureHydrated(); + return EquippedPhone; +} + void UInventorySubsystem::AddToWardrobe(UItemInstance* Item) { EnsureHydrated(); @@ -37,6 +46,12 @@ void UInventorySubsystem::EquipFromWardrobe(UItemInstance* Item) { EnsureHydrated(); + if (UPhoneItemInstance* Phone = Cast(Item)) + { + EquipPhone(Phone); + return; + } + UClothingItemInstance* Clothing = Cast(Item); if (!Clothing || !Clothing->GetClothingItemDefinition()) return; @@ -74,6 +89,12 @@ void UInventorySubsystem::UnequipToWardrobe(UItemInstance* Item) { EnsureHydrated(); + if (UPhoneItemInstance* Phone = Cast(Item)) + { + UnequipPhone(Phone); + return; + } + UClothingItemInstance* Clothing = Cast(Item); if (!Clothing || !Clothing->GetClothingItemDefinition()) return; @@ -90,6 +111,45 @@ void UInventorySubsystem::UnequipToWardrobe(UItemInstance* Item) OnWardrobeChanged.Broadcast(); } +void UInventorySubsystem::EquipPhone(UPhoneItemInstance* Phone) +{ + if (!Phone || Phone == EquippedPhone) + return; + + UGlobalSaveGameData* Save = GetSave(); + + // Hot-swap: the previously equipped phone returns to the wardrobe (§9.9). + if (EquippedPhone) + { + if (Save) + Save->RemoveEquippedItem(EquippedPhone); + StoreItem(EquippedPhone); + } + + UnstoreItem(Phone); + EquippedPhone = Phone; + if (Save) + Save->AddEquippedItem(Phone); + + OnPhoneChanged.Broadcast(EquippedPhone); + OnWardrobeChanged.Broadcast(); +} + +void UInventorySubsystem::UnequipPhone(UPhoneItemInstance* Phone) +{ + if (!Phone || EquippedPhone != Phone) + return; + + if (UGlobalSaveGameData* Save = GetSave()) + Save->RemoveEquippedItem(Phone); + + EquippedPhone = nullptr; + StoreItem(Phone); + + OnPhoneChanged.Broadcast(nullptr); + OnWardrobeChanged.Broadcast(); +} + void UInventorySubsystem::EnsureHydrated() { UGlobalSaveGameData* Save = GetSave(); @@ -108,6 +168,23 @@ void UInventorySubsystem::EnsureHydrated() if (UItemInstance* Instance = UItemInstance::CreateFromRecord(this, Record)) WardrobeItems.Add(Instance); } + + // Only the phone is hydrated from the equipped bucket here; equipped clothing is owned and + // hydrated by the per-character UClothingManager. Pre-check the definition type so we don't + // mint throwaway clothing instances. + EquippedPhone = nullptr; + for (const FItemSaveRecord& Record : Save->GetEquippedItems()) + { + const UItemDefinition* Definition = Record.Definition.LoadSynchronous(); + if (!Definition || !Definition->IsA()) + continue; + + if (UPhoneItemInstance* Phone = Cast(UItemInstance::CreateFromRecord(this, Record))) + { + EquippedPhone = Phone; + break; + } + } } void UInventorySubsystem::StoreItem(UItemInstance* Item) diff --git a/Source/NakedDesire/Inventory/InventorySubsystem.h b/Source/NakedDesire/Inventory/InventorySubsystem.h index e5f1650a..c42a1f61 100644 --- a/Source/NakedDesire/Inventory/InventorySubsystem.h +++ b/Source/NakedDesire/Inventory/InventorySubsystem.h @@ -9,8 +9,10 @@ class UItemInstance; class UClothingManager; class UGlobalSaveGameData; +class UPhoneItemInstance; DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnWardrobeChangedSignature); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnPhoneChangedSignature, UPhoneItemInstance*, Phone); /** * Runtime owner of the off-body item store (the apartment wardrobe, GDD §6.5 / §10.4). @@ -31,9 +33,16 @@ public: UPROPERTY(BlueprintAssignable) FOnWardrobeChangedSignature OnWardrobeChanged; + // Fires when the phone slot changes; broadcasts the newly-equipped phone, or nullptr when emptied. + UPROPERTY(BlueprintAssignable) + FOnPhoneChangedSignature OnPhoneChanged; + /** Live wardrobe instances (off-body, mirrored from the save). */ const TArray>& GetWardrobeItems(); + /** The phone occupying the dedicated phone slot (§6.5 / §9.9), or nullptr if none is equipped. */ + UPhoneItemInstance* GetEquippedPhone(); + /** Bring an item into the wardrobe (purchase, world-return). */ void AddToWardrobe(UItemInstance* Item); @@ -51,12 +60,18 @@ private: void StoreItem(UItemInstance* Item); // live list + save bucket, no broadcast void UnstoreItem(UItemInstance* Item); // live list + save bucket, no broadcast + void EquipPhone(UPhoneItemInstance* Phone); // hot-swaps the previous phone back to the wardrobe + void UnequipPhone(UPhoneItemInstance* Phone); // stores the phone back in the wardrobe + UClothingManager* GetPlayerClothingManager() const; UGlobalSaveGameData* GetSave() const; UPROPERTY() TArray> WardrobeItems; + UPROPERTY() + TObjectPtr EquippedPhone; + // The save the live list was built from; re-hydrate when this changes (e.g. load game). TWeakObjectPtr HydratedSave; }; \ No newline at end of file diff --git a/Source/NakedDesire/Phone/PhoneItemDefinition.cpp b/Source/NakedDesire/Phone/PhoneItemDefinition.cpp new file mode 100644 index 00000000..b3b6cefa --- /dev/null +++ b/Source/NakedDesire/Phone/PhoneItemDefinition.cpp @@ -0,0 +1,11 @@ +// © 2025 Naked People Team. All Rights Reserved. + + +#include "PhoneItemDefinition.h" + +#include "PhoneItemInstance.h" + +TSubclassOf UPhoneItemDefinition::GetInstanceClass() const +{ + return UPhoneItemInstance::StaticClass(); +} diff --git a/Source/NakedDesire/Phone/PhoneItemDefinition.h b/Source/NakedDesire/Phone/PhoneItemDefinition.h new file mode 100644 index 00000000..d10fe3c8 --- /dev/null +++ b/Source/NakedDesire/Phone/PhoneItemDefinition.h @@ -0,0 +1,22 @@ +// © 2025 Naked People Team. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "NakedDesire/Items/ItemDefinition.h" +#include "PhoneItemDefinition.generated.h" + +// Immutable definition for a phone (GDD §6.2 / §9.9). A phone is a regular UItemInstance item that +// lives in the wardrobe / bag and occupies the dedicated phone slot when active (§6.5). +// Phone-tier stats (camera / livestream / battery-capacity multipliers, §9.9) are deferred to Phase 8/9. +UCLASS(BlueprintType) +class NAKEDDESIRE_API UPhoneItemDefinition : public UItemDefinition +{ + GENERATED_BODY() + +public: + virtual TSubclassOf GetInstanceClass() const override; + + UPROPERTY(EditDefaultsOnly, Category = "Phone") + float MaxBattery = 100.0f; +}; diff --git a/Source/NakedDesire/Phone/PhoneItemInstance.cpp b/Source/NakedDesire/Phone/PhoneItemInstance.cpp new file mode 100644 index 00000000..455569f9 --- /dev/null +++ b/Source/NakedDesire/Phone/PhoneItemInstance.cpp @@ -0,0 +1,22 @@ +// © 2025 Naked People Team. All Rights Reserved. + + +#include "PhoneItemInstance.h" + +#include "PhoneItemDefinition.h" +#include "StructUtils/InstancedStruct.h" + +void UPhoneItemInstance::CaptureState(FInstancedStruct& OutState) const +{ + FPhoneInstanceState PhoneState; + PhoneState.CurrentBattery = CurrentBattery; + OutState.InitializeAs(PhoneState); +} + +void UPhoneItemInstance::ApplyState(const FInstancedStruct& InState) +{ + if (const FPhoneInstanceState* PhoneState = InState.GetPtr()) + { + CurrentBattery = PhoneState->CurrentBattery; + } +} diff --git a/Source/NakedDesire/Phone/PhoneItemInstance.h b/Source/NakedDesire/Phone/PhoneItemInstance.h new file mode 100644 index 00000000..2b2e342a --- /dev/null +++ b/Source/NakedDesire/Phone/PhoneItemInstance.h @@ -0,0 +1,35 @@ +// © 2025 Naked People Team. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "PhoneItemDefinition.h" +#include "NakedDesire/Items/ItemInstance.h" +#include "PhoneItemInstance.generated.h" + +/** Per-instance mutable state for a phone. Battery is per-phone-instance (§9.9). */ +USTRUCT() +struct FPhoneInstanceState : public FItemInstanceState +{ + GENERATED_BODY() + + UPROPERTY(SaveGame) + float CurrentBattery = 100.0f; +}; + +UCLASS(BlueprintType) +class NAKEDDESIRE_API UPhoneItemInstance : public UItemInstance +{ + GENERATED_BODY() + +public: + // Charge of this specific phone, [0,1]. Drain / charge logic lands with the phone battery system (Phase 8/9). + UPROPERTY(BlueprintReadOnly, Category = "Phone Item") + float CurrentBattery = 100.0f; + + UPhoneItemDefinition* GetPhoneItemDefinition() const { return Cast(ItemDefinition); } + +protected: + virtual void CaptureState(FInstancedStruct& OutState) const override; + virtual void ApplyState(const FInstancedStruct& InState) override; +}; diff --git a/Source/NakedDesire/SaveGame/SaveSubsystem.cpp b/Source/NakedDesire/SaveGame/SaveSubsystem.cpp index 6e9b0314..4d0192d8 100644 --- a/Source/NakedDesire/SaveGame/SaveSubsystem.cpp +++ b/Source/NakedDesire/SaveGame/SaveSubsystem.cpp @@ -62,7 +62,7 @@ void USaveSubsystem::PopulateStartingData(UGlobalSaveGameData* Save) const if (!GameInstance || !GameInstance->StartingSaveData) return; - for (UItemDefinition* Definition : GameInstance->StartingSaveData->StartingItems) + for (UItemDefinition* Definition : GameInstance->StartingSaveData->EquippedItems) { if (!Definition) continue; diff --git a/Source/NakedDesire/SaveGame/StartingSaveData.h b/Source/NakedDesire/SaveGame/StartingSaveData.h index bfb6c46d..c0eaf08a 100644 --- a/Source/NakedDesire/SaveGame/StartingSaveData.h +++ b/Source/NakedDesire/SaveGame/StartingSaveData.h @@ -6,6 +6,7 @@ #include "Engine/DataAsset.h" #include "StartingSaveData.generated.h" +class UPhoneItemDefinition; class UItemDefinition; UCLASS() @@ -15,5 +16,11 @@ class NAKEDDESIRE_API UStartingSaveData : public UPrimaryDataAsset public: UPROPERTY(EditDefaultsOnly, Category = "Starting State") - TArray> StartingItems; + TArray> EquippedItems; + + UPROPERTY(EditDefaultsOnly, Category = "Starting State") + TArray> WardrobeItems; + + UPROPERTY(EditDefaultsOnly, Category = "Starting State") + TObjectPtr Phone; }; \ No newline at end of file