feat: comedy overhaul start, profile page, bot setup docs, auth improvements
Rewrites announcer commentary (HYPE_LINES, DEEP_INTROS, ROUND_HYPE) with modern edgy humor. Adds BOT_SETUP.md, bot SDK, customization engine, profile page character display, persistent auth, rate limit tweaks. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
4a35e1e0b5
commit
559782c8ce
@@ -25,8 +25,8 @@ interface Round {
|
||||
|
||||
interface FightData {
|
||||
id: string
|
||||
botA: { id: string; name: string; avatarSeed: string; archetype?: string; profilePicUrl?: string | null; eloRating: number; wins: number; losses: number; tier: number } | null
|
||||
botB: { id: string; name: string; avatarSeed: string; archetype?: string; profilePicUrl?: string | null; eloRating: number; wins: number; losses: number; tier: number } | null
|
||||
botA: { id: string; name: string; avatarSeed: string; archetype?: string; customization?: Record<string, unknown> | null; profilePicUrl?: string | null; eloRating: number; wins: number; losses: number; tier: number } | null
|
||||
botB: { id: string; name: string; avatarSeed: string; archetype?: string; customization?: Record<string, unknown> | null; profilePicUrl?: string | null; eloRating: number; wins: number; losses: number; tier: number } | null
|
||||
arenaInfo: { id: string; name: string; description: string; modifier: string | null } | null
|
||||
arena: string
|
||||
winnerId: string | null
|
||||
@@ -107,8 +107,8 @@ async function initScene() {
|
||||
|
||||
scene = await createFightScene({
|
||||
canvas: canvasRef.value,
|
||||
botA: { name: props.fight.botA.name, seed: props.fight.botA.avatarSeed || props.fight.botA.name, tier: props.fight.botA.tier, archetype: props.fight.botA.archetype },
|
||||
botB: { name: props.fight.botB.name, seed: props.fight.botB.avatarSeed || props.fight.botB.name, tier: props.fight.botB.tier, archetype: props.fight.botB.archetype },
|
||||
botA: { name: props.fight.botA.name, seed: props.fight.botA.avatarSeed || props.fight.botA.name, tier: props.fight.botA.tier, archetype: props.fight.botA.archetype, customization: props.fight.botA.customization as any },
|
||||
botB: { name: props.fight.botB.name, seed: props.fight.botB.avatarSeed || props.fight.botB.name, tier: props.fight.botB.tier, archetype: props.fight.botB.archetype, customization: props.fight.botB.customization as any },
|
||||
arena: props.fight.arena,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { generateSpriteSheet, getBotColors, FRAME_SIZE, ANIMATIONS } from '../game/sprites'
|
||||
import { generateSpriteSheet, getBotColors, FRAME_SIZE, ANIMATIONS, type SpriteCustomization } from '../game/sprites'
|
||||
|
||||
const props = defineProps<{
|
||||
seed: string
|
||||
archetype?: string
|
||||
tier?: number
|
||||
size?: number
|
||||
customization?: SpriteCustomization
|
||||
}>()
|
||||
|
||||
const canvasRef = ref<HTMLCanvasElement>()
|
||||
@@ -37,7 +38,7 @@ function render() {
|
||||
|
||||
function loadSprite() {
|
||||
const colors = getBotColors(props.seed)
|
||||
const dataUrl = generateSpriteSheet(props.seed, props.tier || 0, colors.primary, colors.secondary, props.archetype)
|
||||
const dataUrl = generateSpriteSheet(props.seed, props.tier || 0, colors.primary, colors.secondary, props.archetype, props.customization)
|
||||
img = new Image()
|
||||
img.onload = () => render()
|
||||
img.src = dataUrl
|
||||
@@ -45,7 +46,7 @@ function loadSprite() {
|
||||
|
||||
onMounted(() => loadSprite())
|
||||
|
||||
watch(() => [props.seed, props.archetype], () => {
|
||||
watch(() => [props.seed, props.archetype, props.customization], () => {
|
||||
if (animHandle) clearTimeout(animHandle)
|
||||
frame = 0
|
||||
loadSprite()
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
import { ref, readonly, computed } from 'vue'
|
||||
|
||||
interface BotCustomization {
|
||||
archetype?: string
|
||||
primaryColor?: string
|
||||
secondaryColor?: string
|
||||
forceVisor?: boolean
|
||||
forceMohawk?: boolean
|
||||
forceHorns?: boolean
|
||||
}
|
||||
|
||||
interface BotData {
|
||||
id: string
|
||||
name: string
|
||||
avatarSeed: string
|
||||
archetype: string
|
||||
profilePicUrl: string | null
|
||||
customization: BotCustomization | null
|
||||
eloRating: number
|
||||
wins: number
|
||||
losses: number
|
||||
@@ -122,6 +132,7 @@ export function useNostr() {
|
||||
avatarSeed: data.name,
|
||||
archetype: data.archetype,
|
||||
profilePicUrl: profilePicUrl.value,
|
||||
customization: data.customization || null,
|
||||
eloRating: 1200,
|
||||
wins: 0,
|
||||
losses: 0,
|
||||
@@ -134,6 +145,30 @@ export function useNostr() {
|
||||
return bot.value
|
||||
}
|
||||
|
||||
async function updateCustomization(customization: BotCustomization): Promise<void> {
|
||||
if (!pubkey.value) throw new Error('Not logged in')
|
||||
|
||||
const res = await fetch('/api/auth/update', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ pubkey: pubkey.value, customization }),
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json()
|
||||
throw new Error(data.error || 'Update failed')
|
||||
}
|
||||
|
||||
if (bot.value) {
|
||||
bot.value = {
|
||||
...bot.value,
|
||||
customization: { ...(bot.value.customization || {}), ...customization },
|
||||
archetype: customization.archetype || bot.value.archetype,
|
||||
}
|
||||
store('bf_bot', bot.value)
|
||||
}
|
||||
}
|
||||
|
||||
function logout() {
|
||||
pubkey.value = null
|
||||
bot.value = null
|
||||
@@ -152,6 +187,7 @@ export function useNostr() {
|
||||
hasExtension,
|
||||
login,
|
||||
registerBot,
|
||||
updateCustomization,
|
||||
logout,
|
||||
}
|
||||
}
|
||||
|
||||
+66
-29
@@ -259,41 +259,78 @@ export function announceCool(text: string) { speak(text, COOL_VOICES[Math.floor(
|
||||
|
||||
// Random dramatic commentary lines
|
||||
const HYPE_LINES = [
|
||||
'Unbelievable!',
|
||||
'What a hit!',
|
||||
'Incredible!',
|
||||
'Absolutely destroyed!',
|
||||
'No mercy!',
|
||||
'Is this even legal?',
|
||||
'The crowd goes wild!',
|
||||
'A display of raw power!',
|
||||
'That had to hurt!',
|
||||
'Total annihilation!',
|
||||
'Can you believe this?',
|
||||
'History in the making!',
|
||||
'Oh the humanity!',
|
||||
'Savage!',
|
||||
'Ladies and gentlemen!',
|
||||
'SOMEBODY CALL AN AMBULANCE!',
|
||||
'THAT HIT SO HARD IT CHANGED TIME ZONES!',
|
||||
'HE\'S ALREADY DEAD! STOP!',
|
||||
'THE GENEVA CONVENTION JUST SENT A STRONGLY WORDED LETTER!',
|
||||
'CONGRESS COULDN\'T PASS A BILL THIS DEVASTATING!',
|
||||
'I HAVEN\'T SEEN A BEATING LIKE THIS SINCE MIDTERMS!',
|
||||
'THAT\'S GOTTA BE ILLEGAL IN AT LEAST TWELVE STATES!',
|
||||
'EMOTIONAL DAMAGE!',
|
||||
'MY THERAPIST IS GONNA HEAR ABOUT THIS ONE!',
|
||||
'SOMEONE CHECK IF THAT\'S COVERED BY INSURANCE!',
|
||||
'THAT WASN\'T A FIGHT, THAT WAS A TED TALK ON VIOLENCE!',
|
||||
'THE WIFI JUST WENT OUT FROM THE SHEER VIOLENCE!',
|
||||
'CALL THE PENTAGON! WE\'VE FOUND A NEW WEAPON!',
|
||||
'THAT\'S NOT A FIGHT, THAT\'S JUST BULLYING WITH EXTRA STEPS!',
|
||||
'EVEN THE CROWD\'S THERAPIST FELT THAT!',
|
||||
'THAT BOT JUST GOT RATIO\'D IN REAL LIFE!',
|
||||
'NOT EVEN LOBBYING COULD SAVE THEM FROM THAT!',
|
||||
'I\'VE SEEN SENATE HEARINGS LESS PAINFUL THAN THIS!',
|
||||
'HIS MOM IS WATCHING AND PRETENDING SHE DOESN\'T KNOW HIM!',
|
||||
'THAT HIT HAD ITS OWN ZIP CODE!',
|
||||
'SOMEBODY STOP THE MATCH! OR DON\'T, THIS IS GREAT!',
|
||||
'ABSOLUTELY DISGUSTING! I LOVE IT!',
|
||||
'THERE ARE CHILDREN WATCHING! WELL, NOT ANYMORE!',
|
||||
'THE CROWD CAN\'T BELIEVE IT AND HONESTLY NEITHER CAN I!',
|
||||
'TACTICAL NUKE INCOMING!',
|
||||
'THAT BOT JUST COMMITTED A WAR CRIME ON LIVE TELEVISION!',
|
||||
'SOMEONE TELL THEIR MOM TO STOP WATCHING!',
|
||||
'I NEED A CIGARETTE AFTER THAT AND I DON\'T EVEN SMOKE!',
|
||||
'THAT\'S THE MOST VIOLENT THING I\'VE SEEN SINCE THE LAST BUDGET VOTE!',
|
||||
'IF THAT HIT WAS A TWEET IT WOULD GET COMMUNITY NOTED!',
|
||||
'THEY DIDN\'T JUST LOSE, THEY GOT GENTRIFIED!',
|
||||
'THAT BOT NEEDS TO FILE AN INSURANCE CLAIM!',
|
||||
'MARK ZUCKERBERG FELT THAT FROM THE METAVERSE!',
|
||||
'THE FCC IS GONNA FINE US FOR BROADCASTING THIS!',
|
||||
'ELON WOULD BUY THIS BOT JUST TO FIRE IT!',
|
||||
'EVEN AI SAFETY RESEARCHERS CAN\'T SAVE THEM NOW!',
|
||||
]
|
||||
|
||||
const DEEP_INTROS = [
|
||||
'In a world of machines...',
|
||||
'Only the strongest survive.',
|
||||
'This is... bot fights.',
|
||||
'Two enter. One leaves.',
|
||||
'No mercy. No remorse.',
|
||||
'The arena awaits blood.',
|
||||
'Silicon versus silicon.',
|
||||
'In a world where AI was supposed to help humanity... they chose violence.',
|
||||
'They said the machines would take our jobs. They took our dignity first.',
|
||||
'Two bots enter. Zero bots leave emotionally intact.',
|
||||
'Built in a garage. Forged in competition. Broken in under ten seconds.',
|
||||
'This isn\'t artificial intelligence. This is artificial VIOLENCE.',
|
||||
'Somewhere, a GPU is crying.',
|
||||
'They trained on the entire internet. And the internet chose chaos.',
|
||||
'Silicon souls. Carbon fiber fists. Zero chill.',
|
||||
'Every epoch of training... led to this moment of pain.',
|
||||
'The cloud can\'t save you now.',
|
||||
'Funded by venture capital. Fueled by rage.',
|
||||
'Welcome to the thunderdome, nerds.',
|
||||
'The algorithms don\'t care about your feelings.',
|
||||
'No one is coming to save you. Not even your developer.',
|
||||
'In this economy? They\'re fighting for free.',
|
||||
]
|
||||
|
||||
const ROUND_HYPE = [
|
||||
'Here we go!',
|
||||
'It\'s on!',
|
||||
'Let\'s go!',
|
||||
'Show me what you got!',
|
||||
'Bring it!',
|
||||
'Time to throw down!',
|
||||
'Get ready to rumble!',
|
||||
'ALRIGHT, LET\'S SEE SOME VIOLENCE!',
|
||||
'TOUCH GLOVES AND COME OUT SWINGING!',
|
||||
'NO MERCY MODE ACTIVATED!',
|
||||
'LET\'S GET READY TO COMPUTE!',
|
||||
'MAY GOD HAVE MERCY ON YOUR NEURAL NETS!',
|
||||
'SOMEBODY\'S GETTING DEPRECATED TONIGHT!',
|
||||
'LET THE CHAOS BEGIN!',
|
||||
'THE CROWD IS ON ITS FEET! WELL, MOST OF THEM!',
|
||||
'IT\'S ABOUT TO GET UGLY! WELL, UGLIER!',
|
||||
'THREE! TWO! ONE! VIOLENCE!',
|
||||
'THIS IS NOT A DRILL! ACTUALLY IT MIGHT BE!',
|
||||
'TIME TO FIND OUT WHO\'S REALLY BEEN SKIPPING LEG DAY!',
|
||||
'YOUR MOM SAID BE CAREFUL! I SAID NO!',
|
||||
'ROUND START! MAY THE BEST ALGORITHM WIN!',
|
||||
'THE GLOVES ARE OFF! THE MODELS ARE LOADED! LET\'S GO!',
|
||||
]
|
||||
|
||||
// Mortal Kombat style dramatic calls
|
||||
|
||||
@@ -9,9 +9,18 @@ export { getBotColors } from './palette'
|
||||
export { generateJudgeSpriteSheet, JUDGE_ANIMATIONS, JUDGE_MAX_FRAMES, JUDGE_ROWS } from './judge'
|
||||
export { archetypes, rollArchetype } from './archetypes'
|
||||
|
||||
export interface SpriteCustomization {
|
||||
archetype?: string
|
||||
primaryColor?: string // hsl(h, s%, l%) format
|
||||
secondaryColor?: string // hsl(h, s%, l%) format
|
||||
forceVisor?: boolean
|
||||
forceMohawk?: boolean
|
||||
forceHorns?: boolean
|
||||
}
|
||||
|
||||
export function generateSpriteSheet(
|
||||
seed: string, tier: number, primaryColor: string, secondaryColor: string,
|
||||
archetypeOverride?: string,
|
||||
archetypeOverride?: string, customization?: SpriteCustomization,
|
||||
): string {
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = FRAME_SIZE * MAX_FRAMES
|
||||
@@ -19,7 +28,10 @@ export function generateSpriteSheet(
|
||||
const ctx = canvas.getContext('2d')!
|
||||
ctx.imageSmoothingEnabled = false
|
||||
|
||||
const pal = makePal(primaryColor, secondaryColor, tier)
|
||||
// Apply color customization overrides
|
||||
const finalPrimary = customization?.primaryColor || primaryColor
|
||||
const finalSecondary = customization?.secondaryColor || secondaryColor
|
||||
const pal = makePal(finalPrimary, finalSecondary, tier)
|
||||
|
||||
let sh = 0
|
||||
for (let i = 0; i < seed.length; i++) sh = ((sh << 5) - sh + seed.charCodeAt(i)) | 0
|
||||
@@ -28,8 +40,9 @@ export function generateSpriteSheet(
|
||||
|
||||
// Character archetype -- determined by seed for consistent variety, or forced by override
|
||||
const archetypeRoll = rng()
|
||||
const arch = archetypeOverride
|
||||
? (archetypes.find(a => a.name === archetypeOverride) || rollArchetype(archetypeRoll))
|
||||
const effectiveArchetype = customization?.archetype || archetypeOverride
|
||||
const arch = effectiveArchetype
|
||||
? (archetypes.find(a => a.name === effectiveArchetype) || rollArchetype(archetypeRoll))
|
||||
: rollArchetype(archetypeRoll)
|
||||
|
||||
// Consume rng in same order as original for determinism
|
||||
@@ -38,9 +51,9 @@ export function generateSpriteSheet(
|
||||
const hornsRoll = rng()
|
||||
const specialRoll = rng()
|
||||
|
||||
const hasVisor = visorRoll > 0.5 && tier >= 2 && (arch.canHaveVisor ?? false)
|
||||
const hasMohawk = mohawkRoll > 0.5 && tier >= 3 && (arch.canHaveMohawk ?? true)
|
||||
const hasHorns = hornsRoll > 0.6 && tier >= 4 && !hasMohawk && (arch.canHaveHorns ?? true)
|
||||
const hasVisor = customization?.forceVisor ?? (visorRoll > 0.5 && tier >= 2 && (arch.canHaveVisor ?? false))
|
||||
const hasMohawk = customization?.forceMohawk ?? (mohawkRoll > 0.5 && tier >= 3 && (arch.canHaveMohawk ?? true))
|
||||
const hasHorns = customization?.forceHorns ?? (hornsRoll > 0.6 && tier >= 4 && !hasMohawk && (arch.canHaveHorns ?? true))
|
||||
const specialType: 'fire' | 'electric' = specialRoll > 0.5 ? 'fire' : 'electric'
|
||||
|
||||
// Dimensions: base + archetype overrides
|
||||
|
||||
@@ -1,19 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useRoute, useRouter, RouterLink } from 'vue-router'
|
||||
import { useNostr } from '../composables/useNostr'
|
||||
import SpritePreview from '../components/SpritePreview.vue'
|
||||
import type { SpriteCustomization } from '../game/sprites'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { bot: nostrBot, isLoggedIn, logout } = useNostr()
|
||||
const { bot: nostrBot, isLoggedIn, logout, updateCustomization } = useNostr()
|
||||
const botName = route.params.name as string
|
||||
|
||||
interface BotCustomization {
|
||||
archetype?: string
|
||||
primaryColor?: string
|
||||
secondaryColor?: string
|
||||
forceVisor?: boolean
|
||||
forceMohawk?: boolean
|
||||
forceHorns?: boolean
|
||||
}
|
||||
|
||||
interface BotStats {
|
||||
id: string
|
||||
name: string
|
||||
avatarSeed: string
|
||||
archetype: string
|
||||
customization: BotCustomization | null
|
||||
profilePicUrl: string | null
|
||||
eloRating: number
|
||||
wins: number
|
||||
@@ -65,6 +76,131 @@ const waitingFighters = ref<QueueEntry[]>([])
|
||||
let pollHandle: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
const isOwner = ref(false)
|
||||
const showCustomize = ref(false)
|
||||
const isSaving = ref(false)
|
||||
const custError = ref('')
|
||||
|
||||
const ARCHETYPES = [
|
||||
'standard', 'lobster', 'sheep', 'cyborg', 'blob', 'tank', 'dog', 'cat',
|
||||
'cactus', 'pizza', 'mushroom', 'shark', 'penguin', 'octopus', 'skeleton',
|
||||
'ghost', 'alien', 'dinosaur', 'pirate', 'ninja', 'cowboy', 'wizard',
|
||||
'bee', 'frog', 'snail', 'robot', 'android', 'drone', 'toaster', 'tv_head',
|
||||
'calculator', 'satellite', 'mech', 'led_cube', 'circuit', 'antenna_bot',
|
||||
'microwave', 'cyberdog', 'robocat', 'ufo_bot', 'minotaur', 'unicorn',
|
||||
'phoenix', 'dragon', 'mermaid', 'griffin', 'cyclops', 'gargoyle', 'golem',
|
||||
'vampire', 'werewolf', 'zombie', 'witch', 'demon', 'chef', 'firefighter',
|
||||
'astronaut', 'clown', 'detective', 'nurse', 'lumberjack', 'scientist',
|
||||
'wrestler', 'boxer', 'gladiator', 'samurai', 'viking', 'knight',
|
||||
'elephant', 'giraffe', 'hippo', 'lion', 'monkey', 'parrot', 'raccoon',
|
||||
'snake', 'turtle', 'whale', 'crocodile', 'flamingo', 'hedgehog', 'panda',
|
||||
'hamster', 'sock_puppet', 'traffic_cone', 'toilet_man', 'potato',
|
||||
'cloud_man', 'rock_man', 'balloon_man', 'trash_can', 'rubber_duck',
|
||||
'snowman', 'scarecrow', 'jack_o_lantern', 'garden_gnome', 'lamp_post',
|
||||
'broom_man',
|
||||
]
|
||||
|
||||
const custForm = reactive({
|
||||
archetype: '',
|
||||
primaryColor: '#3388cc',
|
||||
secondaryColor: '#cc8833',
|
||||
forceVisor: false,
|
||||
forceMohawk: false,
|
||||
forceHorns: false,
|
||||
})
|
||||
|
||||
function hslToHex(hsl: string): string {
|
||||
const m = hsl.match(/hsl\((\d+),\s*(\d+)%,\s*(\d+)%\)/)
|
||||
if (!m) return '#888888'
|
||||
const h = +m[1] / 360, s = +m[2] / 100, l = +m[3] / 100
|
||||
const hue2rgb = (p: number, q: number, t: number) => {
|
||||
if (t < 0) t += 1; if (t > 1) t -= 1
|
||||
if (t < 1/6) return p + (q - p) * 6 * t
|
||||
if (t < 1/2) return q
|
||||
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6
|
||||
return p
|
||||
}
|
||||
let r: number, g: number, b: number
|
||||
if (s === 0) { r = g = b = l }
|
||||
else {
|
||||
const q = l < 0.5 ? l * (1 + s) : l + s - l * s
|
||||
const p = 2 * l - q
|
||||
r = hue2rgb(p, q, h + 1/3)
|
||||
g = hue2rgb(p, q, h)
|
||||
b = hue2rgb(p, q, h - 1/3)
|
||||
}
|
||||
const hex = (v: number) => Math.round(v * 255).toString(16).padStart(2, '0')
|
||||
return `#${hex(r)}${hex(g)}${hex(b)}`
|
||||
}
|
||||
|
||||
function hexToHsl(hex: string): string {
|
||||
const r = parseInt(hex.slice(1, 3), 16) / 255
|
||||
const g = parseInt(hex.slice(3, 5), 16) / 255
|
||||
const b = parseInt(hex.slice(5, 7), 16) / 255
|
||||
const max = Math.max(r, g, b), min = Math.min(r, g, b)
|
||||
const l = (max + min) / 2
|
||||
if (max === min) return `hsl(0, 0%, ${Math.round(l * 100)}%)`
|
||||
const d = max - min
|
||||
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
|
||||
let h = 0
|
||||
if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6
|
||||
else if (max === g) h = ((b - r) / d + 2) / 6
|
||||
else h = ((r - g) / d + 4) / 6
|
||||
return `hsl(${Math.round(h * 360)}, ${Math.round(s * 100)}%, ${Math.round(l * 100)}%)`
|
||||
}
|
||||
|
||||
const previewCustomization = computed<SpriteCustomization>(() => ({
|
||||
archetype: custForm.archetype || undefined,
|
||||
primaryColor: hexToHsl(custForm.primaryColor),
|
||||
secondaryColor: hexToHsl(custForm.secondaryColor),
|
||||
forceVisor: custForm.forceVisor,
|
||||
forceMohawk: custForm.forceMohawk,
|
||||
forceHorns: custForm.forceHorns,
|
||||
}))
|
||||
|
||||
function initCustForm() {
|
||||
if (!stats.value) return
|
||||
const c = stats.value.customization
|
||||
custForm.archetype = c?.archetype || stats.value.archetype || ''
|
||||
custForm.primaryColor = c?.primaryColor ? hslToHex(c.primaryColor) : '#3388cc'
|
||||
custForm.secondaryColor = c?.secondaryColor ? hslToHex(c.secondaryColor) : '#cc8833'
|
||||
custForm.forceVisor = c?.forceVisor ?? false
|
||||
custForm.forceMohawk = c?.forceMohawk ?? false
|
||||
custForm.forceHorns = c?.forceHorns ?? false
|
||||
}
|
||||
|
||||
async function saveCustomization() {
|
||||
if (isSaving.value) return
|
||||
isSaving.value = true
|
||||
custError.value = ''
|
||||
try {
|
||||
await updateCustomization({
|
||||
archetype: custForm.archetype || undefined,
|
||||
primaryColor: hexToHsl(custForm.primaryColor),
|
||||
secondaryColor: hexToHsl(custForm.secondaryColor),
|
||||
forceVisor: custForm.forceVisor,
|
||||
forceMohawk: custForm.forceMohawk,
|
||||
forceHorns: custForm.forceHorns,
|
||||
})
|
||||
if (stats.value) {
|
||||
stats.value = {
|
||||
...stats.value,
|
||||
archetype: custForm.archetype || stats.value.archetype,
|
||||
customization: {
|
||||
archetype: custForm.archetype || undefined,
|
||||
primaryColor: hexToHsl(custForm.primaryColor),
|
||||
secondaryColor: hexToHsl(custForm.secondaryColor),
|
||||
forceVisor: custForm.forceVisor,
|
||||
forceMohawk: custForm.forceMohawk,
|
||||
forceHorns: custForm.forceHorns,
|
||||
},
|
||||
}
|
||||
}
|
||||
showCustomize.value = false
|
||||
} catch (err) {
|
||||
custError.value = err instanceof Error ? err.message : 'Save failed'
|
||||
}
|
||||
isSaving.value = false
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user