diff --git a/angular.json b/angular.json index 7cb0126..1e77407 100644 --- a/angular.json +++ b/angular.json @@ -1,136 +1,125 @@ { - "$schema": "./node_modules/@angular/cli/lib/config/schema.json", - "version": 1, - "newProjectRoot": "projects", - "projects": { - "RetroSki": { - "projectType": "application", - "schematics": { - "@schematics/angular:component": { - "style": "scss", - "changeDetection": "OnPush", - "skipTests": true - } - }, - "root": "", - "sourceRoot": "src", - "prefix": "app", - "architect": { - "build": { - "builder": "@angular-devkit/build-angular:browser", - "options": { - "outputPath": "dist/", - "index": "src/index.html", - "main": "src/main.ts", - "polyfills": [ - "zone.js" - ], - "tsConfig": "tsconfig.app.json", - "inlineStyleLanguage": "scss", - "assets": [ - "src/assets", - { - "glob": "**/*", - "input": "src/app/game/images", - "output": "assets/images" - }, - { - "glob": "**/*", - "input": "src/app/game/sounds", - "output": "assets/sounds" - }, - { - "glob": "**/*", - "input": "src/app/game/ghosts", - "output": "assets/ghosts" - }, - { - "glob": "**/*", - "input": "src/app/game/tracks", - "output": "assets/tracks" - }, - { - "glob": "**/*", - "input": "public" - } - ], - "styles": [ - "src/styles.scss" - ], - "scripts": [], - "serviceWorker": true, - "ngswConfigPath": "ngsw-config.json" - }, - "configurations": { - "production": { - "budgets": [ - { - "type": "initial", - "maximumWarning": "500kB", - "maximumError": "2MB" - }, - { - "type": "anyComponentStyle", - "maximumWarning": "2kB", - "maximumError": "4kB" + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "RetroSki": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "style": "scss", + "changeDetection": "OnPush", + "skipTests": true } - ], - "outputHashing": "all" }, - "development": { - "optimization": false, - "extractLicenses": false, - "sourceMap": true, - "fileReplacements": [ - { - "replace": "src/environments/environment.ts", - "with": "src/environments/environment.development.ts" + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:browser", + "options": { + "outputPath": "dist/", + "index": "src/index.html", + "main": "src/main.ts", + "polyfills": ["zone.js"], + "tsConfig": "tsconfig.app.json", + "inlineStyleLanguage": "scss", + "assets": [ + "src/assets", + { + "glob": "**/*", + "input": "src/app/game/images", + "output": "assets/images" + }, + { + "glob": "**/*", + "input": "src/app/game/sounds", + "output": "assets/sounds" + }, + { + "glob": "**/*", + "input": "src/app/game/ghosts", + "output": "assets/ghosts" + }, + { + "glob": "**/*", + "input": "src/app/game/tracks", + "output": "assets/tracks" + }, + { + "glob": "**/*", + "input": "public" + } + ], + "styles": ["src/styles.scss"], + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kB", + "maximumError": "2MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "2kB", + "maximumError": "4kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true, + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.development.ts" + } + ] + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "options": { + "ssl": true + }, + "configurations": { + "production": { + "buildTarget": "RetroSki:build:production" + }, + "development": { + "buildTarget": "RetroSki:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n" + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "polyfills": ["zone.js", "zone.js/testing"], + "tsConfig": "tsconfig.spec.json", + "inlineStyleLanguage": "scss", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": ["src/styles.scss"], + "scripts": [] + } } - ] - } - }, - "defaultConfiguration": "production" - }, - "serve": { - "builder": "@angular-devkit/build-angular:dev-server", - "options": { - "ssl": true - }, - "configurations": { - "production": { - "buildTarget": "RetroSki:build:production" - }, - "development": { - "buildTarget": "RetroSki:build:development" } - }, - "defaultConfiguration": "development" - }, - "extract-i18n": { - "builder": "@angular-devkit/build-angular:extract-i18n" - }, - "test": { - "builder": "@angular-devkit/build-angular:karma", - "options": { - "polyfills": [ - "zone.js", - "zone.js/testing" - ], - "tsConfig": "tsconfig.spec.json", - "inlineStyleLanguage": "scss", - "assets": [ - { - "glob": "**/*", - "input": "public" - } - ], - "styles": [ - "src/styles.scss" - ], - "scripts": [] - } } - } } - } } diff --git a/ngsw-config.json b/ngsw-config.json deleted file mode 100644 index 69edd28..0000000 --- a/ngsw-config.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "$schema": "./node_modules/@angular/service-worker/config/schema.json", - "index": "/index.html", - "assetGroups": [ - { - "name": "app", - "installMode": "prefetch", - "resources": { - "files": [ - "/favicon.ico", - "/index.csr.html", - "/index.html", - "/manifest.webmanifest", - "/*.css", - "/*.js" - ] - } - }, - { - "name": "assets", - "installMode": "lazy", - "updateMode": "prefetch", - "resources": { - "files": [ - "/**/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)" - ] - } - } - ] -} diff --git a/src/app/app.config.ts b/src/app/app.config.ts index a8f0c5a..51b684b 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -1,16 +1,8 @@ -import { type ApplicationConfig, provideZoneChangeDetection, isDevMode } from '@angular/core'; -import { provideRouter, withComponentInputBinding, withHashLocation } from '@angular/router'; +import { type ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; +import { provideRouter, withHashLocation } from '@angular/router'; import { routes } from './app.routes'; -import { provideServiceWorker } from '@angular/service-worker'; export const appConfig: ApplicationConfig = { - providers: [ - provideZoneChangeDetection({ eventCoalescing: true }), - provideRouter(routes, withHashLocation()), - provideServiceWorker('ngsw-worker.js', { - enabled: !isDevMode(), - registrationStrategy: 'registerWhenStable:30000' - }) - ] + providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes, withHashLocation())] }; diff --git a/src/app/game/actors/skier.ts b/src/app/game/actors/skier.ts index ad54cee..3bf82d9 100644 --- a/src/app/game/actors/skier.ts +++ b/src/app/game/actors/skier.ts @@ -8,6 +8,18 @@ import type { Game } from '../game'; import { SkierActions } from '../models/skier-actions.enum'; import { SkierGraphics } from '../utils/skier-graphics'; +class SkierIntentions { + public leftCarvingIntention: number; + public rightCarvingIntention: number; + public hasBrakingIntention: boolean; + + constructor(leftCarvingIntention?: number, rightCarvingIntention?: number, hasBrakingIntention?: boolean) { + this.leftCarvingIntention = leftCarvingIntention ?? 0; + this.rightCarvingIntention = rightCarvingIntention ?? 0; + this.hasBrakingIntention = hasBrakingIntention ?? false; + } +} + const LEFT_ANGLE_OFFSET = (3 / 4) * Math.PI; const RIGHT_ANGLE_OFFSET = (1 / 4) * Math.PI; const RADIAN_PI = 2 * Math.PI; @@ -20,6 +32,8 @@ export class Skier extends Actor { public racing = false; public finish = false; + private skierIntentions = new SkierIntentions(); + private leftParticlesEmitter!: GpuParticleEmitter; private rightParticlesEmitter!: GpuParticleEmitter; @@ -42,12 +56,13 @@ export class Skier extends Actor { } override update(engine: Engine): void { - const skierAction = this.getSkierCurrentAction(engine); + this.updateSkierIntentions(engine); + const skierAction = this.getSkierCurrentAction(); this.updateGraphics(skierAction); if (this.racing || this.finish) { - this.updateRotation(engine); - this.updateSpeed(skierAction, engine); - this.updateVelocity(engine); + this.updateRotation(this.skierIntentions); + this.updateSpeed(skierAction, this.skierIntentions); + this.updateVelocity(this.skierIntentions); } else { if ( engine.input.keyboard.wasPressed(Config.KEYBOARD_START_KEY) || @@ -57,8 +72,8 @@ export class Skier extends Actor { (this.scene as Race).startRace(); } } - this.emitParticles(engine, skierAction); - this.emitSounds(engine as Game, this.finish); + this.emitParticles(engine, skierAction, this.skierIntentions); + this.emitSounds(engine as Game, this.finish, this.skierIntentions); } public finishRace(): void { @@ -71,28 +86,34 @@ export class Skier extends Actor { (this.scene!.engine as Game).soundPlayer.playSound(Resources.TurningSound, 0, true, false); } - public getSkierCurrentAction(engine: Engine): SkierActions { + public getSkierCurrentAction(): SkierActions { if (this.racing) { - if (this.slidingIntention(engine)) { - return this.leftSlidingIntention(engine) ? SkierActions.SLIDE_LEFT : SkierActions.SLIDE_RIGHT; + if (Skier.slidingIntention(this.skierIntentions)) { + return this.skierIntentions.leftCarvingIntention ? SkierActions.SLIDE_LEFT : SkierActions.SLIDE_RIGHT; } - if (this.hasBreakingIntention(engine)) { + if (this.skierIntentions.hasBrakingIntention) { return SkierActions.BRAKE; } - if (this.carvingIntention(engine)) { - return this.leftCarvingIntention(engine) ? SkierActions.CARVE_LEFT : SkierActions.CARVE_RIGHT; + if (Skier.carvingIntention(this.skierIntentions)) { + return this.skierIntentions.leftCarvingIntention ? SkierActions.CARVE_LEFT : SkierActions.CARVE_RIGHT; } return SkierActions.NOTHING; } return SkierActions.BRAKE; } - private updateRotation(engine: Engine): void { + private updateSkierIntentions(engine: Engine): void { + this.skierIntentions.hasBrakingIntention = this.hasBreakingIntention(engine); + this.skierIntentions.leftCarvingIntention = this.leftCarvingIntention(engine); + this.skierIntentions.rightCarvingIntention = this.rightCarvingIntention(engine); + } + + private updateRotation(skierIntentions: SkierIntentions): void { let rotationRate = 0; let futurRotation = 0; - if (this.hasTurningIntention(engine)) { - if (this.slidingIntention(engine)) { + if (Skier.hasTurningIntention(skierIntentions)) { + if (Skier.slidingIntention(skierIntentions)) { const rotationSpeedMultiplier = this.speed < this.skierConfig.slidingOptimalSpeed ? Math.max(this.speed, 1) / this.skierConfig.slidingOptimalSpeed @@ -100,8 +121,8 @@ export class Skier extends Actor { rotationRate = (this.skierConfig.slidingRotationRate / (180 / Math.PI)) * rotationSpeedMultiplier * - this.slidingIntention(engine); - } else if (this.carvingIntention(engine)) { + Skier.slidingIntention(skierIntentions); + } else if (Skier.carvingIntention(skierIntentions)) { const rotationSpeedMultiplier = this.speed < this.skierConfig.carvingOptimalSpeed ? Math.max(this.speed, 1) / this.skierConfig.carvingOptimalSpeed @@ -109,9 +130,9 @@ export class Skier extends Actor { rotationRate = (this.skierConfig.carvingRotationRate / (180 / Math.PI)) * rotationSpeedMultiplier * - this.carvingIntention(engine); + Skier.carvingIntention(skierIntentions); } - futurRotation = this.hasLeftTurningIntention(engine) + futurRotation = Skier.hasLeftTurningIntention(skierIntentions) ? this.rotation - rotationRate : this.rotation + rotationRate; } else { @@ -134,7 +155,7 @@ export class Skier extends Actor { } } - private updateSpeed(skierAction: SkierActions, engine: Engine): void { + private updateSpeed(skierAction: SkierActions, skierIntentions: SkierIntentions): void { let angleOfSkier = this.rotation * (180 / Math.PI); if (angleOfSkier >= 270) { angleOfSkier = 360 - angleOfSkier; @@ -144,9 +165,9 @@ export class Skier extends Actor { acceleration -= (acceleration * angleOfSkier) / 90; acceleration -= this.skierConfig.windFrictionRate * this.speed; if (skierAction === SkierActions.SLIDE_LEFT || skierAction === SkierActions.SLIDE_RIGHT) { - acceleration -= Config.SLIDING_BRAKING_RATE * this.slidingIntention(engine); + acceleration -= Config.SLIDING_BRAKING_RATE * Skier.slidingIntention(skierIntentions); } else if (skierAction === SkierActions.CARVE_LEFT || skierAction === SkierActions.CARVE_RIGHT) { - acceleration -= Config.CARVING_BRAKING_RATE * this.carvingIntention(engine); + acceleration -= Config.CARVING_BRAKING_RATE * Skier.carvingIntention(skierIntentions); } else if (skierAction === SkierActions.BRAKE) { acceleration -= Config.BRAKING_RATE; } @@ -160,10 +181,10 @@ export class Skier extends Actor { } } - private updateVelocity(engine: Engine): void { + private updateVelocity(skierIntentions: SkierIntentions): void { let xVelocity = 0; let yVelocity = 0; - const adherenceRate = this.getAdherenceRate(engine); + const adherenceRate = this.getAdherenceRate(skierIntentions); const normalizedRotation = this.rotation * (180 / Math.PI); if (normalizedRotation === 0) { xVelocity = 0; @@ -180,10 +201,10 @@ export class Skier extends Actor { this.vel = vec(xVelocity * Config.VELOCITY_MULTIPLIER_RATE, -yVelocity * Config.VELOCITY_MULTIPLIER_RATE); } - private getAdherenceRate(engine: Engine): number { + private getAdherenceRate(skierIntentions: SkierIntentions): number { let adherenceRate = 1; - if (this.hasTurningIntention(engine)) { - adherenceRate = this.slidingIntention(engine) + if (Skier.hasTurningIntention(skierIntentions)) { + adherenceRate = Skier.slidingIntention(skierIntentions) ? Config.SLIDING_ADHERENCE_RATE : Config.CARVING_ADHERENCE_RATE; } @@ -196,23 +217,23 @@ export class Skier extends Actor { this.graphics.flipHorizontal = !!graphic.flipHorizontal; } - private emitSounds(engine: Game, forceBreaking: boolean): void { - if ((this.hasBreakingIntention(engine) || forceBreaking) && this.speed) { + private emitSounds(engine: Game, forceBreaking: boolean, skierIntentions: SkierIntentions): void { + if ((skierIntentions.hasBrakingIntention || forceBreaking) && this.speed) { Resources.TurningSound.volume = Math.min( Config.BRAKING_SOUND_VOLUME, (this.speed / Config.MAX_SPEED) * Config.BRAKING_SOUND_VOLUME ); - } else if (this.carvingIntention(engine) && this.speed) { + } else if (Skier.carvingIntention(skierIntentions) && this.speed) { Resources.TurningSound.volume = Math.min( Config.CARVING_SOUND_VOLUME, - (this.speed / Config.MAX_SPEED) * Config.CARVING_SOUND_VOLUME * this.carvingIntention(engine) + (this.speed / Config.MAX_SPEED) * Config.CARVING_SOUND_VOLUME * Skier.carvingIntention(skierIntentions) ); } else { Resources.TurningSound.volume = 0; } } - private emitParticles(engine: Engine, skierAction: SkierActions): void { + private emitParticles(engine: Engine, skierAction: SkierActions, skierIntentions: SkierIntentions): void { if ( this.leftParticlesEmitter && this.rightParticlesEmitter && @@ -223,9 +244,9 @@ export class Skier extends Actor { this.computeParticlesAngle(); if (skierAction === SkierActions.SLIDE_LEFT || skierAction === SkierActions.SLIDE_RIGHT) { - this.emitSlidingParticles(speedPercentage, this.slidingIntention(engine), skierAction); + this.emitSlidingParticles(speedPercentage, Skier.slidingIntention(skierIntentions), skierAction); } else if (skierAction === SkierActions.CARVE_LEFT || skierAction === SkierActions.CARVE_RIGHT) { - this.emitCarvingParticles(speedPercentage, this.carvingIntention(engine), skierAction); + this.emitCarvingParticles(speedPercentage, Skier.carvingIntention(skierIntentions), skierAction); } else if (skierAction === SkierActions.BRAKE) { this.emitBrakingParticles(speedPercentage); } @@ -278,27 +299,31 @@ export class Skier extends Actor { ); } - private carvingIntention(engine: Engine): number { - return Math.min(1, this.leftCarvingIntention(engine) + this.rightCarvingIntention(engine)); + private static carvingIntention(skierIntentions: SkierIntentions): number { + return Math.min(1, skierIntentions.leftCarvingIntention + skierIntentions.rightCarvingIntention); + } + + private static slidingIntention(skierIntentions: SkierIntentions): number { + return Math.min(1, Skier.leftSlidingIntention(skierIntentions) + Skier.rightSlidingIntention(skierIntentions)); } - private slidingIntention(engine: Engine): number { - return Math.min(1, this.leftSlidingIntention(engine) + this.rightSlidingIntention(engine)); + private static hasTurningIntention(skierIntentions: SkierIntentions): boolean { + return Skier.carvingIntention(skierIntentions) > 0 || Skier.slidingIntention(skierIntentions) > 0; } - private hasLeftTurningIntention(engine: Engine): boolean { - return this.leftCarvingIntention(engine) + this.leftSlidingIntention(engine) > 0; + private static hasLeftTurningIntention(skierIntentions: SkierIntentions): boolean { + return skierIntentions.leftCarvingIntention + Skier.leftSlidingIntention(skierIntentions) > 0; } - private leftSlidingIntention(engine: Engine): number { - return this.hasBreakingIntention(engine) && this.leftCarvingIntention(engine) > 0 - ? this.leftCarvingIntention(engine) + private static leftSlidingIntention(skierIntentions: SkierIntentions): number { + return skierIntentions.hasBrakingIntention && skierIntentions.leftCarvingIntention > 0 + ? skierIntentions.leftCarvingIntention : 0; } - private rightSlidingIntention(engine: Engine): number { - return this.hasBreakingIntention(engine) && this.rightCarvingIntention(engine) > 0 - ? this.rightCarvingIntention(engine) + private static rightSlidingIntention(skierIntentions: SkierIntentions): number { + return skierIntentions.hasBrakingIntention && skierIntentions.rightCarvingIntention > 0 + ? skierIntentions.rightCarvingIntention : 0; } @@ -328,10 +353,6 @@ export class Skier extends Actor { return 0; } - private hasTurningIntention(engine: Engine): boolean { - return this.carvingIntention(engine) > 0 || this.slidingIntention(engine) > 0; - } - private computeParticlesAngle(): void { const leftAngle = this.rotation ? (this.rotation - LEFT_ANGLE_OFFSET) % RADIAN_PI : 3; const rightAngle = this.rotation ? (this.rotation - RIGHT_ANGLE_OFFSET) % RADIAN_PI : 3; diff --git a/src/app/game/game.ts b/src/app/game/game.ts index 36961c0..31b613e 100644 --- a/src/app/game/game.ts +++ b/src/app/game/game.ts @@ -1,4 +1,4 @@ -import { Color, DisplayMode, Engine, Loader } from 'excalibur'; +import { Color, DisplayMode, Engine, Loader, PointerScope } from 'excalibur'; import { Resources } from './resources'; import { SoundPlayer } from './utils/sounds-player'; import { LogoManager } from './utils/logo-manager'; diff --git a/src/app/game/scenes/race.ts b/src/app/game/scenes/race.ts index 531e355..4f6320a 100644 --- a/src/app/game/scenes/race.ts +++ b/src/app/game/scenes/race.ts @@ -162,7 +162,7 @@ export class Race extends Scene { this.skier!.pos.x, this.skier!.pos.y, this.skier!.rotation, - this.skier!.getSkierCurrentAction(this.engine) + this.skier!.getSkierCurrentAction() ) ); } diff --git a/src/app/game/utils/touch-manager.ts b/src/app/game/utils/touch-manager.ts index 276211f..1cf57de 100644 --- a/src/app/game/utils/touch-manager.ts +++ b/src/app/game/utils/touch-manager.ts @@ -26,15 +26,30 @@ export class TouchManager { private recomputeTouchStatus(type: 'down' | 'up', position: Vector): void { if (this.getTouchZone(position) === 'back') { + if (!this.isTouchingBack && type === 'up') { + this.resetTouching(); + } this.isTouchingBack = type === 'down'; } else if (this.getTouchZone(position) === 'left') { + if (!this.isTouchingLeft && type === 'up') { + this.resetTouching(); + } this.isTouchingLeft = type === 'down'; } else if (this.getTouchZone(position) === 'right') { + if (!this.isTouchingRight && type === 'up') { + this.resetTouching(); + } this.isTouchingRight = type === 'down'; } this.isTouching = this.isTouchingBack || this.isTouchingLeft || this.isTouchingRight; } + private resetTouching(): void { + this.isTouchingBack = false; + this.isTouchingLeft = false; + this.isTouchingRight = false; + } + private getTouchZone(position: Vector): 'back' | 'left' | 'right' { if (position.y > window.innerHeight - Config.TOUCH_BRAKE_ZONE_RATIO * window.innerHeight) { return 'back';