From 33684940853e5736269f1fc006816ea36e3cb6eb Mon Sep 17 00:00:00 2001 From: Admin Date: Mon, 21 Sep 2026 23:31:21 +0000 Subject: [PATCH] Build room-bank house with central elevator journeys --- src/main.js | 875 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 543 insertions(+), 332 deletions(-) diff --git a/src/main.js b/src/main.js index 163a20b..98be623 100644 --- a/src/main.js +++ b/src/main.js @@ -35,16 +35,29 @@ window.addEventListener('appinstalled', () => { const WIDTH = 960 const HEIGHT = 720 -const HOUSE_BOUNDS = { - left: 38, - right: 922, - upper: { minY: 270, maxY: 354, homeY: 336 }, - lower: { minY: 515, maxY: 610, homeY: 585 } -} +const HOUSE_TOP = 74 +const LEFT_X = 50 +const BANK_W = 350 +const ELEVATOR_GAP = 20 +const ELEVATOR_W = 120 +const ELEVATOR_X = LEFT_X + BANK_W + ELEVATOR_GAP +const RIGHT_X = ELEVATOR_X + ELEVATOR_W + ELEVATOR_GAP +const FLOOR_H = 102 +const FLOOR_GAP = 8 +const FLOOR_COUNT = 5 -const STAIRS_X = 675 const PLAYER_DRAG_FOLLOW = 0.18 const PLAYER_DRAG_OFFSET_Y = 42 + +const PLAYER_STATE = { + IDLE: 'idle', + WALKING: 'walking', + GOING_TO_LIFT: 'going-to-lift', + ENTERING_LIFT: 'entering-lift', + RIDING_LIFT: 'riding-lift', + EXITING_LIFT: 'exiting-lift' +} + const PLAYER_IDLE_CROPS = [ { x: 145, y: 12, w: 199, h: 310 }, { x: 344, y: 12, w: 196, h: 310 }, @@ -61,70 +74,71 @@ const PLAYER_WALK_CROPS = [ { x: 899, y: 324, w: 176, h: 294 } ] -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 } +function floorY(floor) { + return HOUSE_TOP + floor * (FLOOR_H + FLOOR_GAP) } -const FURNITURE_ORDER = ['bed', 'couch', 'armchair', 'table', 'bookcase', 'wardrobe'] -const FURNITURE_NAMES = { - bed: 'Bed', - couch: 'Sofa', - armchair: 'Chair', - table: 'Table', - bookcase: 'Shelf', - wardrobe: 'Wardrobe' +function makeRoom(id, name, floor, side, color) { + const x = side === 'left' ? LEFT_X : RIGHT_X + const y = floorY(floor) + + return { + id, + name, + floor, + side, + color, + x, + y, + width: BANK_W, + height: FLOOR_H, + walkBand: { + minY: y + 66, + maxY: y + 94, + homeY: y + 86 + } + } +} + +const ROOMS = [ + makeRoom('disco', 'Disco', 0, 'left', 0xf1e4ff), + makeRoom('top-bedroom', 'Top Bedroom', 0, 'right', 0xffe8f1), + makeRoom('bedroom', 'Bedroom', 1, 'left', 0xffeef4), + makeRoom('study', 'Study', 1, 'right', 0xfff4df), + makeRoom('living', 'Living Room', 2, 'left', 0xe9f5ff), + makeRoom('playroom', 'Playroom', 2, 'right', 0xeee8ff), + makeRoom('kitchen', 'Kitchen', 3, 'left', 0xe6f6ed), + makeRoom('bathroom', 'Bathroom', 3, 'right', 0xe5f4ff), + makeRoom('garage', 'Garage', 4, 'left', 0xe8eef5), + makeRoom('utility', 'Utility', 4, 'right', 0xfff0df) +] + +const ROOM_BY_ID = new Map(ROOMS.map((room) => [room.id, room])) + +function elevatorStopY(room) { + return room.y + 52 +} + +function elevatorEntry(room) { + return { + x: room.side === 'left' ? room.x + room.width - 24 : room.x + 24, + y: room.walkBand.homeY + } } const DEFAULT_STATE = { - version: 2, - player: { x: 510, y: HOUSE_BOUNDS.lower.homeY, floor: 'lower' }, - items: [ - { id: 'bed-1', type: 'bed', x: 168, y: 346, scale: 0.42, floor: 'upper' }, - { id: 'wardrobe-1', type: 'wardrobe', x: 356, y: 348, scale: 0.32, floor: 'upper' }, - { id: 'couch-1', type: 'couch', x: 560, y: 596, scale: 0.31, floor: 'lower' }, - { id: 'table-1', type: 'table', x: 510, y: 612, scale: 0.28, floor: 'lower' }, - { 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' } - ] + version: 3, + player: { + roomId: 'living', + x: LEFT_X + 150, + y: ROOM_BY_ID.get('living').walkBand.homeY + } } function cloneDefaultState() { return JSON.parse(JSON.stringify(DEFAULT_STATE)) } -function roundedButton(scene, x, y, width, height, label, onClick, options = {}) { - const bg = scene.add.rectangle(x, y, width, height, options.fill ?? 0xfff2f7, options.alpha ?? 0.94) - .setStrokeStyle(2, options.stroke ?? 0xe58daf) - .setInteractive({ useHandCursor: true }) - .setScrollFactor(0) - .setDepth(1000) - - const text = scene.add.text(x, y, label, { - fontFamily: 'Arial Rounded MT Bold, ui-rounded, sans-serif', - fontSize: options.fontSize ?? '15px', - color: options.color ?? '#6b3b59', - fontStyle: 'bold' - }).setOrigin(0.5).setScrollFactor(0).setDepth(1001) - - bg.on('pointerdown', () => { - scene.tweens.add({ - targets: [bg, text], - scaleX: 0.95, - scaleY: 0.95, - duration: 55, - yoyo: true - }) - onClick() - }) - - return { bg, text } -} - function cropTexture(scene, sourceKey, key, rect) { if (scene.textures.exists(key)) return const source = scene.textures.get(sourceKey).getSourceImage() @@ -136,256 +150,301 @@ function cropTexture(scene, sourceKey, key, rect) { 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 -} +const ELEVATOR_SVG = ` + + + + + + + + + + + + + + + + + + +` class HouseScene extends Phaser.Scene { constructor() { super('House') + this.state = cloneDefaultState() - this.decorateMode = false - this.moveIntent = { left: false, right: false, up: false, down: false } - this.saveTimer = null - this.itemSprites = [] - this.addIndex = 0 - this.stairCooldown = 0 + this.playerState = PLAYER_STATE.IDLE + this.playerDragging = false + this.playerDragTarget = null + this.latestPointer = null + this.lastPlayerDragX = null this.playerAnimMode = 'idle' this.playerAnimFrame = 0 this.playerAnimClock = 0 - this.playerDragging = false - this.lastPlayerDragX = null - this.playerDragTarget = null + this.journeyActive = false + this.saveTimer = null } preload() { - this.load.image('house', '/assets/open-dollhouse.png') - this.load.image('furniture-sheet', '/assets/furniture-sheet.png') this.load.image('character-sheet', '/assets/character-sheet.png') + this.load.image( + 'elevator-shaft', + 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(ELEVATOR_SVG) + ) } async create() { PLAYER_IDLE_CROPS.forEach((rect, index) => { cropTexture(this, 'character-sheet', 'player-idle-' + index, rect) }) + PLAYER_WALK_CROPS.forEach((rect, index) => { cropTexture(this, 'character-sheet', 'player-walk-' + index, rect) }) - Object.entries(FURNITURE_CROPS).forEach(([key, rect]) => { - cropTexture(this, 'furniture-sheet', 'furniture-' + key, rect) - }) const saved = await loadGameState() - this.state = saved?.version === 2 ? saved : cloneDefaultState() + this.state = saved?.version === 3 && ROOM_BY_ID.has(saved.player?.roomId) + ? saved + : cloneDefaultState() - this.cameras.main.setBackgroundColor('#b7e3ff') - this.background = this.add.image(0, 0, 'house') - .setOrigin(0) - .setDisplaySize(WIDTH, HEIGHT) - .setDepth(0) + this.cameras.main.setBackgroundColor('#ccecff') + this.drawHouse() + this.buildElevator() this.buildUi() - this.bindInput() - this.spawnAllFurniture() this.spawnPlayer() - this.updateUi() + this.bindInput() this.events.once(Phaser.Scenes.Events.SHUTDOWN, () => this.persist()) window.addEventListener('pagehide', () => this.persist(), { once: true }) } - buildUi() { - this.add.rectangle(480, 29, 960, 58, 0xffeff5, 0.88) - .setScrollFactor(0) - .setDepth(990) + drawHouse() { + const graphics = this.add.graphics().setDepth(0) - this.add.text(18, 13, 'My Dollhouse', { + graphics.fillStyle(0xfffbfd, 1) + graphics.fillRoundedRect(28, HOUSE_TOP - 18, 904, FLOOR_COUNT * FLOOR_H + (FLOOR_COUNT - 1) * FLOOR_GAP + 36, 24) + + for (const room of ROOMS) { + graphics.fillStyle(room.color, 1) + graphics.fillRoundedRect(room.x, room.y, room.width, room.height, 14) + + graphics.lineStyle(4, 0xd5b8c6, 1) + graphics.strokeRoundedRect(room.x, room.y, room.width, room.height, 14) + + graphics.fillStyle(0xe6caa9, 1) + graphics.fillRoundedRect( + room.x + 6, + room.walkBand.minY, + room.width - 12, + room.walkBand.maxY - room.walkBand.minY, + 6 + ) + + this.add.text(room.x + 14, room.y + 10, room.name, { + fontFamily: 'Arial Rounded MT Bold, ui-rounded, sans-serif', + fontSize: '16px', + color: '#765466', + fontStyle: 'bold' + }).setDepth(4) + + if (room.id === 'disco') { + this.add.text(room.x + room.width - 48, room.y + 12, '♪ ✦', { + fontSize: '18px', + color: '#9b65c6' + }).setDepth(4) + } + + if (room.id === 'garage') { + this.add.text(room.x + room.width - 44, room.y + 11, '🚗', { + fontSize: '18px' + }).setDepth(4) + } + } + + this.add.text(WIDTH / 2, 48, 'Drag within a room to walk • drag into another room to ride the lift', { fontFamily: 'Arial Rounded MT Bold, ui-rounded, sans-serif', - fontSize: '25px', - color: '#c64f80', + fontSize: '15px', + color: '#6d4c61' + }).setOrigin(0.5).setDepth(100) + } + + buildElevator() { + const shaftHeight = FLOOR_COUNT * FLOOR_H + (FLOOR_COUNT - 1) * FLOOR_GAP + + this.add.image(ELEVATOR_X + ELEVATOR_W / 2, HOUSE_TOP, 'elevator-shaft') + .setOrigin(0.5, 0) + .setDisplaySize(ELEVATOR_W, shaftHeight) + .setDepth(20) + + const currentRoom = this.currentRoom() + const stopY = elevatorStopY(currentRoom) + const cx = ELEVATOR_X + ELEVATOR_W / 2 + + this.elevatorCarBack = this.add.rectangle(cx, stopY, 94, 76, 0xfff8fb, 1) + .setStrokeStyle(3, 0xb98ba2) + .setDepth(190) + + this.elevatorDoorLeft = this.add.rectangle(cx - 23, stopY, 44, 68, 0xf4bfd2, 1) + .setStrokeStyle(2, 0xc17d9d) + .setDepth(210) + + this.elevatorDoorRight = this.add.rectangle(cx + 23, stopY, 44, 68, 0xf4bfd2, 1) + .setStrokeStyle(2, 0xc17d9d) + .setDepth(210) + + this.elevatorHeart = this.add.text(cx, stopY, '♥', { + fontFamily: 'Arial', + fontSize: '24px', + color: '#c95b88', fontStyle: 'bold' - }).setScrollFactor(0).setDepth(1001) + }).setOrigin(0.5).setDepth(211) + } - this.modeButton = roundedButton(this, 430, 29, 174, 40, '✨ Decorate', () => { - this.decorateMode = !this.decorateMode - this.updateUi() - }, { fill: 0xf8bed2 }) - - this.addButton = roundedButton(this, 616, 29, 178, 40, '+ Add Bed', () => { - if (!this.decorateMode) this.decorateMode = true - this.addFurniture() - this.updateUi() - }) - - this.resetButton = roundedButton(this, 744, 29, 70, 40, 'Reset', () => this.resetHouse(), { - fill: 0xffffff, - fontSize: '13px' - }) - - this.helpText = this.add.text(795, 29, '', { - fontFamily: 'Arial, sans-serif', - fontSize: '12px', - color: '#6f5366', - 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, 'Drag her around • use the stairs on the right to change floors', { + buildUi() { + this.statusText = this.add.text(WIDTH / 2, 680, 'Living Room', { fontFamily: 'Arial Rounded MT Bold, ui-rounded, sans-serif', - fontSize: '13px', - color: '#7a526b' - }).setOrigin(0.5).setScrollFactor(0).setDepth(1001) + fontSize: '18px', + color: '#6d4c61', + backgroundColor: '#fff3f8', + padding: { x: 16, y: 8 } + }).setOrigin(0.5).setDepth(1000) + + this.resetButton = this.add.text(892, 680, 'Reset', { + 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.resetButton.on('pointerdown', async () => { + this.state = cloneDefaultState() + await saveGameState(this.state) + this.scene.restart() + }) + } + + spawnPlayer() { + const room = this.currentRoom() + this.state.player.x = Phaser.Math.Clamp( + this.state.player.x, + room.x + 30, + room.x + room.width - 30 + ) + this.state.player.y = Phaser.Math.Clamp( + this.state.player.y, + room.walkBand.minY, + room.walkBand.maxY + ) + + this.player = this.add.image( + this.state.player.x, + this.state.player.y, + 'player-idle-0' + ) + .setOrigin(0.5, 1) + .setScale(0.32) + .setDepth(300) + .setData('kind', 'player') + .setInteractive({ cursor: 'grab' }) + + this.input.setDraggable(this.player) + this.updatePlayerDepthAndScale(room) + this.updateStatus(room) } bindInput() { this.cursors = this.input.keyboard.createCursorKeys() this.keys = this.input.keyboard.addKeys('W,A,S,D') - this.input.on('dragstart', (_pointer, object) => { - if (object.getData('kind') !== 'player' || this.decorateMode) return + this.input.on('dragstart', (pointer, object) => { + if (object.getData('kind') !== 'player' || this.journeyActive) return + this.playerDragging = true this.lastPlayerDragX = object.x - this.playerDragTarget = { x: object.x, y: object.y } + this.latestPointer = { x: pointer.x, y: pointer.y } + this.playerState = PLAYER_STATE.WALKING this.animatePlayer('walk', 999) }) - this.input.on('drag', (_pointer, object, dragX, dragY) => { - const kind = object.getData('kind') + this.input.on('drag', (pointer, object) => { + if (object.getData('kind') !== 'player') return - if (kind === 'player') { - if (this.decorateMode) return + this.latestPointer = { x: pointer.x, y: pointer.y } - let floor = this.state.player.floor ?? floorFromY(object.y) - const nearStairs = dragX >= STAIRS_X - const targetX = Phaser.Math.Clamp(dragX, HOUSE_BOUNDS.left, HOUSE_BOUNDS.right) - let targetY = dragY + PLAYER_DRAG_OFFSET_Y + if (this.journeyActive) return - if (nearStairs && floor === 'lower' && dragY < 475) { - floor = 'upper' - this.state.player.floor = floor - targetY = HOUSE_BOUNDS.upper.homeY - } else if (nearStairs && floor === 'upper' && dragY > 430) { - floor = 'lower' - this.state.player.floor = floor - targetY = HOUSE_BOUNDS.lower.homeY - } + const targetRoom = this.getRoomAt(pointer.x, pointer.y) + const currentRoom = this.currentRoom() - const bounds = floorBounds(floor) - targetY = Phaser.Math.Clamp(targetY, bounds.minY, bounds.maxY) - - const previousX = this.lastPlayerDragX ?? object.x - if (Math.abs(targetX - previousX) > 1) object.setFlipX(targetX < previousX) - this.lastPlayerDragX = targetX - - this.playerDragTarget = { x: targetX, y: targetY } + if (targetRoom && targetRoom.id !== currentRoom.id) { + this.beginElevatorJourney(targetRoom) return } - if (!this.decorateMode || kind !== 'furniture') return + if (!targetRoom || targetRoom.id !== currentRoom.id) return - object.x = Phaser.Math.Clamp(dragX, HOUSE_BOUNDS.left + 28, HOUSE_BOUNDS.right - 28) - object.y = Phaser.Math.Clamp(dragY, 250, HOUSE_BOUNDS.lower.maxY) - object.setDepth(Math.floor(object.y)) + const targetX = Phaser.Math.Clamp( + pointer.x, + currentRoom.x + 26, + currentRoom.x + currentRoom.width - 26 + ) + + const targetY = Phaser.Math.Clamp( + pointer.y + PLAYER_DRAG_OFFSET_Y, + currentRoom.walkBand.minY, + currentRoom.walkBand.maxY + ) + + const previousX = this.lastPlayerDragX ?? this.player.x + if (Math.abs(targetX - previousX) > 1) { + this.player.setFlipX(targetX < previousX) + } + + this.lastPlayerDragX = targetX + this.playerDragTarget = { x: targetX, y: targetY } }) this.input.on('dragend', (_pointer, object) => { - const kind = object.getData('kind') + if (object.getData('kind') !== 'player') return - if (kind === 'player') { - if (this.decorateMode) return + this.playerDragging = false + this.lastPlayerDragX = null - this.playerDragging = false - this.lastPlayerDragX = null + if (this.journeyActive) return - const target = this.playerDragTarget ?? { x: object.x, y: object.y } - const floor = this.state.player.floor ?? floorFromY(target.y) - const bounds = floorBounds(floor) - - this.tweens.add({ - targets: object, - x: Phaser.Math.Clamp(target.x, HOUSE_BOUNDS.left, HOUSE_BOUNDS.right), - y: Phaser.Math.Clamp(target.y, bounds.minY, bounds.maxY), - duration: 110, - ease: 'Quad.out', - onUpdate: () => { - const depthT = Phaser.Math.Clamp((object.y - bounds.minY) / (bounds.maxY - bounds.minY), 0, 1) - object.setScale(Phaser.Math.Linear(0.32, 0.39, depthT)) - object.setDepth(Math.floor(object.y) + 40) - }, - onComplete: () => { - this.playerDragTarget = null - this.animatePlayer('idle', 999) - this.capturePlayer() - this.scheduleSave() - } - }) - return - } - - if (kind !== 'furniture') return - - const item = this.state.items.find((entry) => entry.id === object.getData('id')) - 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.y = Math.round(object.y) - item.floor = floor - object.setDepth(Math.floor(object.y)) - this.scheduleSave() + this.settlePlayer() }) } - spawnAllFurniture() { - this.itemSprites.forEach((sprite) => sprite.destroy()) - this.itemSprites = [] - this.state.items.forEach((item) => this.spawnFurniture(item)) + currentRoom() { + return ROOM_BY_ID.get(this.state.player.roomId) ?? ROOM_BY_ID.get('living') } - spawnFurniture(item) { - const key = 'furniture-' + item.type - if (!this.textures.exists(key)) return null - - const sprite = this.add.image(item.x, item.y, key) - .setOrigin(0.5, 1) - .setScale(item.scale ?? 0.3) - .setDepth(Math.floor(item.y)) - .setData('kind', 'furniture') - .setData('id', item.id) - .setInteractive({ cursor: 'grab' }) - - this.input.setDraggable(sprite) - this.itemSprites.push(sprite) - return sprite + getRoomAt(x, y) { + return ROOMS.find((room) => ( + x >= room.x && + x <= room.x + room.width && + y >= room.y && + y <= room.y + room.height + )) ?? null } - spawnPlayer() { - this.player?.destroy() - this.playerAnimMode = 'idle' - this.playerAnimFrame = 0 - this.playerAnimClock = 0 - this.player = this.add.image(this.state.player.x, this.state.player.y, 'player-idle-0') - .setOrigin(0.5, 1) - .setScale(0.36) - .setDepth(Math.floor(this.state.player.y) + 40) - .setData('kind', 'player') - .setInteractive({ cursor: 'grab' }) - this.input.setDraggable(this.player) + updateStatus(room, message = null) { + this.statusText.setText(message ?? room.name) } animatePlayer(mode, delta) { if (!this.player) return - const frameCount = mode === 'walk' ? PLAYER_WALK_CROPS.length : PLAYER_IDLE_CROPS.length + const frameCount = mode === 'walk' + ? PLAYER_WALK_CROPS.length + : PLAYER_IDLE_CROPS.length + const frameMs = mode === 'walk' ? 105 : 420 if (this.playerAnimMode !== mode) { @@ -404,72 +463,217 @@ class HouseScene extends Phaser.Scene { this.player.setTexture('player-' + mode + '-' + this.playerAnimFrame) } - updateUi() { - this.modeButton.text.setText(this.decorateMode ? '✓ Done decorating' : '✨ Decorate') - const nextType = FURNITURE_ORDER[this.addIndex % FURNITURE_ORDER.length] - this.addButton.text.setText('+ Add ' + FURNITURE_NAMES[nextType]) - this.helpText.setText(this.decorateMode ? 'drag furniture' : 'drag her to move') + updatePlayerDepthAndScale(room) { + const t = Phaser.Math.Clamp( + (this.player.y - room.walkBand.minY) / + (room.walkBand.maxY - room.walkBand.minY), + 0, + 1 + ) - this.itemSprites.forEach((sprite) => { - sprite.setAlpha(this.decorateMode ? 1 : 0.98) - }) - this.player?.setAlpha(this.decorateMode ? 0.68 : 1) + this.player.setScale(Phaser.Math.Linear(0.29, 0.34, t)) + this.player.setDepth(300 + room.floor) } - addFurniture() { - const type = FURNITURE_ORDER[this.addIndex % FURNITURE_ORDER.length] - this.addIndex += 1 + tweenAsync(config) { + return new Promise((resolve) => { + this.tweens.add({ + ...config, + onComplete: resolve + }) + }) + } - const floor = this.state.player.floor ?? floorFromY(this.state.player.y) - const bounds = floorBounds(floor) - const scaleByType = { - bed: 0.38, - couch: 0.29, - armchair: 0.29, - table: 0.27, - bookcase: 0.27, - wardrobe: 0.29 + delayAsync(ms) { + return new Promise((resolve) => { + this.time.delayedCall(ms, resolve) + }) + } + + async setElevatorDoors(open) { + const cx = ELEVATOR_X + ELEVATOR_W / 2 + const leftX = open ? cx - 58 : cx - 23 + const rightX = open ? cx + 58 : cx + 23 + + await Promise.all([ + this.tweenAsync({ + targets: this.elevatorDoorLeft, + x: leftX, + duration: 170, + ease: 'Sine.easeOut' + }), + this.tweenAsync({ + targets: this.elevatorDoorRight, + x: rightX, + duration: 170, + ease: 'Sine.easeOut' + }), + this.tweenAsync({ + targets: this.elevatorHeart, + alpha: open ? 0 : 1, + duration: 120 + }) + ]) + } + + setElevatorY(y) { + this.elevatorCarBack.y = y + this.elevatorDoorLeft.y = y + this.elevatorDoorRight.y = y + this.elevatorHeart.y = y + } + + async beginElevatorJourney(targetRoom) { + if (this.journeyActive || targetRoom.id === this.state.player.roomId) return + + const fromRoom = this.currentRoom() + const fromEntry = elevatorEntry(fromRoom) + const targetEntry = elevatorEntry(targetRoom) + const carX = ELEVATOR_X + ELEVATOR_W / 2 + + this.journeyActive = true + this.playerDragTarget = null + this.playerState = PLAYER_STATE.GOING_TO_LIFT + this.updateStatus(fromRoom, 'Going to lift…') + this.animatePlayer('walk', 999) + + const toLiftDuration = Phaser.Math.Clamp( + Phaser.Math.Distance.Between(this.player.x, this.player.y, fromEntry.x, fromEntry.y) * 2.0, + 220, + 760 + ) + + await this.tweenAsync({ + targets: this.player, + x: fromEntry.x, + y: fromEntry.y, + duration: toLiftDuration, + ease: 'Sine.easeInOut', + onUpdate: () => { + this.updatePlayerDepthAndScale(fromRoom) + } + }) + + this.setElevatorY(elevatorStopY(fromRoom)) + await this.setElevatorDoors(true) + + this.playerState = PLAYER_STATE.ENTERING_LIFT + + await this.tweenAsync({ + targets: this.player, + x: carX, + y: fromRoom.walkBand.homeY, + duration: 220, + ease: 'Sine.easeInOut' + }) + + this.player.setDepth(200) + this.animatePlayer('idle', 999) + await this.setElevatorDoors(false) + + this.playerState = PLAYER_STATE.RIDING_LIFT + this.updateStatus(fromRoom, 'Riding to ' + targetRoom.name + '…') + + const fromStop = elevatorStopY(fromRoom) + const toStop = elevatorStopY(targetRoom) + const floorDistance = Math.abs(targetRoom.floor - fromRoom.floor) + + if (floorDistance === 0) { + await this.delayAsync(340) + } else { + const rideDuration = 280 + floorDistance * 210 + + await Promise.all([ + this.tweenAsync({ + targets: [ + this.elevatorCarBack, + this.elevatorDoorLeft, + this.elevatorDoorRight, + this.elevatorHeart + ], + y: toStop, + duration: rideDuration, + ease: 'Sine.easeInOut' + }), + this.tweenAsync({ + targets: this.player, + y: targetRoom.walkBand.homeY, + duration: rideDuration, + ease: 'Sine.easeInOut' + }) + ]) } - const item = { - id: type + '-' + Date.now(), - type, - floor, - x: Phaser.Math.Clamp(this.state.player.x + 95, 100, 860), - y: bounds.homeY + 8, - scale: scaleByType[type] + await this.setElevatorDoors(true) + + this.playerState = PLAYER_STATE.EXITING_LIFT + this.animatePlayer('walk', 999) + + await this.tweenAsync({ + targets: this.player, + x: targetEntry.x, + y: targetEntry.y, + duration: 240, + ease: 'Sine.easeOut' + }) + + this.state.player.roomId = targetRoom.id + this.state.player.x = Math.round(this.player.x) + this.state.player.y = Math.round(this.player.y) + + this.updatePlayerDepthAndScale(targetRoom) + await this.setElevatorDoors(false) + + this.journeyActive = false + this.playerState = PLAYER_STATE.IDLE + this.updateStatus(targetRoom) + + const pointerRoom = this.latestPointer + ? this.getRoomAt(this.latestPointer.x, this.latestPointer.y) + : null + + if (this.playerDragging && pointerRoom?.id === targetRoom.id) { + const targetX = Phaser.Math.Clamp( + this.latestPointer.x, + targetRoom.x + 26, + targetRoom.x + targetRoom.width - 26 + ) + + const targetY = Phaser.Math.Clamp( + this.latestPointer.y + PLAYER_DRAG_OFFSET_Y, + targetRoom.walkBand.minY, + targetRoom.walkBand.maxY + ) + + this.playerDragTarget = { x: targetX, y: targetY } + this.playerState = PLAYER_STATE.WALKING + this.animatePlayer('walk', 999) + } else { + this.playerDragTarget = null + this.animatePlayer('idle', 999) } - this.state.items.push(item) - this.spawnFurniture(item) this.scheduleSave() } - async resetHouse() { - this.state = cloneDefaultState() - await this.persist() - 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 + settlePlayer() { + const room = this.currentRoom() + const target = this.playerDragTarget ?? { + x: this.player.x, + y: this.player.y + } 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) - }, + x: Phaser.Math.Clamp(target.x, room.x + 26, room.x + room.width - 26), + y: Phaser.Math.Clamp(target.y, room.walkBand.minY, room.walkBand.maxY), + duration: 110, + ease: 'Quad.out', + onUpdate: () => this.updatePlayerDepthAndScale(room), onComplete: () => { + this.playerDragTarget = null + this.playerState = PLAYER_STATE.IDLE + this.animatePlayer('idle', 999) this.capturePlayer() this.scheduleSave() } @@ -478,9 +682,9 @@ class HouseScene extends Phaser.Scene { capturePlayer() { if (!this.player) return + this.state.player.x = Math.round(this.player.x) this.state.player.y = Math.round(this.player.y) - this.state.player.floor = floorFromY(this.player.y) } scheduleSave() { @@ -494,51 +698,55 @@ class HouseScene extends Phaser.Scene { } update(_time, delta) { - if (!this.player || this.decorateMode || this.tweens.isTweening(this.player)) return + if (!this.player) return + + if (this.journeyActive) { + if ( + this.playerState === PLAYER_STATE.GOING_TO_LIFT || + this.playerState === PLAYER_STATE.ENTERING_LIFT || + this.playerState === PLAYER_STATE.EXITING_LIFT + ) { + this.animatePlayer('walk', delta) + } else { + this.animatePlayer('idle', delta) + } + return + } + + const room = this.currentRoom() if (this.playerDragging && this.playerDragTarget) { - const floor = this.state.player.floor ?? floorFromY(this.player.y) - const bounds = floorBounds(floor) const follow = 1 - Math.pow(1 - PLAYER_DRAG_FOLLOW, delta / 16.667) - this.player.x = Phaser.Math.Linear(this.player.x, this.playerDragTarget.x, follow) - this.player.y = Phaser.Math.Linear(this.player.y, this.playerDragTarget.y, follow) + this.player.x = Phaser.Math.Linear( + this.player.x, + this.playerDragTarget.x, + follow + ) - const depthT = Phaser.Math.Clamp((this.player.y - bounds.minY) / (bounds.maxY - bounds.minY), 0, 1) - this.player.setScale(Phaser.Math.Linear(0.32, 0.39, depthT)) - this.player.setDepth(Math.floor(this.player.y) + 40) + this.player.y = Phaser.Math.Linear( + this.player.y, + this.playerDragTarget.y, + follow + ) - this.state.player.x = Math.round(this.player.x) - this.state.player.y = Math.round(this.player.y) + this.updatePlayerDepthAndScale(room) + this.capturePlayer() this.animatePlayer('walk', delta) return } - this.stairCooldown = Math.max(0, this.stairCooldown - delta) - - 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 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 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 - } - } + const left = this.cursors.left.isDown || this.keys.A.isDown + const right = this.cursors.right.isDown || this.keys.D.isDown + const up = this.cursors.up.isDown || this.keys.W.isDown + const down = this.cursors.down.isDown || this.keys.S.isDown let dx = Number(right) - Number(left) let dy = Number(down) - Number(up) + const isMoving = dx !== 0 || dy !== 0 this.animatePlayer(isMoving ? 'walk' : 'idle', delta) + if (!isMoving) return const length = Math.hypot(dx, dy) || 1 @@ -546,22 +754,20 @@ class HouseScene extends Phaser.Scene { dy /= length this.player.x = Phaser.Math.Clamp( - this.player.x + dx * 210 * delta / 1000, - HOUSE_BOUNDS.left, - HOUSE_BOUNDS.right + this.player.x + dx * 190 * delta / 1000, + room.x + 26, + room.x + room.width - 26 ) + this.player.y = Phaser.Math.Clamp( - this.player.y + dy * 76 * delta / 1000, - bounds.minY, - bounds.maxY + this.player.y + dy * 72 * delta / 1000, + room.walkBand.minY, + room.walkBand.maxY ) if (Math.abs(dx) > 0.05) this.player.setFlipX(dx < 0) - const depthT = Phaser.Math.Clamp((this.player.y - bounds.minY) / (bounds.maxY - bounds.minY), 0, 1) - this.player.setScale(Phaser.Math.Linear(0.32, 0.39, depthT)) - this.player.setDepth(Math.floor(this.player.y) + 40) - + this.updatePlayerDepthAndScale(room) this.capturePlayer() this.scheduleSave() } @@ -572,20 +778,25 @@ const game = new Phaser.Game({ parent: 'game', width: WIDTH, height: HEIGHT, - backgroundColor: '#b7e3ff', - render: { antialias: true, pixelArt: false }, + backgroundColor: '#ccecff', + render: { + antialias: true, + pixelArt: false + }, scale: { mode: Phaser.Scale.FIT, autoCenter: Phaser.Scale.CENTER_BOTH, width: WIDTH, height: HEIGHT }, - input: { activePointers: 4 }, + input: { + activePointers: 4 + }, scene: [HouseScene] }) window.__DOLLHOUSE__ = { - version: '0.3.1', + version: '0.4.0', game, async resetAll() { await clearGameState()