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) {
super(key)
this.playerDragging = false
this.playerDragTarget = null
this.playerDragTargetX = null
this.lastDragX = null
this.playerAnimMode = 'idle'
this.playerAnimFrame = 0
this.playerAnimClock = 0
this.walkBounds = null
this.groundY = 0
this.exiting = false
}
createPolygonMask(points) {
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) {
createLevelPlayer(x, groundY, bounds, scale = 0.66) {
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)
.setScale(scale)
.setDepth(300)
@ -1318,35 +1295,37 @@ class PlayableLevelScene extends Phaser.Scene {
if (object !== this.player || this.exiting) return
const targetX = Phaser.Math.Clamp(
pointer.x,
pointer.worldX,
this.walkBounds.left,
this.walkBounds.right
)
const targetY = Phaser.Math.Clamp(
pointer.y + 76,
this.walkBounds.top,
this.walkBounds.bottom
)
const previousX = this.lastDragX ?? this.player.x
if (Math.abs(targetX - previousX) > 1) {
this.player.setFlipX(targetX < previousX)
}
this.lastDragX = targetX
this.playerDragTarget = { x: targetX, y: targetY }
this.playerDragTargetX = targetX
})
this.input.on('dragend', (_pointer, object) => {
if (object !== this.player) return
this.playerDragging = false
this.lastDragX = null
this.playerDragTargetX = null
if (!this.exiting) this.animatePlayer('idle', 999)
})
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) {
@ -1373,7 +1352,9 @@ class PlayableLevelScene extends Phaser.Scene {
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)
.setStrokeStyle(6, 0xd094af)
.setDepth(80)
@ -1408,7 +1389,7 @@ class PlayableLevelScene extends Phaser.Scene {
fontStyle: 'bold'
}).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',
fontSize: '22px',
color: '#6d4c61',
@ -1421,11 +1402,31 @@ class PlayableLevelScene extends Phaser.Scene {
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() {
if (this.exiting) return
this.exiting = true
this.playerDragging = false
this.playerDragTarget = null
this.playerDragTargetX = null
this.cameras.main.stopFollow()
this.cameras.main.fadeOut(180, 255, 247, 251)
this.time.delayedCall(200, () => {
@ -1436,19 +1437,15 @@ class PlayableLevelScene extends Phaser.Scene {
update(_time, delta) {
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)
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,
this.playerDragTargetX,
follow
)
this.player.y = this.groundY
this.animatePlayer('walk', delta)
return
@ -1456,32 +1453,18 @@ class PlayableLevelScene extends Phaser.Scene {
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
const dx = Number(right) - Number(left)
let dx = Number(right) - Number(left)
let dy = Number(down) - Number(up)
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.animatePlayer(dx !== 0 ? 'walk' : 'idle', delta)
if (dx === 0) return
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.right
)
this.player.y = Phaser.Math.Clamp(
this.player.y + dy * 150 * delta / 1000,
this.walkBounds.top,
this.walkBounds.bottom
)
if (Math.abs(dx) > 0.05) this.player.setFlipX(dx < 0)
this.player.y = this.groundY
this.player.setFlipX(dx < 0)
}
}
@ -1498,112 +1481,82 @@ class RoomScene extends PlayableLevelScene {
create() {
const room = ROOM_BY_ID.get(this.roomId) ?? ROOM_BY_ID.get('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.fadeIn(220, 255, 247, 251)
const left = 64
const right = WIDTH - 64
const backLeft = 186
const backRight = WIDTH - 186
const top = 156
const backTop = 210
const horizonY = 930
const bottomY = 1470
const graphics = this.add.graphics().setDepth(0)
// Long interior wall.
graphics.fillStyle(0xfffbfd, 1)
graphics.fillRoundedRect(42, 126, WIDTH - 84, 1372, 30)
graphics.fillRect(0, 110, worldWidth, floorBottom - 110)
this.add.image(
WIDTH / 2,
backTop + (horizonY - backTop) / 2,
this.add.tileSprite(
worldWidth / 2,
wallTop + (floorTop - wallTop) / 2,
worldWidth,
floorTop - wallTop,
skin.wallpaper
)
.setDisplaySize(backRight - backLeft, horizonY - backTop)
.setDepth(1)
).setDepth(1)
this.addMaskedLevelImage(
skin.wallpaper,
[
{ x: left, y: top },
{ x: backLeft, y: backTop },
{ x: backLeft, y: horizonY },
{ x: left, y: bottomY }
],
2,
0xffedf5
)
// Long floor strip.
this.add.tileSprite(
worldWidth / 2,
floorTop + (floorBottom - floorTop) / 2,
worldWidth,
floorBottom - floorTop,
skin.floor
).setDepth(2)
this.addMaskedLevelImage(
skin.wallpaper,
[
{ x: backRight, y: backTop },
{ x: right, y: top },
{ x: right, y: bottomY },
{ x: backRight, y: horizonY }
],
2,
0xf2f6ff
)
// Rear skirting and front floor edge run the whole stage.
this.add.image(worldWidth / 2, floorTop, 'architecture-skirting')
.setDisplaySize(worldWidth, 28)
.setDepth(6)
this.addMaskedLevelImage(
skin.floor,
[
{ x: backLeft, y: horizonY },
{ x: backRight, y: horizonY },
{ x: right, y: bottomY },
{ x: left, y: bottomY }
],
3
)
this.add.image(worldWidth / 2, floorBottom, 'architecture-floorEdge')
.setDisplaySize(worldWidth, 30)
.setDepth(6)
graphics.fillStyle(0x6d4c61, 0.12)
graphics.fillRect(backLeft, horizonY - 3, backRight - backLeft, 7)
this.add.image(worldWidth / 2, wallTop, 'architecture-ceilingTrim')
.setDisplaySize(worldWidth, 32)
.setDepth(6)
this.add.image(WIDTH / 2, backTop, 'architecture-ceilingTrim')
.setDisplaySize(backRight - backLeft + 24, 30)
.setDepth(8)
// Break the long room into visible bays without blocking movement.
graphics.lineStyle(5, 0xd7b4c6, 0.42)
for (let x = 760; x < worldWidth; x += 760) {
graphics.lineBetween(x, wallTop + 20, x, floorTop - 10)
}
this.add.image(WIDTH / 2, horizonY, 'architecture-skirting')
.setDisplaySize(backRight - backLeft + 24, 24)
.setDepth(8)
// Left/right end caps.
graphics.lineStyle(8, 0xd5b8c6, 1)
graphics.lineBetween(22, wallTop, 22, floorBottom)
graphics.lineBetween(worldWidth - 22, wallTop, worldWidth - 22, floorBottom)
this.add.image(WIDTH / 2, bottomY, 'architecture-floorEdge')
.setDisplaySize(right - left, 28)
.setDepth(8)
// Elevator/House exit lives at the beginning of every room level.
this.addHouseExit(170, groundY)
graphics.lineStyle(5, 0xd2aabf, 1)
graphics.lineBetween(left, top, backLeft, backTop)
graphics.lineBetween(right, top, backRight, backTop)
graphics.lineBetween(left, bottomY, backLeft, horizonY)
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)
// A couple of simple depth markers so scrolling reads immediately.
graphics.fillStyle(0xffffff, 0.30)
graphics.fillRoundedRect(1040, 760, 360, 190, 24)
graphics.fillRoundedRect(2150, 690, 420, 250, 24)
this.createLevelPlayer(
780,
1260,
{ left: 150, right: 980, top: 1060, bottom: 1430 },
0.66
470,
groundY,
{ left: 110, right: worldWidth - 110 },
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() {
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.fadeIn(220, 255, 247, 251)
this.add.text(72, 62, 'Roof', {
fontFamily: 'Arial Rounded MT Bold, ui-rounded, sans-serif',
fontSize: '42px',
color: '#684b5d',
fontStyle: 'bold'
}).setDepth(100)
const graphics = this.add.graphics().setDepth(0)
this.add.text(72, 112, 'Drag to explore', {
fontFamily: 'Arial Rounded MT Bold, ui-rounded, sans-serif',
fontSize: '20px',
color: '#6d7184'
}).setDepth(100)
// Long open sky.
graphics.fillStyle(0xccecff, 1)
graphics.fillRect(0, 0, worldWidth, HEIGHT)
// Keep the roof level intentionally bare for now: just the approved shell
// roof language expanded into a large playable surface.
this.add.image(WIDTH / 2, 210, 'shell-roof')
.setOrigin(0.5, 0)
.setDisplaySize(1060, 228)
.setDepth(1)
const roofBackY = 610
const roofFrontY = 1450
const roofLeftBack = 240
const roofRightBack = WIDTH - 240
const roofLeftFront = 92
const roofRightFront = WIDTH - 92
// Repeat the approved pink roof language across a side-scrolling stage.
for (let x = 0; x < worldWidth; x += 1060) {
this.add.image(x + 530, 215, 'shell-roof')
.setOrigin(0.5, 0)
.setDisplaySize(1060, 228)
.setDepth(1)
}
graphics.fillStyle(0xf5a8c3, 1)
graphics.fillPoints([
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.fillRect(0, roofTop, worldWidth, roofBottom - roofTop)
graphics.lineStyle(7, 0xb96f8e, 1)
graphics.strokePoints([
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, true)
graphics.lineStyle(8, 0xb96f8e, 1)
graphics.lineBetween(0, roofTop, worldWidth, roofTop)
graphics.lineBetween(0, roofBottom, worldWidth, roofBottom)
graphics.lineStyle(4, 0xffd2df, 0.9)
graphics.lineBetween(WIDTH / 2, roofBackY, WIDTH / 2, roofFrontY)
// Subtle roof seams to give motion/parallax reference while walking.
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(
WIDTH / 2 + 180,
1200,
{ left: 170, right: WIDTH - 170, top: 880, bottom: 1400 },
0.62
500,
groundY,
{ left: 120, right: worldWidth - 120 },
0.64
)
this.configureSideScroller(worldWidth)
this.addLevelHud('Roof')
this.cameras.main.scrollX = 0
}
}
@ -1698,7 +1639,7 @@ const game = new Phaser.Game({
})
window.__DOLLHOUSE__ = {
version: '0.11.0',
version: '0.12.0',
game,
async resetAll() {
await clearGameState()