Turn room and roof levels into side scrollers

This commit is contained in:
Admin 2026-09-22 01:46:26 +00:00
parent 41bde2fccc
commit 4e0d0a6efe

View File

@ -1261,44 +1261,21 @@ class PlayableLevelScene extends Phaser.Scene {
constructor(key) { constructor(key) {
super(key) super(key)
this.playerDragging = false this.playerDragging = false
this.playerDragTarget = null this.playerDragTargetX = null
this.lastDragX = null this.lastDragX = null
this.playerAnimMode = 'idle' this.playerAnimMode = 'idle'
this.playerAnimFrame = 0 this.playerAnimFrame = 0
this.playerAnimClock = 0 this.playerAnimClock = 0
this.walkBounds = null this.walkBounds = null
this.groundY = 0
this.exiting = false this.exiting = false
} }
createPolygonMask(points) { createLevelPlayer(x, groundY, bounds, scale = 0.66) {
const shape = this.make.graphics({ add: false })
shape.fillStyle(0xffffff, 1)
shape.beginPath()
shape.moveTo(points[0].x, points[0].y)
for (let i = 1; i < points.length; i += 1) {
shape.lineTo(points[i].x, points[i].y)
}
shape.closePath()
shape.fillPath()
return shape.createGeometryMask()
}
addMaskedLevelImage(texture, points, depth, tint = null) {
const image = this.add.image(WIDTH / 2, HEIGHT / 2, texture)
.setDisplaySize(WIDTH, HEIGHT)
.setDepth(depth)
.setMask(this.createPolygonMask(points))
if (tint !== null) image.setTint(tint)
return image
}
createLevelPlayer(x, y, bounds, scale = 0.64) {
this.walkBounds = bounds this.walkBounds = bounds
this.groundY = groundY
this.player = this.add.image(x, y, 'player-idle-0') this.player = this.add.image(x, groundY, 'player-idle-0')
.setOrigin(0.5, 1) .setOrigin(0.5, 1)
.setScale(scale) .setScale(scale)
.setDepth(300) .setDepth(300)
@ -1318,35 +1295,37 @@ class PlayableLevelScene extends Phaser.Scene {
if (object !== this.player || this.exiting) return if (object !== this.player || this.exiting) return
const targetX = Phaser.Math.Clamp( const targetX = Phaser.Math.Clamp(
pointer.x, pointer.worldX,
this.walkBounds.left, this.walkBounds.left,
this.walkBounds.right this.walkBounds.right
) )
const targetY = Phaser.Math.Clamp(
pointer.y + 76,
this.walkBounds.top,
this.walkBounds.bottom
)
const previousX = this.lastDragX ?? this.player.x const previousX = this.lastDragX ?? this.player.x
if (Math.abs(targetX - previousX) > 1) { if (Math.abs(targetX - previousX) > 1) {
this.player.setFlipX(targetX < previousX) this.player.setFlipX(targetX < previousX)
} }
this.lastDragX = targetX this.lastDragX = targetX
this.playerDragTarget = { x: targetX, y: targetY } this.playerDragTargetX = targetX
}) })
this.input.on('dragend', (_pointer, object) => { this.input.on('dragend', (_pointer, object) => {
if (object !== this.player) return if (object !== this.player) return
this.playerDragging = false this.playerDragging = false
this.lastDragX = null this.lastDragX = null
this.playerDragTargetX = null
if (!this.exiting) this.animatePlayer('idle', 999) if (!this.exiting) this.animatePlayer('idle', 999)
}) })
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('A,D')
}
configureSideScroller(worldWidth, worldHeight = HEIGHT) {
this.cameras.main.setBounds(0, 0, worldWidth, worldHeight)
this.cameras.main.startFollow(this.player, true, 0.12, 0.12)
this.cameras.main.setDeadzone(300, HEIGHT)
this.cameras.main.setFollowOffset(0, 0)
} }
animatePlayer(mode, delta) { animatePlayer(mode, delta) {
@ -1373,7 +1352,9 @@ class PlayableLevelScene extends Phaser.Scene {
this.player.setTexture('player-' + mode + '-' + this.playerAnimFrame) this.player.setTexture('player-' + mode + '-' + this.playerAnimFrame)
} }
addHouseExit(x, y, width = 126, height = 250) { addHouseExit(x, groundY, width = 136, height = 260) {
const y = groundY - height / 2
const door = this.add.rectangle(x, y, width, height, 0xfff6fa, 1) const door = this.add.rectangle(x, y, width, height, 0xfff6fa, 1)
.setStrokeStyle(6, 0xd094af) .setStrokeStyle(6, 0xd094af)
.setDepth(80) .setDepth(80)
@ -1408,7 +1389,7 @@ class PlayableLevelScene extends Phaser.Scene {
fontStyle: 'bold' fontStyle: 'bold'
}).setOrigin(0.5).setDepth(82) }).setOrigin(0.5).setDepth(82)
const label = this.add.text(x, y + height / 2 + 28, 'House', { const label = this.add.text(x, groundY + 34, 'House', {
fontFamily: 'Arial Rounded MT Bold, ui-rounded, sans-serif', fontFamily: 'Arial Rounded MT Bold, ui-rounded, sans-serif',
fontSize: '22px', fontSize: '22px',
color: '#6d4c61', color: '#6d4c61',
@ -1421,11 +1402,31 @@ class PlayableLevelScene extends Phaser.Scene {
label.on('pointerdown', exit) label.on('pointerdown', exit)
} }
addLevelHud(title, subtitle = 'Drag left or right to explore') {
this.add.text(42, 48, title, {
fontFamily: 'Arial Rounded MT Bold, ui-rounded, sans-serif',
fontSize: '42px',
color: '#684b5d',
fontStyle: 'bold',
backgroundColor: 'rgba(255,247,251,0.88)',
padding: { x: 14, y: 8 }
}).setScrollFactor(0).setDepth(1000)
this.add.text(54, 108, subtitle, {
fontFamily: 'Arial Rounded MT Bold, ui-rounded, sans-serif',
fontSize: '19px',
color: '#7d6574',
backgroundColor: 'rgba(255,247,251,0.82)',
padding: { x: 10, y: 6 }
}).setScrollFactor(0).setDepth(1000)
}
returnToHouse() { returnToHouse() {
if (this.exiting) return if (this.exiting) return
this.exiting = true this.exiting = true
this.playerDragging = false this.playerDragging = false
this.playerDragTarget = null this.playerDragTargetX = null
this.cameras.main.stopFollow()
this.cameras.main.fadeOut(180, 255, 247, 251) this.cameras.main.fadeOut(180, 255, 247, 251)
this.time.delayedCall(200, () => { this.time.delayedCall(200, () => {
@ -1436,19 +1437,15 @@ class PlayableLevelScene extends Phaser.Scene {
update(_time, delta) { update(_time, delta) {
if (!this.player || !this.walkBounds || this.exiting) return if (!this.player || !this.walkBounds || this.exiting) return
if (this.playerDragging && this.playerDragTarget) { if (this.playerDragging && this.playerDragTargetX !== null) {
const follow = 1 - Math.pow(1 - PLAYER_DRAG_FOLLOW, delta / 16.667) const follow = 1 - Math.pow(1 - PLAYER_DRAG_FOLLOW, delta / 16.667)
this.player.x = Phaser.Math.Linear( this.player.x = Phaser.Math.Linear(
this.player.x, this.player.x,
this.playerDragTarget.x, this.playerDragTargetX,
follow
)
this.player.y = Phaser.Math.Linear(
this.player.y,
this.playerDragTarget.y,
follow follow
) )
this.player.y = this.groundY
this.animatePlayer('walk', delta) this.animatePlayer('walk', delta)
return return
@ -1456,32 +1453,18 @@ class PlayableLevelScene extends Phaser.Scene {
const left = this.cursors.left.isDown || this.keys.A.isDown const left = this.cursors.left.isDown || this.keys.A.isDown
const right = this.cursors.right.isDown || this.keys.D.isDown const right = this.cursors.right.isDown || this.keys.D.isDown
const up = this.cursors.up.isDown || this.keys.W.isDown const dx = Number(right) - Number(left)
const down = this.cursors.down.isDown || this.keys.S.isDown
let dx = Number(right) - Number(left) this.animatePlayer(dx !== 0 ? 'walk' : 'idle', delta)
let dy = Number(down) - Number(up) if (dx === 0) return
const moving = dx !== 0 || dy !== 0
this.animatePlayer(moving ? 'walk' : 'idle', delta)
if (!moving) return
const length = Math.hypot(dx, dy) || 1
dx /= length
dy /= length
this.player.x = Phaser.Math.Clamp( this.player.x = Phaser.Math.Clamp(
this.player.x + dx * 320 * delta / 1000, this.player.x + dx * 360 * delta / 1000,
this.walkBounds.left, this.walkBounds.left,
this.walkBounds.right this.walkBounds.right
) )
this.player.y = Phaser.Math.Clamp( this.player.y = this.groundY
this.player.y + dy * 150 * delta / 1000, this.player.setFlipX(dx < 0)
this.walkBounds.top,
this.walkBounds.bottom
)
if (Math.abs(dx) > 0.05) this.player.setFlipX(dx < 0)
} }
} }
@ -1498,112 +1481,82 @@ class RoomScene extends PlayableLevelScene {
create() { create() {
const room = ROOM_BY_ID.get(this.roomId) ?? ROOM_BY_ID.get('living') const room = ROOM_BY_ID.get(this.roomId) ?? ROOM_BY_ID.get('living')
const skin = ROOM_SKINS[room.id] ?? ROOM_SKINS.living const skin = ROOM_SKINS[room.id] ?? ROOM_SKINS.living
const graphics = this.add.graphics().setDepth(0) const worldWidth = 3100
const wallTop = 170
const floorTop = 1015
const floorBottom = 1515
const groundY = 1410
this.cameras.main.setBackgroundColor('#ccecff') this.cameras.main.setBackgroundColor('#ccecff')
this.cameras.main.fadeIn(220, 255, 247, 251) this.cameras.main.fadeIn(220, 255, 247, 251)
const left = 64 const graphics = this.add.graphics().setDepth(0)
const right = WIDTH - 64
const backLeft = 186
const backRight = WIDTH - 186
const top = 156
const backTop = 210
const horizonY = 930
const bottomY = 1470
// Long interior wall.
graphics.fillStyle(0xfffbfd, 1) graphics.fillStyle(0xfffbfd, 1)
graphics.fillRoundedRect(42, 126, WIDTH - 84, 1372, 30) graphics.fillRect(0, 110, worldWidth, floorBottom - 110)
this.add.image( this.add.tileSprite(
WIDTH / 2, worldWidth / 2,
backTop + (horizonY - backTop) / 2, wallTop + (floorTop - wallTop) / 2,
worldWidth,
floorTop - wallTop,
skin.wallpaper skin.wallpaper
) ).setDepth(1)
.setDisplaySize(backRight - backLeft, horizonY - backTop)
.setDepth(1)
this.addMaskedLevelImage( // Long floor strip.
skin.wallpaper, this.add.tileSprite(
[ worldWidth / 2,
{ x: left, y: top }, floorTop + (floorBottom - floorTop) / 2,
{ x: backLeft, y: backTop }, worldWidth,
{ x: backLeft, y: horizonY }, floorBottom - floorTop,
{ x: left, y: bottomY } skin.floor
], ).setDepth(2)
2,
0xffedf5
)
this.addMaskedLevelImage( // Rear skirting and front floor edge run the whole stage.
skin.wallpaper, this.add.image(worldWidth / 2, floorTop, 'architecture-skirting')
[ .setDisplaySize(worldWidth, 28)
{ x: backRight, y: backTop }, .setDepth(6)
{ x: right, y: top },
{ x: right, y: bottomY },
{ x: backRight, y: horizonY }
],
2,
0xf2f6ff
)
this.addMaskedLevelImage( this.add.image(worldWidth / 2, floorBottom, 'architecture-floorEdge')
skin.floor, .setDisplaySize(worldWidth, 30)
[ .setDepth(6)
{ x: backLeft, y: horizonY },
{ x: backRight, y: horizonY },
{ x: right, y: bottomY },
{ x: left, y: bottomY }
],
3
)
graphics.fillStyle(0x6d4c61, 0.12) this.add.image(worldWidth / 2, wallTop, 'architecture-ceilingTrim')
graphics.fillRect(backLeft, horizonY - 3, backRight - backLeft, 7) .setDisplaySize(worldWidth, 32)
.setDepth(6)
this.add.image(WIDTH / 2, backTop, 'architecture-ceilingTrim') // Break the long room into visible bays without blocking movement.
.setDisplaySize(backRight - backLeft + 24, 30) graphics.lineStyle(5, 0xd7b4c6, 0.42)
.setDepth(8) for (let x = 760; x < worldWidth; x += 760) {
graphics.lineBetween(x, wallTop + 20, x, floorTop - 10)
}
this.add.image(WIDTH / 2, horizonY, 'architecture-skirting') // Left/right end caps.
.setDisplaySize(backRight - backLeft + 24, 24) graphics.lineStyle(8, 0xd5b8c6, 1)
.setDepth(8) graphics.lineBetween(22, wallTop, 22, floorBottom)
graphics.lineBetween(worldWidth - 22, wallTop, worldWidth - 22, floorBottom)
this.add.image(WIDTH / 2, bottomY, 'architecture-floorEdge') // Elevator/House exit lives at the beginning of every room level.
.setDisplaySize(right - left, 28) this.addHouseExit(170, groundY)
.setDepth(8)
graphics.lineStyle(5, 0xd2aabf, 1) // A couple of simple depth markers so scrolling reads immediately.
graphics.lineBetween(left, top, backLeft, backTop) graphics.fillStyle(0xffffff, 0.30)
graphics.lineBetween(right, top, backRight, backTop) graphics.fillRoundedRect(1040, 760, 360, 190, 24)
graphics.lineBetween(left, bottomY, backLeft, horizonY) graphics.fillRoundedRect(2150, 690, 420, 250, 24)
graphics.lineBetween(right, bottomY, backRight, horizonY)
graphics.lineStyle(6, 0xd5b8c6, 1)
graphics.strokeRoundedRect(42, 126, WIDTH - 84, 1372, 30)
this.add.text(72, 62, room.name, {
fontFamily: 'Arial Rounded MT Bold, ui-rounded, sans-serif',
fontSize: '42px',
color: '#684b5d',
fontStyle: 'bold'
}).setDepth(100)
this.add.text(72, 112, 'Drag to explore', {
fontFamily: 'Arial Rounded MT Bold, ui-rounded, sans-serif',
fontSize: '20px',
color: '#8a6c7d'
}).setDepth(100)
// Elevator doorway is the return path to the dollhouse hub.
this.addHouseExit(916, 785, 126, 250)
this.createLevelPlayer( this.createLevelPlayer(
780, 470,
1260, groundY,
{ left: 150, right: 980, top: 1060, bottom: 1430 }, { left: 110, right: worldWidth - 110 },
0.66 0.68
) )
this.configureSideScroller(worldWidth)
this.addLevelHud(room.name)
// Start framed near the elevator rather than centred on the whole stage.
this.cameras.main.scrollX = 0
} }
} }
@ -1613,65 +1566,53 @@ class RoofScene extends PlayableLevelScene {
} }
create() { create() {
const graphics = this.add.graphics().setDepth(0) const worldWidth = 2900
const groundY = 1370
const roofTop = 830
const roofBottom = 1515
this.cameras.main.setBackgroundColor('#ccecff') this.cameras.main.setBackgroundColor('#ccecff')
this.cameras.main.fadeIn(220, 255, 247, 251) this.cameras.main.fadeIn(220, 255, 247, 251)
this.add.text(72, 62, 'Roof', { const graphics = this.add.graphics().setDepth(0)
fontFamily: 'Arial Rounded MT Bold, ui-rounded, sans-serif',
fontSize: '42px',
color: '#684b5d',
fontStyle: 'bold'
}).setDepth(100)
this.add.text(72, 112, 'Drag to explore', { // Long open sky.
fontFamily: 'Arial Rounded MT Bold, ui-rounded, sans-serif', graphics.fillStyle(0xccecff, 1)
fontSize: '20px', graphics.fillRect(0, 0, worldWidth, HEIGHT)
color: '#6d7184'
}).setDepth(100)
// Keep the roof level intentionally bare for now: just the approved shell // Repeat the approved pink roof language across a side-scrolling stage.
// roof language expanded into a large playable surface. for (let x = 0; x < worldWidth; x += 1060) {
this.add.image(WIDTH / 2, 210, 'shell-roof') this.add.image(x + 530, 215, 'shell-roof')
.setOrigin(0.5, 0) .setOrigin(0.5, 0)
.setDisplaySize(1060, 228) .setDisplaySize(1060, 228)
.setDepth(1) .setDepth(1)
}
const roofBackY = 610
const roofFrontY = 1450
const roofLeftBack = 240
const roofRightBack = WIDTH - 240
const roofLeftFront = 92
const roofRightFront = WIDTH - 92
graphics.fillStyle(0xf5a8c3, 1) graphics.fillStyle(0xf5a8c3, 1)
graphics.fillPoints([ graphics.fillRect(0, roofTop, worldWidth, roofBottom - roofTop)
new Phaser.Geom.Point(roofLeftBack, roofBackY),
new Phaser.Geom.Point(roofRightBack, roofBackY),
new Phaser.Geom.Point(roofRightFront, roofFrontY),
new Phaser.Geom.Point(roofLeftFront, roofFrontY)
], true)
graphics.lineStyle(7, 0xb96f8e, 1) graphics.lineStyle(8, 0xb96f8e, 1)
graphics.strokePoints([ graphics.lineBetween(0, roofTop, worldWidth, roofTop)
new Phaser.Geom.Point(roofLeftBack, roofBackY), graphics.lineBetween(0, roofBottom, worldWidth, roofBottom)
new Phaser.Geom.Point(roofRightBack, roofBackY),
new Phaser.Geom.Point(roofRightFront, roofFrontY),
new Phaser.Geom.Point(roofLeftFront, roofFrontY)
], true, true)
graphics.lineStyle(4, 0xffd2df, 0.9) // Subtle roof seams to give motion/parallax reference while walking.
graphics.lineBetween(WIDTH / 2, roofBackY, WIDTH / 2, roofFrontY) graphics.lineStyle(4, 0xffd2df, 0.72)
for (let x = 320; x < worldWidth; x += 420) {
graphics.lineBetween(x, roofTop, x + 150, roofBottom)
}
this.addHouseExit(WIDTH / 2, 690, 124, 236) this.addHouseExit(180, groundY)
this.createLevelPlayer( this.createLevelPlayer(
WIDTH / 2 + 180, 500,
1200, groundY,
{ left: 170, right: WIDTH - 170, top: 880, bottom: 1400 }, { left: 120, right: worldWidth - 120 },
0.62 0.64
) )
this.configureSideScroller(worldWidth)
this.addLevelHud('Roof')
this.cameras.main.scrollX = 0
} }
} }
@ -1698,7 +1639,7 @@ const game = new Phaser.Game({
}) })
window.__DOLLHOUSE__ = { window.__DOLLHOUSE__ = {
version: '0.11.0', version: '0.12.0',
game, game,
async resetAll() { async resetAll() {
await clearGameState() await clearGameState()