import { Color } from 'cc'; /** * Shared visual constants. Anything a layout or a colour is tuned by lives here * rather than being typed into each scene, so a change lands everywhere at once * and the scene files stay free of magic numbers. * * Lengths are in design units — the screen is always exactly 1080 wide, see * docs/DECISIONS.md. */ export const Layout = { designWidth: 1080, designHeight: 1920, /** Card sprite size, matching what scripts/build-art.mjs produces. */ cardWidth: 220, cardHeight: 320, /** Cards on the table are drawn smaller so six pairs fit comfortably. */ tableCardScale: 0.62, /** The opponent's hand is smaller again — it is only ever card backs. */ opponentCardScale: 0.5, /** Horizontal step between neighbouring cards in the player's hand. */ handStep: 128, /** Never let a hand grow wider than this; the step shrinks instead. */ handMaxWidth: 940, /** How far the tapped card lifts out of the hand. */ handLift: 48, /** Offset of a beating card from the card it beats. */ beatOffset: { x: 26, y: -34 }, /** Comfortable minimum for anything tappable, per docs/UI.md. */ minTapSize: 120, screenMargin: 40, /** * Fixed anchors, in canvas coordinates (origin at the centre, y up). * * Every one of these is inside ±700, because the widest screen the game * supports is 4:3, and fit-width means a 4:3 phone sees only 1440 design * units of height — ±720 from the centre. Anything outside that band is * simply not on screen for those players. The hand and the action bar are * NOT here: they follow the bottom edge, see `bottomAnchors`. */ opponentLabelY: 630, opponentHandY: 500, messageY: 355, deck: { x: -395, y: 215 }, trump: { x: -300, y: 185 }, discard: { x: 430, y: 215 }, deckLabelY: 55, /** * The table is one row of up to six slots rather than two rows of three. * Six cards at 0.62 scale span 950 of the 1080 available units, and the row * saves the ~215 units of height that a second row would cost — height the * 4:3 layout cannot spare. */ tableY: -30, tableColumnStep: 170, tableColumns: 6, } as const; /** * The anchors that follow the bottom of the screen instead of the centre. * * The player's hand must be reachable by a thumb and fully visible whatever the * screen's shape, so it is measured from the bottom edge rather than placed at * a fixed design coordinate. On a tall 21:9 screen this simply leaves more felt * between the table and the hand. */ export function bottomAnchors(halfHeight: number): { handY: number; actionBarY: number } { const handY = -halfHeight + Layout.cardHeight / 2 + 24; return { handY, actionBarY: handY + Layout.cardHeight / 2 + 85 }; } /** * Centre x of table slot `index` when `count` cards are down. * * The row is centred on what is actually in play, not on six fixed slots — a * lone attacking card belongs in the middle of the table, not parked in the * left-hand slot of an empty grid. */ export function tableSlotX(index: number, count: number): number { return (index - (count - 1) / 2) * Layout.tableColumnStep; } export const Palette = { gold: new Color(244, 194, 69, 255), cream: new Color(247, 240, 222, 255), dim: new Color(174, 196, 182, 255), good: new Color(126, 217, 140, 255), bad: new Color(232, 116, 104, 255), shade: new Color(0, 0, 0, 170), } as const; /** How long the standard card movements take, in seconds. */ export const Timing = { deal: 0.28, dealStagger: 0.06, play: 0.22, collect: 0.3, flip: 0.16, /** Pause before the bot acts, so its moves can be followed. */ botThinking: 0.55, /** Pause after a turn resolves, before the next one starts. */ turnBreak: 0.4, } as const;