Skip to content

Commit 576cf8e

Browse files
committed
Add DistanceMeter and NightMode
1 parent 33db05d commit 576cf8e

File tree

4 files changed

+260
-20
lines changed

4 files changed

+260
-20
lines changed

game/Horizon.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Cloud from "./Cloud"
22
import HorizonLine from "./HorizonLine"
3+
import NightMode from "./NightMode"
34
import Obstacle from "./Obstacle"
45
import Runner from "./Runner"
56
import { getRandomNum } from "./varibles"
@@ -20,6 +21,8 @@ export default class Horizon {
2021
obstacles: Obstacle[] = []
2122
obstacleHistory: string[] = []
2223

24+
nightMode!: NightMode
25+
2326
constructor(canvas: HTMLCanvasElement, spritePos: SpritePosDef, dimensions: Dimensions, gapCoeffient: number) {
2427
this.canvas = canvas
2528
this.ctx = canvas.getContext("2d") as CanvasRenderingContext2D
@@ -33,10 +36,12 @@ export default class Horizon {
3336
private init() {
3437
this.addCloud()
3538
this.horizonLine = new HorizonLine(this.canvas, this.spritePos.HORIZON)
39+
this.nightMode = new NightMode(this.canvas, this.spritePos.MOON, this.dimensions.WIDTH)
3640
}
3741

38-
update(deltaTime: number, speed: number, hasObstacles?: boolean) {
42+
update(deltaTime: number, speed: number, hasObstacles?: boolean, showNightMode: boolean = false) {
3943
this.horizonLine.update(deltaTime, speed)
44+
this.nightMode.update(showNightMode)
4045
this.updateCloud(deltaTime, speed)
4146
if (hasObstacles) {
4247
this.updateObstacles(deltaTime, speed)

game/NightMode.ts

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import Runner from "./Runner"
2+
import { IS_HIDPI, getRandomNum } from "./varibles"
3+
4+
export default class NightMode {
5+
canvas!: HTMLCanvasElement
6+
ctx!: CanvasRenderingContext2D
7+
8+
spritePos!: Position
9+
containerWidth!: number
10+
11+
xPos = 0
12+
yPos = 30
13+
currentPhase = 0
14+
opacity = 0
15+
stars: any[] = []
16+
drawStars = false
17+
18+
constructor(canvas: HTMLCanvasElement, spritePos: Position, containerWidth: number) {
19+
this.canvas = canvas
20+
this.ctx = canvas.getContext("2d")!
21+
22+
this.spritePos = spritePos
23+
this.containerWidth = containerWidth
24+
25+
this.xPos = containerWidth - 50
26+
this.yPos = 30
27+
28+
this.placeStars()
29+
}
30+
31+
update(activated: boolean) {
32+
// Moon phase.
33+
if (activated && this.opacity === 0) {
34+
this.currentPhase++
35+
36+
if (this.currentPhase >= NightMode.phases.length) {
37+
this.currentPhase = 0
38+
}
39+
}
40+
41+
// Fade in / out.
42+
if (activated && (this.opacity < 1 || this.opacity === 0)) {
43+
this.opacity += NightMode.config.FADE_SPEED
44+
} else if (this.opacity > 0) {
45+
this.opacity -= NightMode.config.FADE_SPEED
46+
}
47+
48+
// Set moon positioning.
49+
if (this.opacity > 0) {
50+
this.xPos = this.updateXPos(this.xPos, NightMode.config.MOON_SPEED)
51+
52+
// Update stars.
53+
if (this.drawStars) {
54+
for (let i = 0; i < NightMode.config.NUM_STARS; i++) {
55+
this.stars[i].x = this.updateXPos(this.stars[i].x, NightMode.config.STAR_SPEED)
56+
}
57+
}
58+
this.draw()
59+
} else {
60+
this.opacity = 0
61+
this.placeStars()
62+
}
63+
this.drawStars = true
64+
}
65+
66+
updateXPos(currentPos: number, speed: number) {
67+
if (currentPos < -NightMode.config.WIDTH) {
68+
currentPos = this.containerWidth
69+
} else {
70+
currentPos -= speed
71+
}
72+
return currentPos
73+
}
74+
75+
draw() {
76+
let moonSourceWidth = this.currentPhase === 3 ? NightMode.config.WIDTH * 2 : NightMode.config.WIDTH
77+
let moonSourceHeight = NightMode.config.HEIGHT
78+
let moonSourceX = this.spritePos.x + NightMode.phases[this.currentPhase]
79+
const moonOutputWidth = moonSourceWidth
80+
let starSize = NightMode.config.STAR_SIZE
81+
let starSourceX = Runner.spriteDefinition.LDPI.STAR.x
82+
83+
if (IS_HIDPI) {
84+
moonSourceWidth *= 2
85+
moonSourceHeight *= 2
86+
moonSourceX = this.spritePos.x + NightMode.phases[this.currentPhase] * 2
87+
starSize *= 2
88+
starSourceX = Runner.spriteDefinition.HDPI.STAR.x
89+
}
90+
91+
this.ctx.save()
92+
this.ctx.globalAlpha = this.opacity
93+
94+
// Stars.
95+
if (this.drawStars) {
96+
for (let i = 0; i < NightMode.config.NUM_STARS; i++) {
97+
this.ctx.drawImage(
98+
Runner.imageSprite,
99+
starSourceX,
100+
this.stars[i].sourceY,
101+
starSize,
102+
starSize,
103+
Math.round(this.stars[i].x),
104+
this.stars[i].y,
105+
NightMode.config.STAR_SIZE,
106+
NightMode.config.STAR_SIZE
107+
)
108+
}
109+
}
110+
111+
// Moon.
112+
this.ctx.drawImage(
113+
Runner.imageSprite,
114+
moonSourceX,
115+
this.spritePos.y,
116+
moonSourceWidth,
117+
moonSourceHeight,
118+
Math.round(this.xPos),
119+
this.yPos,
120+
moonOutputWidth,
121+
NightMode.config.HEIGHT
122+
)
123+
124+
this.ctx.globalAlpha = 1
125+
this.ctx.restore()
126+
}
127+
128+
placeStars() {
129+
const segmentSize = Math.round(this.containerWidth / NightMode.config.NUM_STARS)
130+
131+
for (let i = 0; i < NightMode.config.NUM_STARS; i++) {
132+
this.stars[i] = {}
133+
this.stars[i].x = getRandomNum(segmentSize * i, segmentSize * (i + 1))
134+
this.stars[i].y = getRandomNum(0, NightMode.config.STAR_MAX_Y)
135+
136+
if (IS_HIDPI) {
137+
this.stars[i].sourceY = Runner.spriteDefinition.HDPI.STAR.y + NightMode.config.STAR_SIZE * 2 * i
138+
} else {
139+
this.stars[i].sourceY = Runner.spriteDefinition.LDPI.STAR.y + NightMode.config.STAR_SIZE * i
140+
}
141+
}
142+
}
143+
144+
reset() {
145+
this.currentPhase = 0
146+
this.opacity = 0
147+
this.update(false)
148+
}
149+
150+
static config = {
151+
WIDTH: 20, // 半月的宽度
152+
HEIGHT: 40, // 月亮的高度
153+
FADE_SPEED: 0.035, // 淡入淡出的速度
154+
MOON_SPEED: 0.25, // 月亮的速度
155+
NUM_STARS: 2, // 星星的数量
156+
STAR_SIZE: 9, // 星星的大小
157+
STAR_SPEED: 0.3, // 星星的速度
158+
STAR_MAX_Y: 70 // 星星在画布上的最大 y 坐标
159+
}
160+
161+
// 月亮所处的时期(不同的时期有不同的位置)
162+
static phases = [140, 120, 100, 60, 40, 20, 0]
163+
}

game/Runner.ts

Lines changed: 81 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ export default class Runner {
2525
playing = false
2626
crashed = false
2727
paused = false
28+
inverted = false // Night mode on
29+
inverTimer = 0 // Night mode start time
30+
invertTrigger = false
2831
updatePending = false
2932
resizeTimerId_: NodeJS.Timer | null = null
3033

@@ -40,7 +43,7 @@ export default class Runner {
4043

4144
constructor(outerContainerId: string, optConfig?: ConfigDict) {
4245
this.outerContainerEl = document.querySelector(outerContainerId)!
43-
this.config = optConfig || Runner.config
46+
this.config = optConfig || Object.assign(Runner.config, Runner.normalConfig)
4447

4548
this.loadImages()
4649
}
@@ -85,7 +88,7 @@ export default class Runner {
8588
this.outerContainerEl.appendChild(this.containerEl)
8689

8790
// Load background class Horizon
88-
this.horizon = new Horizon(this.canvas, this.spriteDef, this.dimensions, Runner.config.GAP_COEFFICIENT)
91+
this.horizon = new Horizon(this.canvas, this.spriteDef, this.dimensions, this.config.GAP_COEFFICIENT)
8992

9093
this.distanceMeter = new DistanceMeter(this.canvas, this.spriteDef.TEXT_SPRITE, this.dimensions.WIDTH)
9194

@@ -159,19 +162,45 @@ export default class Runner {
159162
this.playIntro()
160163
}
161164

165+
// The horizon doesn't move until the intro is over.
162166
if (this.playingIntro) {
163167
this.horizon.update(0, this.currentSpeed, hasObstacles)
164-
} else {
168+
} else if (!this.crashed) {
165169
deltaTime = !this.activated ? 0 : deltaTime
166-
this.horizon.update(deltaTime, this.currentSpeed, hasObstacles)
170+
this.horizon.update(deltaTime, this.currentSpeed, hasObstacles, this.inverted)
167171
}
168172

169-
if (this.currentSpeed < Runner.config.MAX_SPEED) {
170-
this.currentSpeed += Runner.config.ACCELERATION
173+
if (this.currentSpeed < this.config.MAX_SPEED) {
174+
this.currentSpeed += this.config.ACCELERATION
171175
}
172176

173177
this.distanceRan += (this.currentSpeed * deltaTime) / this.msPerFrame
174-
let playAchievementSound = this.distanceMeter?.update(deltaTime, Math.ceil(this.distanceRan))
178+
let playAchievementSound = this.distanceMeter.update(deltaTime, Math.ceil(this.distanceRan))
179+
}
180+
181+
// Night mode.
182+
if (this.inverTimer > this.config.INVERT_FADE_DURATION) {
183+
// 夜晚模式结束
184+
this.inverTimer = 0
185+
this.invertTrigger = false
186+
this.invert(false)
187+
} else if (this.inverTimer) {
188+
// 处于夜晚模式,更新其时间
189+
this.inverTimer += deltaTime
190+
} else {
191+
// 还没进入夜晚模式
192+
// 游戏移动的距离
193+
const actualDistance = this.distanceMeter.getActualDistance(Math.ceil(this.distanceRan))
194+
195+
if (actualDistance > 0) {
196+
// 每移动指定距离就触发一次夜晚模式
197+
this.invertTrigger = !(actualDistance % this.config.INVERT_DISTANCE)
198+
199+
if (this.invertTrigger && this.inverTimer === 0) {
200+
this.inverTimer += deltaTime
201+
this.invert(false)
202+
}
203+
}
175204
}
176205

177206
if (this.playing) {
@@ -285,11 +314,26 @@ export default class Runner {
285314
this.containerEl.style.transform = "scale(" + scale + ") translateY(" + translateY + "px)"
286315
}
287316

317+
/**
318+
* Inverts the current page / canvas colors.
319+
*/
320+
invert(reset: boolean) {
321+
const htmlEl = document.firstElementChild
322+
323+
if (reset) {
324+
htmlEl?.classList.toggle(Runner.classes.INVERTED, false)
325+
this.inverTimer = 0
326+
this.inverted = false
327+
} else {
328+
this.inverted = htmlEl?.classList.toggle(Runner.classes.INVERTED, this.invertTrigger)!
329+
}
330+
}
331+
288332
onVisibilityChange(e: Event) {
289333
console.log(e.type)
290334
if (document.hidden || e.type === Runner.events.BLUR || document.visibilityState != "visible") {
291335
this.stop()
292-
336+
293337
this.gameOver()
294338
} else if (!this.crashed) {
295339
this.play()
@@ -442,29 +486,47 @@ export default class Runner {
442486
RESTART: { Enter: 1 } as any // Enter
443487
}
444488

489+
/**
490+
* Default game configuration.
491+
* Shared config for all versions of the game. Additional parameters are
492+
* defined in Runner.normalConfig and Runner.slowConfig.
493+
*/
445494
static config = {
446-
SPEED: 6,
495+
AUDIOCUE_PROXIMITY_THRESHOLD: 190,
496+
AUDIOCUE_PROXIMITY_THRESHOLD_MOBILE_A11Y: 250,
447497
BG_CLOUD_SPEED: 0.2,
498+
BOTTOM_PAD: 10,
499+
// Scroll Y threshold at which the game can be activated.
500+
CANVAS_IN_VIEW_OFFSET: -10,
501+
CLEAR_TIME: 3000,
448502
CLOUD_FREQUENCY: 0.5,
449-
GAP_COEFFICIENT: 0.6,
503+
FADE_DURATION: 1,
504+
FLASH_DURATION: 1000,
505+
GAMEOVER_CLEAR_TIME: 1200,
506+
INITIAL_JUMP_VELOCITY: 12,
507+
INVERT_FADE_DURATION: 12000,
508+
MAX_BLINK_COUNT: 3,
450509
MAX_CLOUDS: 6,
451-
MAX_SPEED: 12,
452510
MAX_OBSTACLE_LENGTH: 3,
453511
MAX_OBSTACLE_DUPLICATION: 2,
454-
CLEAR_TIME: 3000,
455-
ACCELERATION: 0.001,
456-
BOTTOM_PAD: 10,
457-
GAMEOVER_CLEAR_TIME: 750,
458-
GRAVITY: 0.6,
459-
INITIAL_JUMP_VELOCITY: 12,
460-
MIN_JUMP_HEIGHT: 35,
461-
MOBILE_SPEED_COEFFICIENT: 1.2,
462512
RESOURCE_TEMPLATE_ID: "audio-resources",
513+
SPEED: 6,
463514
SPEED_DROP_COEFFICIENT: 3,
464515
ARCADE_MODE_INITIAL_TOP_POSITION: 35,
465516
ARCADE_MODE_TOP_POSITION_PERCENT: 0.1
466517
}
467518

519+
static normalConfig = {
520+
ACCELERATION: 0.001,
521+
AUDIOCUE_PROXIMITY_THRESHOLD: 190,
522+
AUDIOCUE_PROXIMITY_THRESHOLD_MOBILE_A11Y: 250,
523+
GAP_COEFFICIENT: 0.6,
524+
INVERT_DISTANCE: 100,
525+
MAX_SPEED: 13,
526+
MOBILE_SPEED_COEFFICIENT: 1.2,
527+
SPEED: 6
528+
}
529+
468530
static imageSprite: HTMLImageElement
469531

470532
static spriteDefinition = {

styles/globals.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
html {
2+
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);
3+
will-change: filter, background-color;
4+
}
5+
6+
.inverted {
7+
filter: invert(100%);
8+
background-color: #fff;
9+
}
10+
111
.interstitial-wrapper {
212
font-size: 1em;
313
line-height: 1.55;

0 commit comments

Comments
 (0)