diff --git a/docker-compose.yml b/docker-compose.yml index d6438a0..a0fc109 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: diff --git a/frontend/src/game/sprites/human.ts b/frontend/src/game/sprites/human.ts index 7545cd5..dbea60d 100644 --- a/frontend/src/game/sprites/human.ts +++ b/frontend/src/game/sprites/human.ts @@ -127,6 +127,7 @@ const COSTUME_MAP: Record> = { 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 diff --git a/frontend/src/pages/HomePage.vue b/frontend/src/pages/HomePage.vue index c86c3f9..39ad32e 100644 --- a/frontend/src/pages/HomePage.vue +++ b/frontend/src/pages/HomePage.vue @@ -360,45 +360,81 @@ onUnmounted(() => {