JavaScript · 5283 bytes Raw Blame History
1 // Inventory persistence for dougk shop system
2 // Tracks owned items, equipped outfits, and placed buildings
3
4 import { getItem } from './items.js'
5
6 const STORAGE_KEYS = {
7 OWNED: 'dougk-owned-items',
8 EQUIPPED: 'dougk-equipped',
9 BUILDINGS: 'dougk-buildings',
10 BUILDINGS_FREEFORM: 'dougk-buildings-freeform'
11 }
12
13 function loadJSON(key, defaultValue) {
14 try {
15 const data = localStorage.getItem(key)
16 return data ? JSON.parse(data) : defaultValue
17 } catch {
18 return defaultValue
19 }
20 }
21
22 function saveJSON(key, value) {
23 localStorage.setItem(key, JSON.stringify(value))
24 }
25
26 const inventory = {
27 // Owned items (array of item IDs)
28 ownedItems: loadJSON(STORAGE_KEYS.OWNED, []),
29
30 // Equipped outfits per character
31 equipped: loadJSON(STORAGE_KEYS.EQUIPPED, {
32 doug: [],
33 donny: [],
34 ollie: []
35 }),
36
37 // Placed buildings (legacy zone-based format)
38 buildings: loadJSON(STORAGE_KEYS.BUILDINGS, []),
39
40 // Placed buildings (new freeform format with coordinates)
41 buildingsFreeform: loadJSON(STORAGE_KEYS.BUILDINGS_FREEFORM, []),
42
43 // Check if an item is owned
44 owns(itemId) {
45 return this.ownedItems.includes(itemId)
46 },
47
48 // Purchase an item (add to owned)
49 purchase(itemId) {
50 if (!this.owns(itemId)) {
51 this.ownedItems.push(itemId)
52 this.save()
53 return true
54 }
55 return false
56 },
57
58 // Equip an outfit to a character
59 // Automatically unequips any other outfit of the same type or conflicting types
60 equip(character, itemId) {
61 if (!this.equipped[character]) {
62 this.equipped[character] = []
63 }
64
65 // Get the type of the item being equipped
66 const newItem = getItem(itemId)
67 if (!newItem) return
68
69 // Build list of types to unequip: same type + any conflicting types
70 const typesToUnequip = [newItem.type]
71 if (newItem.conflictsWith && Array.isArray(newItem.conflictsWith)) {
72 typesToUnequip.push(...newItem.conflictsWith)
73 }
74
75 // Find and unequip any item of conflicting types
76 const conflictingItems = this.equipped[character].filter(equippedId => {
77 const equippedItem = getItem(equippedId)
78 if (!equippedItem) return false
79 // Check if equipped item's type is in our unequip list
80 if (typesToUnequip.includes(equippedItem.type)) return true
81 // Also check if the equipped item conflicts with our new item's type
82 if (equippedItem.conflictsWith && equippedItem.conflictsWith.includes(newItem.type)) return true
83 return false
84 })
85
86 // Remove conflicting items
87 for (const oldId of conflictingItems) {
88 this.equipped[character] = this.equipped[character].filter(id => id !== oldId)
89 }
90
91 // Now equip the new item
92 if (!this.equipped[character].includes(itemId)) {
93 this.equipped[character].push(itemId)
94 }
95 this.save()
96
97 // Return the unequipped items so the caller can update visuals
98 return conflictingItems
99 },
100
101 // Unequip an outfit from a character
102 unequip(character, itemId) {
103 if (this.equipped[character]) {
104 this.equipped[character] = this.equipped[character].filter(id => id !== itemId)
105 this.save()
106 }
107 },
108
109 // Check if an outfit is equipped on a character
110 isEquipped(character, itemId) {
111 return this.equipped[character]?.includes(itemId) || false
112 },
113
114 // Get all equipped items for a character
115 getEquipped(character) {
116 return this.equipped[character] || []
117 },
118
119 // Place a building
120 placeBuilding(buildingType, zoneId) {
121 this.buildings.push({ type: buildingType, zoneId })
122 this.save()
123 },
124
125 // Check if a zone is occupied
126 isZoneOccupied(zoneId) {
127 return this.buildings.some(b => b.zoneId === zoneId)
128 },
129
130 // Get all placed buildings
131 getBuildings() {
132 return [...this.buildings]
133 },
134
135 // Alias for getBuildings (used by PlacementManager)
136 getPlacedBuildings() {
137 return this.getBuildings()
138 },
139
140 // Remove a placed building by zone ID
141 removeBuilding(zoneId) {
142 const index = this.buildings.findIndex(b => b.zoneId === zoneId)
143 if (index !== -1) {
144 this.buildings.splice(index, 1)
145 this.save()
146 return true
147 }
148 return false
149 },
150
151 // === Freeform placement methods (new system) ===
152
153 // Place a building with freeform coordinates
154 placeBuildingFreeform(data) {
155 // data: { type, x, z, rotation, placementId }
156 this.buildingsFreeform.push(data)
157 this.save()
158 },
159
160 // Get all freeform placed buildings
161 getPlacedBuildingsFreeform() {
162 return [...this.buildingsFreeform]
163 },
164
165 // Remove a freeform building by placement ID
166 removeBuildingFreeform(placementId) {
167 const index = this.buildingsFreeform.findIndex(b => b.placementId === placementId)
168 if (index !== -1) {
169 this.buildingsFreeform.splice(index, 1)
170 this.save()
171 return true
172 }
173 return false
174 },
175
176 // Save all state
177 save() {
178 saveJSON(STORAGE_KEYS.OWNED, this.ownedItems)
179 saveJSON(STORAGE_KEYS.EQUIPPED, this.equipped)
180 saveJSON(STORAGE_KEYS.BUILDINGS, this.buildings)
181 saveJSON(STORAGE_KEYS.BUILDINGS_FREEFORM, this.buildingsFreeform)
182 },
183
184 // Clear all data (for testing)
185 reset() {
186 this.ownedItems = []
187 this.equipped = { doug: [], donny: [], ollie: [] }
188 this.buildings = []
189 this.buildingsFreeform = []
190 this.save()
191 }
192 }
193
194 export default inventory