Compare commits
2 Commits
dea77d463f
...
ba0903714c
| Author | SHA1 | Date | |
|---|---|---|---|
| ba0903714c | |||
| 0540e5ff5b |
@ -1,18 +1,19 @@
|
|||||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
||||||
import { isToolCallEventType } from "@earendil-works/pi-coding-agent";
|
import { isToolCallEventType } from '@earendil-works/pi-coding-agent';
|
||||||
|
|
||||||
export default function (pi: ExtensionAPI) {
|
export default function (pi: ExtensionAPI) {
|
||||||
pi.on("tool_call", async (event, ctx) => {
|
pi.on('tool_call', async (event, ctx) => {
|
||||||
if (!isToolCallEventType("bash", event)) return;
|
if (!isToolCallEventType('bash', event)) return;
|
||||||
|
|
||||||
const cmd = event.input.command || "";
|
const cmd = event.input.command || '';
|
||||||
const isYxCommand = cmd.includes("yx ");
|
const isYxCommand = cmd.includes('yx ');
|
||||||
|
|
||||||
// Allow yaks in interactive mode, block in print mode (sub-agents)
|
// Allow yaks in interactive mode, block in print mode (sub-agents)
|
||||||
if (isYxCommand && ctx.mode === "print") {
|
if (isYxCommand && ctx.mode === 'print') {
|
||||||
return {
|
return {
|
||||||
block: true,
|
block: true,
|
||||||
reason: "yx commands are disabled in print mode. Sub-agents must focus on domain work only.",
|
reason:
|
||||||
|
'yx commands are disabled in print mode. Sub-agents must focus on domain work only.',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -18,6 +18,7 @@ Coordinate sub-agent work using the `yx` CLI. The main agent creates tasks and d
|
|||||||
## Running Sub-Agents
|
## Running Sub-Agents
|
||||||
|
|
||||||
Sub-agents execute in print mode and are **hard-blocked from using `yx` commands** (see `.pi/extensions/yak-mode-gate.ts`). They receive:
|
Sub-agents execute in print mode and are **hard-blocked from using `yx` commands** (see `.pi/extensions/yak-mode-gate.ts`). They receive:
|
||||||
|
|
||||||
- The yak's `.context.md` (task description)
|
- The yak's `.context.md` (task description)
|
||||||
- `AGENTS.md` (project conventions)
|
- `AGENTS.md` (project conventions)
|
||||||
- Domain files to work on
|
- Domain files to work on
|
||||||
@ -43,6 +44,7 @@ cat .yaks/<task-id>/.state
|
|||||||
```
|
```
|
||||||
|
|
||||||
States:
|
States:
|
||||||
|
|
||||||
- `pending` — not yet started
|
- `pending` — not yet started
|
||||||
- `in-progress` — being worked on
|
- `in-progress` — being worked on
|
||||||
- `done` — completed
|
- `done` — completed
|
||||||
@ -54,6 +56,7 @@ yx done <task-id>
|
|||||||
```
|
```
|
||||||
|
|
||||||
Always verify before marking done:
|
Always verify before marking done:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Check state
|
# 1. Check state
|
||||||
cat .yaks/<task-id>/.state
|
cat .yaks/<task-id>/.state
|
||||||
@ -68,6 +71,7 @@ npm run checks
|
|||||||
## Sub-Agent Communication
|
## Sub-Agent Communication
|
||||||
|
|
||||||
Sub-agents can read yak state files directly (no yx CLI needed):
|
Sub-agents can read yak state files directly (no yx CLI needed):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cat .yaks/<task-id>/.name # task name
|
cat .yaks/<task-id>/.name # task name
|
||||||
cat .yaks/<task-id>/.state # current state
|
cat .yaks/<task-id>/.state # current state
|
||||||
|
|||||||
14
README.md
14
README.md
@ -51,13 +51,13 @@ The TypeScript implementation embraces functional patterns:
|
|||||||
|
|
||||||
All five user stories are implemented and verified:
|
All five user stories are implemented and verified:
|
||||||
|
|
||||||
| Story | Topic | Status |
|
| Story | Topic | Status |
|
||||||
|-------|-------|--------|
|
| ----- | --------------------------- | ------- |
|
||||||
| 1 | Character Creation & Damage | ✅ Done |
|
| 1 | Character Creation & Damage | ✅ Done |
|
||||||
| 2 | Levels | ✅ Done |
|
| 2 | Levels | ✅ Done |
|
||||||
| 3 | Factions | ✅ Done |
|
| 3 | Factions | ✅ Done |
|
||||||
| 4 | Magical Objects | ✅ Done |
|
| 4 | Magical Objects | ✅ Done |
|
||||||
| 5 | Changing Level | ✅ Done |
|
| 5 | Changing Level | ✅ Done |
|
||||||
|
|
||||||
**70 tests passing** across 6 spec files.
|
**70 tests passing** across 6 spec files.
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import js from '@eslint/js';
|
import js from '@eslint/js';
|
||||||
import tseslint from 'typescript-eslint';
|
import tseslint from 'typescript-eslint';
|
||||||
import prettier from 'eslint-config-prettier';
|
import prettier from 'eslint-config-prettier';
|
||||||
|
import noPrimitiveValue from './eslint/no-primitive-value-properties.js';
|
||||||
|
|
||||||
export default tseslint.config(
|
export default tseslint.config(
|
||||||
{ ignores: ['dist', 'node_modules', 'coverage', 'allium-main'] },
|
{ ignores: ['dist', 'node_modules', 'coverage', 'allium-main'] },
|
||||||
@ -17,9 +18,17 @@ export default tseslint.config(
|
|||||||
tsconfigRootDir: import.meta.dirname,
|
tsconfigRootDir: import.meta.dirname,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
plugins: {
|
||||||
|
rpg: {
|
||||||
|
rules: {
|
||||||
|
'no-primitive-value-properties': noPrimitiveValue,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
rules: {
|
rules: {
|
||||||
'@typescript-eslint/switch-exhaustiveness-check': 'warn',
|
'@typescript-eslint/switch-exhaustiveness-check': 'warn',
|
||||||
'@typescript-eslint/no-unnecessary-condition': 'warn',
|
'@typescript-eslint/no-unnecessary-condition': 'warn',
|
||||||
|
'rpg/no-primitive-value-properties': 'warn',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
145
eslint/no-primitive-value-properties.js
Normal file
145
eslint/no-primitive-value-properties.js
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
import { readdirSync } from 'node:fs';
|
||||||
|
import { join, parse } from 'node:path';
|
||||||
|
|
||||||
|
const VALUE_OBJECTS_DIR = join(import.meta.dirname, '..', 'src', 'value-objects');
|
||||||
|
|
||||||
|
function discoverValueObjects() {
|
||||||
|
const files = readdirSync(VALUE_OBJECTS_DIR).filter((f) => f.endsWith('.ts') && f !== 'index.ts');
|
||||||
|
return files.map((f) => parse(f).name);
|
||||||
|
}
|
||||||
|
|
||||||
|
const VALUE_OBJECTS = discoverValueObjects();
|
||||||
|
|
||||||
|
function buildPropertyMap() {
|
||||||
|
const map = {};
|
||||||
|
for (const name of VALUE_OBJECTS) {
|
||||||
|
const propertyName = name.charAt(0).toLowerCase() + name.slice(1);
|
||||||
|
map[propertyName] = name;
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PROPERTY_MAP = buildPropertyMap();
|
||||||
|
|
||||||
|
/** Map ESLint AST type names to the primitive keyword string. */
|
||||||
|
const PRIMITIVE_TYPE_MAP = {
|
||||||
|
TSNumberKeyword: 'number',
|
||||||
|
TSStringKeyword: 'string',
|
||||||
|
TSBooleanKeyword: 'boolean',
|
||||||
|
};
|
||||||
|
|
||||||
|
function isPrimitiveType(typeNode) {
|
||||||
|
if (!typeNode) return false;
|
||||||
|
const keyword = PRIMITIVE_TYPE_MAP[typeNode.type];
|
||||||
|
return keyword !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPrimitiveKeyword(typeNode) {
|
||||||
|
if (!typeNode) return null;
|
||||||
|
return PRIMITIVE_TYPE_MAP[typeNode.type] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getParamName(param) {
|
||||||
|
if (param.type === 'TSParameterProperty') {
|
||||||
|
return getParamName(param.parameter);
|
||||||
|
}
|
||||||
|
if (param.type === 'Identifier') {
|
||||||
|
return param.name;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
meta: {
|
||||||
|
type: 'suggestion',
|
||||||
|
docs: {
|
||||||
|
description:
|
||||||
|
'Warn when primitive types are used in class properties or constructor parameters that should be value objects',
|
||||||
|
recommended: false,
|
||||||
|
},
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
includeConstructorParams: { type: 'boolean', default: true },
|
||||||
|
includeProperties: { type: 'boolean', default: true },
|
||||||
|
},
|
||||||
|
additionalProperties: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
messages: {
|
||||||
|
primitiveProperty:
|
||||||
|
'Use value object "{{valueObject}}" instead of primitive "{{primitive}}" for property "{{propertyName}}". Available: {{available}}',
|
||||||
|
primitiveParam:
|
||||||
|
'Use value object "{{valueObject}}" instead of primitive "{{primitive}}" for parameter "{{paramName}}". Available: {{available}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
create(context) {
|
||||||
|
const options = context.options[0] || {};
|
||||||
|
const includeConstructorParams = options.includeConstructorParams !== false;
|
||||||
|
const includeProperties = options.includeProperties !== false;
|
||||||
|
|
||||||
|
function reportPrimitive(node, propName, primitiveType, messageId) {
|
||||||
|
const valueObject = PROPERTY_MAP[propName];
|
||||||
|
if (valueObject) {
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
messageId,
|
||||||
|
data: {
|
||||||
|
valueObject,
|
||||||
|
primitive: primitiveType,
|
||||||
|
propertyName: propName,
|
||||||
|
paramName: propName,
|
||||||
|
available: VALUE_OBJECTS.join(', '),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ClassProperty(node) {
|
||||||
|
if (!includeProperties) return;
|
||||||
|
if (!node.typeAnnotation) return;
|
||||||
|
|
||||||
|
const typeNode = node.typeAnnotation.typeAnnotation;
|
||||||
|
if (!isPrimitiveType(typeNode)) return;
|
||||||
|
|
||||||
|
reportPrimitive(node, node.key.name, getPrimitiveKeyword(typeNode), 'primitiveProperty');
|
||||||
|
},
|
||||||
|
|
||||||
|
PropertyDefinition(node) {
|
||||||
|
if (!includeProperties) return;
|
||||||
|
if (!node.typeAnnotation) return;
|
||||||
|
|
||||||
|
const typeNode = node.typeAnnotation.typeAnnotation;
|
||||||
|
if (!isPrimitiveType(typeNode)) return;
|
||||||
|
|
||||||
|
reportPrimitive(node, node.key.name, getPrimitiveKeyword(typeNode), 'primitiveProperty');
|
||||||
|
},
|
||||||
|
|
||||||
|
MethodDefinition(node) {
|
||||||
|
if (!includeConstructorParams) return;
|
||||||
|
if (node.key.name !== 'constructor') return;
|
||||||
|
if (!node.value.params) return;
|
||||||
|
|
||||||
|
for (const param of node.value.params) {
|
||||||
|
let paramName = null;
|
||||||
|
let typeNode = null;
|
||||||
|
|
||||||
|
if (param.type === 'TSParameterProperty') {
|
||||||
|
paramName = getParamName(param.parameter);
|
||||||
|
typeNode = param.parameter.typeAnnotation?.typeAnnotation;
|
||||||
|
} else if (param.type === 'Identifier') {
|
||||||
|
paramName = param.name;
|
||||||
|
typeNode = param.typeAnnotation?.typeAnnotation;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!paramName || !typeNode || !isPrimitiveType(typeNode)) continue;
|
||||||
|
|
||||||
|
reportPrimitive(param, paramName, getPrimitiveKeyword(typeNode), 'primitiveParam');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -38,7 +38,7 @@ describe('Magical Objects', () => {
|
|||||||
const target = Character.create({ name: 'goblin', level: Level.create(1) });
|
const target = Character.create({ name: 'goblin', level: Level.create(1) });
|
||||||
const weapon = MagicalWeapon.create({ damage, maxHealth: weaponHP, owner: attacker });
|
const weapon = MagicalWeapon.create({ damage, maxHealth: weaponHP, owner: attacker });
|
||||||
const result = attacker.useWeapon(weapon, target);
|
const result = attacker.useWeapon(weapon, target);
|
||||||
return result.weapon.health === weaponHP - 1;
|
return result.weapon.health.value === weaponHP - 1;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -86,11 +86,11 @@ describe('Magical Objects', () => {
|
|||||||
// Kill the attacker using a separate killer
|
// Kill the attacker using a separate killer
|
||||||
const killer = Character.create({ name: 'boss', level: Level.create(1) });
|
const killer = Character.create({ name: 'boss', level: Level.create(1) });
|
||||||
const deadAttacker = killer.dealDamage(attacker, 10000);
|
const deadAttacker = killer.dealDamage(attacker, 10000);
|
||||||
const weaponHPBefore = weapon.health;
|
const weaponHPBefore = weapon.health.value;
|
||||||
const targetHealthBefore = target.health.value;
|
const targetHealthBefore = target.health.value;
|
||||||
const result = deadAttacker.useWeapon(weapon, target);
|
const result = deadAttacker.useWeapon(weapon, target);
|
||||||
return (
|
return (
|
||||||
result.weapon.health === weaponHPBefore &&
|
result.weapon.health.value === weaponHPBefore &&
|
||||||
result.target.health.value === targetHealthBefore
|
result.target.health.value === targetHealthBefore
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -110,11 +110,11 @@ describe('Magical Objects', () => {
|
|||||||
const thief = Character.create({ name: 'thief', level: Level.create(1) });
|
const thief = Character.create({ name: 'thief', level: Level.create(1) });
|
||||||
const target = Character.create({ name: 'goblin', level: Level.create(1) });
|
const target = Character.create({ name: 'goblin', level: Level.create(1) });
|
||||||
const weapon = MagicalWeapon.create({ damage, maxHealth: weaponHP, owner });
|
const weapon = MagicalWeapon.create({ damage, maxHealth: weaponHP, owner });
|
||||||
const weaponHPBefore = weapon.health;
|
const weaponHPBefore = weapon.health.value;
|
||||||
const targetHealthBefore = target.health.value;
|
const targetHealthBefore = target.health.value;
|
||||||
const result = thief.useWeapon(weapon, target);
|
const result = thief.useWeapon(weapon, target);
|
||||||
return (
|
return (
|
||||||
result.weapon.health === weaponHPBefore &&
|
result.weapon.health.value === weaponHPBefore &&
|
||||||
result.target.health.value === targetHealthBefore
|
result.target.health.value === targetHealthBefore
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -136,7 +136,9 @@ describe('Magical Objects', () => {
|
|||||||
const targetHealthBefore = firstUse.target.health.value;
|
const targetHealthBefore = firstUse.target.health.value;
|
||||||
// Try to use again on the destroyed weapon
|
// Try to use again on the destroyed weapon
|
||||||
const result = owner.useWeapon(destroyedWeapon, firstUse.target);
|
const result = owner.useWeapon(destroyedWeapon, firstUse.target);
|
||||||
return result.weapon.health === 0 && result.target.health.value === targetHealthBefore;
|
return (
|
||||||
|
result.weapon.health.value === 0 && result.target.health.value === targetHealthBefore
|
||||||
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -203,7 +205,7 @@ describe('Magical Objects', () => {
|
|||||||
});
|
});
|
||||||
const object = HealingObject.create({ maxHealth: objectHP, currentHealth: objectHP });
|
const object = HealingObject.create({ maxHealth: objectHP, currentHealth: objectHP });
|
||||||
const result = character.useHealingObject(object, healAmount);
|
const result = character.useHealingObject(object, healAmount);
|
||||||
return result.object.health === objectHP - healAmount;
|
return result.object.health.value === objectHP - healAmount;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -255,11 +257,11 @@ describe('Magical Objects', () => {
|
|||||||
// Kill the character using a separate killer
|
// Kill the character using a separate killer
|
||||||
const killer = Character.create({ name: 'boss', level: Level.create(1) });
|
const killer = Character.create({ name: 'boss', level: Level.create(1) });
|
||||||
const deadCharacter = killer.dealDamage(character, 10000);
|
const deadCharacter = killer.dealDamage(character, 10000);
|
||||||
const objectHPBefore = object.health;
|
const objectHPBefore = object.health.value;
|
||||||
const characterHealthBefore = deadCharacter.health.value;
|
const characterHealthBefore = deadCharacter.health.value;
|
||||||
const result = deadCharacter.useHealingObject(object, 100);
|
const result = deadCharacter.useHealingObject(object, 100);
|
||||||
return (
|
return (
|
||||||
result.object.health === objectHPBefore &&
|
result.object.health.value === objectHPBefore &&
|
||||||
result.character.health.value === characterHealthBefore
|
result.character.health.value === characterHealthBefore
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
@ -287,7 +289,8 @@ describe('Magical Objects', () => {
|
|||||||
// Try to use again on the destroyed object
|
// Try to use again on the destroyed object
|
||||||
const result = healedCharacter.useHealingObject(destroyedObject, 100);
|
const result = healedCharacter.useHealingObject(destroyedObject, 100);
|
||||||
return (
|
return (
|
||||||
result.object.health === 0 && result.character.health.value === characterHealthBefore
|
result.object.health.value === 0 &&
|
||||||
|
result.character.health.value === characterHealthBefore
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -305,7 +308,7 @@ describe('Magical Objects', () => {
|
|||||||
const target = Character.create({ name: 'goblin', level: Level.create(1) });
|
const target = Character.create({ name: 'goblin', level: Level.create(1) });
|
||||||
const weapon = MagicalWeapon.create({ damage, maxHealth: weaponHP, owner: attacker });
|
const weapon = MagicalWeapon.create({ damage, maxHealth: weaponHP, owner: attacker });
|
||||||
const result = attacker.useWeapon(weapon, target);
|
const result = attacker.useWeapon(weapon, target);
|
||||||
return result.weapon.health >= 0;
|
return result.weapon.health.value >= 0;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -320,7 +323,7 @@ describe('Magical Objects', () => {
|
|||||||
const character = Character.create({ name: 'hero', level: Level.create(1) });
|
const character = Character.create({ name: 'hero', level: Level.create(1) });
|
||||||
const object = HealingObject.create({ maxHealth: objectHP, currentHealth: objectHP });
|
const object = HealingObject.create({ maxHealth: objectHP, currentHealth: objectHP });
|
||||||
const result = character.useHealingObject(object, healAmount);
|
const result = character.useHealingObject(object, healAmount);
|
||||||
return result.object.health >= 0;
|
return result.object.health.value >= 0;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -336,7 +339,7 @@ describe('Magical Objects', () => {
|
|||||||
const target = Character.create({ name: 'goblin', level: Level.create(1) });
|
const target = Character.create({ name: 'goblin', level: Level.create(1) });
|
||||||
const weapon = MagicalWeapon.create({ damage, maxHealth: weaponHP, owner: attacker });
|
const weapon = MagicalWeapon.create({ damage, maxHealth: weaponHP, owner: attacker });
|
||||||
const result = attacker.useWeapon(weapon, target);
|
const result = attacker.useWeapon(weapon, target);
|
||||||
return result.weapon.health <= weaponHP;
|
return result.weapon.health.value <= weaponHP;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -351,7 +354,7 @@ describe('Magical Objects', () => {
|
|||||||
const character = Character.create({ name: 'hero', level: Level.create(1) });
|
const character = Character.create({ name: 'hero', level: Level.create(1) });
|
||||||
const object = HealingObject.create({ maxHealth: objectHP, currentHealth: objectHP });
|
const object = HealingObject.create({ maxHealth: objectHP, currentHealth: objectHP });
|
||||||
const result = character.useHealingObject(object, healAmount);
|
const result = character.useHealingObject(object, healAmount);
|
||||||
return result.object.health <= objectHP;
|
return result.object.health.value <= objectHP;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -7,14 +7,15 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Character } from '../characters/Character.ts';
|
import { Character } from '../characters/Character.ts';
|
||||||
|
import { Health } from '../value-objects/Health.ts';
|
||||||
import { Level } from '../value-objects/Level.ts';
|
import { Level } from '../value-objects/Level.ts';
|
||||||
import { MagicalObject } from './MagicalObject.ts';
|
import { MagicalObject } from './MagicalObject.ts';
|
||||||
import type { Healer } from './magical-object-types.ts';
|
import type { Healer } from './magical-object-types.ts';
|
||||||
|
|
||||||
export class HealingObject extends MagicalObject implements Healer {
|
export class HealingObject extends MagicalObject implements Healer {
|
||||||
private constructor(
|
private constructor(
|
||||||
health: number,
|
health: Health,
|
||||||
maxHealth: number,
|
maxHealth: Health,
|
||||||
status: { readonly kind: 'alive' } | { readonly kind: 'destroyed' },
|
status: { readonly kind: 'alive' } | { readonly kind: 'destroyed' },
|
||||||
) {
|
) {
|
||||||
super(health, maxHealth, status);
|
super(health, maxHealth, status);
|
||||||
@ -32,7 +33,7 @@ export class HealingObject extends MagicalObject implements Healer {
|
|||||||
if (currentHealth > maxHealth) throw new Error('CurrentHealth cannot exceed maxHealth');
|
if (currentHealth > maxHealth) throw new Error('CurrentHealth cannot exceed maxHealth');
|
||||||
const status =
|
const status =
|
||||||
currentHealth === 0 ? { kind: 'destroyed' as const } : { kind: 'alive' as const };
|
currentHealth === 0 ? { kind: 'destroyed' as const } : { kind: 'alive' as const };
|
||||||
return new HealingObject(currentHealth, maxHealth, status);
|
return new HealingObject(Health.create(currentHealth), Health.create(maxHealth), status);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Use this object to heal a character. Returns updated object and character. */
|
/** Use this object to heal a character. Returns updated object and character. */
|
||||||
@ -44,7 +45,7 @@ export class HealingObject extends MagicalObject implements Healer {
|
|||||||
// Negative amount is invalid
|
// Negative amount is invalid
|
||||||
if (amount < 0) throw new Error('Heal amount must be non-negative');
|
if (amount < 0) throw new Error('Heal amount must be non-negative');
|
||||||
// Calculate actual heal amount: min of requested, object remaining, character headroom
|
// Calculate actual heal amount: min of requested, object remaining, character headroom
|
||||||
const objectRemaining = this.health;
|
const objectRemaining = this.health.value;
|
||||||
const characterMax = Level.maxHealthForLevel(character.level.value);
|
const characterMax = Level.maxHealthForLevel(character.level.value);
|
||||||
const characterHeadroom = characterMax - character.health.value;
|
const characterHeadroom = characterMax - character.health.value;
|
||||||
const actualHeal = Math.min(amount, objectRemaining, characterHeadroom);
|
const actualHeal = Math.min(amount, objectRemaining, characterHeadroom);
|
||||||
@ -53,7 +54,7 @@ export class HealingObject extends MagicalObject implements Healer {
|
|||||||
return { object: this, character };
|
return { object: this, character };
|
||||||
}
|
}
|
||||||
// Create updated object
|
// Create updated object
|
||||||
const newObjectHealth = this.health - actualHeal;
|
const newObjectHealth = this.health.value - actualHeal;
|
||||||
const newObjectStatus =
|
const newObjectStatus =
|
||||||
newObjectHealth === 0 ? { kind: 'destroyed' as const } : { kind: 'alive' as const };
|
newObjectHealth === 0 ? { kind: 'destroyed' as const } : { kind: 'alive' as const };
|
||||||
// Create updated character
|
// Create updated character
|
||||||
@ -64,7 +65,7 @@ export class HealingObject extends MagicalObject implements Healer {
|
|||||||
health: newCharacterHealth,
|
health: newCharacterHealth,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
object: new HealingObject(newObjectHealth, this.maxHealth, newObjectStatus),
|
object: new HealingObject(Health.create(newObjectHealth), this.maxHealth, newObjectStatus),
|
||||||
character: newCharacter,
|
character: newCharacter,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,24 +7,26 @@
|
|||||||
* - Status derived from health (0 = destroyed, > 0 = alive)
|
* - Status derived from health (0 = destroyed, > 0 = alive)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Health } from '../value-objects/Health.ts';
|
||||||
|
|
||||||
export type MagicalObjectStatus = { readonly kind: 'alive' } | { readonly kind: 'destroyed' };
|
export type MagicalObjectStatus = { readonly kind: 'alive' } | { readonly kind: 'destroyed' };
|
||||||
|
|
||||||
export class MagicalObject {
|
export class MagicalObject {
|
||||||
readonly #health: number;
|
readonly #health: Health;
|
||||||
readonly #maxHealth: number;
|
readonly #maxHealth: Health;
|
||||||
readonly #status: MagicalObjectStatus;
|
readonly #status: MagicalObjectStatus;
|
||||||
|
|
||||||
protected constructor(health: number, maxHealth: number, status: MagicalObjectStatus) {
|
protected constructor(health: Health, maxHealth: Health, status: MagicalObjectStatus) {
|
||||||
this.#health = health;
|
this.#health = health;
|
||||||
this.#maxHealth = maxHealth;
|
this.#maxHealth = maxHealth;
|
||||||
this.#status = status;
|
this.#status = status;
|
||||||
}
|
}
|
||||||
|
|
||||||
get health(): number {
|
get health(): Health {
|
||||||
return this.#health;
|
return this.#health;
|
||||||
}
|
}
|
||||||
|
|
||||||
get maxHealth(): number {
|
get maxHealth(): Health {
|
||||||
return this.#maxHealth;
|
return this.#maxHealth;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,7 +37,7 @@ export class MagicalObject {
|
|||||||
/** Create a destroyed object (health = 0). */
|
/** Create a destroyed object (health = 0). */
|
||||||
static createDestroyed(maxHealth: number): MagicalObject {
|
static createDestroyed(maxHealth: number): MagicalObject {
|
||||||
if (maxHealth < 0) throw new Error('MaxHealth cannot be negative');
|
if (maxHealth < 0) throw new Error('MaxHealth cannot be negative');
|
||||||
return new MagicalObject(0, maxHealth, { kind: 'destroyed' });
|
return new MagicalObject(Health.create(0), Health.create(maxHealth), { kind: 'destroyed' });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Check if this object is alive. */
|
/** Check if this object is alive. */
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
* - Damage is non-negative
|
* - Damage is non-negative
|
||||||
*/
|
*/
|
||||||
import { Character } from '../characters/Character.ts';
|
import { Character } from '../characters/Character.ts';
|
||||||
|
import { Health } from '../value-objects/Health.ts';
|
||||||
import { MagicalObject } from './MagicalObject.ts';
|
import { MagicalObject } from './MagicalObject.ts';
|
||||||
import type { DamageDealer } from './magical-object-types.ts';
|
import type { DamageDealer } from './magical-object-types.ts';
|
||||||
|
|
||||||
@ -14,8 +15,8 @@ export class MagicalWeapon extends MagicalObject implements DamageDealer {
|
|||||||
readonly #owner: Character;
|
readonly #owner: Character;
|
||||||
|
|
||||||
private constructor(
|
private constructor(
|
||||||
health: number,
|
health: Health,
|
||||||
maxHealth: number,
|
maxHealth: Health,
|
||||||
status: { readonly kind: 'alive' } | { readonly kind: 'destroyed' },
|
status: { readonly kind: 'alive' } | { readonly kind: 'destroyed' },
|
||||||
damage: number,
|
damage: number,
|
||||||
owner: Character,
|
owner: Character,
|
||||||
@ -36,7 +37,13 @@ export class MagicalWeapon extends MagicalObject implements DamageDealer {
|
|||||||
}): MagicalWeapon {
|
}): MagicalWeapon {
|
||||||
if (maxHealth < 0) throw new Error('MaxHealth cannot be negative');
|
if (maxHealth < 0) throw new Error('MaxHealth cannot be negative');
|
||||||
if (damage < 0) throw new Error('Damage cannot be negative');
|
if (damage < 0) throw new Error('Damage cannot be negative');
|
||||||
return new MagicalWeapon(maxHealth, maxHealth, { kind: 'alive' }, damage, owner);
|
return new MagicalWeapon(
|
||||||
|
Health.create(maxHealth),
|
||||||
|
Health.create(maxHealth),
|
||||||
|
{ kind: 'alive' },
|
||||||
|
damage,
|
||||||
|
owner,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get damage(): number {
|
get damage(): number {
|
||||||
@ -63,12 +70,12 @@ export class MagicalWeapon extends MagicalObject implements DamageDealer {
|
|||||||
status: newTargetStatus,
|
status: newTargetStatus,
|
||||||
});
|
});
|
||||||
// Reduce weapon health by 1
|
// Reduce weapon health by 1
|
||||||
const newWeaponHealth = this.health - 1;
|
const newWeaponHealth = this.health.value - 1;
|
||||||
const newWeaponStatus =
|
const newWeaponStatus =
|
||||||
newWeaponHealth === 0 ? { kind: 'destroyed' as const } : { kind: 'alive' as const };
|
newWeaponHealth === 0 ? { kind: 'destroyed' as const } : { kind: 'alive' as const };
|
||||||
return {
|
return {
|
||||||
weapon: new MagicalWeapon(
|
weapon: new MagicalWeapon(
|
||||||
newWeaponHealth,
|
Health.create(newWeaponHealth),
|
||||||
this.maxHealth,
|
this.maxHealth,
|
||||||
newWeaponStatus,
|
newWeaponStatus,
|
||||||
this.#damage,
|
this.#damage,
|
||||||
|
|||||||
@ -6,19 +6,20 @@
|
|||||||
* so Character can depend on abstractions rather than concrete classes.
|
* so Character can depend on abstractions rather than concrete classes.
|
||||||
*/
|
*/
|
||||||
import type { Character } from '../characters/Character.ts';
|
import type { Character } from '../characters/Character.ts';
|
||||||
|
import type { Health } from '../value-objects/Health.ts';
|
||||||
import type { MagicalObjectStatus } from './MagicalObject.ts';
|
import type { MagicalObjectStatus } from './MagicalObject.ts';
|
||||||
|
|
||||||
/** A magical object that deals damage — from Character's point of view */
|
/** A magical object that deals damage — from Character's point of view */
|
||||||
export interface DamageDealer {
|
export interface DamageDealer {
|
||||||
readonly owner: Character;
|
readonly owner: Character;
|
||||||
readonly health: number;
|
readonly health: Health;
|
||||||
readonly status: MagicalObjectStatus;
|
readonly status: MagicalObjectStatus;
|
||||||
use(target: Character): { weapon: DamageDealer; target: Character };
|
use(target: Character): { weapon: DamageDealer; target: Character };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A magical object that heals — from Character's point of view */
|
/** A magical object that heals — from Character's point of view */
|
||||||
export interface Healer {
|
export interface Healer {
|
||||||
readonly health: number;
|
readonly health: Health;
|
||||||
readonly status: MagicalObjectStatus;
|
readonly status: MagicalObjectStatus;
|
||||||
heal(character: Character, amount: number): { object: Healer; character: Character };
|
heal(character: Character, amount: number): { object: Healer; character: Character };
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
13112
transcripts/eslint-rule-against-value-objects-and-yaks-to-refactor.html
Normal file
13112
transcripts/eslint-rule-against-value-objects-and-yaks-to-refactor.html
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user