From be6abbfcd757f52cb9227edb290a8ac65909b07c Mon Sep 17 00:00:00 2001 From: keguigong Date: Sun, 19 Nov 2023 17:50:39 +0800 Subject: [PATCH] Add DistanceMeter and NightMode --- game/Horizon.ts | 7 +- game/NightMode.ts | 163 +++++++++++++++++++++++++++++++++++++++++++++ game/Runner.ts | 98 ++++++++++++++++++++++----- styles/globals.css | 10 +++ 4 files changed, 259 insertions(+), 19 deletions(-) create mode 100644 game/NightMode.ts diff --git a/game/Horizon.ts b/game/Horizon.ts index 1128034..27eef7e 100644 --- a/game/Horizon.ts +++ b/game/Horizon.ts @@ -1,5 +1,6 @@ import Cloud from "./Cloud" import HorizonLine from "./HorizonLine" +import NightMode from "./NightMode" import Obstacle from "./Obstacle" import Runner from "./Runner" import { getRandomNum } from "./varibles" @@ -20,6 +21,8 @@ export default class Horizon { obstacles: Obstacle[] = [] obstacleHistory: string[] = [] + nightMode!: NightMode + constructor(canvas: HTMLCanvasElement, spritePos: SpritePosDef, dimensions: Dimensions, gapCoeffient: number) { this.canvas = canvas this.ctx = canvas.getContext("2d") as CanvasRenderingContext2D @@ -33,10 +36,12 @@ export default class Horizon { private init() { this.addCloud() this.horizonLine = new HorizonLine(this.canvas, this.spritePos.HORIZON) + this.nightMode = new NightMode(this.canvas, this.spritePos.MOON, this.dimensions.WIDTH) } - update(deltaTime: number, speed: number, hasObstacles?: boolean) { + update(deltaTime: number, speed: number, hasObstacles?: boolean, showNightMode: boolean = false) { this.horizonLine.update(deltaTime, speed) + this.nightMode.update(showNightMode) this.updateCloud(deltaTime, speed) if (hasObstacles) { this.updateObstacles(deltaTime, speed) diff --git a/game/NightMode.ts b/game/NightMode.ts new file mode 100644 index 0000000..3ee4452 --- /dev/null +++ b/game/NightMode.ts @@ -0,0 +1,163 @@ +import Runner from "./Runner" +import { IS_HIDPI, getRandomNum } from "./varibles" + +export default class NightMode { + canvas!: HTMLCanvasElement + ctx!: CanvasRenderingContext2D + + spritePos!: Position + containerWidth!: number + + xPos = 0 + yPos = 30 + currentPhase = 0 + opacity = 0 + stars: any[] = [] + drawStars = false + + constructor(canvas: HTMLCanvasElement, spritePos: Position, containerWidth: number) { + this.canvas = canvas + this.ctx = canvas.getContext("2d")! + + this.spritePos = spritePos + this.containerWidth = containerWidth + + this.xPos = containerWidth - 50 + this.yPos = 30 + + this.placeStars() + } + + update(activated: boolean) { + // Moon phase. + if (activated && this.opacity === 0) { + this.currentPhase++ + + if (this.currentPhase >= NightMode.phases.length) { + this.currentPhase = 0 + } + } + + // Fade in / out. + if (activated && (this.opacity < 1 || this.opacity === 0)) { + this.opacity += NightMode.config.FADE_SPEED + } else if (this.opacity > 0) { + this.opacity -= NightMode.config.FADE_SPEED + } + + // Set moon positioning. + if (this.opacity > 0) { + this.xPos = this.updateXPos(this.xPos, NightMode.config.MOON_SPEED) + + // Update stars. + if (this.drawStars) { + for (let i = 0; i < NightMode.config.NUM_STARS; i++) { + this.stars[i].x = this.updateXPos(this.stars[i].x, NightMode.config.STAR_SPEED) + } + } + this.draw() + } else { + this.opacity = 0 + this.placeStars() + } + this.drawStars = true + } + + updateXPos(currentPos: number, speed: number) { + if (currentPos < -NightMode.config.WIDTH) { + currentPos = this.containerWidth + } else { + currentPos -= speed + } + return currentPos + } + + draw() { + let moonSourceWidth = this.currentPhase === 3 ? NightMode.config.WIDTH * 2 : NightMode.config.WIDTH + let moonSourceHeight = NightMode.config.HEIGHT + let moonSourceX = this.spritePos.x + NightMode.phases[this.currentPhase] + const moonOutputWidth = moonSourceWidth + let starSize = NightMode.config.STAR_SIZE + let starSourceX = Runner.spriteDefinition.LDPI.STAR.x + + if (IS_HIDPI) { + moonSourceHeight *= 2 + moonSourceHeight *= 2 + moonSourceX = this.spritePos.x + NightMode.phases[this.currentPhase] * 2 + starSize *= 2 + starSourceX = Runner.spriteDefinition.HDPI.STAR.x + } + + this.ctx.save() + this.ctx.globalAlpha = this.opacity + + // Stars. + if (this.drawStars) { + for (let i = 0; i < NightMode.config.NUM_STARS; i++) { + this.ctx.drawImage( + Runner.imageSprite, + starSourceX, + this.stars[i].sourceY, + starSize, + starSize, + Math.round(this.stars[i].x), + this.stars[i].y, + NightMode.config.STAR_SIZE, + NightMode.config.STAR_SIZE + ) + } + } + + // Moon. + this.ctx.drawImage( + Runner.imageSprite, + moonSourceX, + this.spritePos.y, + moonSourceWidth, + moonSourceHeight, + Math.round(this.xPos), + this.yPos, + moonOutputWidth, + NightMode.config.HEIGHT + ) + + this.ctx.globalAlpha = 1 + this.ctx.restore() + } + + placeStars() { + const segmentSize = Math.round(this.containerWidth / NightMode.config.NUM_STARS) + + for (let i = 0; i < NightMode.config.NUM_STARS; i++) { + this.stars[i] = {} + this.stars[i].x = getRandomNum(segmentSize * i, segmentSize * (i + 1)) + this.stars[i].y = getRandomNum(0, NightMode.config.STAR_MAX_Y) + + if (IS_HIDPI) { + this.stars[i].sourceY = Runner.spriteDefinition.HDPI.STAR.y + NightMode.config.STAR_SIZE * 2 * i + } else { + this.stars[i].sourceY = Runner.spriteDefinition.LDPI.STAR.y + NightMode.config.STAR_SIZE * i + } + } + } + + reset() { + this.currentPhase = 0 + this.opacity = 0 + this.update(false) + } + + static config = { + WIDTH: 20, // 半月的宽度 + HEIGHT: 40, // 月亮的高度 + FADE_SPEED: 0.035, // 淡入淡出的速度 + MOON_SPEED: 0.25, // 月亮的速度 + NUM_STARS: 2, // 星星的数量 + STAR_SIZE: 9, // 星星的大小 + STAR_SPEED: 0.3, // 星星的速度 + STAR_MAX_Y: 70 // 星星在画布上的最大 y 坐标 + } + + // 月亮所处的时期(不同的时期有不同的位置) + static phases = [140, 120, 100, 60, 40, 20, 0] +} diff --git a/game/Runner.ts b/game/Runner.ts index 1fa3c95..728ad99 100644 --- a/game/Runner.ts +++ b/game/Runner.ts @@ -25,6 +25,9 @@ export default class Runner { playing = false crashed = false paused = false + inverted = false // Night mode on + inverTimer = 0 // Night mode start time + invertTrigger = false updatePending = false resizeTimerId_: NodeJS.Timer | null = null @@ -40,7 +43,7 @@ export default class Runner { constructor(outerContainerId: string, optConfig?: ConfigDict) { this.outerContainerEl = document.querySelector(outerContainerId)! - this.config = optConfig || Runner.config + this.config = optConfig || Object.assign(Runner.config, Runner.normalConfig) this.loadImages() } @@ -159,19 +162,45 @@ export default class Runner { this.playIntro() } + // The horizon doesn't move until the intro is over. if (this.playingIntro) { this.horizon.update(0, this.currentSpeed, hasObstacles) - } else { + } else if (!this.crashed) { deltaTime = !this.activated ? 0 : deltaTime - this.horizon.update(deltaTime, this.currentSpeed, hasObstacles) + this.horizon.update(deltaTime, this.currentSpeed, hasObstacles, this.inverted) } - if (this.currentSpeed < Runner.config.MAX_SPEED) { - this.currentSpeed += Runner.config.ACCELERATION + if (this.currentSpeed < this.config.MAX_SPEED) { + this.currentSpeed += this.config.ACCELERATION } this.distanceRan += (this.currentSpeed * deltaTime) / this.msPerFrame - let playAchievementSound = this.distanceMeter?.update(deltaTime, Math.ceil(this.distanceRan)) + let playAchievementSound = this.distanceMeter.update(deltaTime, Math.ceil(this.distanceRan)) + } + + // Night mode. + if (this.inverTimer > this.config.INVERT_FADE_DURATION) { + // 夜晚模式结束 + this.inverTimer = 0 + this.invertTrigger = false + this.invert(false) + } else if (this.inverTimer) { + // 处于夜晚模式,更新其时间 + this.inverTimer += deltaTime + } else { + // 还没进入夜晚模式 + // 游戏移动的距离 + const actualDistance = this.distanceMeter.getActualDistance(Math.ceil(this.distanceRan)) + + if (actualDistance > 0) { + // 每移动指定距离就触发一次夜晚模式 + this.invertTrigger = !(actualDistance % this.config.INVERT_DISTANCE) + + if (this.invertTrigger && this.inverTimer === 0) { + this.inverTimer += deltaTime + this.invert(false) + } + } } if (this.playing) { @@ -285,11 +314,26 @@ export default class Runner { this.containerEl.style.transform = "scale(" + scale + ") translateY(" + translateY + "px)" } + /** + * Inverts the current page / canvas colors. + */ + invert(reset: boolean) { + const htmlEl = document.firstElementChild + + if (reset) { + htmlEl?.classList.toggle(Runner.classes.INVERTED, false) + this.inverTimer = 0 + this.inverted = false + } else { + this.inverted = htmlEl?.classList.toggle(Runner.classes.INVERTED, this.invertTrigger)! + } + } + onVisibilityChange(e: Event) { console.log(e.type) if (document.hidden || e.type === Runner.events.BLUR || document.visibilityState != "visible") { this.stop() - + this.gameOver() } else if (!this.crashed) { this.play() @@ -442,29 +486,47 @@ export default class Runner { RESTART: { Enter: 1 } as any // Enter } + /** + * Default game configuration. + * Shared config for all versions of the game. Additional parameters are + * defined in Runner.normalConfig and Runner.slowConfig. + */ static config = { - SPEED: 6, + AUDIOCUE_PROXIMITY_THRESHOLD: 190, + AUDIOCUE_PROXIMITY_THRESHOLD_MOBILE_A11Y: 250, BG_CLOUD_SPEED: 0.2, + BOTTOM_PAD: 10, + // Scroll Y threshold at which the game can be activated. + CANVAS_IN_VIEW_OFFSET: -10, + CLEAR_TIME: 3000, CLOUD_FREQUENCY: 0.5, - GAP_COEFFICIENT: 0.6, + FADE_DURATION: 1, + FLASH_DURATION: 1000, + GAMEOVER_CLEAR_TIME: 1200, + INITIAL_JUMP_VELOCITY: 12, + INVERT_FADE_DURATION: 12000, + MAX_BLINK_COUNT: 3, MAX_CLOUDS: 6, - MAX_SPEED: 12, MAX_OBSTACLE_LENGTH: 3, MAX_OBSTACLE_DUPLICATION: 2, - CLEAR_TIME: 3000, - ACCELERATION: 0.001, - BOTTOM_PAD: 10, - GAMEOVER_CLEAR_TIME: 750, - GRAVITY: 0.6, - INITIAL_JUMP_VELOCITY: 12, - MIN_JUMP_HEIGHT: 35, - MOBILE_SPEED_COEFFICIENT: 1.2, RESOURCE_TEMPLATE_ID: "audio-resources", + SPEED: 6, SPEED_DROP_COEFFICIENT: 3, ARCADE_MODE_INITIAL_TOP_POSITION: 35, ARCADE_MODE_TOP_POSITION_PERCENT: 0.1 } + static normalConfig = { + ACCELERATION: 0.001, + AUDIOCUE_PROXIMITY_THRESHOLD: 190, + AUDIOCUE_PROXIMITY_THRESHOLD_MOBILE_A11Y: 250, + GAP_COEFFICIENT: 0.6, + INVERT_DISTANCE: 100, + MAX_SPEED: 13, + MOBILE_SPEED_COEFFICIENT: 1.2, + SPEED: 6 + } + static imageSprite: HTMLImageElement static spriteDefinition = { diff --git a/styles/globals.css b/styles/globals.css index 3ab90c0..f44a2ec 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -1,3 +1,13 @@ +html { + transition: filter 1.5s cubic-bezier(0.65, 0.05, 0.36, 1), background-color 1.5s cubic-bezier(0.65, 0.05, 0.36, 1); + will-change: filter, background-color; +} + +.inverted { + filter: invert(100%); + background-color: #fff; +} + .interstitial-wrapper { font-size: 1em; line-height: 1.55;