diff --git a/game-utils/SpriteDefinition.ts b/game-utils/SpriteDefinition.ts deleted file mode 100644 index 791973b..0000000 --- a/game-utils/SpriteDefinition.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { IS_HIDPI } from "./varibles" -export default class SpriteDefinition { - static originals = { - LDPI: { - BACKGROUND_EL: { x: 86, y: 2 }, - CACTUS_LARGE: { x: 332, y: 2 }, - CACTUS_SMALL: { x: 228, y: 2 }, - OBSTACLE_2: { x: 332, y: 2 }, - OBSTACLE: { x: 228, y: 2 }, - CLOUD: { x: 86, y: 2 }, - HORIZON: { x: 2, y: 54 }, - MOON: { x: 484, y: 2 }, - PTERODACTYL: { x: 134, y: 2 }, - RESTART: { x: 2, y: 68 }, - TEXT_SPRITE: { x: 655, y: 2 }, - TREX: { x: 848, y: 2 }, - STAR: { x: 645, y: 2 }, - COLLECTABLE: { x: 2, y: 2 }, - ALT_GAME_END: { x: 121, y: 2 } - }, - HDPI: { - BACKGROUND_EL: { x: 166, y: 2 }, - CACTUS_LARGE: { x: 652, y: 2 }, - CACTUS_SMALL: { x: 446, y: 2 }, - OBSTACLE_2: { x: 652, y: 2 }, - OBSTACLE: { x: 446, y: 2 }, - CLOUD: { x: 166, y: 2 }, - HORIZON: { x: 2, y: 104 }, - MOON: { x: 954, y: 2 }, - PTERODACTYL: { x: 260, y: 2 }, - RESTART: { x: 2, y: 130 }, - TEXT_SPRITE: { x: 1294, y: 2 }, - TREX: { x: 1678, y: 2 }, - STAR: { x: 1276, y: 2 }, - COLLECTABLE: { x: 4, y: 4 }, - ALT_GAME_END: { x: 242, y: 4 } - } - } - - static getPos(): { [key: string]: any } { - if (IS_HIDPI) return SpriteDefinition.originals.HDPI - else return SpriteDefinition.originals.LDPI - } -} diff --git a/game-utils/Cloud.ts b/game/Cloud.ts similarity index 100% rename from game-utils/Cloud.ts rename to game/Cloud.ts diff --git a/game-utils/CollisionBox.ts b/game/CollisionBox.ts similarity index 100% rename from game-utils/CollisionBox.ts rename to game/CollisionBox.ts diff --git a/game-utils/Horizon.ts b/game/Horizon.ts similarity index 100% rename from game-utils/Horizon.ts rename to game/Horizon.ts diff --git a/game-utils/HorizonLine.ts b/game/HorizonLine.ts similarity index 85% rename from game-utils/HorizonLine.ts rename to game/HorizonLine.ts index 1c583d5..1ad5ffc 100644 --- a/game-utils/HorizonLine.ts +++ b/game/HorizonLine.ts @@ -5,26 +5,28 @@ export default class HorizonLine { canvas!: HTMLCanvasElement ctx!: CanvasRenderingContext2D - sourceDimensions = { ...HorizonLine.dimensions } - dimensions = HorizonLine.dimensions + sourceDimensions!: Dimensions + dimensions!: Dimensions spritePos!: Position - sourceXPos = [0, HorizonLine.dimensions.WIDTH] - xPos: number[] = [] - yPos = 0 - bumpThreshold = 0.5 + sourceXPos!: number[] + xPos!: number[] + yPos!: number + bumpThreshold!: number constructor(canvas: HTMLCanvasElement, spritePos: Position) { this.canvas = canvas this.ctx = canvas.getContext("2d") as CanvasRenderingContext2D + this.sourceDimensions = { ...HorizonLine.dimensions } + this.dimensions = HorizonLine.dimensions this.spritePos = spritePos + this.sourceXPos = [0, HorizonLine.dimensions.WIDTH] + this.xPos = [] + this.yPos = 0 + this.bumpThreshold = 0.5 - this.init() - this.draw() - } - - private init() { this.setSourceDimensions() + this.draw() } private setSourceDimensions() { diff --git a/game-utils/Obstacle.ts b/game/Obstacle.ts similarity index 99% rename from game-utils/Obstacle.ts rename to game/Obstacle.ts index 397bbb4..369b546 100644 --- a/game-utils/Obstacle.ts +++ b/game/Obstacle.ts @@ -5,19 +5,19 @@ export default class Obstacle { canvas!: HTMLCanvasElement ctx!: CanvasRenderingContext2D - typeConfig!: ConfigDict spritePos!: Position + typeConfig!: ConfigDict gapCoefficient!: number - dimensions!: Dimensions // 每组障碍物的数量(随机 1~3 个) size!: number + dimensions!: Dimensions + remove!: boolean xPos!: number yPos!: number width!: number - - remove!: boolean gap!: number speedOffset!: number + currentFrame!: number timer!: number @@ -37,15 +37,13 @@ export default class Obstacle { this.spritePos = spritePos this.typeConfig = type this.gapCoefficient = gapCoefficient - this.dimensions = dimensions - // #obstacles in each obstacle group this.size = getRandomNum(1, Obstacle.MAX_OBSTACLE_LENGTH) - + this.dimensions = dimensions + this.remove = false this.xPos = dimensions.WIDTH + (optXOffset || 0) this.yPos = 0 - this.remove = false this.gap = 0 this.speedOffset = 0 diff --git a/game-utils/Runner.ts b/game/Runner.ts similarity index 79% rename from game-utils/Runner.ts rename to game/Runner.ts index c3bd040..d8fd09f 100644 --- a/game-utils/Runner.ts +++ b/game/Runner.ts @@ -3,6 +3,7 @@ import Horizon from "./Horizon" import Trex from "./Trex" import { IS_HIDPI, IS_MOBILE, RESOURCE_POSTFIX } from "./varibles" +const DEFAULT_WIDTH = 600 export default class Runner { spriteDef!: SpritePosDef @@ -24,6 +25,7 @@ export default class Runner { crashed!: boolean paused!: boolean updatePending!: boolean + resizeTimerId_!: NodeJS.Timer | null raqId!: number @@ -45,6 +47,7 @@ export default class Runner { this.crashed = false this.paused = false this.updatePending = false + this.resizeTimerId_ = null this.raqId = 0 @@ -68,6 +71,8 @@ export default class Runner { } init() { + this.adjustDimensions() + // this.containerEl = document.createElement("div") this.containerEl = document.querySelector(`.${Runner.classes.CONTAINER}`) as HTMLDivElement this.containerEl.setAttribute("role", IS_MOBILE ? "button" : "application") @@ -85,7 +90,7 @@ export default class Runner { this.ctx = this.canvas.getContext("2d") as CanvasRenderingContext2D this.ctx.fillStyle = "#f7f7f7" this.ctx.fill() - this.updateCanvasScaling(this.canvas) + Runner.updateCanvasScaling(this.canvas) // Load background class Horizon this.horizon = new Horizon(this.canvas, this.spriteDef, this.dimensions, Runner.config.GAP_COEFFICIENT) @@ -93,6 +98,56 @@ export default class Runner { this.startListening() this.update() + + window.addEventListener(Runner.events.RESIZE, this.debounceResize.bind(this)) + } + + /** + * Debounce the resize event. + */ + debounceResize() { + if (!this.resizeTimerId_) { + this.resizeTimerId_ = setInterval(this.adjustDimensions.bind(this), 250) + } + } + + adjustDimensions() { + this.resizeTimerId_ && clearInterval(this.resizeTimerId_) + this.resizeTimerId_ = null + + if (typeof window === "undefined") return + const boxStyles = window.getComputedStyle(this.outerContainerEl) + const padding = Number(boxStyles.paddingLeft.substr(0, boxStyles.paddingLeft.length - 2)) + + this.dimensions.WIDTH = this.outerContainerEl.offsetWidth - padding * 2 + if (this.isArcadeMode()) { + this.dimensions.WIDTH = Math.min(DEFAULT_WIDTH, this.dimensions.WIDTH) + if (this.activated) { + this.setArcadeModeContainerScale() + } + } + + if (this.canvas) { + this.canvas.width = this.dimensions.WIDTH + this.canvas.height = this.dimensions.HEIGHT + + Runner.updateCanvasScaling(this.canvas) + + this.clearCanvas() + this.horizon.update(0, 0, true) + + // Outer container and distance meter. + if (this.playing || this.crashed || this.paused) { + this.containerEl.style.width = this.dimensions.WIDTH + "px" + this.containerEl.style.height = this.dimensions.HEIGHT + "px" + // this.stop() + } + + // Game over panel. + // if (this.crashed) { + + // } + } } update() { @@ -128,10 +183,9 @@ export default class Runner { } } - clearCanvas() { - this.ctx.clearRect(0, 0, this.dimensions.WIDTH, this.dimensions.HEIGHT) - } - + /** + * RequestAnimationFrame wrapper. + */ scheduleNextUpdate() { if (!this.updatePending) { this.updatePending = true @@ -139,34 +193,22 @@ export default class Runner { } } - startGame() { - this.playingIntro = false - this.containerEl.style.webkitAnimation = "" - this.setArcadeMode() - - this.runningTime = 0 - this.playingIntro = false - - window.addEventListener(Runner.events.BLUR, this.onVisibilityChange.bind(this)) - window.addEventListener(Runner.events.FOCUS, this.onVisibilityChange.bind(this)) - } - - setPlayStatus(playing: boolean) { - this.playing = playing - } - + /** + * Play the game intro. + * Canvas container width expands out to the full width. + */ playIntro() { if (!this.activated && !this.crashed) { this.playingIntro = true let keyframes = `@-webkit-keyframes intro { - from { width: ${Trex.config.WIDTH}px } - to { width: ${this.dimensions.WIDTH}px } + - }` + from { width: ${Trex.config.WIDTH}px } + to { width: ${this.dimensions.WIDTH}px } + + }` document.styleSheets[0].insertRule(keyframes, 0) this.containerEl.style.webkitAnimation = "intro .4s ease-out 1 both" this.containerEl.style.width = this.dimensions.WIDTH + "px" - this.containerEl.addEventListener(Runner.events.ANIMATION_END, this.startGame.bind(this)) + this.containerEl.addEventListener(Runner.events.ANIM_END, this.startGame.bind(this)) this.setPlayStatus(true) this.activated = true @@ -175,6 +217,39 @@ export default class Runner { } } + /** + * Update the game status to started. + */ + startGame() { + if (this.isArcadeMode()) { + this.setArcadeMode() + } + this.runningTime = 0 + this.playingIntro = false + this.containerEl.style.webkitAnimation = "" + + window.addEventListener(Runner.events.BLUR, this.onVisibilityChange.bind(this)) + window.addEventListener(Runner.events.FOCUS, this.onVisibilityChange.bind(this)) + } + + clearCanvas() { + this.ctx.clearRect(0, 0, this.dimensions.WIDTH, this.dimensions.HEIGHT) + } + + setPlayStatus(playing: boolean) { + this.playing = playing + } + + isArcadeMode() { + return true + } + + /** Hides offline messaging for a fullscreen game only experience. */ + setArcadeMode() { + document.body.classList.add(Runner.classes.ARCADE_MODE) + this.setArcadeModeContainerScale() + } + /** Set arcade mode container scaling when start acade mode */ setArcadeModeContainerScale() { let windowHeight = window.innerHeight @@ -194,45 +269,15 @@ export default class Runner { this.containerEl.style.transform = "scale(" + scale + ") translateY(" + translateY + "px)" } - setArcadeMode() { - document.body.classList.add(Runner.classes.ARCADE_MODE) - this.setArcadeModeContainerScale() - } - - /** Update canvas scale based on devicePixelRatio */ - updateCanvasScaling(canvas: HTMLCanvasElement, opt_width?: number, opt_height?: number) { - const context = canvas.getContext("2d") as CanvasRenderingContext2D - // Query the various pixel ratios - const devicePixelRatio = Math.floor(window.devicePixelRatio) || 1 - const backingStoreRatio = 1 - const ratio = devicePixelRatio / backingStoreRatio - - // Upscale the canvas if the two ratios don't match - if (devicePixelRatio !== backingStoreRatio) { - const oldWidth = opt_width || canvas.width - const oldHeight = opt_height || canvas.height - - canvas.width = oldWidth * ratio - canvas.height = oldHeight * ratio - - canvas.style.width = oldWidth + "px" - canvas.style.height = oldHeight + "px" - - // Scale the context to counter the fact that we've manually scaled - // our canvas element. - context.scale(ratio, ratio) - return true - } else if (devicePixelRatio === 1) { - // Reset the canvas width / height. Fixes scaling bug when the page is - // zoomed and the devicePixelRatio changes accordingly. - canvas.style.width = canvas.width + "px" - canvas.style.height = canvas.height + "px" + restart() { + if (!this.raqId) { + this.runningTime = 0 + this.setPlayStatus(true) + this.paused = false + this.crashed = false } - return false } - restart() {} - onVisibilityChange(e: Event) { console.log(e.type) if (document.hidden || e.type === Runner.events.BLUR || document.visibilityState != "visible") { @@ -244,10 +289,14 @@ export default class Runner { /** Listen to events */ startListening() { - console.log("start") - + // Keys. document.addEventListener(Runner.events.KEYDOWN, this) document.addEventListener(Runner.events.KEYUP, this) + + // Touch / pointer. + this.containerEl.addEventListener(Runner.events.TOUCHSTART, this) + document.addEventListener(Runner.events.POINTERDOWN, this) + document.addEventListener(Runner.events.POINTERUP, this) } stopListening() { @@ -259,13 +308,15 @@ export default class Runner { /** addEventListener default method */ handleEvent(e: KeyboardEvent) { - switch (e.type) { - case Runner.events.KEYDOWN: - this.onKeydown(e) - break - default: - break - } + return function (evtType: string) { + switch (evtType) { + case Runner.events.KEYDOWN: + case Runner.events.TOUCHSTART: + case Runner.events.KEYDOWN: + this.onKeydown(e) + break + } + }.bind(this)(e.type) } onKeydown(e: KeyboardEvent) { @@ -281,6 +332,13 @@ export default class Runner { } } + stop() { + this.setPlayStatus(false) + this.paused = true + cancelAnimationFrame(this.raqId) + this.raqId = 0 + } + play() { if (!this.crashed) { this.setPlayStatus(true) @@ -290,13 +348,6 @@ export default class Runner { } } - stop() { - this.setPlayStatus(false) - this.paused = true - cancelAnimationFrame(this.raqId) - this.raqId = 0 - } - private static _instance: Runner static getInstance(outerContainerId: string, optConfig?: any) { @@ -314,14 +365,55 @@ export default class Runner { HEIGHT: 150 } - /** Set of events */ + /** + * Update canvas scale based on devicePixelRatio + */ + static updateCanvasScaling(canvas: HTMLCanvasElement, opt_width?: number, opt_height?: number) { + const context = canvas.getContext("2d") as CanvasRenderingContext2D + // Query the various pixel ratios + const devicePixelRatio = Math.floor(window.devicePixelRatio) || 1 + const backingStoreRatio = 1 + const ratio = devicePixelRatio / backingStoreRatio + + // Upscale the canvas if the two ratios don't match + if (devicePixelRatio !== backingStoreRatio) { + const oldWidth = opt_width || canvas.width + const oldHeight = opt_height || canvas.height + + canvas.width = oldWidth * ratio + canvas.height = oldHeight * ratio + + canvas.style.width = oldWidth + "px" + canvas.style.height = oldHeight + "px" + + // Scale the context to counter the fact that we've manually scaled + // our canvas element. + context.scale(ratio, ratio) + return true + } else if (devicePixelRatio === 1) { + // Reset the canvas width / height. Fixes scaling bug when the page is + // zoomed and the devicePixelRatio changes accordingly. + canvas.style.width = canvas.width + "px" + canvas.style.height = canvas.height + "px" + } + return false + } + static events = { + ANIM_END: "webkitAnimationEnd", + CLICK: "click", KEYDOWN: "keydown", KEYUP: "keyup", - LOAD: "load", + POINTERDOWN: "pointerdown", + POINTERUP: "pointerup", + RESIZE: "resize", + TOUCHEND: "touchend", + TOUCHSTART: "touchstart", + VISIBILITY: "visibilitychange", BLUR: "blur", FOCUS: "focus", - ANIMATION_END: "webkitAnimationEnd" + LOAD: "load", + GAMEPADCONNECTED: "gamepadconnected" } static classes = { @@ -336,7 +428,6 @@ export default class Runner { TOUCH_CONTROLLER: "controller" } - /** Set of keycodes controling interactivation */ static keycodes = { JUMP: { ArrowUp: 1, Space: 1 } as any, // Up, spacebar DUCK: { ArrowDown: 1 } as any, // Down diff --git a/game-utils/Trex.ts b/game/Trex.ts similarity index 100% rename from game-utils/Trex.ts rename to game/Trex.ts diff --git a/game-utils/index.d.ts b/game/index.d.ts similarity index 100% rename from game-utils/index.d.ts rename to game/index.d.ts diff --git a/game-utils/varibles.ts b/game/varibles.ts similarity index 100% rename from game-utils/varibles.ts rename to game/varibles.ts diff --git a/pages/index.tsx b/pages/index.tsx index 5ea0e68..99c4250 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,6 +1,6 @@ import Head from "next/head" import { useEffect } from "react" -import Runner from "@/game-utils/Runner" +import Runner from "@/game/Runner" const assetPrefix = process.env.NODE_ENV === "production" ? "/chrome-dino" : "" diff --git a/styles/globals.css b/styles/globals.css index 0799871..3ab90c0 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -2,7 +2,6 @@ font-size: 1em; line-height: 1.55; margin: 0 auto; - margin-bottom: 90px; max-width: 600px; padding-top: 100px; position: relative; @@ -31,6 +30,12 @@ display: none; } +.arcade-mode .interstitial-wrapper { + height: 100vh; + max-width: 100%; + overflow: hidden; +} + .arcade-mode, .arcade-mode .runner-container, .arcade-mode .runner-canvas {