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:
parent
3a88ebc626
commit
271cbfe3da
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.
|
|
@ -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
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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); },
|
||||
};
|
||||
}
|
||||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue