+
-
{message}
+
+
{message}
+ {branchName && (
+
+ {branchName}
+
+
+ )}
+
)}
{status === 'error' && (
@@ -137,12 +157,16 @@ export function PublishView() {
>
)}
diff --git a/studio/apps/web-react/src/ui/WeaponKeySelect.tsx b/studio/apps/web-react/src/ui/WeaponKeySelect.tsx
new file mode 100644
index 0000000..2cb652c
--- /dev/null
+++ b/studio/apps/web-react/src/ui/WeaponKeySelect.tsx
@@ -0,0 +1,20 @@
+import type { PawnType } from '@game-data/presentation';
+import { inputClass } from './ui-kit';
+import { weaponKeysForType } from './weaponKeys';
+
+interface Props {
+ readonly type: PawnType;
+ readonly value: string;
+ readonly onChange: (weaponKey: string) => void;
+}
+
+export function WeaponKeySelect({ type, value, onChange }: Props) {
+ return (
+
+ );
+}
diff --git a/studio/apps/web-react/src/ui/weaponKeys.ts b/studio/apps/web-react/src/ui/weaponKeys.ts
new file mode 100644
index 0000000..6a97fb1
--- /dev/null
+++ b/studio/apps/web-react/src/ui/weaponKeys.ts
@@ -0,0 +1,12 @@
+import weaponKeysData from '../../../../../data/weaponKeys.json';
+import type { PawnType } from '@game-data/presentation';
+
+const weaponKeys: Record
= weaponKeysData;
+
+export function weaponKeysForType(type: PawnType): readonly string[] {
+ return weaponKeys[type];
+}
+
+export function weaponKeyAfterTypeChange(currentWeaponKey: string, type: PawnType): string {
+ return weaponKeysForType(type).includes(currentWeaponKey) ? currentWeaponKey : '';
+}
diff --git a/studio/src/presentation/index.ts b/studio/src/presentation/index.ts
index eafb439..d8c8aae 100644
--- a/studio/src/presentation/index.ts
+++ b/studio/src/presentation/index.ts
@@ -15,6 +15,7 @@ export {
type UpdateCommanderViewModel,
} from './presenters/UpdateCommanderPresenter.ts';
export {
+ commanderListItemToForm,
createEmptyCommanderForm,
type CommanderFormModel,
} from './models/CommanderFormModel.ts';
diff --git a/studio/src/presentation/models/CommanderFormModel.ts b/studio/src/presentation/models/CommanderFormModel.ts
index 84f1c16..d32702b 100644
--- a/studio/src/presentation/models/CommanderFormModel.ts
+++ b/studio/src/presentation/models/CommanderFormModel.ts
@@ -1,3 +1,5 @@
+import type { CommanderListItem } from '@game-data/application';
+
export interface CommanderFormModel {
readonly id: string;
readonly name: string;
@@ -21,6 +23,27 @@ export interface CommanderFormModel {
readonly innateSkills?: readonly string[];
}
+export function commanderListItemToForm(item: CommanderListItem): CommanderFormModel {
+ return {
+ id: item.id,
+ name: item.name,
+ description: item.description ?? '',
+ icon: item.icon ?? '',
+ pawnMax: item.pawnMax,
+ health: item.health,
+ maxDefenseLevel: item.maxDefenseLevel,
+ wallVisualSet: item.wallVisualSet,
+ defensePowerPerLevel: item.defensePowerPerLevel,
+ pawnDefinitionIdByColor: item.pawnDefinitionIdByColor,
+ commanderPawnDefinitionIds: item.commanderPawnDefinitionIds,
+ officerPawnDefinitionIds: item.officerPawnDefinitionIds,
+ movementsPerTurn: item.movementsPerTurn,
+ freeRecruitThreshold: item.freeRecruitThreshold,
+ skills: item.skills ?? [],
+ innateSkills: item.innateSkills ?? [],
+ };
+}
+
export function createEmptyCommanderForm(): CommanderFormModel {
return {
id: '',
diff --git a/studio/tests/presentation/models/CommanderFormModel.test.ts b/studio/tests/presentation/models/CommanderFormModel.test.ts
new file mode 100644
index 0000000..9b3fd15
--- /dev/null
+++ b/studio/tests/presentation/models/CommanderFormModel.test.ts
@@ -0,0 +1,30 @@
+import { describe, expect, it } from 'vitest';
+import type { CommanderListItem } from '../../../src/application/use-cases/ListCommanders';
+import { commanderListItemToForm } from '../../../src/presentation/models/CommanderFormModel';
+
+describe('CommanderFormModel', () => {
+ it('préremplit le seuil de recrutement gratuit du commander sélectionné', () => {
+ const commander: CommanderListItem = {
+ id: 'commander-1',
+ name: 'Commander',
+ pawnMax: 40,
+ health: 100,
+ maxDefenseLevel: 2,
+ wallVisualSet: 'default',
+ defensePowerPerLevel: 6,
+ pawnDefinitionIdByColor: {
+ red: 'pawn-red',
+ blue: 'pawn-blue',
+ green: 'pawn-green',
+ },
+ commanderPawnDefinitionIds: [],
+ officerPawnDefinitionIds: [],
+ movementsPerTurn: 3,
+ freeRecruitThreshold: 20,
+ };
+
+ const form = commanderListItemToForm(commander);
+
+ expect(form.freeRecruitThreshold).toBe(20);
+ });
+});
diff --git a/studio/tests/ui/WeaponKeySelect.test.ts b/studio/tests/ui/WeaponKeySelect.test.ts
new file mode 100644
index 0000000..ec700f5
--- /dev/null
+++ b/studio/tests/ui/WeaponKeySelect.test.ts
@@ -0,0 +1,26 @@
+import { describe, expect, it } from 'vitest';
+import {
+ weaponKeyAfterTypeChange,
+ weaponKeysForType,
+} from '../../apps/web-react/src/ui/weaponKeys';
+
+describe('WeaponKeySelect', () => {
+ it('propose uniquement les armes de mêlée pour un pion melee', () => {
+ expect(weaponKeysForType('melee')).toEqual([
+ 'sword',
+ 'spear',
+ 'wooden_spiked_club',
+ 'woodcutter_poleaxe',
+ 'short_iron_katana',
+ ]);
+ });
+
+ it('propose uniquement les projectiles pour un pion ranged', () => {
+ expect(weaponKeysForType('ranged')).toEqual(['arrow', 'shuriken']);
+ });
+
+ it('retire une arme devenue incompatible après un changement de type', () => {
+ expect(weaponKeyAfterTypeChange('sword', 'ranged')).toBe('');
+ expect(weaponKeyAfterTypeChange('arrow', 'ranged')).toBe('arrow');
+ });
+});