Scale Phaser to native dollhouse shell proportions

This commit is contained in:
Admin 2026-09-22 00:39:56 +00:00
parent fc99c2e2af
commit f2511c3673

View File

@ -32,54 +32,47 @@ window.addEventListener('appinstalled', () => {
installWrap.hidden = true installWrap.hidden = true
}) })
const WIDTH = 960 const WIDTH = 1122
const HEIGHT = 1402
// Match the Phaser logical height to the usable browser/PWA viewport. // Native geometry measured from the approved 1122×1402 house shell.
// This keeps the same 960-wide room geometry while allowing the five // Phaser fits this coordinate system to the device; the shell is no longer
// floors to expand vertically on phones instead of letterboxing a 4:3 game. // stretched to a device-dependent room grid.
const VIEWPORT_W = Math.max( const LEFT_X = 88
320, const BANK_W = 379
Math.round(window.visualViewport?.width ?? window.innerWidth) const RIGHT_X = 654
)
const VIEWPORT_H = Math.max( const TOWER_X = 438
480, const TOWER_W = 244
Math.round(window.visualViewport?.height ?? window.innerHeight) const TOWER_CENTER_X = TOWER_X + TOWER_W / 2
)
const HEIGHT = Math.max( // Functional lift lives inside the wider decorative tower.
720, const ELEVATOR_X = 507
Math.min(1600, Math.round(WIDTH * VIEWPORT_H / VIEWPORT_W)) const ELEVATOR_W = 108
)
const FLOOR_SLOTS = [
{ y: 234, h: 179 },
{ y: 451, h: 186 },
{ y: 675, h: 184 },
{ y: 896, h: 180 },
{ y: 1112, h: 169 }
]
const FLOOR_COUNT = FLOOR_SLOTS.length
const SHELL_BEAM_Y = [432, 656, 877, 1094]
const HOUSE_TOP = FLOOR_SLOTS[0].y
const HOUSE_BOTTOM = FLOOR_SLOTS[FLOOR_COUNT - 1].y + FLOOR_SLOTS[FLOOR_COUNT - 1].h
const STATUS_Y = 1352
const HOUSE_TOP = 74
const BOTTOM_UI_H = 72
const HOUSE_BOTTOM_PAD = 28
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_GAP = 8
const FLOOR_COUNT = 5
const FLOOR_H = Math.floor(
(
HEIGHT -
HOUSE_TOP -
BOTTOM_UI_H -
HOUSE_BOTTOM_PAD -
FLOOR_GAP * (FLOOR_COUNT - 1)
) / FLOOR_COUNT
)
const STATUS_Y = HEIGHT - 36
const ELEVATOR_CAR_W = 82 const ELEVATOR_CAR_W = 82
const ELEVATOR_CAR_H = Math.min(104, Math.max(74, Math.round(FLOOR_H * 0.36))) const ELEVATOR_CAR_H = 90
const ELEVATOR_OPENING_W = 72 const ELEVATOR_OPENING_W = 78
const ELEVATOR_OPENING_H = Math.min(96, Math.max(68, Math.round(FLOOR_H * 0.34))) const ELEVATOR_OPENING_H = 82
const PLAYER_DRAG_FOLLOW = 0.18 const PLAYER_DRAG_FOLLOW = 0.18
const PLAYER_DRAG_OFFSET_Y = 42 const PLAYER_DRAG_OFFSET_Y = 50
const ROOM_HOLD_MS = 520 const ROOM_HOLD_MS = 520
const ROOM_HOLD_MOVE_TOLERANCE = 18 const ROOM_HOLD_MOVE_TOLERANCE = 21
const PLAYER_STATE = { const PLAYER_STATE = {
IDLE: 'idle', IDLE: 'idle',
@ -148,12 +141,17 @@ const ROOM_SKINS = {
} }
function floorY(floor) { function floorY(floor) {
return HOUSE_TOP + floor * (FLOOR_H + FLOOR_GAP) return FLOOR_SLOTS[floor].y
}
function floorHeight(floor) {
return FLOOR_SLOTS[floor].h
} }
function makeRoom(id, name, floor, side, color) { function makeRoom(id, name, floor, side, color) {
const x = side === 'left' ? LEFT_X : RIGHT_X const x = side === 'left' ? LEFT_X : RIGHT_X
const y = floorY(floor) const y = floorY(floor)
const height = floorHeight(floor)
return { return {
id, id,
@ -164,11 +162,11 @@ function makeRoom(id, name, floor, side, color) {
x, x,
y, y,
width: BANK_W, width: BANK_W,
height: FLOOR_H, height,
walkBand: { walkBand: {
minY: y + Math.round(FLOOR_H * 0.62), minY: y + Math.round(height * 0.60),
maxY: y + FLOOR_H - 10, maxY: y + height - 10,
homeY: y + Math.round(FLOOR_H * 0.82) homeY: y + Math.round(height * 0.82)
} }
} }
} }
@ -189,7 +187,7 @@ const ROOMS = [
const ROOM_BY_ID = new Map(ROOMS.map((room) => [room.id, room])) const ROOM_BY_ID = new Map(ROOMS.map((room) => [room.id, room]))
function elevatorStopY(room) { function elevatorStopY(room) {
return room.y + Math.round(FLOOR_H / 2) return room.y + Math.round(room.height / 2)
} }
function elevatorEntry(room) { function elevatorEntry(room) {
@ -224,9 +222,9 @@ function cropTexture(scene, sourceKey, key, rect) {
} }
function buildElevatorSvg() { function buildElevatorSvg() {
const shaftHeight = FLOOR_COUNT * FLOOR_H + (FLOOR_COUNT - 1) * FLOOR_GAP const shaftHeight = HOUSE_BOTTOM - HOUSE_TOP
const openings = Array.from({ length: FLOOR_COUNT }, (_, floor) => { const openings = FLOOR_SLOTS.map((slot) => {
const centerY = floor * (FLOOR_H + FLOOR_GAP) + FLOOR_H / 2 const centerY = slot.y + slot.h / 2 - HOUSE_TOP
const y = centerY - ELEVATOR_OPENING_H / 2 const y = centerY - ELEVATOR_OPENING_H / 2
return ` return `
@ -246,7 +244,7 @@ function buildElevatorSvg() {
text-anchor="middle" text-anchor="middle"
dominant-baseline="middle" dominant-baseline="middle"
font-family="Arial, sans-serif" font-family="Arial, sans-serif"
font-size="28" font-size="26"
fill="#f28fb3" fill="#f28fb3"
>♥</text> >♥</text>
` `
@ -260,30 +258,31 @@ function buildElevatorSvg() {
viewBox="0 0 ${ELEVATOR_W} ${shaftHeight}" viewBox="0 0 ${ELEVATOR_W} ${shaftHeight}"
> >
<rect <rect
x="5" x="4"
y="2" y="1"
width="${ELEVATOR_W - 10}" width="${ELEVATOR_W - 8}"
height="${shaftHeight - 4}" height="${shaftHeight - 2}"
rx="16" rx="14"
fill="#dfe8f1" fill="#f7fbff"
stroke="#9aaabd" stroke="#9aaabd"
stroke-width="5" stroke-width="4"
/> />
<rect <rect
x="16" x="12"
y="10" y="8"
width="${ELEVATOR_W - 32}" width="${ELEVATOR_W - 24}"
height="${shaftHeight - 20}" height="${shaftHeight - 16}"
rx="12" rx="10"
fill="#f7fbff" fill="#fffafb"
stroke="#c6d1de" stroke="#c6d1de"
stroke-width="3" stroke-width="2"
/> />
${openings} ${openings}
</svg> </svg>
` `
} }
class HouseScene extends Phaser.Scene { class HouseScene extends Phaser.Scene {
constructor() { constructor() {
super('House') super('House')
@ -403,21 +402,15 @@ class HouseScene extends Phaser.Scene {
const graphics = this.add.graphics().setDepth(0) const graphics = this.add.graphics().setDepth(0)
graphics.fillStyle(0xfffbfd, 1) graphics.fillStyle(0xfffbfd, 1)
graphics.fillRoundedRect( graphics.fillRect(36, 220, 1050, HOUSE_BOTTOM - 220)
28,
HOUSE_TOP - 18,
904,
FLOOR_COUNT * FLOOR_H + (FLOOR_COUNT - 1) * FLOOR_GAP + 36,
24
)
for (const room of ROOMS) { for (const room of ROOMS) {
const skin = ROOM_SKINS[room.id] const skin = ROOM_SKINS[room.id]
const inset = 7 const inset = 7
const sideInset = Math.round(room.width * 0.20) const sideInset = Math.round(room.width * 0.20)
const wallTop = room.y + 9 const wallTop = room.y + 9
const backTop = room.y + Math.max(14, Math.round(FLOOR_H * 0.08)) const backTop = room.y + Math.max(14, Math.round(room.height * 0.08))
const horizonY = room.y + Math.round(FLOOR_H * 0.60) const horizonY = room.y + Math.round(room.height * 0.60)
const bottomY = room.y + room.height - 8 const bottomY = room.y + room.height - 8
const left = room.x + inset const left = room.x + inset
const right = room.x + room.width - inset const right = room.x + room.width - inset
@ -550,106 +543,73 @@ class HouseScene extends Phaser.Scene {
} }
} }
this.add.text(
WIDTH / 2,
48,
'Drag her to walk • press & hold a room to ride the lift',
{
fontFamily: 'Arial Rounded MT Bold, ui-rounded, sans-serif',
fontSize: '15px',
color: '#6d4c61'
}
).setOrigin(0.5).setDepth(100)
} }
buildHouseShell() { buildHouseShell() {
const shellDepth = 7 const shellDepth = 7
const towerDepth = 8 const towerDepth = 8
const stackHeight = FLOOR_COUNT * FLOOR_H + (FLOOR_COUNT - 1) * FLOOR_GAP
const stackBottom = HOUSE_TOP + stackHeight
const leftOuterX = LEFT_X - 10 this.add.image(WIDTH / 2, 0, 'shell-roof')
const rightOuterX = RIGHT_X + BANK_W + 10 .setOrigin(0.5, 0)
const leftBankCenter = LEFT_X + BANK_W / 2 .setDisplaySize(1122, 242)
const rightBankCenter = RIGHT_X + BANK_W / 2
const towerCenterX = ELEVATOR_X + ELEVATOR_W / 2
// Full roof artwork keeps its native proportions instead of being cropped
// from a stretched master sheet.
this.add.image(WIDTH / 2, HOUSE_TOP + 8, 'shell-roof')
.setOrigin(0.5, 1)
.setDisplaySize(1000, 116)
.setDepth(shellDepth) .setDepth(shellDepth)
// The center tower is independent from the moving elevator shaft/car. this.add.image(TOWER_CENTER_X, 0, 'shell-tower')
// The functional shaft renders above this trim so animation remains clear.
this.add.image(towerCenterX, HOUSE_TOP - 34, 'shell-tower')
.setOrigin(0.5, 0) .setOrigin(0.5, 0)
.setDisplaySize(206, stackHeight + 92) .setDisplaySize(TOWER_W, HEIGHT)
.setDepth(towerDepth) .setDepth(towerDepth)
// Build the outside frame from one clean repeatable post segment per room. const leftOuterX = 60.5
const rightOuterX = WIDTH - 60.5
for (let floor = 0; floor < FLOOR_COUNT; floor += 1) { for (let floor = 0; floor < FLOOR_COUNT; floor += 1) {
const segmentY = floorY(floor) + FLOOR_H / 2 const slot = FLOOR_SLOTS[floor]
const segmentY = slot.y + slot.h / 2
this.add.image(leftOuterX, segmentY, 'shell-post-seg') this.add.image(leftOuterX, segmentY, 'shell-post-seg')
.setDisplaySize(34, FLOOR_H + 8) .setDisplaySize(65, slot.h)
.setDepth(shellDepth) .setDepth(shellDepth)
this.add.image(rightOuterX, segmentY, 'shell-post-seg') this.add.image(rightOuterX, segmentY, 'shell-post-seg')
.setDisplaySize(34, FLOOR_H + 8) .setDisplaySize(65, slot.h)
.setFlipX(true) .setFlipX(true)
.setDepth(shellDepth) .setDepth(shellDepth)
} }
// Joints and beams stay at natural floor boundaries regardless of viewport. for (let floor = 0; floor < SHELL_BEAM_Y.length; floor += 1) {
for (let floor = 0; floor < FLOOR_COUNT - 1; floor += 1) { const beamY = SHELL_BEAM_Y[floor]
const beamY = floorY(floor) + FLOOR_H + FLOOR_GAP / 2
const bracketY = floorY(floor) + FLOOR_H - 1
this.add.image(leftOuterX, beamY, 'shell-post-joint') this.add.image(273, beamY, 'shell-beam-left')
.setDisplaySize(42, 28) .setDisplaySize(410, 66)
.setDepth(shellDepth + 1) .setDepth(shellDepth + 1)
this.add.image(rightOuterX, beamY, 'shell-post-joint') this.add.image(843, beamY, 'shell-beam-right')
.setDisplaySize(42, 28) .setDisplaySize(414, 66)
.setDepth(shellDepth + 1)
this.add.image(62.5, beamY - 12, 'shell-post-joint')
.setDisplaySize(77, 55)
.setDepth(shellDepth + 2)
this.add.image(WIDTH - 62.5, beamY - 12, 'shell-post-joint')
.setDisplaySize(77, 55)
.setFlipX(true) .setFlipX(true)
.setDepth(shellDepth + 1) .setDepth(shellDepth + 2)
this.add.image(leftBankCenter, beamY, 'shell-beam-left')
.setDisplaySize(BANK_W + 24, 27)
.setDepth(shellDepth + 1)
this.add.image(rightBankCenter, beamY, 'shell-beam-right')
.setDisplaySize(BANK_W + 24, 27)
.setDepth(shellDepth + 1)
this.add.image(LEFT_X + BANK_W + 3, bracketY, 'shell-bracket-left')
.setOrigin(1, 0)
.setDisplaySize(20, 22)
.setDepth(shellDepth + 1)
this.add.image(RIGHT_X - 3, bracketY, 'shell-bracket-left')
.setOrigin(0, 0)
.setFlipX(true)
.setDisplaySize(20, 22)
.setDepth(shellDepth + 1)
} }
// Split foundations leave the tower/plinth free to define the centre. this.add.image(0, 1240, 'shell-foundation-left')
this.add.image(leftBankCenter, stackBottom + 7, 'shell-foundation-left') .setOrigin(0, 0)
.setOrigin(0.5, 0) .setDisplaySize(462, 162)
.setDisplaySize(BANK_W + 54, 48)
.setDepth(shellDepth + 1) .setDepth(shellDepth + 1)
this.add.image(rightBankCenter, stackBottom + 7, 'shell-foundation-right') this.add.image(660, 1240, 'shell-foundation-right')
.setOrigin(0.5, 0) .setOrigin(0, 0)
.setDisplaySize(BANK_W + 54, 48) .setDisplaySize(462, 162)
.setDepth(shellDepth + 1) .setDepth(shellDepth + 1)
} }
buildElevator() { buildElevator() {
const shaftHeight = FLOOR_COUNT * FLOOR_H + (FLOOR_COUNT - 1) * FLOOR_GAP const shaftHeight = HOUSE_BOTTOM - HOUSE_TOP
this.add.image(ELEVATOR_X + ELEVATOR_W / 2, HOUSE_TOP, 'elevator-shaft') this.add.image(ELEVATOR_X + ELEVATOR_W / 2, HOUSE_TOP, 'elevator-shaft')
.setOrigin(0.5, 0) .setOrigin(0.5, 0)
@ -690,7 +650,7 @@ class HouseScene extends Phaser.Scene {
padding: { x: 16, y: 8 } padding: { x: 16, y: 8 }
}).setOrigin(0.5).setDepth(1000) }).setOrigin(0.5).setDepth(1000)
this.resetButton = this.add.text(892, STATUS_Y, 'Reset', { this.resetButton = this.add.text(1044, STATUS_Y, 'Reset', {
fontFamily: 'Arial Rounded MT Bold, ui-rounded, sans-serif', fontFamily: 'Arial Rounded MT Bold, ui-rounded, sans-serif',
fontSize: '14px', fontSize: '14px',
color: '#7a526b', color: '#7a526b',
@ -951,7 +911,7 @@ class HouseScene extends Phaser.Scene {
1 1
) )
this.player.setScale(Phaser.Math.Linear(0.29, 0.34, t)) this.player.setScale(Phaser.Math.Linear(0.34, 0.40, t))
this.player.setDepth(300 + room.floor) this.player.setDepth(300 + room.floor)
} }
@ -1237,13 +1197,13 @@ class HouseScene extends Phaser.Scene {
dy /= length dy /= length
this.player.x = Phaser.Math.Clamp( this.player.x = Phaser.Math.Clamp(
this.player.x + dx * 190 * delta / 1000, this.player.x + dx * 220 * delta / 1000,
room.x + 26, room.x + 26,
room.x + room.width - 26 room.x + room.width - 26
) )
this.player.y = Phaser.Math.Clamp( this.player.y = Phaser.Math.Clamp(
this.player.y + dy * 72 * delta / 1000, this.player.y + dy * 84 * delta / 1000,
room.walkBand.minY, room.walkBand.minY,
room.walkBand.maxY room.walkBand.maxY
) )
@ -1268,7 +1228,7 @@ const game = new Phaser.Game({
}, },
scale: { scale: {
mode: Phaser.Scale.FIT, mode: Phaser.Scale.FIT,
autoCenter: Phaser.Scale.CENTER_BOTH, autoCenter: Phaser.Scale.CENTER_HORIZONTALLY,
width: WIDTH, width: WIDTH,
height: HEIGHT height: HEIGHT
}, },
@ -1279,7 +1239,7 @@ const game = new Phaser.Game({
}) })
window.__DOLLHOUSE__ = { window.__DOLLHOUSE__ = {
version: '0.7.1', version: '0.8.0',
game, game,
async resetAll() { async resetAll() {
await clearGameState() await clearGameState()