diff --git a/src/main.js b/src/main.js index 006279b..849ba39 100644 --- a/src/main.js +++ b/src/main.js @@ -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()