Use open dollhouse environment and pastel furniture sprites
This commit is contained in:
parent
c8e0135ab5
commit
571f4f0e28
15
README.md
15
README.md
@ -4,14 +4,16 @@ A small Phaser.js PWA built around the dollhouse concept art created in ChatGPT.
|
|||||||
|
|
||||||
## Current prototype
|
## Current prototype
|
||||||
|
|
||||||
- Four playable rooms: bedroom, bathroom, kitchen and living room
|
- Single open two-storey dollhouse environment
|
||||||
|
- Bedroom, bathroom, hall/stairs, kitchen, living and play spaces visible together
|
||||||
- Keyboard and touch movement
|
- Keyboard and touch movement
|
||||||
- Furniture/prop dragging in Decorate mode
|
- Stair interaction to move between floors
|
||||||
- Add/reset room controls
|
- Pastel furniture sprite set layered over the house
|
||||||
- Room layout and player position persisted in IndexedDB
|
- Furniture dragging in Decorate mode
|
||||||
- Offline-capable PWA
|
- Add/reset furniture controls
|
||||||
|
- House layout and player position persisted in IndexedDB
|
||||||
|
- Offline-capable installable PWA
|
||||||
- Service worker configured to take new versions automatically
|
- Service worker configured to take new versions automatically
|
||||||
- Runtime crops extracted from the production concept sheet so gameplay code is independent from future final sprite atlases
|
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
@ -28,7 +30,6 @@ npm run build
|
|||||||
|
|
||||||
The Dockerfile builds the Vite app and serves it with nginx on port 80. `/health` is provided for Coolify health checks.
|
The Dockerfile builds the Vite app and serves it with nginx on port 80. `/health` is provided for Coolify health checks.
|
||||||
|
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
Coolify tracks the `main` branch. The installed PWA is configured to take service-worker updates automatically after a new deployment.
|
Coolify tracks the `main` branch. The installed PWA is configured to take service-worker updates automatically after a new deployment.
|
||||||
|
|||||||
530
src/main.js
530
src/main.js
@ -33,61 +33,48 @@ window.addEventListener('appinstalled', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const WIDTH = 960
|
const WIDTH = 960
|
||||||
const HEIGHT = 640
|
const HEIGHT = 720
|
||||||
const ROOM_X = 30
|
|
||||||
const ROOM_Y = 78
|
|
||||||
const ROOM_W = 900
|
|
||||||
const ROOM_H = 470
|
|
||||||
const FLOOR_TOP = 385
|
|
||||||
const FLOOR_BOTTOM = 525
|
|
||||||
|
|
||||||
const ROOM_CROPS = {
|
const HOUSE_BOUNDS = {
|
||||||
bedroom: { x: 15, y: 425, w: 248, h: 182 },
|
left: 38,
|
||||||
bathroom: { x: 270, y: 425, w: 241, h: 182 },
|
right: 922,
|
||||||
kitchen: { x: 521, y: 425, w: 239, h: 182 },
|
upper: { minY: 270, maxY: 354, homeY: 336 },
|
||||||
living: { x: 774, y: 425, w: 241, h: 182 }
|
lower: { minY: 515, maxY: 610, homeY: 585 }
|
||||||
}
|
}
|
||||||
|
|
||||||
const ASSET_CROPS = {
|
const STAIRS_X = 675
|
||||||
player: { x: 17, y: 128, w: 63, h: 102 },
|
const PLAYER_CROP = { x: 17, y: 128, w: 63, h: 102 }
|
||||||
bed: { x: 15, y: 671, w: 79, h: 113 },
|
|
||||||
couch: { x: 540, y: 675, w: 101, h: 99 },
|
const FURNITURE_CROPS = {
|
||||||
armchair: { x: 644, y: 680, w: 67, h: 91 },
|
bed: { x: 28, y: 68, w: 392, h: 425 },
|
||||||
table: { x: 708, y: 694, w: 68, h: 78 },
|
couch: { x: 449, y: 143, w: 545, h: 337 },
|
||||||
bookcase: { x: 847, y: 675, w: 61, h: 106 },
|
armchair: { x: 1025, y: 132, w: 355, h: 365 },
|
||||||
teddy: { x: 16, y: 839, w: 49, h: 53 },
|
table: { x: 32, y: 686, w: 440, h: 255 },
|
||||||
ball: { x: 224, y: 842, w: 46, h: 49 }
|
bookcase: { x: 554, y: 516, w: 346, h: 454 },
|
||||||
|
wardrobe: { x: 1036, y: 497, w: 353, h: 482 }
|
||||||
}
|
}
|
||||||
|
|
||||||
const ROOM_LABELS = {
|
const FURNITURE_ORDER = ['bed', 'couch', 'armchair', 'table', 'bookcase', 'wardrobe']
|
||||||
bedroom: 'Bedroom',
|
const FURNITURE_NAMES = {
|
||||||
bathroom: 'Bathroom',
|
bed: 'Bed',
|
||||||
kitchen: 'Kitchen',
|
couch: 'Sofa',
|
||||||
living: 'Living Room'
|
armchair: 'Chair',
|
||||||
|
table: 'Table',
|
||||||
|
bookcase: 'Shelf',
|
||||||
|
wardrobe: 'Wardrobe'
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_STATE = {
|
const DEFAULT_STATE = {
|
||||||
version: 1,
|
version: 2,
|
||||||
activeRoom: 'bedroom',
|
player: { x: 510, y: HOUSE_BOUNDS.lower.homeY, floor: 'lower' },
|
||||||
player: { x: 480, y: 492 },
|
items: [
|
||||||
rooms: {
|
{ id: 'bed-1', type: 'bed', x: 168, y: 346, scale: 0.42, floor: 'upper' },
|
||||||
bedroom: [
|
{ id: 'wardrobe-1', type: 'wardrobe', x: 356, y: 348, scale: 0.32, floor: 'upper' },
|
||||||
{ id: 'bed-1', type: 'bed', x: 260, y: 460, scale: 1.15 },
|
{ id: 'couch-1', type: 'couch', x: 560, y: 596, scale: 0.31, floor: 'lower' },
|
||||||
{ id: 'bookcase-1', type: 'bookcase', x: 745, y: 445, scale: 1.05 },
|
{ id: 'table-1', type: 'table', x: 510, y: 612, scale: 0.28, floor: 'lower' },
|
||||||
{ id: 'teddy-1', type: 'teddy', x: 600, y: 500, scale: 1.15 }
|
{ id: 'armchair-1', type: 'armchair', x: 705, y: 596, scale: 0.30, floor: 'lower' },
|
||||||
],
|
{ id: 'bookcase-1', type: 'bookcase', x: 850, y: 598, scale: 0.28, floor: 'lower' }
|
||||||
bathroom: [
|
|
||||||
{ id: 'ball-1', type: 'ball', x: 700, y: 500, scale: 1.15 }
|
|
||||||
],
|
|
||||||
kitchen: [
|
|
||||||
{ id: 'table-1', type: 'table', x: 575, y: 470, scale: 1.2 }
|
|
||||||
],
|
|
||||||
living: [
|
|
||||||
{ id: 'couch-1', type: 'couch', x: 310, y: 465, scale: 1.15 },
|
|
||||||
{ id: 'armchair-1', type: 'armchair', x: 650, y: 475, scale: 1.15 },
|
|
||||||
{ id: 'table-2', type: 'table', x: 480, y: 500, scale: 1.0 }
|
|
||||||
]
|
]
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cloneDefaultState() {
|
function cloneDefaultState() {
|
||||||
@ -95,322 +82,304 @@ function cloneDefaultState() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function roundedButton(scene, x, y, width, height, label, onClick, options = {}) {
|
function roundedButton(scene, x, y, width, height, label, onClick, options = {}) {
|
||||||
const bg = scene.add.rectangle(x, y, width, height, options.fill ?? 0xffeef5, 1)
|
const bg = scene.add.rectangle(x, y, width, height, options.fill ?? 0xfff2f7, options.alpha ?? 0.94)
|
||||||
.setStrokeStyle(2, options.stroke ?? 0xe790b3)
|
.setStrokeStyle(2, options.stroke ?? 0xe58daf)
|
||||||
.setInteractive({ useHandCursor: true })
|
.setInteractive({ useHandCursor: true })
|
||||||
.setDepth(500)
|
.setScrollFactor(0)
|
||||||
|
.setDepth(1000)
|
||||||
|
|
||||||
const text = scene.add.text(x, y, label, {
|
const text = scene.add.text(x, y, label, {
|
||||||
fontFamily: 'Arial Rounded MT Bold, ui-rounded, sans-serif',
|
fontFamily: 'Arial Rounded MT Bold, ui-rounded, sans-serif',
|
||||||
fontSize: options.fontSize ?? '17px',
|
fontSize: options.fontSize ?? '15px',
|
||||||
color: options.color ?? '#6b3b59',
|
color: options.color ?? '#6b3b59',
|
||||||
fontStyle: 'bold'
|
fontStyle: 'bold'
|
||||||
}).setOrigin(0.5).setDepth(501)
|
}).setOrigin(0.5).setScrollFactor(0).setDepth(1001)
|
||||||
|
|
||||||
bg.on('pointerdown', () => {
|
bg.on('pointerdown', () => {
|
||||||
scene.tweens.add({ targets: [bg, text], scaleX: 0.95, scaleY: 0.95, duration: 55, yoyo: true })
|
scene.tweens.add({
|
||||||
|
targets: [bg, text],
|
||||||
|
scaleX: 0.95,
|
||||||
|
scaleY: 0.95,
|
||||||
|
duration: 55,
|
||||||
|
yoyo: true
|
||||||
|
})
|
||||||
onClick()
|
onClick()
|
||||||
})
|
})
|
||||||
|
|
||||||
return { bg, text }
|
return { bg, text }
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeCropTexture(scene, key, rect, removeBackground = false) {
|
function cropTexture(scene, sourceKey, key, rect) {
|
||||||
if (scene.textures.exists(key)) return
|
if (scene.textures.exists(key)) return
|
||||||
const source = scene.textures.get('production').getSourceImage()
|
const source = scene.textures.get(sourceKey).getSourceImage()
|
||||||
const canvas = document.createElement('canvas')
|
const canvas = document.createElement('canvas')
|
||||||
canvas.width = rect.w
|
canvas.width = rect.w
|
||||||
canvas.height = rect.h
|
canvas.height = rect.h
|
||||||
const ctx = canvas.getContext('2d', { willReadFrequently: removeBackground })
|
const ctx = canvas.getContext('2d')
|
||||||
ctx.drawImage(source, rect.x, rect.y, rect.w, rect.h, 0, 0, rect.w, rect.h)
|
ctx.drawImage(source, rect.x, rect.y, rect.w, rect.h, 0, 0, rect.w, rect.h)
|
||||||
|
|
||||||
if (removeBackground) {
|
|
||||||
const imageData = ctx.getImageData(0, 0, rect.w, rect.h)
|
|
||||||
const data = imageData.data
|
|
||||||
const w = rect.w
|
|
||||||
const h = rect.h
|
|
||||||
const seen = new Uint8Array(w * h)
|
|
||||||
const stack = []
|
|
||||||
|
|
||||||
const candidate = (idx) => {
|
|
||||||
const i = idx * 4
|
|
||||||
const r = data[i]
|
|
||||||
const g = data[i + 1]
|
|
||||||
const b = data[i + 2]
|
|
||||||
const max = Math.max(r, g, b)
|
|
||||||
const min = Math.min(r, g, b)
|
|
||||||
return max >= 206 && max - min <= 48
|
|
||||||
}
|
|
||||||
|
|
||||||
const push = (x, y) => {
|
|
||||||
if (x < 0 || y < 0 || x >= w || y >= h) return
|
|
||||||
const idx = y * w + x
|
|
||||||
if (seen[idx] || !candidate(idx)) return
|
|
||||||
seen[idx] = 1
|
|
||||||
stack.push(idx)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let x = 0; x < w; x++) {
|
|
||||||
push(x, 0)
|
|
||||||
push(x, h - 1)
|
|
||||||
}
|
|
||||||
for (let y = 0; y < h; y++) {
|
|
||||||
push(0, y)
|
|
||||||
push(w - 1, y)
|
|
||||||
}
|
|
||||||
|
|
||||||
while (stack.length) {
|
|
||||||
const idx = stack.pop()
|
|
||||||
data[idx * 4 + 3] = 0
|
|
||||||
const x = idx % w
|
|
||||||
const y = Math.floor(idx / w)
|
|
||||||
push(x + 1, y)
|
|
||||||
push(x - 1, y)
|
|
||||||
push(x, y + 1)
|
|
||||||
push(x, y - 1)
|
|
||||||
}
|
|
||||||
ctx.putImageData(imageData, 0, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
scene.textures.addCanvas(key, canvas)
|
scene.textures.addCanvas(key, canvas)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function floorFromY(y) {
|
||||||
|
return y < 435 ? 'upper' : 'lower'
|
||||||
|
}
|
||||||
|
|
||||||
|
function floorBounds(floor) {
|
||||||
|
return floor === 'upper' ? HOUSE_BOUNDS.upper : HOUSE_BOUNDS.lower
|
||||||
|
}
|
||||||
|
|
||||||
class HouseScene extends Phaser.Scene {
|
class HouseScene extends Phaser.Scene {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('House')
|
super('House')
|
||||||
this.state = cloneDefaultState()
|
this.state = cloneDefaultState()
|
||||||
this.decorateMode = false
|
this.decorateMode = false
|
||||||
this.roomObjects = []
|
|
||||||
this.roomButtons = []
|
|
||||||
this.moveIntent = { left: false, right: false, up: false, down: false }
|
this.moveIntent = { left: false, right: false, up: false, down: false }
|
||||||
this.saveTimer = null
|
this.saveTimer = null
|
||||||
|
this.itemSprites = []
|
||||||
|
this.addIndex = 0
|
||||||
|
this.stairCooldown = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
preload() {
|
preload() {
|
||||||
|
this.load.image('house', '/assets/open-dollhouse.png')
|
||||||
|
this.load.image('furniture-sheet', '/assets/furniture-sheet.png')
|
||||||
this.load.image('production', '/assets/production-sheet.png')
|
this.load.image('production', '/assets/production-sheet.png')
|
||||||
}
|
}
|
||||||
|
|
||||||
async create() {
|
async create() {
|
||||||
Object.entries(ROOM_CROPS).forEach(([key, rect]) => makeCropTexture(this, 'room-' + key, rect))
|
cropTexture(this, 'production', 'player', PLAYER_CROP)
|
||||||
Object.entries(ASSET_CROPS).forEach(([key, rect]) => makeCropTexture(this, key, rect, true))
|
Object.entries(FURNITURE_CROPS).forEach(([key, rect]) => {
|
||||||
|
cropTexture(this, 'furniture-sheet', 'furniture-' + key, rect)
|
||||||
|
})
|
||||||
|
|
||||||
const saved = await loadGameState()
|
const saved = await loadGameState()
|
||||||
if (saved?.version === 1) this.state = saved
|
this.state = saved?.version === 2 ? saved : cloneDefaultState()
|
||||||
|
|
||||||
this.cameras.main.setBackgroundColor('#fff4f9')
|
this.cameras.main.setBackgroundColor('#b7e3ff')
|
||||||
this.buildChrome()
|
this.background = this.add.image(0, 0, 'house')
|
||||||
|
.setOrigin(0)
|
||||||
|
.setDisplaySize(WIDTH, HEIGHT)
|
||||||
|
.setDepth(0)
|
||||||
|
|
||||||
|
this.buildUi()
|
||||||
this.buildControls()
|
this.buildControls()
|
||||||
this.bindInput()
|
this.bindInput()
|
||||||
this.renderRoom()
|
this.spawnAllFurniture()
|
||||||
|
this.spawnPlayer()
|
||||||
|
this.updateUi()
|
||||||
|
|
||||||
this.events.once(Phaser.Scenes.Events.SHUTDOWN, () => this.persist())
|
this.events.once(Phaser.Scenes.Events.SHUTDOWN, () => this.persist())
|
||||||
window.addEventListener('pagehide', () => this.persist(), { once: true })
|
window.addEventListener('pagehide', () => this.persist(), { once: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
buildChrome() {
|
buildUi() {
|
||||||
this.add.rectangle(480, 32, 960, 64, 0xffe7f0).setDepth(450)
|
this.add.rectangle(480, 29, 960, 58, 0xffeff5, 0.88)
|
||||||
this.add.text(24, 17, 'My Dollhouse', {
|
.setScrollFactor(0)
|
||||||
|
.setDepth(990)
|
||||||
|
|
||||||
|
this.add.text(18, 13, 'My Dollhouse', {
|
||||||
fontFamily: 'Arial Rounded MT Bold, ui-rounded, sans-serif',
|
fontFamily: 'Arial Rounded MT Bold, ui-rounded, sans-serif',
|
||||||
fontSize: '28px',
|
fontSize: '25px',
|
||||||
color: '#c84f82',
|
color: '#c64f80',
|
||||||
fontStyle: 'bold'
|
fontStyle: 'bold'
|
||||||
}).setDepth(500)
|
}).setScrollFactor(0).setDepth(1001)
|
||||||
|
|
||||||
;['bedroom', 'bathroom', 'kitchen', 'living'].forEach((room, index) => {
|
this.modeButton = roundedButton(this, 430, 29, 174, 40, '✨ Decorate', () => {
|
||||||
const button = roundedButton(
|
|
||||||
this,
|
|
||||||
400 + index * 132,
|
|
||||||
32,
|
|
||||||
120,
|
|
||||||
38,
|
|
||||||
ROOM_LABELS[room],
|
|
||||||
() => this.switchRoom(room),
|
|
||||||
{ fontSize: '14px' }
|
|
||||||
)
|
|
||||||
button.room = room
|
|
||||||
this.roomButtons.push(button)
|
|
||||||
})
|
|
||||||
|
|
||||||
this.modeButton = roundedButton(this, 820, 588, 220, 52, '✨ Decorate', () => {
|
|
||||||
this.decorateMode = !this.decorateMode
|
this.decorateMode = !this.decorateMode
|
||||||
this.updateMode()
|
this.updateUi()
|
||||||
}, { fill: 0xf9bfd3, stroke: 0xdc6d9b })
|
}, { fill: 0xf8bed2 })
|
||||||
|
|
||||||
this.addButton = roundedButton(this, 594, 588, 190, 52, '+ Add something', () => this.addSomething(), {
|
this.addButton = roundedButton(this, 616, 29, 178, 40, '+ Add Bed', () => {
|
||||||
fontSize: '15px'
|
if (!this.decorateMode) this.decorateMode = true
|
||||||
})
|
this.addFurniture()
|
||||||
this.resetButton = roundedButton(this, 480, 588, 82, 52, 'Reset', () => this.resetRoom(), {
|
this.updateUi()
|
||||||
fontSize: '14px',
|
|
||||||
fill: 0xffffff
|
|
||||||
})
|
})
|
||||||
|
|
||||||
this.roomTitle = this.add.text(50, 92, '', {
|
this.resetButton = roundedButton(this, 744, 29, 70, 40, 'Reset', () => this.resetHouse(), {
|
||||||
fontFamily: 'Arial Rounded MT Bold, ui-rounded, sans-serif',
|
fill: 0xffffff,
|
||||||
fontSize: '24px',
|
fontSize: '13px'
|
||||||
color: '#663d59',
|
})
|
||||||
fontStyle: 'bold'
|
|
||||||
}).setDepth(50)
|
|
||||||
|
|
||||||
this.helpText = this.add.text(50, 120, '', {
|
this.helpText = this.add.text(795, 29, '', {
|
||||||
fontFamily: 'Arial, sans-serif',
|
fontFamily: 'Arial, sans-serif',
|
||||||
fontSize: '14px',
|
fontSize: '12px',
|
||||||
color: '#7d5a70'
|
color: '#6f5366',
|
||||||
}).setDepth(50)
|
align: 'center'
|
||||||
|
}).setOrigin(0, 0.5).setScrollFactor(0).setDepth(1001)
|
||||||
|
|
||||||
|
this.add.rectangle(480, 688, 960, 64, 0xfff4f8, 0.82)
|
||||||
|
.setScrollFactor(0)
|
||||||
|
.setDepth(990)
|
||||||
|
|
||||||
|
this.stairsHint = this.add.text(480, 684, 'Use the stairs on the right to change floors ↑ ↓', {
|
||||||
|
fontFamily: 'Arial Rounded MT Bold, ui-rounded, sans-serif',
|
||||||
|
fontSize: '13px',
|
||||||
|
color: '#7a526b'
|
||||||
|
}).setOrigin(0.5).setScrollFactor(0).setDepth(1001)
|
||||||
}
|
}
|
||||||
|
|
||||||
buildControls() {
|
buildControls() {
|
||||||
const makePad = (x, y, label, direction) => {
|
const makePad = (x, label, direction) => {
|
||||||
const circle = this.add.circle(x, y, 29, 0xffffff, 0.88)
|
const circle = this.add.circle(x, 687, 25, 0xffffff, 0.94)
|
||||||
.setStrokeStyle(2, 0xd989ac)
|
.setStrokeStyle(2, 0xd889aa)
|
||||||
.setInteractive()
|
.setInteractive()
|
||||||
.setDepth(520)
|
.setScrollFactor(0)
|
||||||
this.add.text(x, y - 1, label, {
|
.setDepth(1010)
|
||||||
|
|
||||||
|
this.add.text(x, 686, label, {
|
||||||
fontFamily: 'Arial, sans-serif',
|
fontFamily: 'Arial, sans-serif',
|
||||||
fontSize: '26px',
|
fontSize: '23px',
|
||||||
color: '#b95b84',
|
color: '#b75c83',
|
||||||
fontStyle: 'bold'
|
fontStyle: 'bold'
|
||||||
}).setOrigin(0.5).setDepth(521)
|
}).setOrigin(0.5).setScrollFactor(0).setDepth(1011)
|
||||||
|
|
||||||
|
const off = () => { this.moveIntent[direction] = false }
|
||||||
circle.on('pointerdown', () => { this.moveIntent[direction] = true })
|
circle.on('pointerdown', () => { this.moveIntent[direction] = true })
|
||||||
circle.on('pointerup', () => { this.moveIntent[direction] = false })
|
circle.on('pointerup', off)
|
||||||
circle.on('pointerout', () => { this.moveIntent[direction] = false })
|
circle.on('pointerout', off)
|
||||||
}
|
}
|
||||||
|
|
||||||
makePad(72, 588, '◀', 'left')
|
makePad(44, '◀', 'left')
|
||||||
makePad(136, 588, '▶', 'right')
|
makePad(100, '▶', 'right')
|
||||||
makePad(200, 588, '▲', 'up')
|
makePad(156, '▲', 'up')
|
||||||
makePad(264, 588, '▼', 'down')
|
makePad(212, '▼', 'down')
|
||||||
}
|
}
|
||||||
|
|
||||||
bindInput() {
|
bindInput() {
|
||||||
this.cursors = this.input.keyboard.createCursorKeys()
|
this.cursors = this.input.keyboard.createCursorKeys()
|
||||||
this.keys = this.input.keyboard.addKeys('W,A,S,D')
|
this.keys = this.input.keyboard.addKeys('W,A,S,D')
|
||||||
|
|
||||||
this.input.on('drag', (_pointer, object, dragX, dragY) => {
|
this.input.on('drag', (_pointer, object, dragX, dragY) => {
|
||||||
if (!this.decorateMode || object.getData('kind') !== 'furniture') return
|
if (!this.decorateMode || object.getData('kind') !== 'furniture') return
|
||||||
object.x = Phaser.Math.Clamp(dragX, ROOM_X + 35, ROOM_X + ROOM_W - 35)
|
object.x = Phaser.Math.Clamp(dragX, HOUSE_BOUNDS.left + 28, HOUSE_BOUNDS.right - 28)
|
||||||
object.y = Phaser.Math.Clamp(dragY, 215, FLOOR_BOTTOM)
|
object.y = Phaser.Math.Clamp(dragY, 250, HOUSE_BOUNDS.lower.maxY)
|
||||||
object.setDepth(Math.floor(object.y))
|
object.setDepth(Math.floor(object.y))
|
||||||
})
|
})
|
||||||
|
|
||||||
this.input.on('dragend', (_pointer, object) => {
|
this.input.on('dragend', (_pointer, object) => {
|
||||||
if (object.getData('kind') !== 'furniture') return
|
if (object.getData('kind') !== 'furniture') return
|
||||||
const item = this.state.rooms[this.state.activeRoom].find((entry) => entry.id === object.getData('id'))
|
const item = this.state.items.find((entry) => entry.id === object.getData('id'))
|
||||||
if (!item) return
|
if (!item) return
|
||||||
|
|
||||||
|
const floor = floorFromY(object.y)
|
||||||
|
const bounds = floorBounds(floor)
|
||||||
|
object.y = Phaser.Math.Clamp(object.y, bounds.minY + 18, bounds.maxY)
|
||||||
item.x = Math.round(object.x)
|
item.x = Math.round(object.x)
|
||||||
item.y = Math.round(object.y)
|
item.y = Math.round(object.y)
|
||||||
|
item.floor = floor
|
||||||
|
object.setDepth(Math.floor(object.y))
|
||||||
this.scheduleSave()
|
this.scheduleSave()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
renderRoom() {
|
spawnAllFurniture() {
|
||||||
this.roomObjects.forEach((object) => object.destroy())
|
this.itemSprites.forEach((sprite) => sprite.destroy())
|
||||||
this.roomObjects = []
|
this.itemSprites = []
|
||||||
this.background?.destroy()
|
this.state.items.forEach((item) => this.spawnFurniture(item))
|
||||||
this.player?.destroy()
|
|
||||||
|
|
||||||
this.background = this.add.image(ROOM_X, ROOM_Y, 'room-' + this.state.activeRoom)
|
|
||||||
.setOrigin(0)
|
|
||||||
.setDisplaySize(ROOM_W, ROOM_H)
|
|
||||||
.setDepth(0)
|
|
||||||
|
|
||||||
const border = this.add.rectangle(ROOM_X + ROOM_W / 2, ROOM_Y + ROOM_H / 2, ROOM_W, ROOM_H)
|
|
||||||
.setFillStyle(0xffffff, 0)
|
|
||||||
.setStrokeStyle(5, 0xffffff, 0.75)
|
|
||||||
.setDepth(2)
|
|
||||||
this.roomObjects.push(border)
|
|
||||||
|
|
||||||
const items = this.state.rooms[this.state.activeRoom] ?? []
|
|
||||||
items.forEach((item) => this.spawnItem(item))
|
|
||||||
|
|
||||||
const playerPos = this.state.player
|
|
||||||
this.player = this.add.image(playerPos.x, playerPos.y, 'player')
|
|
||||||
.setOrigin(0.5, 1)
|
|
||||||
.setScale(1.35)
|
|
||||||
.setDepth(Math.floor(playerPos.y) + 10)
|
|
||||||
|
|
||||||
this.roomTitle.setText(ROOM_LABELS[this.state.activeRoom])
|
|
||||||
this.updateMode()
|
|
||||||
this.updateRoomButtons()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
spawnItem(item) {
|
spawnFurniture(item) {
|
||||||
if (!this.textures.exists(item.type)) return
|
const key = 'furniture-' + item.type
|
||||||
const object = this.add.image(item.x, item.y, item.type)
|
if (!this.textures.exists(key)) return null
|
||||||
|
|
||||||
|
const sprite = this.add.image(item.x, item.y, key)
|
||||||
.setOrigin(0.5, 1)
|
.setOrigin(0.5, 1)
|
||||||
.setScale(item.scale ?? 1)
|
.setScale(item.scale ?? 0.3)
|
||||||
.setDepth(Math.floor(item.y))
|
.setDepth(Math.floor(item.y))
|
||||||
.setData('kind', 'furniture')
|
.setData('kind', 'furniture')
|
||||||
.setData('id', item.id)
|
.setData('id', item.id)
|
||||||
object.setInteractive({ cursor: 'grab', pixelPerfect: false })
|
.setInteractive({ cursor: 'grab' })
|
||||||
this.input.setDraggable(object)
|
|
||||||
this.roomObjects.push(object)
|
this.input.setDraggable(sprite)
|
||||||
return object
|
this.itemSprites.push(sprite)
|
||||||
|
return sprite
|
||||||
}
|
}
|
||||||
|
|
||||||
switchRoom(room) {
|
spawnPlayer() {
|
||||||
if (room === this.state.activeRoom) return
|
this.player?.destroy()
|
||||||
this.capturePlayer()
|
this.player = this.add.image(this.state.player.x, this.state.player.y, 'player')
|
||||||
this.state.activeRoom = room
|
.setOrigin(0.5, 1)
|
||||||
this.state.player = { x: 480, y: 492 }
|
.setScale(0.95)
|
||||||
this.persist()
|
.setDepth(Math.floor(this.state.player.y) + 40)
|
||||||
this.renderRoom()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateRoomButtons() {
|
updateUi() {
|
||||||
this.roomButtons.forEach((button) => {
|
this.modeButton.text.setText(this.decorateMode ? '✓ Done decorating' : '✨ Decorate')
|
||||||
const active = button.room === this.state.activeRoom
|
const nextType = FURNITURE_ORDER[this.addIndex % FURNITURE_ORDER.length]
|
||||||
button.bg.setFillStyle(active ? 0xf39abc : 0xfff3f7)
|
this.addButton.text.setText('+ Add ' + FURNITURE_NAMES[nextType])
|
||||||
button.text.setColor(active ? '#ffffff' : '#6b3b59')
|
this.helpText.setText(this.decorateMode ? 'drag furniture' : 'walk & play')
|
||||||
|
|
||||||
|
this.itemSprites.forEach((sprite) => {
|
||||||
|
sprite.setAlpha(this.decorateMode ? 1 : 0.98)
|
||||||
})
|
})
|
||||||
|
this.player?.setAlpha(this.decorateMode ? 0.68 : 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateMode() {
|
addFurniture() {
|
||||||
const label = this.decorateMode ? '✓ Done decorating' : '✨ Decorate'
|
const type = FURNITURE_ORDER[this.addIndex % FURNITURE_ORDER.length]
|
||||||
this.modeButton.text.setText(label)
|
this.addIndex += 1
|
||||||
this.helpText.setText(
|
|
||||||
this.decorateMode
|
const floor = this.state.player.floor ?? floorFromY(this.state.player.y)
|
||||||
? 'Drag the furniture around. Add more things whenever you like.'
|
const bounds = floorBounds(floor)
|
||||||
: 'Walk with the arrows, WASD, or the big buttons below.'
|
const scaleByType = {
|
||||||
)
|
bed: 0.38,
|
||||||
this.addButton.bg.setAlpha(this.decorateMode ? 1 : 0.55)
|
couch: 0.29,
|
||||||
this.addButton.text.setAlpha(this.decorateMode ? 1 : 0.55)
|
armchair: 0.29,
|
||||||
this.player?.setAlpha(this.decorateMode ? 0.72 : 1)
|
table: 0.27,
|
||||||
|
bookcase: 0.27,
|
||||||
|
wardrobe: 0.29
|
||||||
}
|
}
|
||||||
|
|
||||||
addSomething() {
|
|
||||||
if (!this.decorateMode) {
|
|
||||||
this.decorateMode = true
|
|
||||||
this.updateMode()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const choicesByRoom = {
|
|
||||||
bedroom: ['teddy', 'bookcase', 'bed'],
|
|
||||||
bathroom: ['ball', 'armchair'],
|
|
||||||
kitchen: ['table', 'bookcase'],
|
|
||||||
living: ['armchair', 'table', 'bookcase', 'teddy']
|
|
||||||
}
|
|
||||||
const choices = choicesByRoom[this.state.activeRoom]
|
|
||||||
const type = choices[Math.floor(Math.random() * choices.length)]
|
|
||||||
const item = {
|
const item = {
|
||||||
id: type + '-' + Date.now(),
|
id: type + '-' + Date.now(),
|
||||||
type,
|
type,
|
||||||
x: 480 + Phaser.Math.Between(-130, 130),
|
floor,
|
||||||
y: 470 + Phaser.Math.Between(-10, 35),
|
x: Phaser.Math.Clamp(this.state.player.x + 95, 100, 860),
|
||||||
scale: type === 'teddy' || type === 'ball' ? 1.25 : 1
|
y: bounds.homeY + 8,
|
||||||
|
scale: scaleByType[type]
|
||||||
}
|
}
|
||||||
this.state.rooms[this.state.activeRoom].push(item)
|
|
||||||
this.spawnItem(item)
|
this.state.items.push(item)
|
||||||
|
this.spawnFurniture(item)
|
||||||
this.scheduleSave()
|
this.scheduleSave()
|
||||||
}
|
}
|
||||||
|
|
||||||
async resetRoom() {
|
async resetHouse() {
|
||||||
const defaults = cloneDefaultState()
|
this.state = cloneDefaultState()
|
||||||
this.state.rooms[this.state.activeRoom] = defaults.rooms[this.state.activeRoom]
|
|
||||||
this.state.player = { x: 480, y: 492 }
|
|
||||||
await this.persist()
|
await this.persist()
|
||||||
this.renderRoom()
|
this.spawnAllFurniture()
|
||||||
|
this.spawnPlayer()
|
||||||
|
this.updateUi()
|
||||||
|
}
|
||||||
|
|
||||||
|
switchFloor(targetFloor) {
|
||||||
|
if (!this.player || targetFloor === this.state.player.floor) return
|
||||||
|
|
||||||
|
const target = floorBounds(targetFloor)
|
||||||
|
this.state.player.floor = targetFloor
|
||||||
|
this.stairCooldown = 650
|
||||||
|
|
||||||
|
this.tweens.add({
|
||||||
|
targets: this.player,
|
||||||
|
x: 760,
|
||||||
|
y: target.homeY,
|
||||||
|
duration: 360,
|
||||||
|
ease: 'Sine.easeInOut',
|
||||||
|
onUpdate: () => {
|
||||||
|
this.player.setDepth(Math.floor(this.player.y) + 40)
|
||||||
|
},
|
||||||
|
onComplete: () => {
|
||||||
|
this.capturePlayer()
|
||||||
|
this.scheduleSave()
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
capturePlayer() {
|
capturePlayer() {
|
||||||
if (!this.player) return
|
if (!this.player) return
|
||||||
this.state.player = {
|
this.state.player.x = Math.round(this.player.x)
|
||||||
x: Math.round(this.player.x),
|
this.state.player.y = Math.round(this.player.y)
|
||||||
y: Math.round(this.player.y)
|
this.state.player.floor = floorFromY(this.player.y)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
scheduleSave() {
|
scheduleSave() {
|
||||||
@ -424,13 +393,29 @@ class HouseScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
update(_time, delta) {
|
update(_time, delta) {
|
||||||
if (!this.player || this.decorateMode) return
|
if (!this.player || this.decorateMode || this.tweens.isTweening(this.player)) return
|
||||||
|
|
||||||
|
this.stairCooldown = Math.max(0, this.stairCooldown - delta)
|
||||||
|
|
||||||
const left = this.cursors.left.isDown || this.keys.A.isDown || this.moveIntent.left
|
const left = this.cursors.left.isDown || this.keys.A.isDown || this.moveIntent.left
|
||||||
const right = this.cursors.right.isDown || this.keys.D.isDown || this.moveIntent.right
|
const right = this.cursors.right.isDown || this.keys.D.isDown || this.moveIntent.right
|
||||||
const up = this.cursors.up.isDown || this.keys.W.isDown || this.moveIntent.up
|
const up = this.cursors.up.isDown || this.keys.W.isDown || this.moveIntent.up
|
||||||
const down = this.cursors.down.isDown || this.keys.S.isDown || this.moveIntent.down
|
const down = this.cursors.down.isDown || this.keys.S.isDown || this.moveIntent.down
|
||||||
|
|
||||||
|
const floor = this.state.player.floor ?? floorFromY(this.player.y)
|
||||||
|
const bounds = floorBounds(floor)
|
||||||
|
|
||||||
|
if (this.stairCooldown <= 0 && this.player.x >= STAIRS_X) {
|
||||||
|
if (up && floor === 'lower') {
|
||||||
|
this.switchFloor('upper')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (down && floor === 'upper') {
|
||||||
|
this.switchFloor('lower')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let dx = Number(right) - Number(left)
|
let dx = Number(right) - Number(left)
|
||||||
let dy = Number(down) - Number(up)
|
let dy = Number(down) - Number(up)
|
||||||
if (dx === 0 && dy === 0) return
|
if (dx === 0 && dy === 0) return
|
||||||
@ -439,15 +424,24 @@ class HouseScene extends Phaser.Scene {
|
|||||||
dx /= length
|
dx /= length
|
||||||
dy /= length
|
dy /= length
|
||||||
|
|
||||||
const speed = 220
|
this.player.x = Phaser.Math.Clamp(
|
||||||
const depthSpeed = 105
|
this.player.x + dx * 210 * delta / 1000,
|
||||||
this.player.x = Phaser.Math.Clamp(this.player.x + dx * speed * delta / 1000, ROOM_X + 38, ROOM_X + ROOM_W - 38)
|
HOUSE_BOUNDS.left,
|
||||||
this.player.y = Phaser.Math.Clamp(this.player.y + dy * depthSpeed * delta / 1000, FLOOR_TOP, FLOOR_BOTTOM)
|
HOUSE_BOUNDS.right
|
||||||
this.player.setFlipX(dx < 0)
|
)
|
||||||
this.player.setDepth(Math.floor(this.player.y) + 10)
|
this.player.y = Phaser.Math.Clamp(
|
||||||
|
this.player.y + dy * 76 * delta / 1000,
|
||||||
|
bounds.minY,
|
||||||
|
bounds.maxY
|
||||||
|
)
|
||||||
|
|
||||||
const depthScale = Phaser.Math.Linear(1.18, 1.42, (this.player.y - FLOOR_TOP) / (FLOOR_BOTTOM - FLOOR_TOP))
|
if (Math.abs(dx) > 0.05) this.player.setFlipX(dx < 0)
|
||||||
this.player.setScale(depthScale)
|
|
||||||
|
const depthT = Phaser.Math.Clamp((this.player.y - bounds.minY) / (bounds.maxY - bounds.minY), 0, 1)
|
||||||
|
this.player.setScale(Phaser.Math.Linear(0.84, 1.02, depthT))
|
||||||
|
this.player.setDepth(Math.floor(this.player.y) + 40)
|
||||||
|
|
||||||
|
this.capturePlayer()
|
||||||
this.scheduleSave()
|
this.scheduleSave()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -457,7 +451,7 @@ const game = new Phaser.Game({
|
|||||||
parent: 'game',
|
parent: 'game',
|
||||||
width: WIDTH,
|
width: WIDTH,
|
||||||
height: HEIGHT,
|
height: HEIGHT,
|
||||||
backgroundColor: '#fff4f9',
|
backgroundColor: '#b7e3ff',
|
||||||
render: { antialias: true, pixelArt: false },
|
render: { antialias: true, pixelArt: false },
|
||||||
scale: {
|
scale: {
|
||||||
mode: Phaser.Scale.FIT,
|
mode: Phaser.Scale.FIT,
|
||||||
@ -470,7 +464,7 @@ const game = new Phaser.Game({
|
|||||||
})
|
})
|
||||||
|
|
||||||
window.__DOLLHOUSE__ = {
|
window.__DOLLHOUSE__ = {
|
||||||
version: '0.1.0',
|
version: '0.2.0',
|
||||||
game,
|
game,
|
||||||
async resetAll() {
|
async resetAll() {
|
||||||
await clearGameState()
|
await clearGameState()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user