Skip to content

Commit af2890a

Browse files
Merge pull request #49 from justindujardin/test/combat-components
Test/combat components
2 parents cc0d4fc + 331a654 commit af2890a

File tree

70 files changed

+2027
-1291
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

70 files changed

+2027
-1291
lines changed

.storybook/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"../src/test.ts",
1010
"../src/**/*.tsx",
1111
"../src/**/*.spec.ts",
12+
"../src/**/*.testing.ts",
1213
"../projects/**/*.spec.ts"
1314
],
1415
"include": [

karma.conf.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,18 @@ module.exports = function (config) {
3434
customLaunchers: {
3535
ChromeDebug: {
3636
base: "Chrome",
37-
flags: ["--remote-debugging-port=9333"],
37+
flags: [
38+
"--remote-debugging-port=9333",
39+
"--autoplay-policy=no-user-gesture-required",
40+
],
3841
},
3942
ChromeHeadlessCustom: {
4043
base: "ChromeHeadless",
41-
flags: ["--no-sandbox", "--disable-gpu"],
44+
flags: [
45+
"--no-sandbox",
46+
"--disable-gpu",
47+
"--autoplay-policy=no-user-gesture-required",
48+
],
4249
},
4350
},
4451
restartOnFileChange: true,

package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,14 @@
144144
"type": "feat",
145145
"release": "patch"
146146
},
147+
{
148+
"type": "refactor",
149+
"release": "patch"
150+
},
151+
{
152+
"type": "test",
153+
"release": "patch"
154+
},
147155
{
148156
"type": "fix",
149157
"release": "patch"

src/app/app.testing.ts

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { TestBed } from '@angular/core/testing';
2+
import { Store } from '@ngrx/store';
3+
import { map, take } from 'rxjs/operators';
4+
import { AppState } from './app.model';
5+
import { IPartyMember } from './models/base-entity';
6+
import { EntityAddItemAction } from './models/entity/entity.actions';
7+
import { EntityWithEquipment } from './models/entity/entity.model';
8+
import { EntityItemTypes } from './models/entity/entity.reducer';
9+
import {
10+
instantiateEntity,
11+
ITemplateBaseItem,
12+
} from './models/game-data/game-data.model';
13+
import {
14+
GameStateAddInventoryAction,
15+
GameStateHurtPartyAction,
16+
} from './models/game-state/game-state.actions';
17+
import { Item } from './models/item';
18+
import {
19+
getGameBoardedShip,
20+
getGameInventory,
21+
getGameKey,
22+
getGameParty,
23+
getGamePartyGold,
24+
getGamePartyWithEquipment,
25+
} from './models/selectors';
26+
import { SpritesService } from './models/sprites/sprites.service';
27+
import { WindowService } from './services/window';
28+
29+
/**
30+
* Testing providers for the app.
31+
*/
32+
export const APP_TESTING_PROVIDERS = [
33+
{
34+
// Mock reload() so it doesn't actually reload the page
35+
provide: WindowService,
36+
useValue: {
37+
reload: jasmine.createSpy('reload'),
38+
},
39+
},
40+
];
41+
42+
export function testAppGetBoarded(store: Store<AppState>): boolean {
43+
let result: boolean = false;
44+
store
45+
.select(getGameBoardedShip)
46+
.pipe(take(1))
47+
.subscribe((s) => (result = s));
48+
return result;
49+
}
50+
51+
export function testAppGetKeyData(
52+
store: Store<AppState>,
53+
keyName?: string
54+
): boolean | undefined {
55+
if (!keyName) {
56+
return false;
57+
}
58+
let result: boolean | undefined = undefined;
59+
store
60+
.select(getGameKey(keyName))
61+
.pipe(take(1))
62+
.subscribe((s) => (result = s));
63+
return result;
64+
}
65+
66+
export function testAppGetParty(store: Store<AppState>): IPartyMember[] {
67+
let result: IPartyMember[] = [];
68+
store
69+
.select(getGameParty)
70+
.pipe(
71+
map((f) => f.toJS()),
72+
take(1)
73+
)
74+
.subscribe((s) => (result = s));
75+
return result;
76+
}
77+
78+
export function testAppGetPartyWithEquipment(
79+
store: Store<AppState>
80+
): EntityWithEquipment[] {
81+
let result: EntityWithEquipment[] = [];
82+
store
83+
.select(getGamePartyWithEquipment)
84+
.pipe(
85+
map((f) => f.toJS()),
86+
take(1)
87+
)
88+
.subscribe((s) => (result = s));
89+
return result;
90+
}
91+
92+
export function testAppGetInventory(store: Store<AppState>): EntityItemTypes[] {
93+
let result: EntityItemTypes[] | undefined;
94+
store
95+
.select(getGameInventory)
96+
.pipe(
97+
take(1),
98+
map((f) => f.toJS())
99+
)
100+
.subscribe((s) => (result = s));
101+
return result as EntityItemTypes[];
102+
}
103+
104+
export function testAppGetPartyGold(store: Store<AppState>): number {
105+
let result = 0;
106+
store
107+
.select(getGamePartyGold)
108+
.pipe(take(1))
109+
.subscribe((s) => (result = s));
110+
return result;
111+
}
112+
113+
export function testAppAddToInventory<T extends Item>(
114+
store: Store<AppState>,
115+
itemId: string,
116+
from: ITemplateBaseItem[],
117+
values?: Partial<T>
118+
): T {
119+
const itemInstance = instantiateEntity<T>(
120+
from.find((f) => f.id === itemId),
121+
values
122+
);
123+
store.dispatch(new EntityAddItemAction(itemInstance));
124+
store.dispatch(new GameStateAddInventoryAction(itemInstance));
125+
return itemInstance;
126+
}
127+
128+
export function testAppDamageParty(
129+
store: Store<AppState>,
130+
party: EntityWithEquipment[],
131+
damage: number
132+
) {
133+
store.dispatch(
134+
new GameStateHurtPartyAction({ partyIds: party.map((p) => p.eid), damage })
135+
);
136+
}
137+
138+
export async function testAppLoadSprites() {
139+
const spritesService = TestBed.inject(SpritesService);
140+
await spritesService.loadSprites('assets/images/index.json').toPromise();
141+
}

src/app/behaviors/index.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
11
import { CommonModule } from '@angular/common';
22
import { ModuleWithProviders, NgModule } from '@angular/core';
3-
import { AnimatedBehaviorComponent } from './animated.behavior';
43
import { CollisionBehaviorComponent } from './collision.behavior';
54
import { SpriteRenderBehaviorComponent } from './sprite-render.behavior';
65

7-
export * from './animated.behavior';
8-
96
/** Common behavior behaviors */
107
export const BEHAVIOR_COMPONENTS = [
11-
AnimatedBehaviorComponent,
128
CollisionBehaviorComponent,
139
SpriteRenderBehaviorComponent,
1410
];

src/app/behaviors/sprite-render.behavior.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export class SpriteRenderBehaviorComponent
4444
return this._icon$.value;
4545
}
4646

47-
private _subscription: Subscription;
47+
private _subscription: Subscription | null = null;
4848

4949
meta: ISpriteMeta | null = null;
5050
image: HTMLImageElement | null = null;
@@ -73,6 +73,6 @@ export class SpriteRenderBehaviorComponent
7373
}
7474

7575
ngOnDestroy(): void {
76-
this._subscription.unsubscribe();
76+
this._subscription?.unsubscribe();
7777
}
7878
}

src/app/behaviors/sprite.behavior.ts

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -64,26 +64,24 @@ export class SpriteComponent extends SceneObjectBehavior {
6464
/**
6565
* Set the current sprite name. Returns the previous sprite name.
6666
*/
67-
setSprite(name: string | null = null, frame: number = 0): Promise<string | null> {
68-
return new Promise<string | null>((resolve, reject) => {
69-
if (name && name === this.icon && this.image && this.meta) {
70-
return resolve(this.icon);
71-
}
72-
this.icon = name;
73-
if (!name) {
74-
this.meta = null;
75-
return resolve(null);
76-
}
77-
this.meta = this.host.world.sprites.getSpriteMeta(name);
78-
assertTrue(this.meta?.source, `invalid sprite source: ${name}`);
79-
this.host.world.sprites
80-
.getSpriteSheet(this.meta.source)
81-
.then((images: ImageResource[]) => {
82-
this.image = images[0].data;
83-
this.frame = frame;
84-
resolve(this.icon);
85-
})
86-
.catch(reject);
87-
});
67+
async setSprite(name: string | null = null, frame?: number): Promise<string | null> {
68+
if (name && name === this.icon && this.image && this.meta) {
69+
return this.icon;
70+
}
71+
if (typeof frame !== 'undefined') {
72+
this.frame = frame;
73+
}
74+
this.icon = name;
75+
if (!name) {
76+
this.meta = null;
77+
return null;
78+
}
79+
this.meta = this.host.world.sprites.getSpriteMeta(name);
80+
assertTrue(this.meta?.source, `invalid sprite source: ${name}`);
81+
const images: ImageResource[] = await this.host.world.sprites.getSpriteSheet(
82+
this.meta.source
83+
);
84+
this.image = images[0].data;
85+
return this.icon;
8886
}
8987
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { TestBed } from '@angular/core/testing';
2+
import { RouterTestingModule } from '@angular/router/testing';
3+
import { APP_IMPORTS } from '../../app.imports';
4+
import { Point } from '../../core';
5+
import { Scene } from '../../scene/scene';
6+
import { GameWorld } from '../../services/game-world';
7+
import { RPGGame } from '../../services/rpg-game';
8+
import { AnimatedComponent, IAnimationConfig } from './animated.component';
9+
10+
function getFixture() {
11+
const fixture = TestBed.createComponent(AnimatedComponent);
12+
const comp: AnimatedComponent = fixture.componentInstance;
13+
fixture.detectChanges();
14+
return { fixture, comp };
15+
}
16+
17+
describe('AnimatedComponent', () => {
18+
let world: GameWorld;
19+
let scene: Scene;
20+
21+
beforeEach(async () => {
22+
await TestBed.configureTestingModule({
23+
imports: [RouterTestingModule, ...APP_IMPORTS],
24+
declarations: [AnimatedComponent],
25+
}).compileComponents();
26+
world = TestBed.inject(GameWorld);
27+
scene = new Scene();
28+
world.mark(scene);
29+
world.time.start();
30+
const game = TestBed.inject(RPGGame);
31+
await game.initGame(false);
32+
});
33+
afterEach(async () => {
34+
world.erase(scene);
35+
world.time.stop();
36+
scene.destroy();
37+
});
38+
39+
it('initializes', async () => {
40+
const { fixture, comp } = getFixture();
41+
await fixture.whenRenderingDone();
42+
expect(() => {
43+
comp.update(0);
44+
}).not.toThrow();
45+
});
46+
47+
it('plays async animations', async () => {
48+
const { fixture, comp } = getFixture();
49+
await fixture.whenRenderingDone();
50+
scene.addObject(comp);
51+
52+
let cb1 = false;
53+
let cb2 = false;
54+
const anim: IAnimationConfig[] = [
55+
{
56+
name: 'Prep Animation',
57+
host: comp,
58+
duration: 0,
59+
callback: async () => {
60+
cb1 = true;
61+
return true;
62+
},
63+
},
64+
{
65+
name: 'Move Forward for Attack',
66+
host: comp,
67+
repeats: 0,
68+
duration: 10,
69+
move: new Point(1, 0),
70+
frames: [1, 2],
71+
callback: async () => {
72+
cb2 = true;
73+
return true;
74+
},
75+
},
76+
];
77+
78+
await comp.playChain(anim);
79+
expect(cb1).toBe(true);
80+
expect(cb2).toBe(true);
81+
});
82+
});

0 commit comments

Comments
 (0)