ranged combat refinement
This commit is contained in:
+17
-2
@@ -157,15 +157,30 @@ button {
|
||||
color: var(--muted);
|
||||
}
|
||||
.field input,
|
||||
.field select,
|
||||
.field textarea {
|
||||
padding: 0.45rem 0.55rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--input-border);
|
||||
background: var(--input-bg);
|
||||
background-color: var(--input-bg);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* Do not use `background:` shorthand on select — it clears the chevron from global `select` rules */
|
||||
.field select {
|
||||
padding: 0.45rem 0.55rem;
|
||||
padding-right: 2rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--input-border);
|
||||
background-color: var(--input-bg);
|
||||
background-image: var(--select-chevron);
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 0.55rem center;
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.grid-2 {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
||||
@@ -55,12 +55,6 @@ function rowById<T extends { id: string }>(rows: T[], id: string): T | undefined
|
||||
return rows.find((r) => r.id === id);
|
||||
}
|
||||
|
||||
function parseBonusDamage(bonusDamage: string, rangeIndex: number): number {
|
||||
const parts = bonusDamage.split('/').map((p) => parseInt(p.trim(), 10));
|
||||
const v = parts[rangeIndex];
|
||||
return Number.isFinite(v) ? v : 0;
|
||||
}
|
||||
|
||||
function rangeIndex(id: RangeId): number {
|
||||
const order: RangeId[] = ['very_near', 'near', 'medium', 'far', 'very_far'];
|
||||
return order.indexOf(id);
|
||||
@@ -198,9 +192,12 @@ export function computeRangedTarget(
|
||||
const finalTarget = baseTarget - totalModifier;
|
||||
|
||||
const ri = rangeIndex(selections.range);
|
||||
const bonus = parseBonusDamage(weapon.bonusDamage, ri);
|
||||
const bonus =
|
||||
ri >= 0 && ri < weapon.bonusDamageByRange.length ? weapon.bonusDamageByRange[ri] : 0;
|
||||
const damageSummary =
|
||||
bonus === 0 ? `${weapon.damage}` : `${weapon.damage} + ${bonus} (Entfernungs-Bonus)`;
|
||||
bonus === 0
|
||||
? `${weapon.damage}`
|
||||
: `${weapon.damage} ${bonus > 0 ? `+ ${bonus}` : `− ${Math.abs(bonus)}`} (Entfernungs-Bonus)`;
|
||||
|
||||
return {
|
||||
fkBase,
|
||||
|
||||
@@ -9,15 +9,26 @@ export type RangedWeaponId =
|
||||
/** Maps to Kampftalent id in talents (Fernkampf subtypes). */
|
||||
export type RangedSkillId = 'bogen' | 'armbrust' | 'wurfwaffe' | 'schleuder';
|
||||
|
||||
/**
|
||||
* Maximale Reichweite in Schritt je Fernkampf-Distanzstufe.
|
||||
* Reihenfolge wie in der Regeltabelle / wie `RANGES` in `modifiers-ranged.ts`:
|
||||
* sehr nah, nah, mittel, weit, extrem weit.
|
||||
*/
|
||||
export type WeaponRangeStepsSchritt = readonly [number, number, number, number, number];
|
||||
|
||||
/**
|
||||
* Entfernungs-Bonusschaden (TP) je Distanzstufe.
|
||||
* Reihenfolge wie `RANGES` / `rangeStepsSchritt`: sehr nah, nah, mittel, weit, extrem weit.
|
||||
*/
|
||||
export type WeaponBonusDamageByRange = readonly [number, number, number, number, number];
|
||||
|
||||
export type RangedWeaponDef = {
|
||||
id: RangedWeaponId;
|
||||
name: string;
|
||||
skill: RangedSkillId;
|
||||
damage: string;
|
||||
/** Entfernungs-Bonusschaden pro Reichweitenstufe (nah → extrem weit), slash-getrennt */
|
||||
bonusDamage: string;
|
||||
/** Reichweiten in Schritt, slash-getrennt */
|
||||
ranges: string;
|
||||
bonusDamageByRange: WeaponBonusDamageByRange;
|
||||
rangeStepsSchritt: WeaponRangeStepsSchritt;
|
||||
reload: number;
|
||||
/** Mindest-KK für ungehinderten Einsatz (optional) */
|
||||
strengthRequirement?: number;
|
||||
@@ -31,8 +42,8 @@ export const RANGED_WEAPONS: RangedWeaponDef[] = [
|
||||
name: 'Elfenbogen',
|
||||
skill: 'bogen',
|
||||
damage: '1W6+5',
|
||||
bonusDamage: '3/2/1/1/0',
|
||||
ranges: '10/25/50/100/200',
|
||||
bonusDamageByRange: [3, 2, 1, 1, 0],
|
||||
rangeStepsSchritt: [10, 25, 50, 100, 200],
|
||||
reload: 3
|
||||
},
|
||||
{
|
||||
@@ -40,8 +51,8 @@ export const RANGED_WEAPONS: RangedWeaponDef[] = [
|
||||
name: 'Kompositbogen',
|
||||
skill: 'bogen',
|
||||
damage: '1W6+5',
|
||||
bonusDamage: '2/1/1/0/0',
|
||||
ranges: '10/20/35/50/80',
|
||||
bonusDamageByRange: [2, 1, 1, 0, 0],
|
||||
rangeStepsSchritt: [10, 20, 35, 50, 80],
|
||||
reload: 3,
|
||||
strengthRequirement: 15,
|
||||
strengthModifier: -2
|
||||
@@ -51,8 +62,8 @@ export const RANGED_WEAPONS: RangedWeaponDef[] = [
|
||||
name: 'Kriegsbogen',
|
||||
skill: 'bogen',
|
||||
damage: '1W6+7',
|
||||
bonusDamage: '3/2/1/0/0',
|
||||
ranges: '10/20/40/80/150',
|
||||
bonusDamageByRange: [3, 2, 1, 0, 0],
|
||||
rangeStepsSchritt: [10, 20, 40, 80, 150],
|
||||
reload: 4,
|
||||
strengthRequirement: 16,
|
||||
strengthModifier: -2
|
||||
@@ -62,8 +73,8 @@ export const RANGED_WEAPONS: RangedWeaponDef[] = [
|
||||
name: 'Kurzbogen',
|
||||
skill: 'bogen',
|
||||
damage: '1W6+4',
|
||||
bonusDamage: '1/1/0/0/-1',
|
||||
ranges: '5/15/25/40/60',
|
||||
bonusDamageByRange: [1, 1, 0, 0, -1],
|
||||
rangeStepsSchritt: [5, 15, 25, 40, 60],
|
||||
reload: 2
|
||||
},
|
||||
{
|
||||
@@ -71,8 +82,8 @@ export const RANGED_WEAPONS: RangedWeaponDef[] = [
|
||||
name: 'Langbogen',
|
||||
skill: 'bogen',
|
||||
damage: '1W6+6',
|
||||
bonusDamage: '3/2/1/0/-1',
|
||||
ranges: '10/25/50/100/200',
|
||||
bonusDamageByRange: [3, 2, 1, 0, -1],
|
||||
rangeStepsSchritt: [10, 25, 50, 100, 200],
|
||||
reload: 4,
|
||||
strengthRequirement: 15,
|
||||
strengthModifier: -2
|
||||
@@ -82,8 +93,8 @@ export const RANGED_WEAPONS: RangedWeaponDef[] = [
|
||||
name: 'Ork. Reiterbogen',
|
||||
skill: 'bogen',
|
||||
damage: '1W6+5',
|
||||
bonusDamage: '3/1/0/-1/-2',
|
||||
ranges: '5/15/30/60/100',
|
||||
bonusDamageByRange: [3, 1, 0, -1, -2],
|
||||
rangeStepsSchritt: [5, 15, 30, 60, 100],
|
||||
reload: 3,
|
||||
strengthRequirement: 15,
|
||||
strengthModifier: -2
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import type { Character } from '$lib/schema/character';
|
||||
import { getCharacter } from '$lib/storage/repo';
|
||||
import { computeRangedTarget, type Expertise } from '$lib/engine/ranged';
|
||||
import { RANGED_WEAPONS, type RangedWeaponId } from '$lib/rules/weapons-ranged';
|
||||
import { getRangedWeapon, RANGED_WEAPONS, type RangedWeaponId } from '$lib/rules/weapons-ranged';
|
||||
import {
|
||||
RANGES,
|
||||
TARGET_SIZES,
|
||||
@@ -15,10 +15,19 @@
|
||||
RELOAD_STATES,
|
||||
type ExtraModifierId,
|
||||
type HitZoneId,
|
||||
type ModifierRow,
|
||||
type MovementId,
|
||||
type RangeId,
|
||||
type ReloadStateId
|
||||
type ReloadStateId,
|
||||
type TargetSizeId,
|
||||
type VisibilityId
|
||||
} from '$lib/rules/modifiers-ranged';
|
||||
|
||||
function optionLabel(row: ModifierRow): string {
|
||||
const m = row.modifier;
|
||||
return `${row.name} (${m})`;
|
||||
}
|
||||
|
||||
let char: Character | undefined;
|
||||
let err = '';
|
||||
const id = $page.params.id ?? '';
|
||||
@@ -26,9 +35,9 @@
|
||||
let weaponId: RangedWeaponId = 'shortbow';
|
||||
let expertise: Expertise = 'none';
|
||||
let range: RangeId = 'near';
|
||||
let targetSize = 'medium' as const;
|
||||
let visibility = 'clear' as const;
|
||||
let movement = 'slow' as const;
|
||||
let targetSize: TargetSizeId = 'medium';
|
||||
let visibility: VisibilityId = 'clear';
|
||||
let movement: MovementId = 'slow';
|
||||
let hitZone = '' as HitZoneId | '';
|
||||
let reloadState = '' as ReloadStateId | '';
|
||||
let targetBuild: 'biped' | 'quadruped' = 'biped';
|
||||
@@ -79,7 +88,9 @@
|
||||
}
|
||||
})();
|
||||
|
||||
const hitZoneOptions = HIT_ZONES.filter((z) => z.build === targetBuild);
|
||||
$: hitZoneOptions = HIT_ZONES.filter((z) => z.build === targetBuild);
|
||||
|
||||
$: weaponForExamples = getRangedWeapon(weaponId);
|
||||
</script>
|
||||
|
||||
{#if err}
|
||||
@@ -97,6 +108,16 @@
|
||||
<option value={w.id}>{w.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if weaponForExamples}
|
||||
<p class="weapon-stat-line muted">
|
||||
<strong>Reichweiten:</strong>
|
||||
{weaponForExamples.rangeStepsSchritt.join('/')}
|
||||
</p>
|
||||
<p class="weapon-stat-line muted">
|
||||
<strong>TP+:</strong>
|
||||
{weaponForExamples.bonusDamageByRange.join('/')}
|
||||
</p>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<section class="card stack">
|
||||
@@ -121,45 +142,72 @@
|
||||
|
||||
<section class="card stack">
|
||||
<h2>Entfernung</h2>
|
||||
<div class="chips">
|
||||
{#each RANGES as r}
|
||||
<label class="chip"
|
||||
><input type="radio" bind:group={range} value={r.id} /> {r.name} ({r.modifier})</label
|
||||
>
|
||||
{/each}
|
||||
<div class="field">
|
||||
<label for="range-select">Distanz</label>
|
||||
<select id="range-select" bind:value={range}>
|
||||
{#each RANGES as r}
|
||||
<option value={r.id}>{optionLabel(r)}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
{#if weaponForExamples}
|
||||
<div class="ref-examples">
|
||||
<p class="examples-label"><strong>Beispiele:</strong></p>
|
||||
<ul class="examples-list muted">
|
||||
{#each RANGES as r, i}
|
||||
<li>
|
||||
<span class="examples-name">{r.name}</span>
|
||||
— {weaponForExamples.rangeStepsSchritt[i]} Schritt
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<section class="card stack">
|
||||
<h2>Zielgröße</h2>
|
||||
<div class="chips">
|
||||
{#each TARGET_SIZES as r}
|
||||
<label class="chip"
|
||||
><input type="radio" bind:group={targetSize} value={r.id} /> {r.name}</label
|
||||
>
|
||||
{/each}
|
||||
<div class="field">
|
||||
<label for="target-size-select">Größe</label>
|
||||
<select id="target-size-select" bind:value={targetSize}>
|
||||
{#each TARGET_SIZES as r}
|
||||
<option value={r.id}>{optionLabel(r)}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div class="ref-examples">
|
||||
<p class="examples-label"><strong>Beispiele:</strong></p>
|
||||
<ul class="examples-list muted">
|
||||
{#each TARGET_SIZES as r}
|
||||
{#if r.description}
|
||||
<li><span class="examples-name">{r.name}</span> — {r.description}</li>
|
||||
{/if}
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card stack">
|
||||
<h2>Sicht</h2>
|
||||
<div class="chips">
|
||||
{#each VISIBILITY as r}
|
||||
<label class="chip"
|
||||
><input type="radio" bind:group={visibility} value={r.id} /> {r.name}</label
|
||||
>
|
||||
{/each}
|
||||
<div class="field">
|
||||
<label for="visibility-select">Sichtverhältnisse</label>
|
||||
<select id="visibility-select" bind:value={visibility}>
|
||||
{#each VISIBILITY as r}
|
||||
<option value={r.id}>{optionLabel(r)}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card stack">
|
||||
<h2>Bewegung</h2>
|
||||
<div class="chips">
|
||||
{#each TARGET_MOVEMENT as r}
|
||||
<label class="chip"
|
||||
><input type="radio" bind:group={movement} value={r.id} /> {r.name}</label
|
||||
>
|
||||
{/each}
|
||||
<div class="field">
|
||||
<label for="movement-select">Bewegung des Ziels</label>
|
||||
<select id="movement-select" bind:value={movement}>
|
||||
{#each TARGET_MOVEMENT as r}
|
||||
<option value={r.id}>{optionLabel(r)}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -264,4 +312,35 @@
|
||||
.err {
|
||||
color: var(--danger);
|
||||
}
|
||||
.ref-examples {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
.examples-label {
|
||||
margin: 0 0 0.35rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text);
|
||||
}
|
||||
.examples-list {
|
||||
margin: 0;
|
||||
padding-left: 1.15rem;
|
||||
font-size: 0.88rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.examples-list li {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
.examples-name {
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
.weapon-stat-line {
|
||||
margin: 0.5rem 0 0;
|
||||
font-size: 0.9rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
word-break: break-all;
|
||||
}
|
||||
.weapon-stat-line strong {
|
||||
color: var(--text);
|
||||
margin-right: 0.35rem;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user