zeroed-some/dougk / 95a35f3

Browse files

rethink building placement

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
95a35f358ec94109995dd5aba44526ebea34254c
Parents
adc7b26
Tree
988a0b3

4 changed files

StatusFile+-
M src/renderers/three/buildingPlacement.js 262 80
M src/renderers/three/index.js 1 0
M src/renderers/three/pond.js 7 0
M src/renderers/three/shop/inventory.js 33 2
src/renderers/three/buildingPlacement.jsmodified
@@ -1,10 +1,11 @@
11
 // Building placement system for dougk
2
-// Handles ghost preview, snap zones, and building instantiation
2
+// Constraint-based freeform placement with real-time validation
33
 
44
 import * as THREE from 'three'
55
 import { createBuilding, createGhostBuilding } from './buildings.js'
66
 import inventory from './shop/inventory.js'
77
 import { playPlaceBuilding } from './sounds.js'
8
+import { BUILDINGS } from './shop/items.js'
89
 
910
 // Placement states
1011
 const STATE = {
@@ -12,6 +13,34 @@ const STATE = {
1213
   SELECTING: 'selecting'
1314
 }
1415
 
16
+// Terrain types
17
+const TERRAIN = {
18
+  WATER: 'water',
19
+  WATER_EDGE: 'waterEdge',
20
+  SHORE: 'shore',
21
+  OUT_OF_BOUNDS: 'outOfBounds'
22
+}
23
+
24
+// Building terrain requirements
25
+const BUILDING_TERRAIN = {
26
+  dock_wooden: [TERRAIN.WATER_EDGE],
27
+  fishing_hut: [TERRAIN.WATER_EDGE],
28
+  lighthouse: [TERRAIN.SHORE],
29
+  reeds: [TERRAIN.WATER, TERRAIN.WATER_EDGE],
30
+  fence: [TERRAIN.SHORE],
31
+  onion_house: [TERRAIN.SHORE]
32
+}
33
+
34
+// Building collision radii (for overlap detection)
35
+const BUILDING_COLLISION_RADIUS = {
36
+  dock_wooden: 0.8,
37
+  fishing_hut: 0.7,
38
+  lighthouse: 0.5,
39
+  reeds: 0.3,
40
+  fence: 0.25,
41
+  onion_house: 0.5
42
+}
43
+
1544
 export class PlacementManager {
1645
   constructor(scene, pond, camera, gradientMap) {
1746
     this.scene = scene
@@ -22,13 +51,29 @@ export class PlacementManager {
2251
     this.state = STATE.INACTIVE
2352
     this.currentBuildingType = null
2453
     this.ghostMesh = null
25
-    this.hoveredZone = null
54
+    this.isValidPosition = false
55
+    this.currentPosition = new THREE.Vector3()
56
+    this.currentRotation = 0
2657
     this.placedBuildings = new THREE.Group()
2758
 
2859
     this.raycaster = new THREE.Raycaster()
2960
     this.mouse = new THREE.Vector2()
3061
 
31
-    // Invisible ground plane for raycasting (so ghost is always visible)
62
+    // Pond geometry constants
63
+    this.pondCenter = new THREE.Vector2(0, 0)
64
+    this.pondRadius = 4.0  // Water radius
65
+
66
+    // Terrain zone boundaries (distance from center)
67
+    this.terrainBounds = {
68
+      waterInner: 3.2,      // Deep water
69
+      waterEdgeInner: 3.2,  // Water edge starts
70
+      waterEdgeOuter: 4.8,  // Water edge ends
71
+      shoreInner: 4.3,      // Shore starts
72
+      shoreOuter: 7.0,      // Shore ends (map bounds)
73
+      mapBounds: 8.0        // Absolute map edge
74
+    }
75
+
76
+    // Invisible ground plane for raycasting
3277
     this.groundPlane = new THREE.Mesh(
3378
       new THREE.PlaneGeometry(50, 50),
3479
       new THREE.MeshBasicMaterial({ visible: false })
@@ -47,6 +92,12 @@ export class PlacementManager {
4792
       onion_house: 0.6
4893
     }
4994
 
95
+    // Placed building positions for collision detection
96
+    this.placedPositions = []
97
+
98
+    // Forbidden zones (rowboat, dock area, etc.)
99
+    this.forbiddenZones = []
100
+
50101
     scene.add(this.placedBuildings)
51102
 
52103
     // Callbacks
@@ -54,6 +105,103 @@ export class PlacementManager {
54105
     this.onPlacementCancel = null
55106
   }
56107
 
108
+  // Get terrain type at a position
109
+  getTerrainType(x, z) {
110
+    const dx = x - this.pondCenter.x
111
+    const dz = z - this.pondCenter.y
112
+    const distance = Math.sqrt(dx * dx + dz * dz)
113
+
114
+    if (distance > this.terrainBounds.mapBounds) {
115
+      return TERRAIN.OUT_OF_BOUNDS
116
+    }
117
+
118
+    if (distance > this.terrainBounds.shoreOuter) {
119
+      return TERRAIN.OUT_OF_BOUNDS
120
+    }
121
+
122
+    if (distance >= this.terrainBounds.shoreInner) {
123
+      return TERRAIN.SHORE
124
+    }
125
+
126
+    if (distance >= this.terrainBounds.waterEdgeInner && distance <= this.terrainBounds.waterEdgeOuter) {
127
+      return TERRAIN.WATER_EDGE
128
+    }
129
+
130
+    if (distance < this.terrainBounds.waterInner) {
131
+      return TERRAIN.WATER
132
+    }
133
+
134
+    // Transition zone - allow water edge
135
+    return TERRAIN.WATER_EDGE
136
+  }
137
+
138
+  // Check if position collides with existing buildings
139
+  checkBuildingCollision(x, z, buildingType) {
140
+    const newRadius = BUILDING_COLLISION_RADIUS[buildingType] || 0.5
141
+
142
+    for (const placed of this.placedPositions) {
143
+      const dx = x - placed.x
144
+      const dz = z - placed.z
145
+      const distance = Math.sqrt(dx * dx + dz * dz)
146
+      const minDistance = newRadius + placed.radius
147
+
148
+      if (distance < minDistance) {
149
+        return true // Collision!
150
+      }
151
+    }
152
+
153
+    return false
154
+  }
155
+
156
+  // Check if position is in a forbidden zone
157
+  checkForbiddenZone(x, z) {
158
+    for (const zone of this.forbiddenZones) {
159
+      const dx = x - zone.x
160
+      const dz = z - zone.z
161
+      const distance = Math.sqrt(dx * dx + dz * dz)
162
+
163
+      if (distance < zone.radius) {
164
+        return true // In forbidden zone
165
+      }
166
+    }
167
+
168
+    return false
169
+  }
170
+
171
+  // Validate placement position
172
+  validatePosition(x, z, buildingType) {
173
+    // Check map bounds
174
+    const terrain = this.getTerrainType(x, z)
175
+    if (terrain === TERRAIN.OUT_OF_BOUNDS) {
176
+      return { valid: false, reason: 'out_of_bounds' }
177
+    }
178
+
179
+    // Check terrain requirements for building type
180
+    const allowedTerrain = BUILDING_TERRAIN[buildingType] || []
181
+    if (!allowedTerrain.includes(terrain)) {
182
+      return { valid: false, reason: 'wrong_terrain', terrain, required: allowedTerrain }
183
+    }
184
+
185
+    // Check collision with existing buildings
186
+    if (this.checkBuildingCollision(x, z, buildingType)) {
187
+      return { valid: false, reason: 'collision' }
188
+    }
189
+
190
+    // Check forbidden zones (rowboat area, etc.)
191
+    if (this.checkForbiddenZone(x, z)) {
192
+      return { valid: false, reason: 'forbidden_zone' }
193
+    }
194
+
195
+    return { valid: true, terrain }
196
+  }
197
+
198
+  // Calculate rotation angle to face pond center
199
+  calculateRotation(x, z) {
200
+    const dx = this.pondCenter.x - x
201
+    const dz = this.pondCenter.y - z
202
+    return Math.atan2(dx, dz)
203
+  }
204
+
57205
   // Start placement mode for a building type
58206
   startPlacement(buildingType, callbacks = {}) {
59207
     if (this.state !== STATE.INACTIVE) {
@@ -64,14 +212,14 @@ export class PlacementManager {
64212
     this.onPlacementComplete = callbacks.onComplete
65213
     this.onPlacementCancel = callbacks.onCancel
66214
 
67
-    // Create ghost mesh (starts as invalid/red, visible immediately)
215
+    // Create ghost mesh (starts as invalid/red)
68216
     this.ghostMesh = createGhostBuilding(buildingType, this.gradientMap, false)
69217
     this.ghostMesh.visible = true
70
-    this.ghostMesh.position.set(0, 0, 0) // Will be updated on mouse move
218
+    this.ghostMesh.position.set(0, 0, 0)
71219
     this.scene.add(this.ghostMesh)
72220
 
73221
     this.state = STATE.SELECTING
74
-    this.hoveredZone = null
222
+    this.isValidPosition = false
75223
 
76224
     return true
77225
   }
@@ -91,7 +239,7 @@ export class PlacementManager {
91239
 
92240
     this.state = STATE.INACTIVE
93241
     this.currentBuildingType = null
94
-    this.hoveredZone = null
242
+    this.isValidPosition = false
95243
 
96244
     if (this.onPlacementCancel) {
97245
       this.onPlacementCancel()
@@ -107,33 +255,27 @@ export class PlacementManager {
107255
     this.mouse.x = (event.clientX / containerWidth) * 2 - 1
108256
     this.mouse.y = -(event.clientY / containerHeight) * 2 + 1
109257
 
110
-    // Raycast to invisible ground plane (always hits)
258
+    // Raycast to invisible ground plane
111259
     this.raycaster.setFromCamera(this.mouse, this.camera)
112260
     const intersects = this.raycaster.intersectObject(this.groundPlane)
113261
 
114262
     if (intersects.length > 0) {
115263
       const point = intersects[0].point
116264
 
117
-      // Find nearest valid zone
118
-      const nearestZone = this.pond.findNearestZone(
119
-        point.x,
120
-        point.z,
121
-        this.currentBuildingType
122
-      )
265
+      // Ghost always follows cursor
266
+      this.currentPosition.set(point.x, 0, point.z)
267
+      this.ghostMesh.position.copy(this.currentPosition)
123268
 
124
-      if (nearestZone) {
125
-        // Snap to zone - show green
126
-        this.hoveredZone = nearestZone
127
-        this.ghostMesh.position.set(nearestZone.x, 0, nearestZone.z)
128
-        this.ghostMesh.rotation.y = nearestZone.angle
129
-        this.updateGhostColor(true)
130
-      } else {
131
-        // No valid zone - follow cursor, show red
132
-        this.hoveredZone = null
133
-        this.ghostMesh.position.set(point.x, 0, point.z)
134
-        this.ghostMesh.rotation.y = 0
135
-        this.updateGhostColor(false)
136
-      }
269
+      // Calculate rotation to face pond center
270
+      this.currentRotation = this.calculateRotation(point.x, point.z)
271
+      this.ghostMesh.rotation.y = this.currentRotation
272
+
273
+      // Validate position
274
+      const validation = this.validatePosition(point.x, point.z, this.currentBuildingType)
275
+      this.isValidPosition = validation.valid
276
+
277
+      // Update ghost color
278
+      this.updateGhostColor(this.isValidPosition)
137279
 
138280
       // Always visible during placement
139281
       this.ghostMesh.visible = true
@@ -157,9 +299,9 @@ export class PlacementManager {
157299
     // Update position one more time
158300
     this.onMouseMove(event, containerWidth, containerHeight)
159301
 
160
-    if (this.hoveredZone) {
161
-      // Valid zone - confirm placement
162
-      this.confirmPlacement(this.hoveredZone)
302
+    if (this.isValidPosition) {
303
+      // Valid position - confirm placement
304
+      this.confirmPlacement()
163305
       return true
164306
     }
165307
 
@@ -167,28 +309,43 @@ export class PlacementManager {
167309
   }
168310
 
169311
   // Confirm and place the building
170
-  confirmPlacement(zone) {
312
+  confirmPlacement() {
313
+    const x = this.currentPosition.x
314
+    const z = this.currentPosition.z
315
+    const rotation = this.currentRotation
316
+
171317
     // Create the real building
172318
     const building = createBuilding(this.currentBuildingType, this.gradientMap)
173
-    building.position.set(zone.x, 0, zone.z)
174
-    building.rotation.y = zone.angle
319
+    building.position.set(x, 0, z)
320
+    building.rotation.y = rotation
175321
     building.userData.buildingType = this.currentBuildingType
176
-    building.userData.zoneId = zone.id
322
+    building.userData.placementId = `placed_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
177323
 
178324
     this.placedBuildings.add(building)
179325
 
180326
     // Play placement sound
181327
     playPlaceBuilding()
182328
 
183
-    // Mark zone as occupied
184
-    this.pond.occupyZone(zone.id)
329
+    // Track placed position for collision detection
330
+    const collisionRadius = BUILDING_COLLISION_RADIUS[this.currentBuildingType] || 0.5
331
+    this.placedPositions.push({
332
+      x, z,
333
+      radius: collisionRadius,
334
+      buildingType: this.currentBuildingType,
335
+      placementId: building.userData.placementId
336
+    })
185337
 
186338
     // Add forbidden zone for creature emergence
187339
     const forbiddenRadius = this.buildingRadius[this.currentBuildingType] || 0.5
188
-    this.pond.addForbiddenZone(zone.x, zone.z, forbiddenRadius)
340
+    this.pond.addForbiddenZone(x, z, forbiddenRadius)
189341
 
190
-    // Save to inventory
191
-    inventory.placeBuilding(this.currentBuildingType, zone.id)
342
+    // Save to inventory (use coordinates as ID for freeform placement)
343
+    const placementData = {
344
+      type: this.currentBuildingType,
345
+      x, z, rotation,
346
+      placementId: building.userData.placementId
347
+    }
348
+    inventory.placeBuildingFreeform(placementData)
192349
 
193350
     // Clean up ghost
194351
     if (this.ghostMesh) {
@@ -206,42 +363,73 @@ export class PlacementManager {
206363
     const buildingType = this.currentBuildingType
207364
     this.state = STATE.INACTIVE
208365
     this.currentBuildingType = null
209
-    this.hoveredZone = null
366
+    this.isValidPosition = false
210367
 
211368
     // Callback
212369
     if (this.onPlacementComplete) {
213
-      this.onPlacementComplete(buildingType, zone)
370
+      this.onPlacementComplete(buildingType, { x, z, rotation })
214371
     }
215372
 
216373
     return building
217374
   }
218375
 
376
+  // Initialize forbidden zones from pond
377
+  initForbiddenZones(zones) {
378
+    this.forbiddenZones = zones || []
379
+  }
380
+
381
+  // Add a forbidden zone
382
+  addForbiddenZone(x, z, radius) {
383
+    this.forbiddenZones.push({ x, z, radius })
384
+  }
385
+
219386
   // Load saved buildings from inventory
220387
   loadSavedBuildings() {
221
-    const savedBuildings = inventory.getPlacedBuildings()
222
-
223
-    for (const { type, zoneId } of savedBuildings) {
224
-      const zone = this.pond.getZone(zoneId)
225
-      if (zone && !zone.occupied) {
226
-        // Create and place building
227
-        const building = createBuilding(type, this.gradientMap)
228
-        building.position.set(zone.x, 0, zone.z)
229
-        building.rotation.y = zone.angle
230
-        building.userData.buildingType = type
231
-        building.userData.zoneId = zone.id
232
-
233
-        this.placedBuildings.add(building)
234
-
235
-        // Mark zone as occupied
236
-        this.pond.occupyZone(zone.id)
237
-
238
-        // Add forbidden zone
239
-        const forbiddenRadius = this.buildingRadius[type] || 0.5
240
-        this.pond.addForbiddenZone(zone.x, zone.z, forbiddenRadius)
388
+    // Try new freeform format first
389
+    const savedFreeform = inventory.getPlacedBuildingsFreeform()
390
+
391
+    if (savedFreeform && savedFreeform.length > 0) {
392
+      for (const data of savedFreeform) {
393
+        this.loadBuilding(data.type, data.x, data.z, data.rotation, data.placementId)
394
+      }
395
+    } else {
396
+      // Fall back to old zone-based format for backwards compatibility
397
+      const savedBuildings = inventory.getPlacedBuildings()
398
+
399
+      for (const { type, zoneId } of savedBuildings) {
400
+        const zone = this.pond.getZone(zoneId)
401
+        if (zone && !zone.occupied) {
402
+          this.loadBuilding(type, zone.x, zone.z, zone.angle, zoneId)
403
+          this.pond.occupyZone(zoneId)
404
+        }
241405
       }
242406
     }
243407
   }
244408
 
409
+  // Load a single building
410
+  loadBuilding(type, x, z, rotation, placementId) {
411
+    const building = createBuilding(type, this.gradientMap)
412
+    building.position.set(x, 0, z)
413
+    building.rotation.y = rotation
414
+    building.userData.buildingType = type
415
+    building.userData.placementId = placementId
416
+
417
+    this.placedBuildings.add(building)
418
+
419
+    // Track for collision detection
420
+    const collisionRadius = BUILDING_COLLISION_RADIUS[type] || 0.5
421
+    this.placedPositions.push({
422
+      x, z,
423
+      radius: collisionRadius,
424
+      buildingType: type,
425
+      placementId
426
+    })
427
+
428
+    // Add forbidden zone
429
+    const forbiddenRadius = this.buildingRadius[type] || 0.5
430
+    this.pond.addForbiddenZone(x, z, forbiddenRadius)
431
+  }
432
+
245433
   // Check if currently placing
246434
   isPlacing() {
247435
     return this.state === STATE.SELECTING
@@ -252,10 +440,10 @@ export class PlacementManager {
252440
     return this.placedBuildings
253441
   }
254442
 
255
-  // Remove a placed building (for future use)
256
-  removeBuilding(zoneId) {
443
+  // Remove a placed building
444
+  removeBuilding(placementId) {
257445
     const building = this.placedBuildings.children.find(
258
-      b => b.userData.zoneId === zoneId
446
+      b => b.userData.placementId === placementId
259447
     )
260448
 
261449
     if (building) {
@@ -267,14 +455,13 @@ export class PlacementManager {
267455
         }
268456
       })
269457
 
270
-      // Free up the zone
271
-      const zone = this.pond.getZone(zoneId)
272
-      if (zone) {
273
-        zone.occupied = false
274
-      }
458
+      // Remove from collision tracking
459
+      this.placedPositions = this.placedPositions.filter(
460
+        p => p.placementId !== placementId
461
+      )
275462
 
276463
       // Remove from inventory
277
-      inventory.removeBuilding(zoneId)
464
+      inventory.removeBuildingFreeform(placementId)
278465
 
279466
       return true
280467
     }
@@ -289,7 +476,7 @@ export class PlacementManager {
289476
       if (building.userData.buildingType === 'lighthouse') {
290477
         building.traverse((child) => {
291478
           if (child.userData.isLightBeam) {
292
-            child.rotation.y = elapsed * 0.8 // Slow rotation
479
+            child.rotation.y = elapsed * 0.8
293480
           }
294481
         })
295482
       }
@@ -301,7 +488,6 @@ export class PlacementManager {
301488
             const phase = child.userData.phase
302489
             const baseX = child.userData.baseRotX
303490
             const baseZ = child.userData.baseRotZ
304
-            // Gentle swaying motion
305491
             child.rotation.x = baseX + Math.sin(elapsed * 1.5 + phase) * 0.15
306492
             child.rotation.z = baseZ + Math.cos(elapsed * 1.2 + phase) * 0.1
307493
           }
@@ -313,22 +499,17 @@ export class PlacementManager {
313499
         building.traverse((child) => {
314500
           if (child.userData.isSmokePuff) {
315501
             const phase = child.userData.phase
316
-            const cycleTime = 3.0  // Seconds for full cycle
502
+            const cycleTime = 3.0
317503
             const t = ((elapsed * 0.5 + phase * cycleTime) % cycleTime) / cycleTime
318504
 
319
-            // Rise from 0 to 0.5 units
320505
             child.position.y = t * 0.5
321
-
322
-            // Expand as it rises (scale from 1 to 2)
323506
             const scale = 1 + t * 1.2
324507
             child.scale.set(scale, scale, scale)
325508
 
326
-            // Fade out as it rises
327509
             if (child.material) {
328510
               child.material.opacity = 0.6 * (1 - t * 0.8)
329511
             }
330512
 
331
-            // Gentle horizontal drift
332513
             child.position.x = Math.sin(elapsed * 0.8 + phase * 5) * 0.03
333514
             child.position.z = Math.cos(elapsed * 0.6 + phase * 3) * 0.02
334515
           }
@@ -341,7 +522,6 @@ export class PlacementManager {
341522
   dispose() {
342523
     this.cancelPlacement()
343524
 
344
-    // Clean up placed buildings
345525
     while (this.placedBuildings.children.length > 0) {
346526
       const building = this.placedBuildings.children[0]
347527
       this.placedBuildings.remove(building)
@@ -355,11 +535,13 @@ export class PlacementManager {
355535
 
356536
     this.scene.remove(this.placedBuildings)
357537
 
358
-    // Clean up ground plane
359538
     if (this.groundPlane) {
360539
       this.scene.remove(this.groundPlane)
361540
       this.groundPlane.geometry?.dispose()
362541
       this.groundPlane.material?.dispose()
363542
     }
543
+
544
+    this.placedPositions = []
545
+    this.forbiddenZones = []
364546
   }
365547
 }
src/renderers/three/index.jsmodified
@@ -158,6 +158,7 @@ export function start(container) {
158158
   koiSchool = createKoiSchool(scene, toonGradient, pond.radius)
159159
   ollie = createOllie(scene, toonGradient)
160160
   placementManager = new PlacementManager(scene, pond, camera, toonGradient)
161
+  placementManager.initForbiddenZones(pond.getInitialForbiddenZones())
161162
 
162163
   // Post-processing
163164
   composer = new EffectComposer(renderer)
src/renderers/three/pond.jsmodified
@@ -743,6 +743,12 @@ export function createPond(scene, gradientMap) {
743743
     forbiddenZones.push({ x, z, radius: zoneRadius })
744744
   }
745745
 
746
+  // Get initial forbidden zones (for building placement)
747
+  function getInitialForbiddenZones() {
748
+    // Return copy of initial zones (rowboat and dock area)
749
+    return forbiddenZones.slice(0, 2).map(z => ({ ...z }))
750
+  }
751
+
746752
   // Snap zones for building placement
747753
   // Each zone has: id, position, angle (rotation), type, allowed buildings, occupied status
748754
   const snapZones = [
@@ -817,6 +823,7 @@ export function createPond(scene, gradientMap) {
817823
     update,
818824
     isValidEmergenceSpot,
819825
     addForbiddenZone,
826
+    getInitialForbiddenZones,
820827
     snapZones,
821828
     getAvailableZones,
822829
     getZone,
src/renderers/three/shop/inventory.jsmodified
@@ -6,7 +6,8 @@ import { getItem } from './items.js'
66
 const STORAGE_KEYS = {
77
   OWNED: 'dougk-owned-items',
88
   EQUIPPED: 'dougk-equipped',
9
-  BUILDINGS: 'dougk-buildings'
9
+  BUILDINGS: 'dougk-buildings',
10
+  BUILDINGS_FREEFORM: 'dougk-buildings-freeform'
1011
 }
1112
 
1213
 function loadJSON(key, defaultValue) {
@@ -33,9 +34,12 @@ const inventory = {
3334
     ollie: []
3435
   }),
3536
 
36
-  // Placed buildings
37
+  // Placed buildings (legacy zone-based format)
3738
   buildings: loadJSON(STORAGE_KEYS.BUILDINGS, []),
3839
 
40
+  // Placed buildings (new freeform format with coordinates)
41
+  buildingsFreeform: loadJSON(STORAGE_KEYS.BUILDINGS_FREEFORM, []),
42
+
3943
   // Check if an item is owned
4044
   owns(itemId) {
4145
     return this.ownedItems.includes(itemId)
@@ -133,11 +137,37 @@ const inventory = {
133137
     return false
134138
   },
135139
 
140
+  // === Freeform placement methods (new system) ===
141
+
142
+  // Place a building with freeform coordinates
143
+  placeBuildingFreeform(data) {
144
+    // data: { type, x, z, rotation, placementId }
145
+    this.buildingsFreeform.push(data)
146
+    this.save()
147
+  },
148
+
149
+  // Get all freeform placed buildings
150
+  getPlacedBuildingsFreeform() {
151
+    return [...this.buildingsFreeform]
152
+  },
153
+
154
+  // Remove a freeform building by placement ID
155
+  removeBuildingFreeform(placementId) {
156
+    const index = this.buildingsFreeform.findIndex(b => b.placementId === placementId)
157
+    if (index !== -1) {
158
+      this.buildingsFreeform.splice(index, 1)
159
+      this.save()
160
+      return true
161
+    }
162
+    return false
163
+  },
164
+
136165
   // Save all state
137166
   save() {
138167
     saveJSON(STORAGE_KEYS.OWNED, this.ownedItems)
139168
     saveJSON(STORAGE_KEYS.EQUIPPED, this.equipped)
140169
     saveJSON(STORAGE_KEYS.BUILDINGS, this.buildings)
170
+    saveJSON(STORAGE_KEYS.BUILDINGS_FREEFORM, this.buildingsFreeform)
141171
   },
142172
 
143173
   // Clear all data (for testing)
@@ -145,6 +175,7 @@ const inventory = {
145175
     this.ownedItems = []
146176
     this.equipped = { doug: [], donny: [], ollie: [] }
147177
     this.buildings = []
178
+    this.buildingsFreeform = []
148179
     this.save()
149180
   }
150181
 }