Files
ashen-realms/apps/web/src/app/features/npc/merchant-page.component.html
Bastian Wagner ff8b7c8b78 feat(web): run quest steps from the NPC screen
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 23:19:16 +02:00

371 lines
13 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@if (store.loading()) {
<p class="merchant__status">Approaching…</p>
} @else if (store.error()) {
<div class="merchant__status merchant__status--error" role="alert">
<p>{{ store.error() }}</p>
<button type="button" class="merchant__button" (click)="leave()">
Back to the gate
</button>
</div>
} @else if (store.interaction(); as interaction) {
<article class="merchant">
<header class="merchant__identity">
<img
class="merchant__portrait"
[src]="interaction.npc.portraitPath"
[alt]="interaction.npc.name"
(error)="($any($event.target).style.visibility = 'hidden')"
/>
<div class="merchant__naming">
<h1 class="merchant__name">{{ interaction.npc.name }}</h1>
@if (interaction.npc.title) {
<p class="merchant__title">{{ interaction.npc.title }}</p>
}
@if (interaction.npc.description) {
<p class="merchant__description">{{ interaction.npc.description }}</p>
}
</div>
</header>
@if (interaction.dialogue) {
<blockquote class="merchant__dialogue" data-dialogue>
{{ interaction.dialogue.text }}
</blockquote>
}
<nav class="merchant__actions" aria-label="Interactions">
@for (action of interaction.availableActions; track action.type) {
<button
type="button"
class="merchant__button"
[class.merchant__button--active]="
(action.type === 'OPEN_EXCHANGE' && isPanel('EXCHANGE')) ||
(action.type === 'OPEN_SHOP' && isPanel('SHOP')) ||
(action.type === 'VIEW_QUESTS' && isPanel('QUESTS')) ||
(action.type === 'TALK' && isPanel('DIALOGUE'))
"
[attr.data-action]="action.type"
(click)="activate(action.type, action.key)"
>
{{ action.label }}
</button>
}
<button type="button" class="merchant__button" (click)="leave()">
Leave
</button>
</nav>
@if (store.actionError()) {
<p class="merchant__error" role="alert">{{ store.actionError() }}</p>
}
@if (store.questLine(); as line) {
<blockquote class="merchant__dialogue" data-quest-line aria-live="polite">
{{ line }}
</blockquote>
}
@if (store.grantedBag(); as bag) {
<section class="bag-granted" data-granted-bag aria-live="polite">
<h2 class="bag-granted__heading">New Loot Bag</h2>
<p class="bag-granted__name">{{ bag.name }}</p>
<p class="bag-granted__capacity">{{ capacityLabel(bag) }}</p>
<button
type="button"
class="merchant__link"
(click)="store.dismissGrantedBag()"
>
Dismiss
</button>
</section>
}
@if (isPanel('QUESTS')) {
<section class="merchant__panel" aria-label="Quests">
<h2 class="merchant__panel-title">Matters at Hand</h2>
@for (quest of store.quests(); track quest.key) {
<article class="npc-quest" [attr.data-quest]="quest.key">
<h3 class="npc-quest__title">{{ quest.title }}</h3>
<p class="npc-quest__description">{{ quest.description }}</p>
@if (currentObjective(quest); as objective) {
<app-quest-objective-line
[objective]="objective"
[hint]="quest.hint"
/>
}
@if (quest.status === 'AVAILABLE') {
<button
type="button"
class="merchant__button"
data-quest-accept
[disabled]="store.pending() !== null"
(click)="store.acceptQuest(quest.key)"
>
Accept
</button>
} @else if (quest.status === 'ACTIVE' && isCurrentStepHere(quest)) {
<button
type="button"
class="merchant__button"
data-quest-advance
[disabled]="store.pending() !== null"
(click)="store.advanceQuest(quest.key)"
>
Continue
</button>
}
</article>
} @empty {
<p class="merchant__empty">There is nothing to discuss.</p>
}
</section>
}
@if (isPanel('EXCHANGE') && store.exchange(); as exchange) {
<section class="merchant__panel" aria-label="Trade in goods">
<h2 class="merchant__panel-title">{{ exchange.profileName }}</h2>
<app-loot-capacity-strip [capacities]="exchange.capacities" />
@if (exchange.offers.length === 0) {
<p class="merchant__empty">There is nothing here he will take.</p>
} @else {
<ul class="trade-list">
@for (offer of exchange.offers; track offer.itemKey) {
<li
class="trade-row"
[class.trade-row--empty]="offer.quantityCarried === 0"
[attr.data-trade-item]="offer.itemKey"
>
<img
class="trade-row__icon"
[src]="offer.iconPath"
[alt]=""
aria-hidden="true"
/>
<div class="trade-row__naming">
<span class="trade-row__name">{{ offer.itemName }}</span>
<span class="trade-row__carried" data-carried>
Carrying {{ offer.quantityCarried }}
</span>
</div>
<div class="trade-row__value" data-value>
<span class="trade-row__silver"
>{{ offer.silverPerStep }} Silver</span
>
<span class="trade-row__rep"
>+{{ offer.reputationPerStep }}
{{ offer.factionName }}</span
>
</div>
<div class="trade-row__picker">
<button
type="button"
class="trade-row__step"
aria-label="Trade fewer"
[disabled]="
offer.quantityCarried === 0 || store.pending() !== null
"
(click)="step(offer.itemKey, -1, offer.inputQuantity)"
>
</button>
<input
class="trade-row__quantity"
type="number"
min="0"
[max]="offer.quantityCarried"
[step]="offer.inputQuantity"
[value]="quantityFor(offer.itemKey)"
[disabled]="
offer.quantityCarried === 0 || store.pending() !== null
"
[attr.aria-label]="'Quantity of ' + offer.itemName"
(input)="
onQuantityInput(offer.itemKey, $any($event.target).value)
"
/>
<button
type="button"
class="trade-row__step"
aria-label="Trade more"
[disabled]="
offer.quantityCarried === 0 || store.pending() !== null
"
(click)="step(offer.itemKey, 1, offer.inputQuantity)"
>
+
</button>
</div>
</li>
}
</ul>
<footer class="trade-footer">
<p class="trade-footer__preview" data-preview>
Selected:
<strong>{{ store.preview().silver }} Silver</strong>
and
<strong>{{ store.preview().reputation }} Reputation</strong>
</p>
<div class="trade-footer__buttons">
<button
type="button"
class="merchant__button"
[disabled]="store.pending() !== null"
(click)="store.selectAll()"
>
Select All
</button>
<button
type="button"
class="merchant__button merchant__button--primary"
data-trade-confirm
[disabled]="!store.hasSelection() || store.pending() !== null"
(click)="store.tradeSelected()"
>
{{ store.pending() === 'trade' ? 'Handing over…' : 'Trade Selected' }}
</button>
</div>
</footer>
}
@if (store.lastTrade(); as trade) {
<section class="trade-summary" data-trade-summary aria-live="polite">
<h3 class="trade-summary__title">Trade Complete</h3>
<ul class="trade-summary__consumed">
@for (entry of trade.consumed; track entry.itemKey) {
<li>{{ entry.quantity }} × {{ entry.itemName }} handed in</li>
}
</ul>
<ul class="trade-summary__rewards">
<li>+{{ trade.rewards.silver }} Silver</li>
<li>+{{ trade.rewards.regionalReputation }} Border Watch Reputation</li>
@if (trade.rewards.worldRenown > 0) {
<li class="trade-summary__renown" data-renown>
+{{ trade.rewards.worldRenown }} World Renown
</li>
}
</ul>
@if (trade.reputationRankChanged) {
<p class="trade-summary__rank" data-rank-changed>
The Border Watch now regards you differently.
</p>
}
<button
type="button"
class="merchant__button"
(click)="store.dismissTradeSummary()"
>
Done
</button>
</section>
}
</section>
}
@if (isPanel('SHOP') && store.shop(); as shop) {
<section class="merchant__panel" aria-label="Wares for sale">
<h2 class="merchant__panel-title">{{ shop.shopName }}</h2>
<p class="merchant__purse" data-purse>{{ shop.silver }} Silver</p>
@if (store.newlyUnlocked().length > 0) {
<p class="shop-unlocked" data-unlocked aria-live="polite">
@for (name of store.newlyUnlocked(); track name) {
<span>New merchant offer unlocked: {{ name }}</span>
}
<button
type="button"
class="merchant__link"
(click)="store.dismissUnlocked()"
>
Dismiss
</button>
</p>
}
<ul class="shop-list">
@for (offer of shop.offers; track offer.itemKey) {
<li class="shop-row" [attr.data-shop-item]="offer.itemKey">
<img
class="shop-row__icon"
[src]="offer.iconPath"
[alt]=""
aria-hidden="true"
/>
<div class="shop-row__naming">
<span class="shop-row__name">{{ offer.itemName }}</span>
@if (offer.itemDescription) {
<span class="shop-row__description">{{
offer.itemDescription
}}</span>
}
@if (offer.effectSummary) {
<span class="shop-row__effect" data-effect>{{
offer.effectSummary
}}</span>
}
@for (
requirement of offer.requirements;
track requirement.label
) {
<span
class="shop-row__requirement"
[class.shop-row__requirement--met]="requirement.met"
data-requirement
>
{{ requirement.label }}
@if (!requirement.met && requirement.current !== null) {
<span class="shop-row__current"
>· Current: {{ requirement.current }}</span
>
}
</span>
}
</div>
<span class="shop-row__price">{{ offer.price }} Silver</span>
<button
type="button"
class="merchant__button"
[disabled]="
!offer.unlocked ||
!offer.affordable ||
store.pending() !== null
"
(click)="store.buy(offer.itemKey)"
>
@if (!offer.unlocked) {
Locked
} @else if (!offer.affordable) {
Too costly
} @else {
Buy
}
</button>
</li>
}
</ul>
@if (store.lastPurchase(); as purchase) {
<p class="shop-receipt" data-purchase aria-live="polite">
Bought {{ purchase.quantity }} × {{ purchase.itemName }} for
{{ purchase.silverSpent }} Silver.
<button
type="button"
class="merchant__link"
(click)="store.dismissPurchase()"
>
Dismiss
</button>
</p>
}
</section>
}
</article>
}