From ce80b40137e304a7c9b9593189fde26127c444d5 Mon Sep 17 00:00:00 2001 From: Admin Date: Sat, 3 Oct 2026 07:14:49 +0000 Subject: [PATCH] Restore placeable furniture in room-bank layout --- README.md | 8 +- src/main.js | 278 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 273 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 2d918d3..056e9a3 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,10 @@ A small Phaser.js PWA built around the dollhouse concept art created in ChatGPT. - Bedroom, bathroom, hall/stairs, kitchen, living and play spaces visible together - Keyboard and touch movement - Stair interaction to move between floors -- Pastel furniture sprite set layered over the house -- Furniture dragging in Decorate mode -- Add/reset furniture controls -- House layout and player position persisted in IndexedDB +- Pastel furniture sprite set layered into the current room-bank layout +- Furniture dragging between rooms in Decorate mode +- Add/reset furniture controls with bed, sofa, chair, table, shelf and wardrobe +- House layout, furniture placement and player position persisted in IndexedDB - Offline-capable installable PWA - Service worker configured to take new versions automatically diff --git a/src/main.js b/src/main.js index a3d7fd5..910f203 100644 --- a/src/main.js +++ b/src/main.js @@ -140,6 +140,35 @@ const ARCHITECTURE_CROPS = { floorEdge: { x: 210, y: 306, w: 1007, h: 87 } } +const FURNITURE_CROPS = { + bed: { x: 28, y: 68, w: 392, h: 425 }, + couch: { x: 449, y: 143, w: 545, h: 337 }, + armchair: { x: 1025, y: 132, w: 355, h: 365 }, + table: { x: 32, y: 686, w: 440, h: 255 }, + bookcase: { x: 554, y: 516, w: 346, h: 454 }, + wardrobe: { x: 1036, y: 497, w: 353, h: 482 } +} + +const FURNITURE_ORDER = ['bed', 'couch', 'armchair', 'table', 'bookcase', 'wardrobe'] + +const FURNITURE_NAMES = { + bed: 'Bed', + couch: 'Sofa', + armchair: 'Chair', + table: 'Table', + bookcase: 'Shelf', + wardrobe: 'Wardrobe' +} + +const FURNITURE_SCALES = { + bed: 0.22, + couch: 0.24, + armchair: 0.22, + table: 0.22, + bookcase: 0.19, + wardrobe: 0.19 +} + const ROOM_SKINS = { disco: { wallpaper: 'wallpaper-disco', floor: 'floor-disco' }, 'top-bedroom': { wallpaper: 'wallpaper-topBedroom', floor: 'floor-lightWood' }, @@ -222,6 +251,32 @@ const ROOMS = [ const ROOM_BY_ID = new Map(ROOMS.map((room) => [room.id, room])) +function makeFurnitureItem(id, type, roomId, xRatio = 0.5, yOffset = 0) { + const room = ROOM_BY_ID.get(roomId) + + return { + id, + type, + roomId, + x: Math.round(room.x + room.width * xRatio), + y: Math.round(Phaser.Math.Clamp( + room.walkBand.homeY + yOffset, + room.walkBand.minY, + room.walkBand.maxY + )), + scale: FURNITURE_SCALES[type] + } +} + +const DEFAULT_FURNITURE = [ + makeFurnitureItem('bed-1', 'bed', 'top-bedroom', 0.38), + makeFurnitureItem('wardrobe-1', 'wardrobe', 'bedroom', 0.74), + makeFurnitureItem('bookcase-1', 'bookcase', 'study', 0.70), + makeFurnitureItem('couch-1', 'couch', 'living', 0.46), + makeFurnitureItem('armchair-1', 'armchair', 'playroom', 0.66), + makeFurnitureItem('table-1', 'table', 'kitchen', 0.48) +] + function floorLiftStopY(slot) { return slot.y + Math.round(slot.h * 0.84) } @@ -246,18 +301,40 @@ function elevatorEntry(room) { } const DEFAULT_STATE = { - version: 3, + version: 4, player: { roomId: 'living', x: LEFT_X + 150, y: ROOM_BY_ID.get('living').walkBand.homeY - } + }, + items: DEFAULT_FURNITURE } function cloneDefaultState() { return JSON.parse(JSON.stringify(DEFAULT_STATE)) } +function normalizeSavedState(saved) { + if (!saved || !ROOM_BY_ID.has(saved.player?.roomId)) { + return cloneDefaultState() + } + + if (saved.version === 4) { + return { + ...saved, + items: Array.isArray(saved.items) ? saved.items : cloneDefaultState().items + } + } + + if (saved.version === 3) { + const migrated = cloneDefaultState() + migrated.player = { ...saved.player } + return migrated + } + + return cloneDefaultState() +} + function cropTexture(scene, sourceKey, key, rect) { if (scene.textures.exists(key)) return const source = scene.textures.get(sourceKey).getSourceImage() @@ -356,6 +433,9 @@ class HouseScene extends Phaser.Scene { this.roomHoldRoom = null this.roomHoldStart = null this.roomHoldPointerId = null + this.decorateMode = false + this.itemSprites = [] + this.addIndex = 0 } preload() { @@ -363,6 +443,7 @@ class HouseScene extends Phaser.Scene { this.load.image('architecture-sheet', '/assets/architecture-sheet.png') this.load.image('wallpaper-sheet', '/assets/wallpaper-sheet.png') this.load.image('floor-sheet', '/assets/floor-sheet.png') + this.load.image('furniture-sheet', '/assets/furniture-sheet.png') this.load.image('shell-roof', '/assets/shell/roof_full.png') this.load.image('shell-tower', '/assets/shell/central_tower_full.png') this.load.image('shell-post-seg', '/assets/shell/outer_post_segment.png') @@ -399,10 +480,12 @@ class HouseScene extends Phaser.Scene { cropTexture(this, 'architecture-sheet', 'architecture-' + key, rect) }) + Object.entries(FURNITURE_CROPS).forEach(([key, rect]) => { + cropTexture(this, 'furniture-sheet', 'furniture-' + key, rect) + }) + const saved = await loadGameState() - this.state = saved?.version === 3 && ROOM_BY_ID.has(saved.player?.roomId) - ? saved - : cloneDefaultState() + this.state = normalizeSavedState(saved) this.cameras.main.setBackgroundColor('#ccecff') @@ -410,6 +493,7 @@ class HouseScene extends Phaser.Scene { this.buildHouseShell() this.buildElevator() this.buildUi() + this.spawnAllFurniture() this.spawnPlayer() this.buildRoomHoldTargets() this.bindInput() @@ -702,6 +786,22 @@ class HouseScene extends Phaser.Scene { } buildUi() { + this.decorateButton = this.add.text(92, STATUS_Y, '✨ Decorate', { + fontFamily: 'Arial Rounded MT Bold, ui-rounded, sans-serif', + fontSize: '14px', + color: '#7a526b', + backgroundColor: '#f8bed2', + padding: { x: 10, y: 8 } + }).setOrigin(0.5).setInteractive({ useHandCursor: true }).setDepth(1000) + + this.addFurnitureButton = this.add.text(256, STATUS_Y, '+ Add Bed', { + fontFamily: 'Arial Rounded MT Bold, ui-rounded, sans-serif', + fontSize: '14px', + color: '#7a526b', + backgroundColor: '#ffffff', + padding: { x: 10, y: 8 } + }).setOrigin(0.5).setInteractive({ useHandCursor: true }).setDepth(1000) + this.statusText = this.add.text(WIDTH / 2, STATUS_Y, 'Living Room', { fontFamily: 'Arial Rounded MT Bold, ui-rounded, sans-serif', fontSize: '18px', @@ -718,11 +818,109 @@ class HouseScene extends Phaser.Scene { padding: { x: 10, y: 8 } }).setOrigin(0.5).setInteractive({ useHandCursor: true }).setDepth(1000) + this.decorateButton.on('pointerdown', () => { + if (this.journeyActive) return + this.decorateMode = !this.decorateMode + this.cancelRoomHold(false) + this.updateDecorateUi() + }) + + this.addFurnitureButton.on('pointerdown', () => { + if (this.journeyActive) return + if (!this.decorateMode) this.decorateMode = true + this.addFurniture() + this.updateDecorateUi() + }) + this.resetButton.on('pointerdown', async () => { this.state = cloneDefaultState() await saveGameState(this.state) this.scene.restart() }) + + this.updateDecorateUi() + } + + updateDecorateUi() { + if (!this.decorateButton || !this.addFurnitureButton) return + + this.decorateButton.setText(this.decorateMode ? '✓ Done' : '✨ Decorate') + const nextType = FURNITURE_ORDER[this.addIndex % FURNITURE_ORDER.length] + this.addFurnitureButton.setText('+ Add ' + FURNITURE_NAMES[nextType]) + + this.itemSprites.forEach((sprite) => { + sprite.setAlpha(this.decorateMode ? 1 : 0.98) + }) + + this.player?.setAlpha(this.decorateMode ? 0.72 : 1) + + if (this.decorateMode) { + this.updateStatus(this.currentRoom(), 'Decorate · drag furniture') + } else if (!this.journeyActive) { + this.updateStatus(this.currentRoom()) + } + } + + furnitureDepth(item) { + const room = ROOM_BY_ID.get(item.roomId) ?? this.currentRoom() + const t = Phaser.Math.Clamp( + (item.y - room.walkBand.minY) / + Math.max(1, room.walkBand.maxY - room.walkBand.minY), + 0, + 1 + ) + + return 130 + room.floor * 4 + Math.round(t * 20) + } + + spawnAllFurniture() { + this.itemSprites.forEach((sprite) => sprite.destroy()) + this.itemSprites = [] + this.state.items = Array.isArray(this.state.items) ? this.state.items : [] + + this.state.items.forEach((item) => this.spawnFurniture(item)) + } + + spawnFurniture(item) { + const room = ROOM_BY_ID.get(item.roomId) + const key = 'furniture-' + item.type + if (!room || !this.textures.exists(key)) return null + + item.x = Phaser.Math.Clamp(item.x, room.x + 24, room.x + room.width - 24) + item.y = Phaser.Math.Clamp(item.y, room.walkBand.minY, room.walkBand.maxY) + item.scale = item.scale ?? FURNITURE_SCALES[item.type] ?? 0.2 + + const sprite = this.add.image(item.x, item.y, key) + .setOrigin(0.5, 1) + .setScale(item.scale) + .setDepth(this.furnitureDepth(item)) + .setData('kind', 'furniture') + .setData('id', item.id) + .setData('roomId', item.roomId) + .setInteractive({ cursor: 'grab' }) + + this.input.setDraggable(sprite) + this.itemSprites.push(sprite) + return sprite + } + + addFurniture() { + const type = FURNITURE_ORDER[this.addIndex % FURNITURE_ORDER.length] + this.addIndex += 1 + + const room = this.currentRoom() + const item = { + id: type + '-' + Date.now(), + type, + roomId: room.id, + x: Math.round(room.x + room.width * 0.52), + y: room.walkBand.homeY, + scale: FURNITURE_SCALES[type] + } + + this.state.items.push(item) + this.spawnFurniture(item) + this.scheduleSave() } spawnPlayer() { @@ -804,7 +1002,7 @@ class HouseScene extends Phaser.Scene { } startRoomHold(room, pointer) { - if (this.journeyActive) return + if (this.journeyActive || this.decorateMode) return this.cancelRoomHold(false) @@ -864,7 +1062,16 @@ class HouseScene extends Phaser.Scene { this.keys = this.input.keyboard.addKeys('W,A,S,D') this.input.on('dragstart', (pointer, object) => { - if (object.getData('kind') !== 'player' || this.journeyActive) return + const kind = object.getData('kind') + + if (kind === 'furniture') { + if (!this.decorateMode || this.journeyActive) return + this.cancelRoomHold(false) + object.setAlpha(0.82) + return + } + + if (kind !== 'player' || this.journeyActive || this.decorateMode) return this.playerDragging = true this.lastPlayerDragX = object.x @@ -874,7 +1081,29 @@ class HouseScene extends Phaser.Scene { }) this.input.on('drag', (pointer, object) => { - if (object.getData('kind') !== 'player') return + const kind = object.getData('kind') + + if (kind === 'furniture') { + if (!this.decorateMode || this.journeyActive) return + + const room = this.getRoomAt(pointer.x, pointer.y) + if (!room) return + + object.x = Phaser.Math.Clamp(pointer.x, room.x + 24, room.x + room.width - 24) + object.y = Phaser.Math.Clamp(pointer.y, room.walkBand.minY, room.walkBand.maxY) + object.setData('roomId', room.id) + + const item = this.state.items.find((entry) => entry.id === object.getData('id')) + if (item) { + item.roomId = room.id + item.x = Math.round(object.x) + item.y = Math.round(object.y) + object.setDepth(this.furnitureDepth(item)) + } + return + } + + if (kind !== 'player') return this.latestPointer = { x: pointer.x, y: pointer.y } @@ -909,7 +1138,28 @@ class HouseScene extends Phaser.Scene { }) this.input.on('dragend', (_pointer, object) => { - if (object.getData('kind') !== 'player') return + const kind = object.getData('kind') + + if (kind === 'furniture') { + const item = this.state.items.find((entry) => entry.id === object.getData('id')) + if (!item) return + + const room = ROOM_BY_ID.get(object.getData('roomId')) ?? ROOM_BY_ID.get(item.roomId) + if (!room) return + + object.x = Phaser.Math.Clamp(object.x, room.x + 24, room.x + room.width - 24) + object.y = Phaser.Math.Clamp(object.y, room.walkBand.minY, room.walkBand.maxY) + object.setAlpha(this.decorateMode ? 1 : 0.98) + + item.roomId = room.id + item.x = Math.round(object.x) + item.y = Math.round(object.y) + object.setDepth(this.furnitureDepth(item)) + this.scheduleSave() + return + } + + if (kind !== 'player') return this.playerDragging = false this.lastPlayerDragX = null @@ -934,6 +1184,11 @@ class HouseScene extends Phaser.Scene { } updateStatus(room, message = null) { + if (!this.statusText) return + if (this.decorateMode && !message) { + this.statusText.setText('Decorate · ' + room.name) + return + } this.statusText.setText(message ?? room.name) } @@ -1183,6 +1438,11 @@ class HouseScene extends Phaser.Scene { update(_time, delta) { if (!this.player) return + if (this.decorateMode) { + this.animatePlayer('idle', delta) + return + } + if (this.journeyActive) { if ( this.playerState === PLAYER_STATE.GOING_TO_LIFT || -- 2.47.2