Skip to content

Commit

Permalink
feat(combat): add auto-combat button and setting
Browse files Browse the repository at this point in the history
 - there's a floating action button on the combat view now. When pressed it automatically selects combat actions for the party.
 - currently it just attacks, and everyone targets the first enemy in the enemies array. This works pretty well to take enemies out of combat quickly.
 - TODO: some useful abstraction for choosing actions that enemies and players can use.
  • Loading branch information
justindujardin committed Feb 9, 2024
1 parent b0a262f commit 55325a0
Show file tree
Hide file tree
Showing 7 changed files with 88 additions and 3 deletions.
5 changes: 5 additions & 0 deletions src/app/routes/combat/combat.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,10 @@
[enemies]="enemies"
[defaultState]="defaultState"
[encounter]="encounter$ | async"
#machine
></combat-state-machine>

<combat-hud [scene]="scene" [combat]="machine"></combat-hud>
<button mat-fab color="accent" (click)="toggleAutoCombat()">
<mat-icon>{{ machine.autoCombat ? "pause" : "fast_forward" }}</mat-icon>
</button>
5 changes: 5 additions & 0 deletions src/app/routes/combat/combat.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,9 @@
animation-iteration-count: infinite;
}
}
button[mat-fab] {
position: fixed;
top: 48px;
left: 12px;
}
}
12 changes: 11 additions & 1 deletion src/app/routes/combat/combat.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,12 @@ export class CombatComponent
if (data.damage > 0 && data.defender instanceof CombatPlayerComponent) {
this.shake(this.canvasElementRef.nativeElement);
}
this.notify.show(msg, _done);
if (this.machine.autoCombat) {
// Wait a moment between turns
_.delay(() => _done(), 750);
} else {
this.notify.show(msg, _done);
}
});
const runSub = this.machine.onRun$.subscribe((data: CombatRunSummary) => {
const _done = this.machine.onRun$.notifyWait();
Expand Down Expand Up @@ -252,6 +257,11 @@ export class CombatComponent
// Events
//

toggleAutoCombat() {
this.machine.autoCombat = !this.machine.autoCombat;
localStorage.setItem('rpgAutoCombat', `${this.machine.autoCombat}`);
}

onAddToScene(scene: Scene) {
super.onAddToScene(scene);
if (scene.world && scene.world.input) {
Expand Down
11 changes: 10 additions & 1 deletion src/app/routes/combat/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { CommonModule } from '@angular/common';
import { ModuleWithProviders, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { BehaviorsModule } from '../../behaviors/index';
import { AppComponentsModule } from '../../components/index';
import { ServicesModule } from '../../services';
Expand Down Expand Up @@ -53,7 +55,14 @@ export const COMBAT_PROVIDERS = [CanActivateCombat];
declarations: COMBAT_EXPORTS,
exports: COMBAT_EXPORTS,
providers: COMBAT_PROVIDERS,
imports: [CommonModule, BehaviorsModule, AppComponentsModule, ServicesModule],
imports: [
CommonModule,
BehaviorsModule,
AppComponentsModule,
ServicesModule,
MatButtonModule,
MatIconModule,
],
})
export class CombatModule {
static forRoot(): ModuleWithProviders<CombatModule> {
Expand Down
54 changes: 53 additions & 1 deletion src/app/routes/combat/states/combat-choose-action.state.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Component, Input } from '@angular/core';
import { interval, Observable } from 'rxjs';
import { BehaviorSubject, interval, Observable } from 'rxjs';
import { distinctUntilChanged, filter, map } from 'rxjs/operators';
import * as _ from 'underscore';
import { Point } from '../../../../app/core/point';
import { assertTrue } from '../../../models/util';
import { GameEntityObject } from '../../../scene/objects/game-entity-object';
import { SceneView } from '../../../scene/scene-view';
import { CombatAttackBehaviorComponent } from '../behaviors/actions';
import { ChooseActionStateMachine } from '../behaviors/choose-action.machine';
import { CombatActionBehavior } from '../behaviors/combat-action.behavior';
import { ICombatMenuItem } from '../combat.types';
Expand Down Expand Up @@ -45,6 +46,18 @@ export class CombatChooseActionStateComponent extends CombatMachineState {
/** The scene view container. Used to calculating screen space pointer coordinates */
@Input() view: SceneView | null = null;

private _autoCombat$ = new BehaviorSubject<boolean>(false);
/** Automatically select moves for players for interactionless combat */
@Input() set autoCombat(value: boolean) {
this._autoCombat$.next(value);
if (value && this.machine) {
this._doAutoSelection();
}
}
get autoCombat(): boolean {
return this._autoCombat$.value;
}

private _currentMachine: ChooseActionStateMachine | null = null;
private toChoose: GameEntityObject[] = [];

Expand Down Expand Up @@ -93,6 +106,10 @@ export class CombatChooseActionStateComponent extends CombatMachineState {
machine.currentDone = true;
machine.playerChoices = {};

if (this.autoCombat) {
return this._doAutoSelection();
}

this._currentMachine = new ChooseActionStateMachine(
this,
machine.scene,
Expand Down Expand Up @@ -123,6 +140,41 @@ export class CombatChooseActionStateComponent extends CombatMachineState {
this._next();
}

private _doAutoSelection() {
const remainder = [...this.toChoose];
if (this._currentMachine?.current) {
remainder.unshift(this._currentMachine.current);
}
if (this._currentMachine) {
this._currentMachine.destroy();
this._currentMachine = null;
}
this.toChoose.length = 0;
this.pointer = false;
this.pointerClass = '';

assertTrue(this.machine, 'invalid state machine');

for (let i = 0; i < remainder.length; i++) {
const player = remainder[i];
// TODO: Support more than attack actions
const action = player.findBehavior<CombatAttackBehaviorComponent>(
CombatAttackBehaviorComponent
);

assertTrue(this.machine.enemies, 'no enemies');
const enemy = this.machine.enemies.toArray()[0];

assertTrue(action, `attack action not found on: ${player.name}`);
const id = player._uid;
action.from = player;
action.to = enemy;
this.machine.playerChoices[id] = action;
console.log(`[autoCombat] ${player.model?.name} chose ${action.name}`);
}
this.machine.setCurrentState('begin-turn');
}

private _next() {
const p: GameEntityObject | null = this.toChoose.shift() || null;
if (!p || !this._currentMachine) {
Expand Down
1 change: 1 addition & 0 deletions src/app/routes/combat/states/combat.machine.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<combat-start-state #start></combat-start-state>
<combat-begin-turn-state #beginTurn></combat-begin-turn-state>
<combat-choose-action-state
[autoCombat]="autoCombat"
[pointAt]="current"
[view]="combat"
#chooseAction
Expand Down
3 changes: 3 additions & 0 deletions src/app/routes/combat/states/combat.machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ export class CombatStateMachineComponent
@Input() view: SceneView;
@Input() defaultState: CombatStateNames | null = 'start';

/** Automatically select moves for players for interactionless combat */
@Input() autoCombat: boolean = localStorage.getItem('rpgAutoCombat') === 'true';

@ViewChildren('start,beginTurn,chooseAction,endTurn,defeat,victory,escape')
childStates: QueryList<CombatMachineState>;
ngOnDestroy(): void {
Expand Down

0 comments on commit 55325a0

Please sign in to comment.