diff --git a/src/samples/slay-the-spire-like/data/effectDesert.csv.d.ts b/src/samples/slay-the-spire-like/data/effectDesert.csv.d.ts new file mode 100644 index 0000000..0c471cf --- /dev/null +++ b/src/samples/slay-the-spire-like/data/effectDesert.csv.d.ts @@ -0,0 +1,10 @@ +type EffectDesertTable = readonly { + readonly id: string; + readonly name: string; + readonly description: string; +}[]; + +export type EffectDesert = EffectDesertTable[number]; + +declare const data: EffectDesertTable; +export default data; diff --git a/src/samples/slay-the-spire-like/data/encounterDesert.csv.d.ts b/src/samples/slay-the-spire-like/data/encounterDesert.csv.d.ts index 76124b9..c36c42a 100644 --- a/src/samples/slay-the-spire-like/data/encounterDesert.csv.d.ts +++ b/src/samples/slay-the-spire-like/data/encounterDesert.csv.d.ts @@ -1,7 +1,11 @@ +import type { EnemyDesert } from './enemyDesert.csv'; + type EncounterDesertTable = readonly { readonly type: "minion" | "elite" | "event" | "shop" | "camp" | "curio"; readonly name: string; readonly description: string; + readonly enemies: readonly [EnemyDesert, number]; + readonly dialogue: string; }[]; export type EncounterDesert = EncounterDesertTable[number]; diff --git a/src/samples/slay-the-spire-like/data/enemyDesert.csv.d.ts b/src/samples/slay-the-spire-like/data/enemyDesert.csv.d.ts new file mode 100644 index 0000000..7c7e084 --- /dev/null +++ b/src/samples/slay-the-spire-like/data/enemyDesert.csv.d.ts @@ -0,0 +1,11 @@ +type EnemyDesertTable = readonly { + readonly id: string; + readonly initHp: number; + readonly initBuffs: readonly [string, number]; + readonly initialIntent: string; +}[]; + +export type EnemyDesert = EnemyDesertTable[number]; + +declare const data: EnemyDesertTable; +export default data; diff --git a/src/samples/slay-the-spire-like/data/enemyIntentDesert.csv.d.ts b/src/samples/slay-the-spire-like/data/enemyIntentDesert.csv.d.ts new file mode 100644 index 0000000..0c264ce --- /dev/null +++ b/src/samples/slay-the-spire-like/data/enemyIntentDesert.csv.d.ts @@ -0,0 +1,15 @@ +import type { EnemyDesert } from './enemyDesert.csv'; +import type { EffectDesert } from './effectDesert.csv'; + +type EnemyIntentDesertTable = readonly { + readonly enemy: EnemyDesert; + readonly intent: string; + readonly nextIntents: readonly string[]; + readonly brokenIntent: readonly string[]; + readonly effects: readonly ["self" | "opponent", EffectDesert, number]; +}[]; + +export type EnemyIntentDesert = EnemyIntentDesertTable[number]; + +declare const data: EnemyIntentDesertTable; +export default data; diff --git a/src/samples/slay-the-spire-like/data/index.ts b/src/samples/slay-the-spire-like/data/index.ts index f45ffea..3d7bc32 100644 --- a/src/samples/slay-the-spire-like/data/index.ts +++ b/src/samples/slay-the-spire-like/data/index.ts @@ -1,6 +1,16 @@ import heroItemFighter1Csv from './heroItemFighter1.csv'; import encounterDesertCsv from './encounterDesert.csv'; +import enemyDesertCsv from './enemyDesert.csv'; +import enemyIntentDesertCsv from './enemyIntentDesert.csv'; +import effectDesertCsv from './effectDesert.csv'; export const heroItemFighter1Data = heroItemFighter1Csv; export const encounterDesertData = encounterDesertCsv; +export const enemyDesertData = enemyDesertCsv; +export const enemyIntentDesertData = enemyIntentDesertCsv; +export const effectDesertData = effectDesertCsv; + export { default as encounterDesertCsv, type EncounterDesert } from './encounterDesert.csv'; +export { default as enemyDesertCsv, type EnemyDesert } from './enemyDesert.csv'; +export { default as enemyIntentDesertCsv, type EnemyIntentDesert } from './enemyIntentDesert.csv'; +export { default as effectDesertCsv, type EffectDesert } from './effectDesert.csv'; diff --git a/tests/samples/slay-the-spire-like/data/index.test.ts b/tests/samples/slay-the-spire-like/data/index.test.ts index 51c2ba9..e8ee18c 100644 --- a/tests/samples/slay-the-spire-like/data/index.test.ts +++ b/tests/samples/slay-the-spire-like/data/index.test.ts @@ -1,5 +1,11 @@ import { describe, it, expect } from 'vitest'; -import { heroItemFighter1Data } from '@/samples/slay-the-spire-like/data'; +import { + heroItemFighter1Data, + encounterDesertData, + enemyDesertData, + enemyIntentDesertData, + effectDesertData, +} from '@/samples/slay-the-spire-like/data'; describe('heroItemFighter1.csv import', () => { it('should import data as an array', () => { @@ -8,7 +14,6 @@ describe('heroItemFighter1.csv import', () => { }); it('should have expected number of items', () => { - // CSV has 24 data rows (excluding header and type rows) expect(heroItemFighter1Data.length).toBe(24); }); @@ -71,3 +76,162 @@ describe('heroItemFighter1.csv import', () => { expect(typeCounts['tool']).toBe(6); }); }); + +describe('encounterDesert.csv import', () => { + it('should import data as an array', () => { + expect(Array.isArray(encounterDesertData)).toBe(true); + expect(encounterDesertData.length).toBeGreaterThan(0); + }); + + it('should have correct encounter type counts', () => { + const typeCounts = encounterDesertData.reduce((acc, e) => { + acc[e.type] = (acc[e.type] || 0) + 1; + return acc; + }, {} as Record); + + expect(typeCounts['minion']).toBe(10); + expect(typeCounts['elite']).toBe(4); + expect(typeCounts['shop']).toBe(2); + expect(typeCounts['camp']).toBe(2); + expect(typeCounts['curio']).toBe(8); + expect(typeCounts['event']).toBe(1); + }); + + it('should have enemies for combat encounters', () => { + for (const e of encounterDesertData) { + if (e.type === 'minion' || e.type === 'elite') { + expect(Array.isArray(e.enemies)).toBe(true); + expect(e.enemies.length).toBeGreaterThan(0); + for (const [enemyId, bonusHp] of e.enemies) { + expect(typeof enemyId).toBe('string'); + expect(typeof bonusHp).toBe('number'); + } + } + } + }); + + it('should have empty enemies for non-combat encounters', () => { + for (const e of encounterDesertData) { + if (e.type === 'shop' || e.type === 'camp') { + expect(e.enemies.length).toBe(0); + } + } + }); + + it('should have dialogue for curio and event encounters', () => { + for (const e of encounterDesertData) { + if (e.type === 'curio' || e.type === 'event') { + expect(e.dialogue).toBeTruthy(); + expect(e.dialogue.startsWith('desert_')).toBe(true); + } + } + }); +}); + +describe('effectDesert.csv import', () => { + it('should import data as an array', () => { + expect(Array.isArray(effectDesertData)).toBe(true); + expect(effectDesertData.length).toBeGreaterThan(0); + }); + + it('should have expected number of effects', () => { + expect(effectDesertData.length).toBe(19); + }); + + it('should have correct fields for each effect', () => { + for (const effect of effectDesertData) { + expect(effect).toHaveProperty('id'); + expect(effect).toHaveProperty('name'); + expect(effect).toHaveProperty('description'); + } + }); + + it('should contain core effect types', () => { + const ids = effectDesertData.map(e => e.id); + expect(ids).toContain('attack'); + expect(ids).toContain('defend'); + expect(ids).toContain('spike'); + expect(ids).toContain('venom'); + }); +}); + +describe('enemyDesert.csv import', () => { + it('should import data as an array', () => { + expect(Array.isArray(enemyDesertData)).toBe(true); + expect(enemyDesertData.length).toBeGreaterThan(0); + }); + + it('should have expected number of enemies', () => { + expect(enemyDesertData.length).toBe(14); + }); + + it('should have correct fields for each enemy', () => { + for (const enemy of enemyDesertData) { + expect(enemy).toHaveProperty('id'); + expect(enemy).toHaveProperty('initHp'); + expect(enemy).toHaveProperty('initBuffs'); + expect(enemy).toHaveProperty('initialIntent'); + expect(typeof enemy.initHp).toBe('number'); + expect(typeof enemy.initialIntent).toBe('string'); + } + }); + + it('should have valid HP ranges', () => { + for (const enemy of enemyDesertData) { + expect(enemy.initHp).toBeGreaterThan(0); + } + }); + + it('should have minions with lower HP than elites', () => { + const minionIds = ['仙人掌怪', '蛇', '木乃伊', '枪手', '风卷草', '秃鹫', '沙蝎', '幼沙虫', '蜥蜴', '沙匪']; + const eliteIds = ['风暴之灵', '骑马枪手', '沙虫王', '沙漠守卫']; + const byId = Object.fromEntries(enemyDesertData.map(e => [e.id, e])); + + for (const id of minionIds) { + expect(byId[id].initHp).toBeLessThan(40); + } + for (const id of eliteIds) { + expect(byId[id].initHp).toBeGreaterThanOrEqual(40); + } + }); +}); + +describe('enemyIntentDesert.csv import', () => { + it('should import data as an array', () => { + expect(Array.isArray(enemyIntentDesertData)).toBe(true); + expect(enemyIntentDesertData.length).toBeGreaterThan(0); + }); + + it('should have expected number of intent rows', () => { + expect(enemyIntentDesertData.length).toBe(41); + }); + + it('should have correct fields for each intent', () => { + for (const intent of enemyIntentDesertData) { + expect(intent).toHaveProperty('enemy'); + expect(intent).toHaveProperty('intent'); + expect(intent).toHaveProperty('nextIntents'); + expect(intent).toHaveProperty('brokenIntent'); + expect(intent).toHaveProperty('effects'); + expect(intent.enemy).toHaveProperty('id'); + expect(Array.isArray(intent.nextIntents)).toBe(true); + expect(Array.isArray(intent.brokenIntent)).toBe(true); + expect(Array.isArray(intent.effects)).toBe(true); + } + }); + + it('should have effects with target, effect ref, and value', () => { + for (const intent of enemyIntentDesertData) { + for (const [target, effectId, value] of intent.effects) { + expect(target === 'self' || target === 'opponent').toBe(true); + expect(typeof effectId).toBe('string'); + expect(typeof value).toBe('number'); + } + } + }); + + it('should cover all 14 enemies', () => { + const enemyIds = new Set(enemyIntentDesertData.map(i => typeof i.enemy === 'string' ? i.enemy : i.enemy.id)); + expect(enemyIds.size).toBe(14); + }); +});