| 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 |