dune-tools/character-builder/frontend/src/components/FactionTrack.vue
Vantz Stockwell 98a1792106 Add Dune Awakening character builder + initial project scaffolding
- character-builder/: Vue 3 + NestJS + Valkey app for planning house, class,
  character XP, 5 spec tracks, faction standing, and skill trees. Shareable
  via short link (POST /api/builds → 8-char nanoid).
- character-builder/data/: parsed JSON tables (character XP through L200,
  5 specs to L100, 2 faction standing tables, 5 class skill trees).
- character-builder/scripts/extract.py: parser that regenerates data/*.json
  from the gitignored sample-data/*.html snapshots.
- Dockerfile + docker-compose.yml: two-container deploy (app + Valkey).
- specialization-calculator/: pre-existing single-file XP/quest calculator,
  carried into the repo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 07:30:37 -04:00

94 lines
2.5 KiB
Vue

<script setup lang="ts">
import { computed } from 'vue';
import type { FactionTable, House } from '../types';
const props = defineProps<{
house: House;
table: FactionTable | null;
tier: number;
standingInto: number;
}>();
const emit = defineEmits<{
'update:tier': [n: number];
'update:standingInto': [n: number];
}>();
const tierCount = computed(() => props.table?.tiers.length ?? 0);
const maxTier = computed(() => Math.max(0, tierCount.value - 1));
const currentTier = computed(() => {
if (!props.table) return null;
return props.table.tiers[props.tier] || null;
});
const nextTier = computed(() => {
if (!props.table) return null;
return props.table.tiers[props.tier + 1] || null;
});
const pct = computed(() => {
if (!nextTier.value || !nextTier.value.standingRequired) return 0;
const p = (props.standingInto / nextTier.value.standingRequired) * 100;
return Math.min(100, Math.max(0, p));
});
function setTier(e: Event) {
const v = Number((e.target as HTMLInputElement).value) || 0;
emit('update:tier', Math.max(0, Math.min(maxTier.value, Math.floor(v))));
}
function setInto(e: Event) {
const v = Number((e.target as HTMLInputElement).value) || 0;
emit('update:standingInto', Math.max(0, Math.floor(v)));
}
function fmt(n: number): string {
return n.toLocaleString('en-US');
}
</script>
<template>
<div>
<div class="row">
<div class="field">
<label>Current Tier</label>
<input
type="number"
min="0"
:max="maxTier"
:value="tier"
@input="setTier"
/>
</div>
<div class="field">
<label>Rep into next tier</label>
<input type="number" min="0" :value="standingInto" @input="setInto" />
</div>
</div>
<div class="progress" :style="{ '--pct': pct + '%' }"></div>
<div class="progress-meta">
<span>
{{ currentTier?.name || '—' }} {{ nextTier?.name || 'Max' }}
</span>
<span>
{{ fmt(standingInto) }} / {{ fmt(nextTier?.standingRequired || 0) }} rep
</span>
</div>
<div class="tier-list" v-if="table">
<div
v-for="t in table.tiers"
:key="t.tier"
:class="[
'tier-row',
t.tier < tier ? 'reached' : '',
t.tier === tier ? 'current' : '',
]"
>
<span class="num">{{ t.tier }}</span>
<span>{{ t.name || '—' }}</span>
<span>{{ fmt(t.standingRequired) }}</span>
<span style="color: var(--ink-muted)">{{ fmt(t.totalStanding) }}</span>
</div>
</div>
</div>
</template>