feat(balatro): add CRT post-processing, rounded art rendering, and UI improvements

- Introduce BalatroCrtPipeline for subtle CRT effects (scanlines, grain, glitch bursts)
- Attach CRT pipeline to main camera with event-driven pulse on boss rounds and game over
- Add frameIsBlank check to detect blank/unpainted texture frames and fall back to procedural cards
- Implement roundedArtKey and addRoundedArt for rendering art with rounded corners
- Update joker, tarot, planet, spectral, deck, shop, and voucher rendering to use rounded art
- Increase font sizes across UI elements for better readability
- Add border strokes to joker cards with custom art
- Trigger CRT pulse on high multipliers (x2+) and score announcements
- Reference art image paths in balatro-artwork.json
This commit is contained in:
Brian Fertig 2026-07-10 20:50:06 -06:00
parent 3a88ebc626
commit 271cbfe3da
10 changed files with 217 additions and 34 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 MiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 792 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

View File

@ -6,13 +6,13 @@
],
"jokerSheet": {
"key": "balatro-jokers",
"path": null,
"path": "assets/images/balatro-artwork.png",
"frameWidth": 150,
"frameHeight": 200
},
"tarotSheet": {
"key": "balatro-tarots",
"path": null,
"path": "assets/images/balatro-tarots.png",
"frameWidth": 150,
"frameHeight": 200
},
@ -36,7 +36,7 @@
},
"packSheet": {
"key": "balatro-packs",
"path": null,
"path": "assets/images/balatro-packs.png",
"frameWidth": 180,
"frameHeight": 240
},

View File

@ -0,0 +1,109 @@
// BalatroCrtPipeline.js
// A subtle full-screen CRT treatment applied as a camera post-FX pipeline
// (like the real game's optional CRT shader): constant faint scanlines and
// grain, plus short "glitch" bursts — horizontal band tearing and RGB
// chromatic aberration — that fire ambiently every so often and can be
// pulsed harder from game events (boss rounds, big hands, game over).
// Canvas renderer (no WebGL) gets a no-op stub; the game is unaffected.
import * as Phaser from 'phaser';
// Tuning knobs. Everything is keyed to "subtle at rest".
const SCANLINE_STRENGTH = 0.12; // max darkening of a scanline trough (0..1)
const SCANLINE_PERIOD = 6.0; // scanline spacing in framebuffer pixels
const GRAIN_STRENGTH = 0.015; // constant animated noise amplitude
const BAND_COUNT = 24.0; // horizontal tear bands during a glitch
const TEAR_STRENGTH = 0.04; // max uv.x displacement of a torn band
const ABERRATION = 0.006; // max R/B channel offset during a glitch
const PULSE_DECAY = 0.92; // per-frame decay of the glitch envelope
const AMBIENT_STRENGTH = 0.35; // envelope kick of an ambient micro-glitch
const AMBIENT_MIN_MS = 8000; // ambient glitch interval range
const AMBIENT_MAX_MS = 15000;
const FRAG = `
precision mediump float;
uniform sampler2D uMainSampler;
uniform float uTime;
uniform float uGlitch;
uniform vec2 uResolution;
varying vec2 outTexCoord;
float hash(vec2 p) {
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
}
void main(void) {
vec2 uv = outTexCoord;
// Horizontal band tearing: a minority of bands shear sideways, with the
// pattern re-rolled ~14 times a second so a burst reads as flickery.
float band = floor(uv.y * ${BAND_COUNT.toFixed(1)});
float h = hash(vec2(band, floor(uTime * 14.0)));
float tear = step(0.72, h) * (h - 0.5) * ${TEAR_STRENGTH.toFixed(3)} * uGlitch;
uv.x = clamp(uv.x + tear, 0.0, 1.0);
// RGB split, only during a glitch so text stays crisp at rest.
float ca = ${ABERRATION.toFixed(4)} * uGlitch;
vec3 col;
col.r = texture2D(uMainSampler, vec2(clamp(uv.x + ca, 0.0, 1.0), uv.y)).r;
col.g = texture2D(uMainSampler, uv).g;
col.b = texture2D(uMainSampler, vec2(clamp(uv.x - ca, 0.0, 1.0), uv.y)).b;
// Constant faint scanlines in framebuffer pixels.
float scan = 0.5 + 0.5 * sin(gl_FragCoord.y * 6.28318 / ${SCANLINE_PERIOD.toFixed(1)});
col *= 1.0 - ${SCANLINE_STRENGTH.toFixed(3)} * scan;
// Faint animated grain, a touch stronger while glitching.
float grain = hash(gl_FragCoord.xy + fract(uTime) * 61.7) - 0.5;
col += grain * (${GRAIN_STRENGTH.toFixed(3)} + 0.03 * uGlitch);
gl_FragColor = vec4(col, 1.0);
}
`;
export class BalatroCrtPipeline extends Phaser.Renderer.WebGL.Pipelines.PostFXPipeline {
constructor(game) {
super({ game, name: 'BalatroCrt', fragShader: FRAG });
this._pulse = 0;
this._nextAmbient = 0;
}
onPreRender() {
const t = this.game.loop.time;
if (t > this._nextAmbient) {
// First tick only schedules; later ticks fire a micro-glitch.
if (this._nextAmbient > 0) this._pulse = Math.max(this._pulse, AMBIENT_STRENGTH);
this._nextAmbient = t + AMBIENT_MIN_MS + Math.random() * (AMBIENT_MAX_MS - AMBIENT_MIN_MS);
}
this._pulse *= PULSE_DECAY;
if (this._pulse < 0.005) this._pulse = 0;
this.set1f('uTime', t / 1000);
this.set1f('uGlitch', this._pulse);
this.set2f('uResolution', this.renderer.width, this.renderer.height);
}
// Event hook: momentarily intensify the glitch (0..1).
pulse(strength = 0.5) {
this._pulse = Math.min(1, Math.max(this._pulse, strength));
}
}
// Attach the CRT post-FX to the scene's main camera. Returns a control
// object mirroring makeSwirlBackground's shape; on Canvas it is a stub.
export function attachCrt(scene) {
if (!scene.renderer || scene.renderer.type !== Phaser.WEBGL) {
return { pulse() {}, destroy() {} };
}
scene.renderer.pipelines.addPostPipeline('BalatroCrt', BalatroCrtPipeline);
const cam = scene.cameras.main;
cam.setPostPipeline(BalatroCrtPipeline);
const got = cam.getPostPipeline(BalatroCrtPipeline);
const inst = Array.isArray(got) ? got[0] : got;
return {
pulse(strength) { if (inst && inst.pulse) inst.pulse(strength); },
destroy() { cam.removePostPipeline(BalatroCrtPipeline); },
};
}

View File

@ -28,6 +28,7 @@ import {
skipPack, useConsumable, consumableDef, isDebuffed,
} from './BalatroLogic.js';
import { makeSwirlBackground } from './BalatroSwirlPipeline.js';
import { attachCrt } from './BalatroCrtPipeline.js';
import {
renderTitle, renderDeckSelect, renderBlindSelect, renderShop, renderPackOpen,
renderGameOver, renderCashout,
@ -99,6 +100,8 @@ export default class BalatroGame extends Phaser.Scene {
this.handLayer = this.add.container(0, 0).setDepth(40);
this.fxLayer = this.add.container(0, 0).setDepth(80);
this.swirl = makeSwirlBackground(this, this.bgLayer);
this.crt = attachCrt(this);
this.events.once('shutdown', () => this.crt.destroy());
// Fast-forward: tapping during a scoring animation snaps to the end.
this.input.on('pointerdown', () => { if (this.animating) this._skipAnim = true; });
@ -204,6 +207,8 @@ export default class BalatroGame extends Phaser.Scene {
this.selected = [];
this.pendingConsumable = null;
this.pendingJoker = null;
if (v === 'play' && this.run && this.run.blind === 'boss') this.crt.pulse(0.7);
if (v === 'gameover') this.crt.pulse(1.0);
this.renderView();
}
@ -279,7 +284,73 @@ export default class BalatroGame extends Phaser.Scene {
const map = this.art[mapKey];
if (!sheet || !map || map[id] === undefined) return null;
if (!this.textures.exists(sheet.key)) return null;
return { key: sheet.key, frame: map[id] };
const frame = map[id];
if (this.frameIsBlank(sheet.key, frame)) return null;
return { key: sheet.key, frame };
}
// Sheets are painted incrementally — unpainted frames are left transparent/white.
// Sample a few points per frame and treat it as "not yet drawn" if none hit ink,
// so unfinished sheets fall back to procedural cards instead of showing blanks.
frameIsBlank(key, frame) {
if (!this._blankFrameCache) this._blankFrameCache = {};
const cacheKey = `${key}:${frame}`;
if (this._blankFrameCache[cacheKey] !== undefined) return this._blankFrameCache[cacheKey];
const src = this.textures.getFrame(key, frame);
let blank = true;
if (src) {
const steps = 5;
outer:
for (let i = 1; i < steps; i++) {
for (let j = 1; j < steps; j++) {
const x = Math.floor((i / steps) * src.width);
const y = Math.floor((j / steps) * src.height);
const px = this.textures.getPixel(x, y, key, frame);
if (px && px.alpha > 10 && !(px.red > 245 && px.green > 245 && px.blue > 245)) { blank = false; break outer; }
}
}
}
this._blankFrameCache[cacheKey] = blank;
return blank;
}
// Bakes a rounded-corner copy of a sheet frame into its own canvas texture
// (cached by key:frame:radius) so drop-in art matches the rounded look of
// the procedural cards without needing a live geometry mask on the sprite.
roundedArtKey(key, frame, radius = 10) {
if (!this._roundedArtCache) this._roundedArtCache = {};
const cacheKey = `${key}:${frame}:${radius}`;
if (this._roundedArtCache[cacheKey]) return this._roundedArtCache[cacheKey];
const src = this.textures.getFrame(key, frame);
if (!src) return null;
const outKey = `balatro-rounded:${cacheKey}`;
if (!this.textures.exists(outKey)) {
const w = src.cutWidth, h = src.cutHeight;
const r = Math.min(radius, w / 2, h / 2);
const canvasTex = this.textures.createCanvas(outKey, w, h);
const ctx = canvasTex.getContext();
ctx.beginPath();
ctx.moveTo(r, 0);
ctx.arcTo(w, 0, w, h, r);
ctx.arcTo(w, h, 0, h, r);
ctx.arcTo(0, h, 0, 0, r);
ctx.arcTo(0, 0, w, 0, r);
ctx.closePath();
ctx.clip();
ctx.drawImage(src.source.image, src.cutX, src.cutY, w, h, 0, 0, w, h);
canvasTex.refresh();
}
this._roundedArtCache[cacheKey] = outKey;
return outKey;
}
// Draws a drop-in art frame with rounded corners at (0,0) inside a container.
addRoundedArt(container, key, frame, w, h, radius, x = 0, y = 0) {
const roundedKey = this.roundedArtKey(key, frame, radius);
const img = roundedKey ? this.add.image(x, y, roundedKey) : this.add.image(x, y, key, frame);
img.setDisplaySize(w, h);
container.add(img);
return img;
}
// ── card rendering ────────────────────────────────────────────────────────
@ -378,8 +449,10 @@ export default class BalatroGame extends Phaser.Scene {
const cont = this.add.container(x, y);
const af = this.artFrame('jokerSheet', 'jokers', id);
if (af) {
const img = this.add.image(0, 0, af.key, af.frame).setDisplaySize(w, h);
cont.add(img);
this.addRoundedArt(cont, af.key, af.frame, w, h, 10);
const rg = this.add.graphics();
rg.lineStyle(3, C.rarity[def.rarity], 1); rg.strokeRoundedRect(-w / 2, -h / 2, w, h, 10);
cont.add(rg);
} else {
const g = this.add.graphics();
g.fillStyle(0x241d33, 1); g.fillRoundedRect(-w / 2, -h / 2, w, h, 10);
@ -387,11 +460,11 @@ export default class BalatroGame extends Phaser.Scene {
g.lineStyle(2, C.rarity[def.rarity], 1); g.strokeRoundedRect(-w / 2, -h / 2, w, h, 10);
cont.add(g);
const name = this.add.text(0, -h / 2 + 15, def.name, {
fontFamily: 'm6x11, "Julius Sans One"', fontSize: `${Math.min(17, Math.round(280 / def.name.length))}px`, color: '#141019', fontStyle: 'bold',
fontFamily: 'm6x11, "Julius Sans One"', fontSize: `${Math.min(20, Math.round(320 / def.name.length))}px`, color: '#141019', fontStyle: 'bold',
}).setOrigin(0.5);
const desc = typeof def.desc === 'function' ? def.desc(opts.inst || { state: def.initState ? def.initState() : {} }) : def.desc;
const body = this.add.text(0, 8, desc, {
fontFamily: 'm6x11, "Julius Sans One"', fontSize: '15px', color: C.ink, align: 'center',
fontFamily: 'm6x11, "Julius Sans One"', fontSize: '17px', color: C.ink, align: 'center',
wordWrap: { width: w - 16 },
}).setOrigin(0.5);
cont.add([name, body]);
@ -403,14 +476,14 @@ export default class BalatroGame extends Phaser.Scene {
eg.lineStyle(4, edColor, 0.95); eg.strokeRoundedRect(-w / 2, -h / 2, w, h, 10);
cont.add(eg);
const tag = this.add.text(0, h / 2 - 16, EDITIONS[edition].name.toUpperCase(), {
fontFamily: 'm6x11, "Julius Sans One"', fontSize: '11px', color: '#ffffff',
fontFamily: 'm6x11, "Julius Sans One"', fontSize: '13px', color: '#ffffff',
}).setOrigin(0.5).setAlpha(0.9);
cont.add(tag);
if (edition === 'negative') cont.setAlpha(0.8);
}
if (opts.inst && opts.inst.debuffedByBoss) {
cont.add(this.add.rectangle(0, 0, w, h, 0x1a1420, 0.6));
cont.add(this.add.text(0, 0, 'DISABLED', { fontFamily: 'm6x11, "Julius Sans One"', fontSize: '16px', color: '#c86a6a' }).setOrigin(0.5));
cont.add(this.add.text(0, 0, 'DISABLED', { fontFamily: 'm6x11, "Julius Sans One"', fontSize: '18px', color: '#c86a6a' }).setOrigin(0.5));
}
cont.setSize(w, h);
return cont;
@ -424,7 +497,7 @@ export default class BalatroGame extends Phaser.Scene {
const sheet = { tarot: ['tarotSheet', 'tarots'], planet: ['planetSheet', 'planets'], spectral: ['spectralSheet', 'spectrals'] }[kind];
const af = this.artFrame(sheet[0], sheet[1], id);
if (af) {
cont.add(this.add.image(0, 0, af.key, af.frame).setDisplaySize(w, h));
this.addRoundedArt(cont, af.key, af.frame, w, h, 10);
} else {
const tint = { tarot: C.tarot, planet: C.planet, spectral: C.spectral }[kind];
const g = this.add.graphics();
@ -433,16 +506,16 @@ export default class BalatroGame extends Phaser.Scene {
g.lineStyle(1, tint, 0.6); g.strokeRoundedRect(-w / 2 + 6, -h / 2 + 6, w - 12, h - 12, 8);
cont.add(g);
const name = this.add.text(0, -h / 2 + 20, def.name, {
fontFamily: 'm6x11, "Julius Sans One"', fontSize: `${Math.min(16, Math.round(300 / def.name.length))}px`,
fontFamily: 'm6x11, "Julius Sans One"', fontSize: `${Math.min(19, Math.round(340 / def.name.length))}px`,
color: `#${tint.toString(16).padStart(6, '0')}`, fontStyle: 'bold', align: 'center', wordWrap: { width: w - 14 },
}).setOrigin(0.5, 0);
const descText = kind === 'planet' ? `+1 level: ${HAND_BY_ID[def.hand].name}` : def.desc;
const body = this.add.text(0, 16, descText, {
fontFamily: 'm6x11, "Julius Sans One"', fontSize: '14px', color: C.ink, align: 'center', wordWrap: { width: w - 18 },
fontFamily: 'm6x11, "Julius Sans One"', fontSize: '16px', color: C.ink, align: 'center', wordWrap: { width: w - 18 },
}).setOrigin(0.5);
cont.add([name, body]);
const glyph = { tarot: '☽', planet: '✶', spectral: '✧' }[kind];
cont.add(this.add.text(0, h / 2 - 22, glyph, { fontFamily: 'm6x11, Georgia, serif', fontSize: '20px', color: `#${tint.toString(16).padStart(6, '0')}` }).setOrigin(0.5));
cont.add(this.add.text(0, h / 2 - 22, glyph, { fontFamily: 'm6x11, Georgia, serif', fontSize: '22px', color: `#${tint.toString(16).padStart(6, '0')}` }).setOrigin(0.5));
}
cont.setSize(w, h);
return cont;
@ -455,19 +528,19 @@ export default class BalatroGame extends Phaser.Scene {
showTooltip(anchor, info) {
this.hideTooltip();
if (!info || !anchor.active) return;
const W = info.width || 330, PAD = 16;
const W = info.width || 370, PAD = 16;
const cont = this.add.container(0, 0);
this.fxLayer.add(cont);
let y = PAD;
const title = this.add.text(PAD, y, info.title, {
fontFamily: 'm6x11, "Julius Sans One"', fontSize: '22px', color: info.titleColor || C.ink,
fontFamily: 'm6x11, "Julius Sans One"', fontSize: '26px', color: info.titleColor || C.ink,
fontStyle: 'bold', wordWrap: { width: W - PAD * 2 },
});
cont.add(title);
y += title.height + 4;
if (info.tag) {
const tag = this.add.text(PAD, y, info.tag, {
fontFamily: 'm6x11, "Julius Sans One"', fontSize: '14px', color: info.tagColor || C.muted,
fontFamily: 'm6x11, "Julius Sans One"', fontSize: '16px', color: info.tagColor || C.muted,
});
cont.add(tag);
y += tag.height + 8;
@ -476,7 +549,7 @@ export default class BalatroGame extends Phaser.Scene {
}
for (const line of info.lines || []) {
const t = this.add.text(PAD, y, line.text, {
fontFamily: 'm6x11, "Julius Sans One"', fontSize: '16px', color: line.color || C.muted,
fontFamily: 'm6x11, "Julius Sans One"', fontSize: '19px', color: line.color || C.muted,
wordWrap: { width: W - PAD * 2 },
});
cont.add(t);
@ -769,14 +842,14 @@ export default class BalatroGame extends Phaser.Scene {
this.dismissPanel();
const cont = this.add.container(0, 0).setDepth(92);
this.fxLayer.add(cont);
const px = 700, py = 280, pw = 460, ph = 190;
const px = 700, py = 280, pw = 460, ph = 210;
const g = this.add.graphics();
g.fillStyle(C.panel, 0.97); g.fillRoundedRect(px, py, pw, ph, 12);
g.lineStyle(2, C.rarity[def.rarity], 1); g.strokeRoundedRect(px, py, pw, ph, 12);
cont.add(g);
const desc = typeof def.desc === 'function' ? def.desc(inst) : def.desc;
cont.add(this.add.text(px + 20, py + 14, `${def.name}${inst.edition ? ` (${EDITIONS[inst.edition].name})` : ''}`, { fontFamily: 'm6x11, "Julius Sans One"', fontSize: '24px', color: C.ink }));
cont.add(this.add.text(px + 20, py + 52, desc, { fontFamily: 'm6x11, "Julius Sans One"', fontSize: '17px', color: C.muted, wordWrap: { width: pw - 40 } }));
cont.add(this.add.text(px + 20, py + 14, `${def.name}${inst.edition ? ` (${EDITIONS[inst.edition].name})` : ''}`, { fontFamily: 'm6x11, "Julius Sans One"', fontSize: '28px', color: C.ink }));
cont.add(this.add.text(px + 20, py + 56, desc, { fontFamily: 'm6x11, "Julius Sans One"', fontSize: '20px', color: C.muted, wordWrap: { width: pw - 40 } }));
const sell = new Button(this, px + pw - 110, py + ph - 40, `Sell $${inst.sellValue}`, () => {
const r = sellJoker(this.run, inst.uid);
if (r.ok) { playSound(this, SFX.COINS); this.save(); this.renderView(); }
@ -817,17 +890,17 @@ export default class BalatroGame extends Phaser.Scene {
this.dismissPanel();
const cont = this.add.container(0, 0).setDepth(92);
this.fxLayer.add(cont);
const px = 1230, py = 260, pw = 470, ph = 210;
const px = 1230, py = 260, pw = 470, ph = 230;
const g = this.add.graphics();
g.fillStyle(C.panel, 0.97); g.fillRoundedRect(px, py, pw, ph, 12);
const tint = { tarot: C.tarot, planet: C.planet, spectral: C.spectral }[inst.kind];
g.lineStyle(2, tint, 1); g.strokeRoundedRect(px, py, pw, ph, 12);
cont.add(g);
cont.add(this.add.text(px + 20, py + 14, def.name, { fontFamily: 'm6x11, "Julius Sans One"', fontSize: '24px', color: C.ink }));
cont.add(this.add.text(px + 20, py + 14, def.name, { fontFamily: 'm6x11, "Julius Sans One"', fontSize: '28px', color: C.ink }));
const descText = inst.kind === 'planet' ? `+1 level: ${HAND_BY_ID[def.hand].name}` : def.desc;
cont.add(this.add.text(px + 20, py + 52, descText, { fontFamily: 'm6x11, "Julius Sans One"', fontSize: '17px', color: C.muted, wordWrap: { width: pw - 40 } }));
cont.add(this.add.text(px + 20, py + 56, descText, { fontFamily: 'm6x11, "Julius Sans One"', fontSize: '20px', color: C.muted, wordWrap: { width: pw - 40 } }));
const needs = def.targets || 0;
if (needs) cont.add(this.add.text(px + 20, py + ph - 96, `Select up to ${needs} card${needs > 1 ? 's' : ''} in hand first`, { fontFamily: 'm6x11, "Julius Sans One"', fontSize: '15px', color: '#a8d8ff' }));
if (needs) cont.add(this.add.text(px + 20, py + ph - 96, `Select up to ${needs} card${needs > 1 ? 's' : ''} in hand first`, { fontFamily: 'm6x11, "Julius Sans One"', fontSize: '17px', color: '#a8d8ff' }));
const use = new Button(this, px + pw - 110, py + ph - 40, 'Use', () => {
const r = useConsumable(this.run, inst.uid, this.selected.slice(0, Math.max(needs, 2)));
if (!r.ok) { this.toast(r.error); return; }
@ -1170,7 +1243,7 @@ export default class BalatroGame extends Phaser.Scene {
case 'xmult':
floater(sprite, `×${ev.v}`, '#ffa24f'); pop(sprite, true);
playSound(this, SFX.SCIFI_PLINK);
if (ev.v >= 2) this.cameras.main.shake(120, 0.004);
if (ev.v >= 2) { this.cameras.main.shake(120, 0.004); this.crt.pulse(0.5); }
break;
case 'money': floater(sprite, `+$${ev.v}`, C.money); playSound(this, SFX.COINS); if (this._goldText && this._goldText.active) this._goldText.setText(`$${this.run.gold}`); break;
case 'retrigger': if (sprite) this.ringPulse(sprite); break;
@ -1195,6 +1268,7 @@ export default class BalatroGame extends Phaser.Scene {
this.fxLayer.add(slam);
this.tweens.add({ targets: slam, alpha: 1, scale: 1, duration: 180, ease: 'Back.easeOut' });
this.cameras.main.shake(160, score >= this.run.blindChips ? 0.008 : 0.004);
this.crt.pulse(score >= this.run.blindChips ? 0.8 : 0.4);
playSound(this, score >= 10000 ? SFX.SCIFI_EXPLODE : SFX.CASINO_BLACKJACK);
this._roundScoreOverride = null;
if (this._roundScoreText && this._roundScoreText.active) this._roundScoreText.setText(fmtChips(this.run.roundScore));

View File

@ -86,7 +86,7 @@ export function renderDeckSelect(scene) {
cont.add(g);
// Deck back: art frame if present, else colored card back.
const af = scene.artFrame('deckBackSheet', 'deckBacks', deck.id);
if (af) cont.add(scene.add.image(0, -60, af.key, af.frame).setDisplaySize(120, 164));
if (af) scene.addRoundedArt(cont, af.key, af.frame, 120, 164, 8, 0, -60);
else {
const bg = scene.add.graphics();
bg.fillStyle(deck.color, 0.95); bg.fillRoundedRect(-60, -142, 120, 164, 8);
@ -94,8 +94,8 @@ export function renderDeckSelect(scene) {
bg.lineStyle(1, 0xffffff, 0.25); bg.strokeRoundedRect(-48, -130, 96, 140, 6);
cont.add(bg);
}
cont.add(scene.add.text(0, 52, deck.name, { fontFamily: 'm6x11, "Julius Sans One"', fontSize: '26px', color: C.ink, fontStyle: 'bold' }).setOrigin(0.5));
cont.add(scene.add.text(0, 108, deck.desc, { fontFamily: 'm6x11, "Julius Sans One"', fontSize: '17px', color: C.muted, align: 'center', wordWrap: { width: w - 30 } }).setOrigin(0.5));
cont.add(scene.add.text(0, 52, deck.name, { fontFamily: 'm6x11, "Julius Sans One"', fontSize: '28px', color: C.ink, fontStyle: 'bold' }).setOrigin(0.5));
cont.add(scene.add.text(0, 108, deck.desc, { fontFamily: 'm6x11, "Julius Sans One"', fontSize: '19px', color: C.muted, align: 'center', wordWrap: { width: w - 30 } }).setOrigin(0.5));
cont.setSize(w, h);
scene.add2(cont);
scene.addHoverTilt(cont);
@ -240,7 +240,7 @@ export function renderShop(scene) {
const def = BOOSTER_BY_ID[slot.id];
const cont = scene.add.container(x, py2);
const af = scene.artFrame('packSheet', 'packs', slot.id);
if (af) cont.add(scene.add.image(0, 0, af.key, af.frame).setDisplaySize(180, 240));
if (af) scene.addRoundedArt(cont, af.key, af.frame, 180, 240, 12);
else {
const tint = { playing: 0x4f9ad4, tarot: C.tarot, planet: C.planet, joker: 0xd4884f, spectral: C.spectral }[def.what];
const g = scene.add.graphics();
@ -248,7 +248,7 @@ export function renderShop(scene) {
g.lineStyle(3, 0xffffff, 0.5); g.strokeRoundedRect(-90, -120, 180, 240, 12);
g.fillStyle(0x141019, 0.35); g.fillRoundedRect(-90, -30, 180, 60, 0);
cont.add(g);
cont.add(scene.add.text(0, 0, def.name.replace(' Pack', '\nPack'), { fontFamily: 'm6x11, "Julius Sans One"', fontSize: '22px', color: '#ffffff', align: 'center', fontStyle: 'bold' }).setOrigin(0.5));
cont.add(scene.add.text(0, 0, def.name.replace(' Pack', '\nPack'), { fontFamily: 'm6x11, "Julius Sans One"', fontSize: '25px', color: '#ffffff', align: 'center', fontStyle: 'bold' }).setOrigin(0.5));
}
cont.setSize(180, 240);
scene.add2(cont);
@ -277,15 +277,15 @@ export function renderShop(scene) {
scene.text(vx, vy - 180, 'Voucher', 20, C.muted, { ox: 0.5 });
const cont = scene.add.container(vx, vy);
const af = scene.artFrame('voucherSheet', 'vouchers', v.id);
if (af) cont.add(scene.add.image(0, 0, af.key, af.frame).setDisplaySize(190, 250));
if (af) scene.addRoundedArt(cont, af.key, af.frame, 190, 250, 10);
else {
const g = scene.add.graphics();
g.fillStyle(0x143a2a, 0.95); g.fillRoundedRect(-95, -125, 190, 250, 10);
g.lineStyle(3, C.voucher, 1); g.strokeRoundedRect(-95, -125, 190, 250, 10);
g.lineStyle(1, C.voucher, 0.5); g.strokeRoundedRect(-85, -115, 170, 230, 8);
cont.add(g);
cont.add(scene.add.text(0, -80, v.name, { fontFamily: 'm6x11, "Julius Sans One"', fontSize: '22px', color: '#7ce0c0', align: 'center', fontStyle: 'bold', wordWrap: { width: 170 } }).setOrigin(0.5));
cont.add(scene.add.text(0, 20, v.desc, { fontFamily: 'm6x11, "Julius Sans One"', fontSize: '16px', color: C.ink, align: 'center', wordWrap: { width: 165 } }).setOrigin(0.5));
cont.add(scene.add.text(0, -80, v.name, { fontFamily: 'm6x11, "Julius Sans One"', fontSize: '25px', color: '#7ce0c0', align: 'center', fontStyle: 'bold', wordWrap: { width: 170 } }).setOrigin(0.5));
cont.add(scene.add.text(0, 20, v.desc, { fontFamily: 'm6x11, "Julius Sans One"', fontSize: '18px', color: C.ink, align: 'center', wordWrap: { width: 165 } }).setOrigin(0.5));
}
cont.setSize(190, 250);
scene.add2(cont);