Skip to content

Commit

Permalink
Add DistanceMeter and NightMode
Browse files Browse the repository at this point in the history
  • Loading branch information
keguigong committed Nov 19, 2023
1 parent 33db05d commit be6abbf
Show file tree
Hide file tree
Showing 4 changed files with 259 additions and 19 deletions.
7 changes: 6 additions & 1 deletion game/Horizon.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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
Expand All @@ -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)
Expand Down
163 changes: 163 additions & 0 deletions game/NightMode.ts
Original file line number Diff line number Diff line change
@@ -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]
}
98 changes: 80 additions & 18 deletions game/Runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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()
}
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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 = {
Expand Down
10 changes: 10 additions & 0 deletions styles/globals.css
Original file line number Diff line number Diff line change
@@ -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;
Expand Down

0 comments on commit be6abbf

Please sign in to comment.