Restore placeable furniture in room-bank layout
This commit is contained in:
parent
0aeaaabae7
commit
ce80b40137
@ -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
|
||||
|
||||
|
||||
278
src/main.js
278
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 ||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user