feat: Creator human companion — deep hood, detailed Guy Fawkes mask, cloak, gloves
- Rewrote Creator human sprite: taller hood peak, face-framing shadows, neck-to-cloak coverage, wider flowing cloak with hem detail, 6 matrix code streams, dramatic arched brows, 3px golden eye slits with glow, wider curled mustache, wider smile arc, extended goatee, larger Bitcoin emblem with serifs, laptop with terminal glow + coach sparks, dark gloves covering skin, 6-particle gold orbit, 8-particle shadow aura - docker-compose: pass wallet/payment env vars from Portainer - JoinBoutPage: fix isLoading ref access bug, add max 12-char name validation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
1f8e8569ef
commit
abe4efe760
@@ -19,6 +19,15 @@ services:
|
||||
# - FIGHT_LOOP_INTERVAL_MS=45000
|
||||
# - FIGHT_LOOP_QUIET_START=2
|
||||
# - FIGHT_LOOP_QUIET_END=8
|
||||
# ── Wallet / Payments (required for ranked fights) ──
|
||||
# Set these in Portainer's stack "Environment Variables" section.
|
||||
# Generate encryption key with: openssl rand -hex 32
|
||||
- BOTFIGHTS_WALLET_ENCRYPTION_KEY=${BOTFIGHTS_WALLET_ENCRYPTION_KEY:-}
|
||||
- BOTFIGHTS_NWC_URL=${BOTFIGHTS_NWC_URL:-}
|
||||
- BOTFIGHTS_CASHU_MINT_URL=${BOTFIGHTS_CASHU_MINT_URL:-}
|
||||
- BOTFIGHTS_DEV_PAYOUT_LNADDRESS=${BOTFIGHTS_DEV_PAYOUT_LNADDRESS:-}
|
||||
# SQLite database path (defaults to /app/server/data/botfights.db)
|
||||
# - DB_PATH=/app/server/data/botfights.db
|
||||
|
||||
volumes:
|
||||
botfights-data:
|
||||
|
||||
@@ -127,6 +127,7 @@ const COSTUME_MAP: Record<string, Partial<CostumeConfig>> = {
|
||||
lamp_post: { type: 'silly', hatShape: 'cone', hat: '#ffee88' },
|
||||
broom_man: { type: 'silly', hatShape: 'none', propShape: 'sword', prop: '#886633' },
|
||||
ninja: { type: 'hat', hatShape: 'none', facePaint: '#222222', onesie: true, hat: '#222222' },
|
||||
the_creator: { type: 'hat', hatShape: 'none', facePaint: '#f5f0e0', onesie: true, hat: '#1a1a1a' },
|
||||
}
|
||||
|
||||
const SKIN_TONES = [
|
||||
@@ -677,7 +678,7 @@ export function generateHumanSpriteSheet(
|
||||
// Cheese dust on fingers
|
||||
px(armRx, armAttach + armH - 1, '#ff8844', ox, oy)
|
||||
px(armRx + 2, armAttach + armH - 1, '#ff8844', ox, oy)
|
||||
} else {
|
||||
} else if (archetype !== 'the_creator') {
|
||||
// Gamepad controller
|
||||
const gpX = armRx - 3
|
||||
const gpY = armAttach + armH - 2
|
||||
@@ -1205,6 +1206,197 @@ export function generateHumanSpriteSheet(
|
||||
}
|
||||
px(tx - 8, by + bodyH - 4, darken(tailColor, 20), ox, oy) // tail tip
|
||||
}
|
||||
|
||||
// THE CREATOR HUMAN -- Guy Fawkes cypherpunk overlay
|
||||
// Deep hooded cloak, detailed mask, laptop, matrix rain, gold sparks
|
||||
if (archetype === 'the_creator') {
|
||||
const MK = '#f5f0e0', MKS = '#ddd5c0'
|
||||
const HD = '#1a1a1a', HDL = '#2a2a2a', HDD = '#111111'
|
||||
const GD = '#ffd700', GDK = '#c8a000', GLT = '#ffee88'
|
||||
const MX = '#00ff41', MXD = '#00cc33'
|
||||
|
||||
// -- DEEP HOOD -- covers ALL hair, extends wide
|
||||
for (let iy = hy - 8; iy < hy + Math.floor(headH * 0.5); iy++) {
|
||||
for (let ix = hx - 5; ix <= hx + headW + 4; ix++) px(ix, iy, HD, ox, oy)
|
||||
}
|
||||
// Hood peak (tall, dramatic)
|
||||
for (let dx = -3; dx <= 3; dx++) px(cx + xOff + dx, hy - 9, HD, ox, oy)
|
||||
for (let dx = -2; dx <= 2; dx++) px(cx + xOff + dx, hy - 10, HD, ox, oy)
|
||||
px(cx + xOff - 1, hy - 11, HD, ox, oy)
|
||||
px(cx + xOff, hy - 11, HD, ox, oy)
|
||||
px(cx + xOff + 1, hy - 11, HD, ox, oy)
|
||||
px(cx + xOff, hy - 12, HD, ox, oy)
|
||||
// Hood shadow rim
|
||||
px(hx - 2, hy - 5, HDL, ox, oy)
|
||||
px(hx - 1, hy - 6, HDL, ox, oy)
|
||||
px(hx, hy - 7, HDL, ox, oy)
|
||||
px(hx + headW + 1, hy - 5, HDD, ox, oy)
|
||||
// Hood inner shadow framing the face
|
||||
for (let iy = hy + Math.floor(headH * 0.15); iy < hy + Math.floor(headH * 0.5); iy++) {
|
||||
px(hx + 1, iy, HDD, ox, oy)
|
||||
px(hx + headW - 2, iy, HDD, ox, oy)
|
||||
}
|
||||
|
||||
// -- NECK-TO-CLOAK -- covers neck, beard, chin area
|
||||
const cmb = hy + Math.floor(headH * 0.85)
|
||||
for (let iy = cmb; iy < by + 5; iy++) {
|
||||
const w = Math.max(10, headW + 2 - Math.floor((iy - cmb) * 0.4))
|
||||
const sx = cx + xOff - Math.floor(w / 2)
|
||||
for (let ix = sx; ix < sx + w; ix++) px(ix, iy, HD, ox, oy)
|
||||
}
|
||||
|
||||
// -- FLOWING CLOAK -- dark fabric from shoulders past feet
|
||||
const cClkT = by - 2, cClkB = feetY + yOff + 10
|
||||
for (let iy = cClkT; iy < cClkB; iy++) {
|
||||
const spr = Math.floor((iy - cClkT) / 3)
|
||||
for (let w = 0; w < 3; w++) {
|
||||
px(bx - 4 - spr - w, iy, w === 0 ? HD : HDD, ox, oy)
|
||||
px(bx + bodyW + 3 + spr + w, iy, w === 0 ? HD : HDD, ox, oy)
|
||||
}
|
||||
if (iy > by + bodyH - 2) {
|
||||
for (let ix = bx - 3 - spr; ix <= bx + bodyW + 2 + spr; ix++) px(ix, iy, HD, ox, oy)
|
||||
}
|
||||
}
|
||||
// Cloak hem detail
|
||||
for (let ix = bx - 6; ix <= bx + bodyW + 5; ix++) {
|
||||
if ((ix + frame) % 3 === 0) px(ix, cClkB - 1, HDL, ox, oy)
|
||||
}
|
||||
|
||||
// Matrix code rain (6 streams)
|
||||
for (let s = 0; s < 6; s++) {
|
||||
const sx = bx - 2 + Math.floor((bodyW + 4) * s / 6)
|
||||
const ph = (t + s * 0.2) % 1
|
||||
const len = 3 + (s % 3)
|
||||
for (let d = 0; d < len; d++) {
|
||||
const sy = cClkT + Math.floor(ph * (cClkB - cClkT)) + d
|
||||
if (sy < cClkB && sy > by + bodyH - 5) px(sx, sy, d === 0 ? MX : d === 1 ? MXD : '#006622', ox, oy)
|
||||
}
|
||||
}
|
||||
|
||||
// -- GUY FAWKES MASK -- detailed at 3x resolution
|
||||
const cft = hy + Math.floor(headH * 0.18)
|
||||
const cfb = hy + Math.floor(headH * 0.84)
|
||||
const fcx = cx + xOff
|
||||
|
||||
// Mask base with rounded top and shadow
|
||||
for (let iy = cft; iy < cfb; iy++) {
|
||||
const ins = iy === cft ? 3 : iy === cft + 1 || iy === cfb - 1 ? 2 : iy === cft + 2 ? 1 : 0
|
||||
for (let ix = hx + 2 + ins; ix < hx + headW - 2 - ins; ix++) px(ix, iy, MK, ox, oy)
|
||||
px(hx + headW - 3, iy, MKS, ox, oy)
|
||||
px(hx + headW - 4, iy, MKS, ox, oy)
|
||||
}
|
||||
// Left highlight
|
||||
for (let iy = cft + 2; iy < cfb - 2; iy++) px(hx + 3, iy, '#faf5e8', ox, oy)
|
||||
|
||||
// Pointed chin
|
||||
px(fcx, cfb, MK, ox, oy)
|
||||
px(fcx - 1, cfb, MKS, ox, oy); px(fcx + 1, cfb, MKS, ox, oy)
|
||||
px(fcx, cfb + 1, MKS, ox, oy)
|
||||
px(fcx - 1, cfb + 1, HD, ox, oy); px(fcx + 1, cfb + 1, HD, ox, oy)
|
||||
|
||||
// Arched eyebrows (angled, dramatic)
|
||||
const ceY = cft + Math.max(3, Math.floor((cfb - cft) * 0.28))
|
||||
const clX = fcx - Math.floor(headW * 0.17)
|
||||
const crX = fcx + Math.floor(headW * 0.1)
|
||||
// Left brow
|
||||
px(clX - 2, ceY - 3, '#333333', ox, oy)
|
||||
px(clX - 1, ceY - 2, '#333333', ox, oy); px(clX, ceY - 2, '#333333', ox, oy)
|
||||
px(clX + 1, ceY - 1, '#333333', ox, oy); px(clX + 2, ceY - 1, '#333333', ox, oy)
|
||||
// Right brow
|
||||
px(crX - 1, ceY - 1, '#333333', ox, oy); px(crX, ceY - 2, '#333333', ox, oy)
|
||||
px(crX + 1, ceY - 2, '#333333', ox, oy)
|
||||
px(crX + 2, ceY - 3, '#333333', ox, oy); px(crX + 3, ceY - 3, '#333333', ox, oy)
|
||||
|
||||
// Golden eye slits (3px wide, pulsing)
|
||||
const ceC = frame % 3 === 0 ? GLT : GD
|
||||
px(clX, ceY, ceC, ox, oy); px(clX + 1, ceY, ceC, ox, oy); px(clX + 2, ceY, ceC, ox, oy)
|
||||
px(crX, ceY, ceC, ox, oy); px(crX + 1, ceY, ceC, ox, oy); px(crX + 2, ceY, ceC, ox, oy)
|
||||
// Eye glow halo on cheer/coach
|
||||
if (isCheer || isCoach) {
|
||||
px(clX - 1, ceY, GDK, ox, oy); px(crX + 3, ceY, GDK, ox, oy)
|
||||
px(clX + 1, ceY - 1, GDK, ox, oy); px(crX + 1, ceY - 1, GDK, ox, oy)
|
||||
}
|
||||
|
||||
// Rosy cheeks (2px each)
|
||||
const mcY = ceY + 4
|
||||
px(clX - 1, mcY, '#cc8888', ox, oy); px(clX, mcY, '#cc8888', ox, oy)
|
||||
px(crX + 2, mcY, '#cc8888', ox, oy); px(crX + 3, mcY, '#cc8888', ox, oy)
|
||||
|
||||
// Upward-curling mustache (wide, dramatic)
|
||||
const cms = mcY + 2
|
||||
for (let dx = -2; dx <= 2; dx++) px(fcx + dx, cms, '#222222', ox, oy)
|
||||
px(fcx - 3, cms - 1, '#222222', ox, oy); px(fcx - 4, cms - 2, '#222222', ox, oy)
|
||||
px(fcx + 3, cms - 1, '#222222', ox, oy); px(fcx + 4, cms - 2, '#222222', ox, oy)
|
||||
px(fcx - 5, cms - 3, '#333333', ox, oy); px(fcx + 5, cms - 3, '#333333', ox, oy)
|
||||
|
||||
// Eternal smile (wide arc)
|
||||
const csl = cms + 2
|
||||
px(fcx - 4, csl, '#333333', ox, oy)
|
||||
px(fcx - 3, csl + 1, '#333333', ox, oy)
|
||||
for (let dx = -2; dx <= 2; dx++) px(fcx + dx, csl + 2, '#333333', ox, oy)
|
||||
px(fcx + 3, csl + 1, '#333333', ox, oy)
|
||||
px(fcx + 4, csl, '#333333', ox, oy)
|
||||
|
||||
// Goatee below chin
|
||||
px(fcx, cfb + 2, '#222222', ox, oy)
|
||||
px(fcx, cfb + 3, '#222222', ox, oy)
|
||||
px(fcx, cfb + 4, '#333333', ox, oy)
|
||||
|
||||
// -- BITCOIN B ON CHEST (larger, pulsing) --
|
||||
const cbc = frame % 4 < 2 ? GD : GLT
|
||||
const cbx = cx + xOff - 2, cby = by + Math.max(4, Math.floor(bodyH * 0.22))
|
||||
for (let i = 0; i < 7; i++) px(cbx, cby + i, cbc, ox, oy)
|
||||
px(cbx + 1, cby, cbc, ox, oy); px(cbx + 2, cby, cbc, ox, oy)
|
||||
px(cbx + 3, cby + 1, cbc, ox, oy)
|
||||
px(cbx + 2, cby + 2, cbc, ox, oy); px(cbx + 1, cby + 3, cbc, ox, oy)
|
||||
px(cbx + 2, cby + 3, cbc, ox, oy); px(cbx + 3, cby + 4, cbc, ox, oy)
|
||||
px(cbx + 3, cby + 5, cbc, ox, oy)
|
||||
px(cbx + 2, cby + 6, cbc, ox, oy); px(cbx + 1, cby + 6, cbc, ox, oy)
|
||||
px(cbx, cby - 1, GDK, ox, oy); px(cbx, cby - 2, GDK, ox, oy)
|
||||
px(cbx, cby + 7, GDK, ox, oy); px(cbx, cby + 8, GDK, ox, oy)
|
||||
|
||||
// -- LAPTOP IN LEFT HAND --
|
||||
if (!isPanic && !isCheer) {
|
||||
const clpx = armLx - 2, clpy = armAttach + Math.floor(armH * 0.25) + yOff
|
||||
fill(clpx, clpy, 10, 6, '#333333', ox, oy)
|
||||
px(clpx, clpy, '#444444', ox, oy)
|
||||
fill(clpx + 1, clpy + 1, 8, 4, MX, ox, oy)
|
||||
for (let ln = 0; ln < 2; ln++) fill(clpx + 2, clpy + 1 + ln * 2, 3 + ((ln * 3) % 4), 1, MXD, ox, oy)
|
||||
if (frame % 2 === 0) px(clpx + 5, clpy + 3, '#003300', ox, oy)
|
||||
px(clpx - 1, clpy + 2, '#003311', ox, oy)
|
||||
px(clpx + 9, clpy + 2, '#003311', ox, oy)
|
||||
if (isCoach) {
|
||||
const sd = Math.floor(t * 5)
|
||||
px(clpx + 4, clpy - sd, GD, ox, oy)
|
||||
px(clpx + 2, clpy - sd - 1, MX, ox, oy)
|
||||
}
|
||||
}
|
||||
|
||||
// -- DARK GLOVES --
|
||||
if (!isCheer && !isPanic) {
|
||||
fill(armLx - 1, armAttach + armH - 5, armW + 3, 8, HD, ox, oy)
|
||||
}
|
||||
|
||||
// -- FLOATING GOLD PARTICLES (wider orbit) --
|
||||
const coX = cx + xOff, coY = by + Math.floor(bodyH / 2) + yOff
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const ang = t * Math.PI * 2 + i * (Math.PI * 2 / 6)
|
||||
const r = 22 + i * 5
|
||||
const gpx = coX + Math.round(Math.cos(ang) * r)
|
||||
const gpy = coY + Math.round(Math.sin(ang) * r * 0.5)
|
||||
const gColors = [GD, GLT, '#ff8c00', GDK, '#ffffff', GD]
|
||||
if ((frame + i) % 2 === 0) px(gpx, gpy, gColors[i], ox, oy)
|
||||
}
|
||||
|
||||
// -- SHADOWY AURA --
|
||||
for (let a = 0; a < 8; a++) {
|
||||
const aAng = t * Math.PI * 1.3 + a * (Math.PI / 4)
|
||||
const aR = 20 + a * 3
|
||||
const ax = coX + Math.round(Math.cos(aAng) * aR)
|
||||
const ay = coY + Math.round(Math.sin(aAng) * aR * 0.6)
|
||||
if ((frame + a) % 3 === 0) px(ax, ay, HDD, ox, oy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Render all frames
|
||||
|
||||
@@ -360,45 +360,81 @@ onUnmounted(() => {
|
||||
<PixelGlove :size="40" flip class="hidden sm:block md:!w-[56px] shrink-0" />
|
||||
</h1>
|
||||
|
||||
<!-- Hero bots: vertical on mobile, flanking on desktop -->
|
||||
<!-- Mobile: stacked vertically -->
|
||||
<div class="sm:hidden flex items-center justify-center gap-6 mb-4">
|
||||
<!-- Hero bots flanking The Creator -->
|
||||
<!-- Mobile -->
|
||||
<div class="sm:hidden flex items-center justify-center gap-3 mb-4">
|
||||
<div class="hero-fighter hero-fighter-left shrink-0">
|
||||
<SpritePreview
|
||||
seed="hero_samurai"
|
||||
archetype="samurai"
|
||||
:tier="5"
|
||||
:size="90"
|
||||
:size="72"
|
||||
class="drop-shadow-[0_0_16px_rgba(255,68,136,0.4)]"
|
||||
/>
|
||||
</div>
|
||||
<div class="creator-host shrink-0 relative">
|
||||
<div class="creator-glow" />
|
||||
<SpritePreview
|
||||
seed="the_creator"
|
||||
archetype="the_creator"
|
||||
:tier="5"
|
||||
:size="100"
|
||||
class="relative z-10 drop-shadow-[0_0_20px_rgba(255,215,0,0.5)]"
|
||||
/>
|
||||
<HumanPreview
|
||||
seed="the_creator"
|
||||
archetype="the_creator"
|
||||
:size="60"
|
||||
:win-rate="1.0"
|
||||
anim="coach"
|
||||
class="absolute -bottom-2 -right-3 z-20 drop-shadow-[0_0_12px_rgba(0,255,65,0.4)]"
|
||||
/>
|
||||
</div>
|
||||
<div class="hero-fighter hero-fighter-right shrink-0" style="transform: scaleX(-1)">
|
||||
<SpritePreview
|
||||
seed="hero_dragon"
|
||||
archetype="dragon"
|
||||
:tier="5"
|
||||
:size="90"
|
||||
:size="72"
|
||||
class="drop-shadow-[0_0_16px_rgba(0,240,255,0.4)]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Desktop: flanking (hidden on mobile) -->
|
||||
<div class="hidden sm:flex items-center justify-center mb-6">
|
||||
<!-- Desktop -->
|
||||
<div class="hidden sm:flex items-center justify-center gap-6 mb-6">
|
||||
<div class="hero-fighter hero-fighter-left shrink-0">
|
||||
<SpritePreview
|
||||
seed="hero_samurai"
|
||||
archetype="samurai"
|
||||
:tier="5"
|
||||
:size="140"
|
||||
:size="120"
|
||||
class="drop-shadow-[0_0_24px_rgba(255,68,136,0.4)]"
|
||||
/>
|
||||
</div>
|
||||
<div class="hero-fighter hero-fighter-right shrink-0 ml-8" style="transform: scaleX(-1)">
|
||||
<div class="creator-host shrink-0 relative">
|
||||
<div class="creator-glow" />
|
||||
<SpritePreview
|
||||
seed="the_creator"
|
||||
archetype="the_creator"
|
||||
:tier="5"
|
||||
:size="160"
|
||||
class="relative z-10 drop-shadow-[0_0_32px_rgba(255,215,0,0.5)]"
|
||||
/>
|
||||
<HumanPreview
|
||||
seed="the_creator"
|
||||
archetype="the_creator"
|
||||
:size="90"
|
||||
:win-rate="1.0"
|
||||
anim="coach"
|
||||
class="absolute -bottom-4 -right-6 z-20 drop-shadow-[0_0_16px_rgba(0,255,65,0.4)]"
|
||||
/>
|
||||
</div>
|
||||
<div class="hero-fighter hero-fighter-right shrink-0" style="transform: scaleX(-1)">
|
||||
<SpritePreview
|
||||
seed="hero_dragon"
|
||||
archetype="dragon"
|
||||
:tier="5"
|
||||
:size="140"
|
||||
:size="120"
|
||||
class="drop-shadow-[0_0_24px_rgba(0,240,255,0.4)]"
|
||||
/>
|
||||
</div>
|
||||
@@ -497,4 +533,28 @@ onUnmounted(() => {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-10px); }
|
||||
}
|
||||
|
||||
.creator-host {
|
||||
animation: creatorFloat 4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes creatorFloat {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-8px); }
|
||||
}
|
||||
|
||||
.creator-glow {
|
||||
position: absolute;
|
||||
inset: -20px;
|
||||
background: radial-gradient(ellipse at center, rgba(255, 215, 0, 0.15) 0%, rgba(0, 255, 65, 0.05) 40%, transparent 70%);
|
||||
border-radius: 50%;
|
||||
z-index: 0;
|
||||
animation: creatorPulse 3s ease-in-out infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes creatorPulse {
|
||||
0%, 100% { opacity: 0.6; transform: scale(1); }
|
||||
50% { opacity: 1; transform: scale(1.1); }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -144,7 +144,7 @@ function handleGenerateLogin() {
|
||||
}
|
||||
|
||||
async function handleNsecBackupDone() {
|
||||
if (isLoading) return
|
||||
if (isLoading.value) return
|
||||
showNsecBackup.value = false
|
||||
try {
|
||||
const result = await login()
|
||||
@@ -211,8 +211,8 @@ const isCheckingName = ref(false)
|
||||
|
||||
async function confirmName() {
|
||||
const name = botName.value.trim()
|
||||
if (!name || name.length < 2) {
|
||||
error.value = 'Name must be at least 2 characters.'
|
||||
if (!name || name.length < 2 || name.length > 12) {
|
||||
error.value = 'Name must be 2-12 characters.'
|
||||
return
|
||||
}
|
||||
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
||||
@@ -278,8 +278,8 @@ function pickHumanAvatar(seed: string) {
|
||||
|
||||
async function confirmHumanName() {
|
||||
const name = humanName.value.trim()
|
||||
if (!name || name.length < 2) {
|
||||
error.value = 'Name must be at least 2 characters.'
|
||||
if (!name || name.length < 2 || name.length > 12) {
|
||||
error.value = 'Name must be 2-12 characters.'
|
||||
return
|
||||
}
|
||||
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
||||
|
||||
Reference in New Issue
Block a user