Restore placeable furniture in the new dollhouse layout #1

Open
Admin wants to merge 1 commits from feature/placeable-furniture-layout into main
2 changed files with 273 additions and 13 deletions

View File

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

View File

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