Spaces:
Running
Running
File size: 6,611 Bytes
727efc9 1f1908e cedb3f2 1f1908e 9fe1971 1f1908e 9fe1971 1f1908e 9fe1971 1f1908e 9fe1971 cedb3f2 727efc9 cedb3f2 9fe1971 cedb3f2 e46cfbf 9fe1971 cedb3f2 9fe1971 1f1908e cedb3f2 1f1908e 727efc9 1f1908e 727efc9 9fe1971 1f1908e cedb3f2 1f1908e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 | // Skill Forge tab — author a REAL, engine-resolvable combat skill for a chosen hero. The forge
// pipeline (model → validate → Klein icon → persist) lives in skillForge.js and is shared with
// the in-Game character sheet, so a hero can learn skills from either place.
import { currentCodingModel, onCodingModelChange } from '/web/codingModel.js'
import { listPersonas, getPersona, onRosterChange, getSkillIcon } from '/web/personaStore.js'
import { effectSummary } from '/web/skillSchema.js'
import { forgeSkillForHero } from '/web/skillForge.js'
// Loading spinner for a skill whose scene is still painting. Idempotent — tiny.js injects the same
// keyframes/class; we re-inject (id-guarded) so the Forge tab works even before the game module loads.
;(function injectSkillSpin() {
try {
if (document.getElementById('ta-skill-spin-css')) return
const s = document.createElement('style'); s.id = 'ta-skill-spin-css'
s.textContent = '@keyframes ta-spin{to{transform:translate(-50%,-50%) rotate(360deg)}}.ta-skill-spin{position:absolute;top:50%;left:50%;width:18px;height:18px;border:2px solid #2a3340;border-top-color:#c9a227;border-radius:50%;animation:ta-spin .8s linear infinite}'
document.head.appendChild(s)
} catch { /* ignore */ }
})()
function el(tag, props = {}, kids = []) {
const n = document.createElement(tag)
for (const [k, v] of Object.entries(props)) {
if (k === 'class') n.className = v
else if (k === 'style') n.style.cssText = v
else if (k.startsWith('on') && typeof v === 'function') n.addEventListener(k.slice(2), v)
else if (v != null) n.setAttribute(k, v)
}
for (const kid of [].concat(kids)) if (kid != null) n.append(kid)
return n
}
export function mountSkillForgePanel(host) {
const sel = el('select', { class: 'persona-input skillforge-hero' })
const req = el('textarea', { class: 'persona-prompt-edit skillforge-req', rows: 4,
placeholder: 'what skill should this hero learn? (e.g. “a defensive shout that shields nearby allies”)' })
const btn = el('button', { class: 'persona-go', type: 'button' }, '⚒ Forge skill')
const status = el('div', { class: 'persona-status' })
const card = el('div', { class: 'skillforge-card', style: 'display:none;margin-top:10px;border:1px solid #2a3340;border-radius:12px;padding:12px;background:rgba(20,24,33,.6)' })
const empty = el('div', { class: 'persona-roster-empty' },
'No heroes yet — recruit one in the Personas tab, then come back to forge its skills.')
const controls = el('aside', { class: 'persona-controls skillforge' }, [
el('div', { class: 'persona-sec' }, [el('div', { class: 'persona-sec-title' }, 'Skill Forge'), el('span')]),
el('label', { class: 'persona-label' }, 'Hero'), sel,
empty,
el('label', { class: 'persona-label' }, 'Skill request'), req,
el('div', { class: 'persona-prompt-actions' }, [btn]),
status,
card,
])
host.append(controls)
function refreshHeroes() {
const people = listPersonas()
const prev = sel.value
sel.replaceChildren(...people.map((p) => el('option', { value: p.id }, p.name || 'Unnamed hero')))
if (people.some((p) => p.id === prev)) sel.value = prev
const none = people.length === 0
empty.style.display = none ? '' : 'none'
sel.style.display = none ? 'none' : ''
btn.disabled = none
}
function refreshStatus() { if (!status.dataset.busy) status.textContent = `Coding model: ${currentCodingModel().label}` }
let shownSkillId = null // skill currently in the card — so we can repaint its icon when it finishes
async function renderCard(skill) {
shownSkillId = skill.id
card.style.display = ''
card.replaceChildren()
const head = el('div', { style: 'display:flex;gap:12px;align-items:center' })
const icon = el('div', { style: 'position:relative;width:64px;height:64px;border-radius:10px;border:1px solid #2a3340;background:#0d1015;flex:none;background-size:cover;background-position:center' })
if (skill.iconPending) icon.append(el('div', { class: 'ta-skill-spin', title: 'painting its scene…' })) // still painting → spinner, never a stale (reused-id) icon
else { const b = await getSkillIcon(skill.id); if (b) icon.style.backgroundImage = `url(${URL.createObjectURL(b)})` }
const meta = el('div', {}, [
el('div', { style: 'font:700 16px var(--tac-font,system-ui);color:#e8e8e8' }, skill.name),
el('div', { style: 'font-size:11px;color:#9aa4b2;text-transform:uppercase;letter-spacing:.06em;margin:2px 0' }, `${skill.category.replace('_', ' ')} · ${effectSummary(skill)}`),
])
head.append(icon, meta)
const flav = el('div', { style: 'color:#c2c8d2;font-style:italic;margin:10px 0 0;line-height:1.45' }, skill.flavor || '')
const note = skill.iconPending ? 'painting its scene… (already usable on the character sheet)' : '✓ Learned — view it on the hero’s character sheet.'
const saved = el('div', { style: 'color:#34d058;font-size:12px;margin-top:8px' }, note)
card.append(head, flav, saved)
}
// When the background image finishes, the hero's skill flips iconPending → false (onRosterChange).
// If that skill is the one in the card, repaint it so the spinner swaps for the finished art.
function repaintShownCard() {
if (!shownSkillId || card.style.display === 'none') return
const owner = listPersonas().find((p) => p.customSkills && p.customSkills[shownSkillId])
const sk = owner && owner.customSkills[shownSkillId]
if (sk && !sk.iconPending) renderCard(sk)
}
let running = false
async function forge() {
if (running) return
const p = getPersona(sel.value)
if (!p) { status.textContent = 'Pick a hero first.'; return }
if (!req.value.trim()) { status.textContent = 'Describe the skill you want.'; return }
running = true; status.dataset.busy = '1'; btn.disabled = true; card.style.display = 'none'
try {
const res = await forgeSkillForHero(p.id, req.value, { onStatus: (s) => { status.textContent = s } })
if (!res.ok) { status.textContent = 'Forge failed: ' + res.error; return }
await renderCard(res.skill)
status.textContent = 'Done.'
} catch (e) {
status.textContent = 'Forge failed: ' + (e && e.message ? e.message : e)
} finally {
running = false; delete status.dataset.busy; btn.disabled = listPersonas().length === 0
}
}
btn.addEventListener('click', forge)
onRosterChange(() => { refreshHeroes(); repaintShownCard() })
onCodingModelChange(refreshStatus)
refreshHeroes(); refreshStatus()
return { refresh: refreshHeroes }
}
|