Skip to content

Commit

Permalink
feat(academy-precision): add precision lesson to the academy
Browse files Browse the repository at this point in the history
  • Loading branch information
mathieuher committed Feb 17, 2025
1 parent 74bad38 commit ae48f0b
Show file tree
Hide file tree
Showing 12 changed files with 292 additions and 20 deletions.
5 changes: 5 additions & 0 deletions src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ export const routes: Routes = [
path: 'academy-basics',
loadComponent: () => import('./pages/academy/lessons/basics/basics.component').then(m => m.BasicsComponent)
},
{
path: 'academy-precision',
loadComponent: () =>
import('./pages/academy/lessons/precision/precision.component').then(m => m.PrecisionComponent)
},
{
path: 'settings',
loadComponent: () => import('./pages/settings/settings.component').then(m => m.SettingsComponent)
Expand Down
10 changes: 10 additions & 0 deletions src/app/common/scss/components.scss
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,16 @@ canvas#game {
color: var(--color-surface);
}

&.error {
background-color: var(--color-error-lightest);
color: var(--color-error-darkest);

.retro-title,
.retro-text {
color: var(--color-error-darkest);
}
}

.retro-title {
margin-top: 0;
background-color: inherit;
Expand Down
8 changes: 5 additions & 3 deletions src/app/game/actors/gate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { GatesConfig } from '../models/gates-config';
import { ScreenManager } from '../utils/screen-manager';
import type { Game } from '../game';
import type { Settings } from '../../common/models/settings';
import type { Academy } from '../scenes/academy';

export class Gate extends Actor {
public config: GatesConfig;
Expand Down Expand Up @@ -50,7 +51,6 @@ export class Gate extends Actor {
anchor: Gate.getAnchor(vertical, pivot),
z: isFinalGate ? 5 : 1
});

this.engine = engine;
this.config = config;
this.isFinalGate = isFinalGate;
Expand Down Expand Up @@ -103,17 +103,19 @@ export class Gate extends Actor {

if (this.passed && !this.straddled) {
this.updatePassedPolesGraphics();
this.engine.customEvents.emit({ name: 'gate-event', content: 'passed' });
} else {
(this.scene as Race).addPenalty();
this.missed = true;
this.engine.customEvents.emit({ name: 'gate-event', content: 'missed' });
}

if (this.sectorNumber) {
if (this.sectorNumber && this.engine.mode === 'race') {
(this.scene as Race).setSector(this.sectorNumber);
}

if (this.isFinalGate) {
(this.scene as Race).stop();
(this.scene as Race | Academy).stop();
}
}
}
Expand Down
7 changes: 4 additions & 3 deletions src/app/game/game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ import { EventEmitter } from '@angular/core';
import type { SkierIntentions } from './actors/skier';

export type GameMode = 'academy' | 'career' | 'race';

export type GateEvent = 'passed' | 'missed';
export type AcademyEvent = 'stopped';
export interface CustomGameEvent {
name: string;
content: SkierIntentions;
content: SkierIntentions | GateEvent | AcademyEvent;
}
export class Game extends Engine {
public settingsService: SettingsService;
Expand All @@ -26,8 +27,8 @@ export class Game extends Engine {
public raceStopped = new EventEmitter<RaceResult | undefined>();
public customEvents = new EventEmitter<CustomGameEvent>();
public paused = false;
public mode: GameMode;

private mode: GameMode;
private rideConfig: RideConfig;
private resourcesToLoad = Object.values(Resources);

Expand Down
54 changes: 50 additions & 4 deletions src/app/game/scenes/academy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,28 @@ import { TouchManager } from '../utils/touch-manager';
import type { AcademyConfig } from '../models/academy-config';
import type { Game } from '../game';
import { Resources } from '../resources';
import type { Track } from '../models/track';
import { Gate } from '../actors/gate';
import { TrackBuilder } from '../utils/track-builder';
import { Decoration } from '../actors/decoration';

export class Academy extends Scene {
public touchManager: TouchManager;
public config: AcademyConfig;
private skier?: Skier;
private cameraGhost?: Actor;
private gates: Gate[] = [];
private startingHouse = new StartingHouse();

constructor(engine: Engine, config: AcademyConfig) {
constructor(engine: Game, config: AcademyConfig) {
super();
this.config = config;
this.touchManager = new TouchManager(engine);
}

override onActivate(): void {
if (this.config) {
this.prepare();
this.prepare(this.config);
}
}

Expand All @@ -36,16 +41,25 @@ export class Academy extends Scene {
(this.engine as Game).soundPlayer.playSound(Resources.StartRaceSound, 0.3);
}

public stop(): void {}
public stop(): void {
this.skier?.finishRace();
(this.engine as Game).customEvents.emit({ name: 'academy-event', content: 'stopped' });
}

public addPenalty(): void {}

public setSector(): void {}

public clean(): void {
this.clear();
}

private prepare(): void {
private prepare(config: AcademyConfig): void {
this.buildTrack(config.track);
this.skier = new Skier(this.config.skierInfos, Config.GS_SKIER_CONFIG);
this.add(this.skier);
this.add(this.startingHouse);

this.setupCamera();
}

Expand All @@ -63,4 +77,36 @@ export class Academy extends Scene {
private updateCameraGhost(): void {
this.cameraGhost!.pos = vec(0, this.skier!.pos.y + Config.FRONT_GHOST_DISTANCE);
}

private buildTrack(track: Track): void {
for (const stockableGate of track.gates) {
const gate = new Gate(
this.engine as Game,
TrackBuilder.getGatesConfig(track.style),
vec(stockableGate.x, stockableGate.y),
stockableGate.width,
stockableGate.color,
stockableGate.gateNumber,
stockableGate.polesAmount,
stockableGate.pivot,
stockableGate.vertical,
stockableGate.isFinal,
stockableGate.sectorNumber
);
this.gates.push(gate);
this.add(gate);
}

if ((this.engine as Game).settingsService.getSettings().decorations && track.decorations?.length) {
for (const stockableDecoration of track.decorations) {
const decoration = new Decoration(
vec(stockableDecoration.x, stockableDecoration.y),
stockableDecoration.type,
stockableDecoration.sizeRatio
);

this.add(decoration);
}
}
}
}
4 changes: 2 additions & 2 deletions src/app/game/utils/track-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ export class TrackBuilder {
* @param trackStyle style of the track
* @returns new track
*/
public static designTrack(name: string, trackStyle: TrackStyles): Track {
public static designTrack(name: string, trackStyle: TrackStyles, gatesAmount?: number): Track {
const gatesConfig = TrackBuilder.getGatesConfig(trackStyle);
const numberOfGates = TrackBuilder.getRandomGatesNumber(gatesConfig);
const numberOfGates = gatesAmount ?? TrackBuilder.getRandomGatesNumber(gatesConfig);
const sectorGateNumbers = TrackBuilder.getSectorGateNumbers(numberOfGates);
const followingGateNumbers = TrackBuilder.getFollowingGateNumbers(gatesConfig, numberOfGates);
const doubleGateNumbers = TrackBuilder.getDoubleGateNumbers(gatesConfig, numberOfGates, followingGateNumbers);
Expand Down
4 changes: 2 additions & 2 deletions src/app/pages/academy/academy.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,17 @@
<span class="material-symbols-outlined">check</span>
}
</button>
<div class="retro-subtitle">Soon available</div>
<button
class="retro-button"
[class.tertiary]="!precisionCompleted"
[disabled]="true"
[disabled]="!basicsCompleted"
routerLink="/academy-precision"
>
2. Precision and focus @if(precisionCompleted) {
<span class="material-symbols-outlined">check</span>
}
</button>
<div class="retro-subtitle">Soon available</div>
<button
class="retro-button"
[class.tertiary]="!fasterCompleted"
Expand Down
4 changes: 2 additions & 2 deletions src/app/pages/academy/lessons/basics/basics.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
<div class="retro-title">1. Learn the basics</div>

<div class="retro-text">
Hey rider! Ready to hit the slopes? In this first lesson, we’ll
cover the <strong>fundamentals</strong> to get you skiing smoothly.
In this first lesson, we’ll cover the
<strong>fundamentals</strong> to get you skiing smoothly.
</div>

<div class="retro-text">
Expand Down
9 changes: 5 additions & 4 deletions src/app/pages/academy/lessons/basics/basics.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Config } from '../../../../game/config';
import { AcademyComponent } from '../../academy.component';
import { Track } from '../../../../game/models/track';
import { Resources } from '../../../../game/resources';
import type { SkierIntentions } from '../../../../game/actors/skier';

@Component({
selector: 'app-basics',
Expand Down Expand Up @@ -77,7 +78,7 @@ export class BasicsComponent implements AfterViewInit, OnDestroy {

protected startStep2(): void {
this.lessonStep.set(2);
const listener = this.game!.customEvents.subscribe(event => {
const listener = this.game!.customEvents.subscribe((event: { name: string; content: SkierIntentions }) => {
if (!this.step2Completed() && event.name === 'skier-actions' && event.content.hasStartingIntention) {
this.step2Completed.set(true);
setTimeout(() => {
Expand All @@ -91,7 +92,7 @@ export class BasicsComponent implements AfterViewInit, OnDestroy {

protected startStep3(): void {
this.lessonStep.set(3);
const listener = this.game!.customEvents.subscribe(event => {
const listener = this.game!.customEvents.subscribe((event: { name: string; content: SkierIntentions }) => {
if ((!this.step3Completed().left || !this.step3Completed().right) && event.name === 'skier-actions') {
if (!this.step3Completed().right && event.content.rightCarvingIntention) {
this.step3Completed.set({ left: this.step3Completed().left, right: true });
Expand All @@ -113,7 +114,7 @@ export class BasicsComponent implements AfterViewInit, OnDestroy {
protected startStep4(): void {
this.lessonStep.set(4);

const listener = this.game!.customEvents.subscribe(event => {
const listener = this.game!.customEvents.subscribe((event: { name: string; content: SkierIntentions }) => {
if (!this.step4Completed() && event.name === 'skier-actions' && event.content.hasBrakingIntention) {
this.step4Completed.set(true);
setTimeout(() => {
Expand All @@ -126,7 +127,7 @@ export class BasicsComponent implements AfterViewInit, OnDestroy {

protected startStep5(): void {
this.lessonStep.set(5);
const listener = this.game!.customEvents.subscribe(event => {
const listener = this.game!.customEvents.subscribe((event: { name: string; content: SkierIntentions }) => {
if (
!this.step5Completed() &&
event.name === 'skier-actions' &&
Expand Down
103 changes: 103 additions & 0 deletions src/app/pages/academy/lessons/precision/precision.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<canvas id="game"></canvas>
<div class="retro-ui-overlay">
<app-button-icon icon="arrow_back" (click)="exitLesson()"></app-button-icon>
@if(lessonFailed()) {
<ng-container *ngTemplateOutlet="lesson_failed"></ng-container>
} @else { @if(lessonStep() === 1) {
<ng-container *ngTemplateOutlet="step_1"></ng-container>
} @else if (lessonStep() === 2) {
<ng-container *ngTemplateOutlet="step_2"></ng-container>
} @else if (lessonStep() === 3) {
<ng-container *ngTemplateOutlet="step_3"></ng-container>
} }
</div>

<ng-template #step_1>
<div class="retro-dialog">
<div class="retro-title">2. Precision and Focus</div>

<div class="retro-text">
Welcome back, rider! Now that you know how to ride, it’s time to
level up your skills.
</div>

<div class="retro-text">
In this lesson, you’ll train to
<strong>stay on course and pass every gate</strong>. Missing a gate
adds <strong>+3 seconds</strong> to your time, so precision is key
and the most efficient way to lower your times!
</div>

<div class="retro-text">
Your challenge: <strong>Pass 15 consecutive gates</strong> without
missing before reaching the finish line! Stay focused, time your
turns, and let’s ride! ⛷️🔥
</div>

<div class="retro-button tertiary" (click)="startStep2()">
Start the lesson
</div>
</div>
</ng-template>

<ng-template #step_2>
<app-academy-objective
title="Pass 15 consecutive gates"
[detail]="gatesPassed() + '/15'"
[completed]="step2Completed()"
>
</app-academy-objective>
</ng-template>

<ng-template #step_3>
<div class="retro-dialog">
<div class="retro-title">Lesson completed!</div>

<div class="retro-text">
Nailed it! <strong>15 gates in a row</strong>, that’s some serious
precision, rider!
</div>

<div class="retro-text">
Keeping your line clean and passing every gate is the key to
crushing the slopes. The better your focus, the faster your runs!
</div>

<div class="retro-text">
Now, take this skill into your races and shave those seconds off
your time. Stay sharp, and we’ll see you in the next lesson!
</div>

<button class="retro-button" routerLink="/academy">
Back to the Academy
</button>
</div>
</ng-template>

<ng-template #lesson_failed>
<div class="retro-dialog error">
<div class="retro-title">Lesson not completed!</div>

<div class="retro-text">
Whoa, tough break! You reached the finish line, but you need to pass
<strong>15 consecutive gates</strong> to complete this lesson.
</div>

<div class="retro-text">
Precision is everything, stay focused, keep your line clean, and
don’t let those gates slip by! The more you practice, the sharper
your turns will be.
</div>

<div class="retro-text">
Ready to give it another go? You’ve got this, rider! ⛷️🔥
</div>

<button class="retro-button tertiary" (click)="exitLesson(true)">
Try again
</button>
<button class="retro-button" (click)="exitLesson()">
Back to the Academy
</button>
</div>
</ng-template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
:host {
display: flex;
flex: 1 1 auto;
flex-direction: column;
position: relative;
}
Loading

0 comments on commit ae48f0b

Please sign in to comment.