diff --git a/apps/web/src/app/features/hunting/encounter-card/encounter-card.component.html b/apps/web/src/app/features/hunting/encounter-card/encounter-card.component.html new file mode 100644 index 0000000..5d46fcd --- /dev/null +++ b/apps/web/src/app/features/hunting/encounter-card/encounter-card.component.html @@ -0,0 +1,15 @@ +
+
+ +
+
+

{{ encounter.monster.name }}

+ Stufe {{ encounter.monster.level }} + + +
+
diff --git a/apps/web/src/app/features/hunting/encounter-card/encounter-card.component.scss b/apps/web/src/app/features/hunting/encounter-card/encounter-card.component.scss new file mode 100644 index 0000000..85198b3 --- /dev/null +++ b/apps/web/src/app/features/hunting/encounter-card/encounter-card.component.scss @@ -0,0 +1,108 @@ +:host { + display: block; +} + +.encounter-card { + display: grid; + overflow: hidden; + border: 1px solid var(--ar-border-highlight); + border-radius: var(--ar-radius-md); + background: + linear-gradient(125deg, rgb(255 255 255 / 0.045), transparent 42%), rgb(12 15 17 / 0.96); + box-shadow: var(--ar-shadow-raised); +} + +.encounter-card__artwork-frame { + position: relative; + overflow: hidden; + aspect-ratio: 4 / 3; + border-block-end: 1px solid rgb(155 122 66 / 0.45); + background: #050607; +} + +.encounter-card__artwork-frame::after { + content: ''; + position: absolute; + inset: 0; + box-shadow: inset 0 -3.5rem 3rem -1.5rem rgb(0 0 0 / 0.75); + pointer-events: none; +} + +.encounter-card__artwork { + inline-size: 100%; + block-size: 100%; + object-fit: cover; + object-position: center; +} + +.encounter-card__body { + display: grid; + justify-items: center; + gap: var(--ar-space-2); + padding: var(--ar-space-4); + text-align: center; +} + +.encounter-card__name { + margin: 0; + color: var(--ar-text); + font-family: Georgia, 'Times New Roman', serif; + font-size: 1.4rem; + font-weight: 400; +} + +.encounter-card__level { + color: var(--ar-gold); + font-size: var(--ar-font-sm); + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.encounter-card__danger { + margin-block: var(--ar-space-1); +} + +.encounter-card__attack { + inline-size: 100%; + margin-block-start: var(--ar-space-2); + padding: var(--ar-space-2) var(--ar-space-4); + border: 1px solid var(--ar-border-highlight); + border-radius: var(--ar-radius-sm); + color: var(--ar-text); + background: linear-gradient(180deg, #263b4b, #17232d); + cursor: pointer; + font-family: Georgia, 'Times New Roman', serif; + font-size: 1rem; + letter-spacing: 0.02em; +} + +.encounter-card__attack:hover { + border-color: #d6b26b; + background: linear-gradient(180deg, #315067, #1a2c3a); +} + +.encounter-card__attack:focus-visible { + outline: 2px solid var(--ar-blue); + outline-offset: 2px; +} + +@media (prefers-reduced-motion: no-preference) { + .encounter-card { + transition: + border-color var(--ar-motion-base), + box-shadow var(--ar-motion-base); + } + + .encounter-card:hover { + border-color: #d6b26b; + box-shadow: + var(--ar-shadow-raised), + 0 0 1.1rem rgb(214 178 107 / 0.35); + } + + .encounter-card__attack { + transition: + border-color var(--ar-motion-fast), + background var(--ar-motion-fast); + } +} diff --git a/apps/web/src/app/features/hunting/encounter-card/encounter-card.component.spec.ts b/apps/web/src/app/features/hunting/encounter-card/encounter-card.component.spec.ts new file mode 100644 index 0000000..e5459c7 --- /dev/null +++ b/apps/web/src/app/features/hunting/encounter-card/encounter-card.component.spec.ts @@ -0,0 +1,58 @@ +import { TestBed } from '@angular/core/testing'; +import { vi } from 'vitest'; +import type { HuntEncounter } from '../../../core/api/game-api.models'; +import { EncounterCardComponent } from './encounter-card.component'; + +const dawnwolfEncounter: HuntEncounter = { + id: 'encounter-id-9f2c', + monster: { + key: 'dawnwolf', + name: 'Dämmerwolf', + level: 3, + artworkPath: '/images/enemies/Dawnwolf.png', + }, + dangerRating: 'MATCH', +}; + +describe('EncounterCardComponent', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [EncounterCardComponent], + }).compileComponents(); + }); + + it('renders the monster name, level, danger label, and artwork', () => { + const fixture = TestBed.createComponent(EncounterCardComponent); + fixture.componentRef.setInput('encounter', dawnwolfEncounter); + fixture.detectChanges(); + + const element = fixture.nativeElement as HTMLElement; + const img = element.querySelector('img'); + + expect(element.textContent).toContain('Dämmerwolf'); + expect(element.textContent).toContain('Stufe 3'); + expect(element.textContent).toContain('Passend'); + expect(img?.getAttribute('src')).toBe('/images/enemies/Dawnwolf.png'); + expect(img?.getAttribute('alt')).toBe('Dämmerwolf'); + }); + + it('emits the encounter id (never the monster key) when Angreifen is clicked', () => { + const fixture = TestBed.createComponent(EncounterCardComponent); + fixture.componentRef.setInput('encounter', dawnwolfEncounter); + fixture.detectChanges(); + + const emitted = vi.fn(); + fixture.componentInstance.attack.subscribe(emitted); + + const button = Array.from(fixture.nativeElement.querySelectorAll('button')).find( + (candidate): candidate is HTMLButtonElement => + candidate instanceof HTMLButtonElement && candidate.textContent?.trim() === 'Angreifen', + ); + button?.click(); + + expect(emitted).toHaveBeenCalledOnce(); + expect(emitted).toHaveBeenCalledWith(dawnwolfEncounter.id); + expect(emitted).not.toHaveBeenCalledWith(dawnwolfEncounter.monster.key); + expect(dawnwolfEncounter.id).not.toBe(dawnwolfEncounter.monster.key); + }); +}); diff --git a/apps/web/src/app/features/hunting/encounter-card/encounter-card.component.ts b/apps/web/src/app/features/hunting/encounter-card/encounter-card.component.ts new file mode 100644 index 0000000..c741057 --- /dev/null +++ b/apps/web/src/app/features/hunting/encounter-card/encounter-card.component.ts @@ -0,0 +1,18 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { HuntEncounter } from '../../../core/api/game-api.models'; +import { DangerBadgeComponent } from '../../../shared/danger-badge/danger-badge.component'; + +@Component({ + selector: 'app-encounter-card', + imports: [DangerBadgeComponent], + templateUrl: './encounter-card.component.html', + styleUrl: './encounter-card.component.scss', +}) +export class EncounterCardComponent { + @Input({ required: true }) encounter!: HuntEncounter; + @Output() readonly attack = new EventEmitter(); + + protected onAttack(): void { + this.attack.emit(this.encounter.id); + } +}