feat(inventory): remove the requiredLevel equip gate from the detail panel
The API can no longer emit ITEM_LEVEL_REQUIREMENT_NOT_MET (Task 8 removed the level gate server-side) and InventoryItem.item no longer carries requiredLevel (Task 12). Drop the characterLevel input, the meetsLevelRequirement computed, the associated template branch and copy, and the dead German error-message mapping. Update fixtures across the inventory specs to the real InventoryItem/CharacterResponse shapes.
This commit is contained in:
@@ -6,7 +6,7 @@
|
|||||||
<h2 class="inventory-detail__name" data-detail-name>{{ item.item.name }}</h2>
|
<h2 class="inventory-detail__name" data-detail-name>{{ item.item.name }}</h2>
|
||||||
<p class="inventory-detail__rarity" data-detail-rarity>{{ rarityLabel() }}</p>
|
<p class="inventory-detail__rarity" data-detail-rarity>{{ rarityLabel() }}</p>
|
||||||
@if (slotLabel(); as slot) {
|
@if (slotLabel(); as slot) {
|
||||||
<p class="inventory-detail__meta">{{ slot }} · Stufe {{ item.item.requiredLevel }}</p>
|
<p class="inventory-detail__meta">{{ slot }}</p>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -38,10 +38,6 @@
|
|||||||
<span class="inventory-detail__equipped" data-detail-equipped>Ausgerüstet</span>
|
<span class="inventory-detail__equipped" data-detail-equipped>Ausgerüstet</span>
|
||||||
} @else if (!isEquippable()) {
|
} @else if (!isEquippable()) {
|
||||||
<span class="inventory-detail__note">Nicht ausrüstbar</span>
|
<span class="inventory-detail__note">Nicht ausrüstbar</span>
|
||||||
} @else if (!meetsLevelRequirement()) {
|
|
||||||
<button type="button" class="inventory-detail__equip" data-detail-equip disabled>
|
|
||||||
Benötigt Stufe {{ item.item.requiredLevel }}
|
|
||||||
</button>
|
|
||||||
} @else {
|
} @else {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ const wornSword: InventoryItem = {
|
|||||||
name: 'Abgenutztes Kurzschwert',
|
name: 'Abgenutztes Kurzschwert',
|
||||||
rarity: 'COMMON',
|
rarity: 'COMMON',
|
||||||
equipmentSlot: 'WEAPON',
|
equipmentSlot: 'WEAPON',
|
||||||
requiredLevel: 1,
|
|
||||||
weaponDamage: 8,
|
weaponDamage: 8,
|
||||||
bonusAttack: 0,
|
bonusAttack: 0,
|
||||||
bonusHp: 0,
|
bonusHp: 0,
|
||||||
@@ -30,7 +29,6 @@ const banditBlade: InventoryItem = {
|
|||||||
name: 'Räuberklinge',
|
name: 'Räuberklinge',
|
||||||
rarity: 'COMMON',
|
rarity: 'COMMON',
|
||||||
equipmentSlot: 'WEAPON',
|
equipmentSlot: 'WEAPON',
|
||||||
requiredLevel: 1,
|
|
||||||
weaponDamage: 11,
|
weaponDamage: 11,
|
||||||
bonusAttack: 1,
|
bonusAttack: 1,
|
||||||
bonusHp: 0,
|
bonusHp: 0,
|
||||||
@@ -42,7 +40,6 @@ const banditBlade: InventoryItem = {
|
|||||||
async function setup(overrides: {
|
async function setup(overrides: {
|
||||||
item?: InventoryItem | null;
|
item?: InventoryItem | null;
|
||||||
equippedItemInSlot?: InventoryItem | null;
|
equippedItemInSlot?: InventoryItem | null;
|
||||||
characterLevel?: number;
|
|
||||||
busy?: boolean;
|
busy?: boolean;
|
||||||
}) {
|
}) {
|
||||||
TestBed.resetTestingModule();
|
TestBed.resetTestingModule();
|
||||||
@@ -50,7 +47,6 @@ async function setup(overrides: {
|
|||||||
const fixture = TestBed.createComponent(InventoryDetailPanelComponent);
|
const fixture = TestBed.createComponent(InventoryDetailPanelComponent);
|
||||||
fixture.componentRef.setInput('item', overrides.item ?? null);
|
fixture.componentRef.setInput('item', overrides.item ?? null);
|
||||||
fixture.componentRef.setInput('equippedItemInSlot', overrides.equippedItemInSlot ?? null);
|
fixture.componentRef.setInput('equippedItemInSlot', overrides.equippedItemInSlot ?? null);
|
||||||
fixture.componentRef.setInput('characterLevel', overrides.characterLevel ?? 1);
|
|
||||||
fixture.componentRef.setInput('busy', overrides.busy ?? false);
|
fixture.componentRef.setInput('busy', overrides.busy ?? false);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
return fixture;
|
return fixture;
|
||||||
@@ -79,12 +75,13 @@ describe('InventoryDetailPanelComponent', () => {
|
|||||||
expect(text).toContain('+1');
|
expect(text).toContain('+1');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows a disabled Benötigt Stufe X button when the level requirement is not met', async () => {
|
it('always shows the plain equip button for an owned equippable item', async () => {
|
||||||
const fixture = await setup({ item: { ...banditBlade, item: { ...banditBlade.item, requiredLevel: 5 } }, characterLevel: 1 });
|
const fixture = await setup({ item: banditBlade });
|
||||||
const button = (fixture.nativeElement as HTMLElement).querySelector<HTMLButtonElement>('[data-detail-equip]');
|
const element = fixture.nativeElement as HTMLElement;
|
||||||
|
|
||||||
expect(button?.textContent).toContain('Benötigt Stufe 5');
|
const button = element.querySelector<HTMLButtonElement>('[data-detail-equip]');
|
||||||
expect(button?.disabled).toBe(true);
|
expect(button?.disabled).toBe(false);
|
||||||
|
expect(button?.textContent?.trim()).toBe('Ausrüsten');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('emits equip with the characterItemId when Ausrüsten is clicked', async () => {
|
it('emits equip with the characterItemId when Ausrüsten is clicked', async () => {
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ const STAT_LABELS: ReadonlyArray<{ label: string; key: StatKey }> = [
|
|||||||
export class InventoryDetailPanelComponent {
|
export class InventoryDetailPanelComponent {
|
||||||
readonly item = input<InventoryItem | null>(null);
|
readonly item = input<InventoryItem | null>(null);
|
||||||
readonly equippedItemInSlot = input<InventoryItem | null>(null);
|
readonly equippedItemInSlot = input<InventoryItem | null>(null);
|
||||||
readonly characterLevel = input(1);
|
|
||||||
readonly busy = input(false);
|
readonly busy = input(false);
|
||||||
readonly equip = output<string>();
|
readonly equip = output<string>();
|
||||||
|
|
||||||
@@ -66,11 +65,6 @@ export class InventoryDetailPanelComponent {
|
|||||||
|
|
||||||
protected readonly isEquippable = computed(() => !!this.item()?.item.equipmentSlot);
|
protected readonly isEquippable = computed(() => !!this.item()?.item.equipmentSlot);
|
||||||
|
|
||||||
protected readonly meetsLevelRequirement = computed(() => {
|
|
||||||
const item = this.item();
|
|
||||||
return item ? item.item.requiredLevel <= this.characterLevel() : true;
|
|
||||||
});
|
|
||||||
|
|
||||||
protected onEquip(): void {
|
protected onEquip(): void {
|
||||||
const item = this.item();
|
const item = this.item();
|
||||||
if (item) {
|
if (item) {
|
||||||
|
|||||||
@@ -26,7 +26,6 @@
|
|||||||
<app-inventory-detail-panel
|
<app-inventory-detail-panel
|
||||||
[item]="inventoryStore.selectedItem()"
|
[item]="inventoryStore.selectedItem()"
|
||||||
[equippedItemInSlot]="equippedItemInSelectedSlot()"
|
[equippedItemInSlot]="equippedItemInSelectedSlot()"
|
||||||
[characterLevel]="characterLevel()"
|
|
||||||
[busy]="inventoryStore.equipping()"
|
[busy]="inventoryStore.equipping()"
|
||||||
(equip)="equipSelected($event)"
|
(equip)="equipSelected($event)"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ const inventory: InventoryResponse = {
|
|||||||
name: 'Abgenutztes Kurzschwert',
|
name: 'Abgenutztes Kurzschwert',
|
||||||
rarity: 'COMMON',
|
rarity: 'COMMON',
|
||||||
equipmentSlot: 'WEAPON',
|
equipmentSlot: 'WEAPON',
|
||||||
requiredLevel: 1,
|
|
||||||
weaponDamage: 8,
|
weaponDamage: 8,
|
||||||
bonusAttack: 0,
|
bonusAttack: 0,
|
||||||
bonusHp: 0,
|
bonusHp: 0,
|
||||||
@@ -36,7 +35,6 @@ const inventory: InventoryResponse = {
|
|||||||
name: 'Räuberklinge',
|
name: 'Räuberklinge',
|
||||||
rarity: 'COMMON',
|
rarity: 'COMMON',
|
||||||
equipmentSlot: 'WEAPON',
|
equipmentSlot: 'WEAPON',
|
||||||
requiredLevel: 1,
|
|
||||||
weaponDamage: 11,
|
weaponDamage: 11,
|
||||||
bonusAttack: 1,
|
bonusAttack: 1,
|
||||||
bonusHp: 0,
|
bonusHp: 0,
|
||||||
@@ -63,8 +61,7 @@ const equipment: EquipmentResponse = {
|
|||||||
const character: CharacterResponse = {
|
const character: CharacterResponse = {
|
||||||
id: 'character-1',
|
id: 'character-1',
|
||||||
name: 'Aric Duskwalker',
|
name: 'Aric Duskwalker',
|
||||||
level: 1,
|
renown: 0,
|
||||||
experience: 0,
|
|
||||||
silver: 0,
|
silver: 0,
|
||||||
currentHp: 100,
|
currentHp: 100,
|
||||||
maxHp: 100,
|
maxHp: 100,
|
||||||
@@ -178,7 +175,6 @@ describe('InventoryPageComponent', () => {
|
|||||||
name: 'Eiserner Helm',
|
name: 'Eiserner Helm',
|
||||||
rarity: 'COMMON',
|
rarity: 'COMMON',
|
||||||
equipmentSlot: 'HEAD',
|
equipmentSlot: 'HEAD',
|
||||||
requiredLevel: 1,
|
|
||||||
weaponDamage: 0,
|
weaponDamage: 0,
|
||||||
bonusAttack: 0,
|
bonusAttack: 0,
|
||||||
bonusHp: 5,
|
bonusHp: 5,
|
||||||
|
|||||||
@@ -37,8 +37,6 @@ export class InventoryPageComponent implements OnInit {
|
|||||||
protected readonly slotOrder = SLOT_ORDER;
|
protected readonly slotOrder = SLOT_ORDER;
|
||||||
protected readonly slotLabels = SLOT_LABELS;
|
protected readonly slotLabels = SLOT_LABELS;
|
||||||
|
|
||||||
protected readonly characterLevel = computed(() => this.worldStore.character()?.level ?? 1);
|
|
||||||
|
|
||||||
protected readonly equippedItemInSelectedSlot = computed(() => {
|
protected readonly equippedItemInSelectedSlot = computed(() => {
|
||||||
const selected = this.inventoryStore.selectedItem();
|
const selected = this.inventoryStore.selectedItem();
|
||||||
if (!selected?.item.equipmentSlot) {
|
if (!selected?.item.equipmentSlot) {
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ const inventory: InventoryResponse = {
|
|||||||
name: 'Abgenutztes Kurzschwert',
|
name: 'Abgenutztes Kurzschwert',
|
||||||
rarity: 'COMMON',
|
rarity: 'COMMON',
|
||||||
equipmentSlot: 'WEAPON',
|
equipmentSlot: 'WEAPON',
|
||||||
requiredLevel: 1,
|
|
||||||
weaponDamage: 8,
|
weaponDamage: 8,
|
||||||
bonusAttack: 0,
|
bonusAttack: 0,
|
||||||
bonusHp: 0,
|
bonusHp: 0,
|
||||||
@@ -35,7 +34,6 @@ const inventory: InventoryResponse = {
|
|||||||
name: 'Räuberklinge',
|
name: 'Räuberklinge',
|
||||||
rarity: 'COMMON',
|
rarity: 'COMMON',
|
||||||
equipmentSlot: 'WEAPON',
|
equipmentSlot: 'WEAPON',
|
||||||
requiredLevel: 1,
|
|
||||||
weaponDamage: 11,
|
weaponDamage: 11,
|
||||||
bonusAttack: 1,
|
bonusAttack: 1,
|
||||||
bonusHp: 0,
|
bonusHp: 0,
|
||||||
@@ -135,11 +133,11 @@ describe('InventoryStore', () => {
|
|||||||
it('surfaces a German message for a known equip error', async () => {
|
it('surfaces a German message for a known equip error', async () => {
|
||||||
await store.load();
|
await store.load();
|
||||||
api.equipItem.mockReturnValue(
|
api.equipItem.mockReturnValue(
|
||||||
throwError(() => new HttpErrorResponse({ error: { code: 'ITEM_LEVEL_REQUIREMENT_NOT_MET' }, status: 400 })),
|
throwError(() => new HttpErrorResponse({ error: { code: 'ITEM_NOT_EQUIPPABLE' }, status: 400 })),
|
||||||
);
|
);
|
||||||
|
|
||||||
await store.equip('item-blade');
|
await store.equip('item-blade');
|
||||||
|
|
||||||
expect(store.error()).toBe('Du erfüllst die Stufenanforderung nicht.');
|
expect(store.error()).toBe('Dieser Gegenstand kann nicht ausgerüstet werden.');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ const EQUIPMENT_ERROR_MESSAGES: Readonly<Record<string, string>> = {
|
|||||||
CHARACTER_ITEM_NOT_FOUND: 'Dieser Gegenstand konnte nicht gefunden werden.',
|
CHARACTER_ITEM_NOT_FOUND: 'Dieser Gegenstand konnte nicht gefunden werden.',
|
||||||
ITEM_NOT_OWNED: 'Dieser Gegenstand gehört dir nicht.',
|
ITEM_NOT_OWNED: 'Dieser Gegenstand gehört dir nicht.',
|
||||||
ITEM_NOT_EQUIPPABLE: 'Dieser Gegenstand kann nicht ausgerüstet werden.',
|
ITEM_NOT_EQUIPPABLE: 'Dieser Gegenstand kann nicht ausgerüstet werden.',
|
||||||
ITEM_LEVEL_REQUIREMENT_NOT_MET: 'Du erfüllst die Stufenanforderung nicht.',
|
|
||||||
INVALID_EQUIPMENT_SLOT: 'Dieser Ausrüstungsplatz ist ungültig.',
|
INVALID_EQUIPMENT_SLOT: 'Dieser Ausrüstungsplatz ist ungültig.',
|
||||||
CHARACTER_IN_COMBAT: 'Ausrüstung kann während eines Kampfes nicht geändert werden.',
|
CHARACTER_IN_COMBAT: 'Ausrüstung kann während eines Kampfes nicht geändert werden.',
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user