Skip to content

Commit

Permalink
feat(testing): add testCombatCreateComponent for building testable co…
Browse files Browse the repository at this point in the history
…mbat components

- used to create fixtures with proper party/enemies, etc.
  • Loading branch information
justindujardin committed Nov 9, 2022
1 parent fbf3e35 commit b813191
Show file tree
Hide file tree
Showing 6 changed files with 186 additions and 124 deletions.
64 changes: 59 additions & 5 deletions src/app/routes/combat/combat.testing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@ import {
ITemplateFixedEncounter,
ITemplateRandomEncounter,
} from '../../models/game-data/game-data.model';
import { RANDOM_ENCOUNTERS_DATA } from '../../models/game-data/random-encounters';
import { getCombatEncounter } from '../../models/selectors';
import { Scene } from '../../scene/scene';
import { CombatEnemyComponent } from './combat-enemy.component';
import { CombatPlayerComponent } from './combat-player.component';
import { CombatComponent } from './combat.component';
import { CombatStateMachineComponent } from './states';
import { CombatStateNames } from './states/states';

export function testCombatGetStateMachine(): CombatStateMachineComponent {
const fixture = TestBed.createComponent(CombatStateMachineComponent);
Expand All @@ -33,11 +36,12 @@ export function testCombatGetStateMachine(): CombatStateMachineComponent {

export function testCombatAddPartyCombatants(
store: Store<AppState>,
comp: CombatStateMachineComponent,
comp: CombatComponent,
onlyFirst: boolean = false
): CombatPlayerComponent[] {
const party = testAppGetParty(store);
const players = [];

for (let pm = 0; pm < party.length; pm++) {
if (onlyFirst && pm > 0) {
break;
Expand All @@ -48,16 +52,17 @@ export function testCombatAddPartyCombatants(
player.componentInstance.icon = member.icon;
player.componentInstance.combat = comp;
comp.scene.addObject(player.componentInstance);
player.detectChanges();
players.push(player.componentInstance);
}
comp.party = new QueryList<CombatPlayerComponent>();
comp.party.reset(players);
return players;
}

export async function testCombatAddEnemyCombatants(
comp: CombatStateMachineComponent
): Promise<CombatEnemyComponent[]> {
export function testCombatAddEnemyCombatants(
comp: CombatComponent
): CombatEnemyComponent[] {
const items = ['imp', 'imp', 'imp'].map((id) => {
const itemTemplate: IEnemy = getEnemyById(id) as any;
return instantiateEntity<IEnemy>(itemTemplate, {
Expand All @@ -80,6 +85,26 @@ export async function testCombatAddEnemyCombatants(
return enemies;
}

export function testCombatSetEnemyCombatants(
comp: CombatStateMachineComponent,
enemies: IEnemy[]
): CombatEnemyComponent[] {
const results = [];
for (let i = 0; i < enemies.length; i++) {
const obj = enemies[i] as IEnemy;
const fixture = TestBed.createComponent(CombatEnemyComponent);
fixture.componentInstance.model = obj;
fixture.componentInstance.icon = obj.icon;
fixture.componentInstance.combat = comp;
comp.scene.addObject(fixture.componentInstance);
results.push(fixture.componentInstance);
fixture.detectChanges();
}
comp.enemies = new QueryList<CombatEnemyComponent>();
comp.enemies.reset(results);
return results;
}

export function testCombatSetRandomEncounter(
store: Store<AppState>,
party: Entity[],
Expand All @@ -93,18 +118,20 @@ export function testCombatSetRandomEncounter(
};

const zone = encounter.zones.length ? encounter.zones[0] : 'none';
const enemies = List<IEnemy>(encounter.enemies.map(toCombatant));

const payload: CombatEncounter = {
type: 'random',
id: encounter.id,
enemies: List<IEnemy>(encounter.enemies.map(toCombatant)),
enemies,
zone,
gold: 0,
items: List<string>([]),
message: List<string>(encounter.message || []),
party: List<Entity>(party),
};
store.dispatch(new CombatEncounterAction(payload));
return enemies.toJS();
}

export function testCombatSetFixedEncounter(
Expand Down Expand Up @@ -141,3 +168,30 @@ export function testCombatGetEncounter(store: Store<AppState>): CombatEncounter
.subscribe((s) => (result = s));
return result as CombatEncounter;
}

export function testCombatCreateComponent(
defaultState?: CombatStateNames | null,
encounter = RANDOM_ENCOUNTERS_DATA[0]
): CombatComponent {
const { combat } = testCombatCreateComponentFixture(defaultState, encounter);
return combat;
}

export function testCombatCreateComponentFixture(
defaultState: CombatStateNames | null = null,
encounter = RANDOM_ENCOUNTERS_DATA[0]
) {
const combatFixture = TestBed.createComponent(CombatComponent);
const store = combatFixture.componentInstance.store;
const combatComp = combatFixture.componentInstance;
combatComp.defaultState = null;
if (defaultState) {
combatComp.defaultState = defaultState;
}
if (encounter) {
const party = testAppGetParty(store);
testCombatSetRandomEncounter(store, party, encounter);
}
combatFixture.detectChanges();
return { fixture: combatFixture, combat: combatComp, machine: combatComp.machine };
}
42 changes: 20 additions & 22 deletions src/app/routes/combat/states/combat-begin-turn.state.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,17 @@ import { assertTrue } from '../../../models/util';
import { GameWorld } from '../../../services/game-world';
import { RPGGame } from '../../../services/rpg-game';
import { CombatAttackBehaviorComponent } from '../behaviors/actions';
import {
testCombatAddEnemyCombatants,
testCombatAddPartyCombatants,
testCombatGetEncounter,
testCombatGetStateMachine,
} from '../combat.testing';
import { testCombatCreateComponent, testCombatGetEncounter } from '../combat.testing';
import { CombatBeginTurnStateComponent } from './combat-begin-turn.state';

describe('CombatBeginTurnStateComponent', () => {
let world: GameWorld;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [RouterTestingModule, ...APP_IMPORTS],
declarations: [CombatBeginTurnStateComponent],
providers: APP_TESTING_PROVIDERS,
}).compileComponents();
world = TestBed.inject(GameWorld);
TestBed.inject(GameWorld);
const game = TestBed.inject(RPGGame);
await game.initGame(false);
await testAppLoadSprites();
Expand All @@ -31,9 +25,10 @@ describe('CombatBeginTurnStateComponent', () => {
it('calls act() on current party member action choice', async () => {
const fixture = TestBed.createComponent(CombatBeginTurnStateComponent);
const comp = fixture.componentInstance;
const machine = testCombatGetStateMachine();
const players = testCombatAddPartyCombatants(machine.store, machine);
const enemies = await testCombatAddEnemyCombatants(machine);
const combat = testCombatCreateComponent(null);
const machine = combat.machine;
const players = combat.party.toArray();
const enemies = combat.enemies.toArray();
machine.current = players[0];
let called = false;
machine.playerChoices[machine.current._uid] = {
Expand All @@ -54,9 +49,10 @@ describe('CombatBeginTurnStateComponent', () => {
it('selects a new target is player target dies before their turn', async () => {
const fixture = TestBed.createComponent(CombatBeginTurnStateComponent);
const comp = fixture.componentInstance;
const machine = testCombatGetStateMachine();
const players = testCombatAddPartyCombatants(machine.store, machine);
const enemies = await testCombatAddEnemyCombatants(machine);
const combat = testCombatCreateComponent(null);
const machine = combat.machine;
const players = combat.party.toArray();
const enemies = combat.enemies.toArray();
const deadEnemy = enemies[1];
assertTrue(deadEnemy.model, 'missing second enemy');
deadEnemy.model = { ...deadEnemy.model, hp: 0 };
Expand Down Expand Up @@ -85,9 +81,10 @@ describe('CombatBeginTurnStateComponent', () => {
it('scales current player up on enter and back down on exit', async () => {
const fixture = TestBed.createComponent(CombatBeginTurnStateComponent);
const comp = fixture.componentInstance;
const machine = testCombatGetStateMachine();
const players = testCombatAddPartyCombatants(machine.store, machine);
const enemies = await testCombatAddEnemyCombatants(machine);
const combat = testCombatCreateComponent(null);
const machine = combat.machine;
const players = combat.party.toArray();
const enemies = combat.enemies.toArray();
machine.playerChoices[players[0]._uid] = {
from: players[0],
to: enemies[0],
Expand All @@ -107,9 +104,10 @@ describe('CombatBeginTurnStateComponent', () => {
const fixture = TestBed.createComponent(CombatBeginTurnStateComponent);
const comp = fixture.componentInstance;
await testAppLoadSprites();
const machine = testCombatGetStateMachine();
const player = testCombatAddPartyCombatants(machine.store, machine, true)[0];
const enemies = await testCombatAddEnemyCombatants(machine);
const combat = testCombatCreateComponent(null);
const machine = combat.machine;
const party = combat.party.toArray();
const enemies = combat.enemies.toArray();
const enemy = enemies[0];
const attackComponent = enemy.findBehavior<CombatAttackBehaviorComponent>(
CombatAttackBehaviorComponent
Expand All @@ -118,9 +116,9 @@ describe('CombatBeginTurnStateComponent', () => {
let called = false;
attackComponent.act = async function () {
// Enemy is the attcker
expect(attackComponent.from).toEqual(enemy);
expect(attackComponent.from?._uid).toEqual(enemy._uid);
// Player is the defender
expect(attackComponent.to).toEqual(player);
expect(party.find((p) => p._uid === attackComponent.to?._uid)).toBeDefined();
called = true;
return true;
};
Expand Down
38 changes: 18 additions & 20 deletions src/app/routes/combat/states/combat-end-turn.state.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { APP_IMPORTS } from '../../../app.imports';
import { APP_TESTING_PROVIDERS } from '../../../app.testing';
import { ITemplateRandomEncounter } from '../../../models/game-data/game-data.model';
import { RANDOM_ENCOUNTERS_DATA } from '../../../models/game-data/random-encounters';
import { GameWorld } from '../../../services/game-world';
import { RPGGame } from '../../../services/rpg-game';
import { WindowService } from '../../../services/window';
import { CombatPlayerComponent } from '../combat-player.component';
import {
testCombatAddEnemyCombatants,
testCombatAddPartyCombatants,
testCombatCreateComponent,
testCombatGetStateMachine,
} from '../combat.testing';
import { CombatEndTurnStateComponent } from './combat-end-turn.state';
Expand All @@ -17,14 +19,7 @@ describe('CombatEndTurnStateComponent', () => {
await TestBed.configureTestingModule({
imports: [RouterTestingModule, ...APP_IMPORTS],
declarations: [CombatEndTurnStateComponent],
providers: [
{
provide: WindowService,
useValue: {
reload: jasmine.createSpy('reload'),
},
},
],
providers: [...APP_TESTING_PROVIDERS],
}).compileComponents();
world = TestBed.inject(GameWorld);
const game = TestBed.inject(RPGGame);
Expand All @@ -43,8 +38,12 @@ describe('CombatEndTurnStateComponent', () => {
it('transitions to victory if no live enemies', async () => {
const fixture = TestBed.createComponent(CombatEndTurnStateComponent);
const comp = fixture.componentInstance;
const machine = testCombatGetStateMachine();
testCombatAddPartyCombatants(machine.store, machine);
const encounter: ITemplateRandomEncounter = {
...RANDOM_ENCOUNTERS_DATA[0],
enemies: [],
};
const combat = testCombatCreateComponent('start', encounter);
const machine = combat.machine;
spyOn(machine, 'setCurrentState');
await comp.enter(machine);
fixture.detectChanges();
Expand All @@ -53,9 +52,8 @@ describe('CombatEndTurnStateComponent', () => {
it('transitions to choose-action if living enemies and party', async () => {
const fixture = TestBed.createComponent(CombatEndTurnStateComponent);
const comp = fixture.componentInstance;
const machine = testCombatGetStateMachine();
testCombatAddPartyCombatants(machine.store, machine);
testCombatAddEnemyCombatants(machine);
const combat = testCombatCreateComponent();
const machine = combat.machine;
spyOn(machine, 'setCurrentState');
await comp.enter(machine);
fixture.detectChanges();
Expand All @@ -64,10 +62,10 @@ describe('CombatEndTurnStateComponent', () => {
it('transitions to begin-turn if there are more players waiting to execute turns', async () => {
const fixture = TestBed.createComponent(CombatEndTurnStateComponent);
const comp = fixture.componentInstance;
const machine = testCombatGetStateMachine();
const players = testCombatAddPartyCombatants(machine.store, machine);
testCombatAddEnemyCombatants(machine);
machine.turnList.push(players[0]);
const combat = testCombatCreateComponent();
const machine = combat.machine;
const player = combat.party.get(0) as CombatPlayerComponent;
machine.turnList.push(player);
spyOn(machine, 'setCurrentState');
await comp.enter(machine);
fixture.detectChanges();
Expand Down
5 changes: 3 additions & 2 deletions src/app/routes/combat/states/combat-end-turn.state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export class CombatEndTurnStateComponent extends CombatMachineState {
}

async enter(machine: CombatStateMachineComponent) {
super.enter(machine);
await super.enter(machine);
machine.current = null;
// Find the next turn.
while (machine.turnList.length > 0 && !machine.current) {
Expand All @@ -54,6 +54,7 @@ export class CombatEndTurnStateComponent extends CombatMachineState {
if (!targetState) {
throw new Error('Invalid transition from end turn');
}
await machine.setCurrentState(targetState);
// NOTE: don't await state changes in enter, or you'll deadlock
machine.setCurrentState(targetState);
}
}

0 comments on commit b813191

Please sign in to comment.