Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
877ab6f
feat(abilities): add Ability, Action, and Sense base classes
olistic Mar 16, 2026
de40962
refactor(abilities): convert action abilities to classes
olistic Mar 16, 2026
19da9dc
refactor(abilities): convert sense abilities to classes
olistic Mar 16, 2026
ba187d1
refactor(abilities): remove legacy types and fix AbilityBinding
olistic Mar 16, 2026
b1001fa
refactor(core): use instanceof Action for ability type checks
olistic Mar 16, 2026
f2a7b32
refactor(cli): update renderTypes for class-based abilities
olistic Mar 16, 2026
28a2fcf
refactor(units): use class-based ability bindings
olistic Mar 16, 2026
7016343
refactor(towers): use class-based ability bindings
olistic Mar 16, 2026
3a7078f
refactor(units): convert unit definitions to classes
olistic Mar 16, 2026
706d18e
fix(towers): restore () in player-facing API references in tips/clues
olistic Mar 17, 2026
d58e475
refactor(abilities): capitalize ability filenames to match class names
olistic Mar 17, 2026
84214c9
test(abilities): add tests for Ability, Action, and Sense base classes
olistic Mar 17, 2026
df89d52
style(abilities): reorder class members in configurable abilities
olistic Mar 17, 2026
631129b
refactor(core): move Ability, Action, Sense base classes to core
olistic Mar 17, 2026
c55e90d
fix(core): add missing color fields in runLevel test config
olistic Mar 17, 2026
aad8da9
refactor(core): reuse Ability type from Ability.ts in Unit
olistic Mar 17, 2026
0728e05
refactor(effects): convert ticking effect to class-based architecture
olistic Mar 17, 2026
4fd3c5b
feat(core): add EffectBinding type
olistic Mar 17, 2026
37b5bbb
refactor(effects): capitalize Ticking filename
olistic Mar 17, 2026
03e61eb
test(core): add tests for Effect base class
olistic Mar 17, 2026
99a84c8
fix(effects): fix Ticking import path in test
olistic Mar 17, 2026
bd25f35
refactor: stop re-exporting core types from abilities and effects
olistic Mar 17, 2026
fe74cd0
refactor(core): remove TowerFloorUnit and legacy unit loading
olistic Mar 17, 2026
921fa1a
refactor: pass unit classes instead of instances in tower configs
olistic Mar 17, 2026
1180fa0
style(towers): expand Narrow Path tower config to multi-line
olistic Mar 17, 2026
b70520d
refactor: move warrior config to towers and unify config types
olistic Mar 17, 2026
64b834f
refactor(core): add tower-level warrior and level definition types
olistic Mar 17, 2026
4aef7b2
refactor(cli): update Tower class for tower-level warrior config
olistic Mar 17, 2026
5f840ef
refactor(towers): move warrior config to tower top level
olistic Mar 17, 2026
c99ca8f
refactor: make declaredAbilities a static field on Unit
olistic Mar 17, 2026
900eb0c
refactor(core): replace any with typed class interfaces
olistic Mar 17, 2026
71032f3
refactor(units): don't export abstract unit classes
olistic Mar 17, 2026
4a2503d
refactor(cli): consistency
olistic Mar 17, 2026
2a94ec7
refactor(core): deepClone cleanup
olistic Mar 17, 2026
5c13f90
refactor(cli): stop handling legacy factories
olistic Mar 17, 2026
10254f6
refactor(core): remove internal AbilityClass and EffectClass from exp…
olistic Mar 17, 2026
f6ae6e1
refactor: convert playTurn to method and add Turn type
olistic Mar 17, 2026
a3a59c4
test(units): add tests for MeleeUnit and RangedUnit base classes
olistic Mar 17, 2026
bb50e42
test(units): simplify concrete unit tests
olistic Mar 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"mock-fs": "^5.5.0"
},
"dependencies": {
"@warriorjs/abilities": "workspace:^",
"@warriorjs/core": "workspace:^",
"@warriorjs/scoring": "workspace:^",
"@warriorjs/tower-the-narrow-path": "workspace:^",
Expand Down
6 changes: 5 additions & 1 deletion apps/cli/src/Tower.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ describe('Tower', () => {
let tower: Tower;

beforeEach(() => {
tower = new Tower('foo', 'Foo', 'bar baz', ['level1', 'level2']);
tower = new Tower('foo', 'Foo', 'bar baz', 'warrior' as any, ['level1', 'level2'] as any);
});

test('has an id', () => {
Expand All @@ -21,6 +21,10 @@ describe('Tower', () => {
expect(tower.description).toBe('bar baz');
});

test('has a warrior', () => {
expect(tower.warrior).toEqual('warrior');
});

test('has some levels', () => {
expect(tower.levels).toEqual(['level1', 'level2']);
});
Expand Down
16 changes: 12 additions & 4 deletions apps/cli/src/Tower.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,32 @@
import type { LevelConfig } from '@warriorjs/core';
import type { LevelDefinition, WarriorDefinition } from '@warriorjs/core';

/** Class representing a tower. */
class Tower {
id: string;
name: string;
description: string;
levels: LevelConfig[];
warrior: WarriorDefinition;
levels: LevelDefinition[];

constructor(id: string, name: string, description: string, levels: LevelConfig[]) {
constructor(
id: string,
name: string,
description: string,
warrior: WarriorDefinition,
levels: LevelDefinition[],
) {
this.id = id;
this.name = name;
this.description = description;
this.warrior = warrior;
this.levels = levels;
}

hasLevel(levelNumber: number): boolean {
return !!this.getLevel(levelNumber);
}

getLevel(levelNumber: number): LevelConfig | undefined {
getLevel(levelNumber: number): LevelDefinition | undefined {
return this.levels[levelNumber - 1];
}

Expand Down
36 changes: 26 additions & 10 deletions apps/cli/src/loadTowers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ test('loads internal towers', () => {
mockRequire.mockReturnValue({
name: 'The Narrow Path',
description: 'A corridor of stone where the only way out is forward',
warrior: 'warrior',
levels: ['level1', 'level2'],
});
mock({ '/path/to/node_modules/@warriorjs/cli': {} });
Expand All @@ -34,18 +35,25 @@ test('loads internal towers', () => {
'the-narrow-path',
'The Narrow Path',
'A corridor of stone where the only way out is forward',
'warrior',
['level1', 'level2'],
);
});

test('loads external official towers', () => {
mockRequire.mockImplementation((path: string) => {
if (path.includes('tower-foo')) {
return { name: 'Foo', description: 'bar', levels: ['level1', 'level2'] };
return {
name: 'Foo',
description: 'bar',
warrior: 'warrior',
levels: ['level1', 'level2'],
};
}
return {
name: 'The Narrow Path',
description: 'A corridor of stone where the only way out is forward',
warrior: 'warrior',
levels: ['level1', 'level2'],
};
});
Expand All @@ -56,24 +64,30 @@ test('loads external official towers', () => {
'tower-foo': {
'package.json': '',
'index.js':
"module.exports = { name: 'Foo', description: 'bar', levels: ['level1', 'level2'] }",
"module.exports = { name: 'Foo', description: 'bar', warrior: 'warrior, levels: ['level1', 'level2'] }",
},
},
},
});
loadTowers();
mock.restore();
expect(Tower).toHaveBeenCalledWith('foo', 'Foo', 'bar', ['level1', 'level2']);
expect(Tower).toHaveBeenCalledWith('foo', 'Foo', 'bar', 'warrior', ['level1', 'level2']);
});

test('loads external community towers', () => {
mockRequire.mockImplementation((path: string) => {
if (path.includes('warriorjs-tower-foo')) {
return { name: 'Foo', description: 'bar', levels: ['level1', 'level2'] };
return {
name: 'Foo',
description: 'bar',
warrior: 'warrior',
levels: ['level1', 'level2'],
};
}
return {
name: 'The Narrow Path',
description: 'A corridor of stone where the only way out is forward',
warrior: 'warrior',
levels: ['level1', 'level2'],
};
});
Expand All @@ -85,19 +99,20 @@ test('loads external community towers', () => {
'warriorjs-tower-foo': {
'package.json': '',
'index.js':
"module.exports = { name: 'Foo', description: 'bar', levels: ['level1', 'level2'] }",
"module.exports = { name: 'Foo', description: 'bar', warrior: 'warrior, levels: ['level1', 'level2'] }",
},
},
});
loadTowers();
mock.restore();
expect(Tower).toHaveBeenCalledWith('foo', 'Foo', 'bar', ['level1', 'level2']);
expect(Tower).toHaveBeenCalledWith('foo', 'Foo', 'bar', 'warrior', ['level1', 'level2']);
});

test("ignores directories that are seemingly towers but don't have a package.json", () => {
mockRequire.mockReturnValue({
name: 'The Narrow Path',
description: 'A corridor of stone where the only way out is forward',
warrior: 'warrior',
levels: ['level1', 'level2'],
});
mock({
Expand All @@ -106,25 +121,26 @@ test("ignores directories that are seemingly towers but don't have a package.jso
cli: {},
'tower-foo': {
'index.js':
"module.exports = { name: 'Foo', description: 'baz', levels: ['level1', 'level2'] }",
"module.exports = { name: 'Foo', description: 'baz', warrior: 'warrior, levels: ['level1', 'level2'] }",
},
},
'warriorjs-tower-bar': {
'index.js':
"module.exports = { name: 'Bar', description: 'baz', levels: ['level1', 'level2'] }",
"module.exports = { name: 'Bar', description: 'baz', warrior: 'warrior, levels: ['level1', 'level2'] }",
},
},
});
loadTowers();
mock.restore();
expect(Tower).not.toHaveBeenCalledWith('foo', 'Foo', 'baz', ['level1', 'level2']);
expect(Tower).not.toHaveBeenCalledWith('bar', 'Bar', 'baz', ['level1', 'level2']);
expect(Tower).not.toHaveBeenCalledWith('foo', 'Foo', 'baz', 'warrior', ['level1', 'level2']);
expect(Tower).not.toHaveBeenCalledWith('bar', 'Bar', 'baz', 'warrior', ['level1', 'level2']);
});

test("doesn't throw when @warriorjs/cli doesn't exist", async () => {
mockRequire.mockReturnValue({
name: 'The Narrow Path',
description: 'A corridor of stone where the only way out is forward',
warrior: 'warrior',
levels: ['level1', 'level2'],
});
const { findUpSync } = await import('find-up');
Expand Down
4 changes: 2 additions & 2 deletions apps/cli/src/loadTowers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ function loadTowers(): Tower[] {
const uniqueInfo = [...new Map(allInfo.map((item) => [item.id, item])).values()];
return uniqueInfo.map(({ id, requirePath }) => {
const mod = require(requirePath);
const { name, description, levels } = mod.default || mod;
return new Tower(id, name, description, levels);
const { name, description, warrior, levels } = mod.default || mod;
return new Tower(id, name, description, warrior, levels);
});
}

Expand Down
112 changes: 42 additions & 70 deletions apps/cli/src/utils/renderTypes.test.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,34 @@
import { type AbilityMeta, Action, Sense } from '@warriorjs/core';
import { describe, expect, test } from 'vitest';

import renderTypes from './renderTypes.js';

const mockAbilities = {
walk: () => ({
action: true,
description: 'Walks forward',
perform() {},
meta: {
params: [{ name: 'direction', type: 'Direction' as const, optional: true }],
returns: 'void' as const,
},
}),
feel: () => ({
description: 'Feels the space ahead',
perform() {},
meta: {
params: [{ name: 'direction', type: 'Direction' as const, optional: true }],
returns: 'Space' as const,
},
}),
health: () => ({
description: 'Returns current health',
perform() {},
meta: {
params: [] as any[],
returns: 'number' as const,
},
}),
};
class MockWalk extends Action {
readonly description = 'Walks forward';
readonly meta: AbilityMeta = {
params: [{ name: 'direction', type: 'Direction', optional: true }],
returns: 'void',
};
perform() {}
}

class MockFeel extends Sense {
readonly description = 'Feels the space ahead';
readonly meta: AbilityMeta = {
params: [{ name: 'direction', type: 'Direction', optional: true }],
returns: 'Space',
};
perform() {}
}

class MockHealth extends Sense {
readonly description = 'Returns current health';
readonly meta: AbilityMeta = {
params: [],
returns: 'number',
};
perform() {}
}

const profile: any = { language: 'typescript' };

Expand All @@ -38,7 +38,7 @@ function makeLevelConfig(abilities: Record<string, any>): any {

describe('renderTypes', () => {
test('renders types with a single action', () => {
expect(renderTypes(profile, makeLevelConfig({ walk: mockAbilities.walk }))).toBe(
expect(renderTypes(profile, makeLevelConfig({ walk: MockWalk }))).toBe(
[
'// @generated — Auto-generated each level. Do not edit.',
'',
Expand All @@ -58,9 +58,9 @@ describe('renderTypes', () => {
renderTypes(
profile,
makeLevelConfig({
health: mockAbilities.health,
walk: mockAbilities.walk,
feel: mockAbilities.feel,
health: MockHealth,
walk: MockWalk,
feel: MockFeel,
}),
),
).toBe(
Expand Down Expand Up @@ -107,12 +107,7 @@ describe('renderTypes', () => {
});

test('omits Space and Unit interfaces when no abilities use Space', () => {
expect(
renderTypes(
profile,
makeLevelConfig({ walk: mockAbilities.walk, health: mockAbilities.health }),
),
).toBe(
expect(renderTypes(profile, makeLevelConfig({ walk: MockWalk, health: MockHealth }))).toBe(
[
'// @generated — Auto-generated each level. Do not edit.',
'',
Expand All @@ -129,47 +124,24 @@ describe('renderTypes', () => {
);
});

test('skips abilities without meta', () => {
const noMetaAbility = () => ({
description: 'No meta',
perform() {},
});
expect(
renderTypes(profile, makeLevelConfig({ walk: mockAbilities.walk, legacy: noMetaAbility })),
).toBe(
[
'// @generated — Auto-generated each level. Do not edit.',
'',
"export type Direction = 'forward' | 'right' | 'backward' | 'left';",
'',
'export interface Warrior {',
' /** Walks forward */',
' walk(direction?: Direction): void;',
'}',
'',
].join('\n'),
);
});

test('handles rest parameters', () => {
const restAbility = () => ({
action: true,
description: 'Does something with rest params',
perform() {},
meta: {
params: [{ name: 'targets', type: 'string', rest: true }],
returns: 'void' as const,
},
});
expect(renderTypes(profile, makeLevelConfig({ multi: restAbility }))).toBe(
class RestAction extends Action {
readonly description = 'Does something with rest params';
readonly meta: AbilityMeta = {
params: [{ name: 'targets', type: 'any', rest: true }],
returns: 'void',
};
perform() {}
}
expect(renderTypes(profile, makeLevelConfig({ multi: RestAction }))).toBe(
[
'// @generated — Auto-generated each level. Do not edit.',
'',
"export type Direction = 'forward' | 'right' | 'backward' | 'left';",
'',
'export interface Warrior {',
' /** Does something with rest params */',
' multi(...targets: string[]): void;',
' multi(...targets: any[]): void;',
'}',
'',
].join('\n'),
Expand Down
Loading
Loading