Newer
Older
gnexus-tasks / frontend / src / game / sprites.ts
// Каталог пиксель-арт спрайтов сада (ТЗ 3.13). Геометрия строится программно
// (эллипсы, стволы, крыши) — надёжнее, чем ASCII-матрицы переменной ширины.
// Всё рисуется в матрицах «нативного пикселя» (клетка = 16px, мир = ×2).
import { blank, flipX, stamp } from './pixelart'
import type { Matrix, Palette } from './pixelart'

/** Палитра сцены — тон в тон тёмной теме (фон ~#1a1f2e). */
export const PAL: Palette = {
  g: '#2e4a2b', // трава тёмная
  G: '#385a32', // трава
  H: '#41663a', // трава светлая / травинки
  w: '#3b3226', // дикая земля
  W: '#45392b', // дикая земля светлая
  e: '#35682c', // изгородь
  E: '#4a8a3c', // изгородь светлая
  F: '#254c1f', // изгородь тень
  o: '#8a6d4b', // дерево (постройки)
  O: '#6b5238', // дерево тёмное
  r: '#c96a7c', // крыша
  R: '#a35264', // крыша тень
  n: '#8f7a4f', // стена
  N: '#6e5c3c', // стена тень
  d: '#5b74c9', // дверь
  c: '#5fc4b4', // окно
  C: '#f5d76e', // свет (окна, фонарь)
  u: '#4a6fd4', // вода
  U: '#7aa2f7', // вода светлая
  b: '#6b4f3a', // клумба (земля)
  s: '#4a3826', // почва тёмная
  l: '#4a8f3c', // листва
  L: '#5fae4b', // листва светлая
  k: '#356b2c', // листва тёмная
  t: '#3f7a33', // стебель
  p: '#f7768e', // цветок розовый
  P: '#bb9af7', // цветок лиловый
  y: '#e0af68', // золото (редкость)
  h: '#d6dae4', // белый
  x: '#7d8496', // камень
  q: '#f5d76e', // свечение (эпик)
  m: '#243022', // тень на земле
}

/** Поставить пиксель (без выхода за границы). */
function put(m: Matrix, x: number, y: number, ch: string): void {
  const row = m[y]
  if (row && x >= 0 && x < row.length) m[y] = row.substring(0, x) + ch + row.substring(x + 1)
}

/** Заполненный эллипс. */
function ellipse(m: Matrix, cx: number, cy: number, rx: number, ry: number, ch: string): void {
  for (let y = Math.floor(cy - ry); y <= Math.ceil(cy + ry); y++) {
    for (let x = Math.floor(cx - rx); x <= Math.ceil(cx + rx); x++) {
      const dx = (x - cx) / rx
      const dy = (y - cy) / ry
      if (dx * dx + dy * dy <= 1) put(m, x, y, ch)
    }
  }
}

/** Вертикальная линия. */
function vline(m: Matrix, x: number, y0: number, y1: number, ch: string): void {
  for (let y = y0; y <= y1; y++) put(m, x, y, ch)
}

/** Горизонтальная полоса. */
function hline(m: Matrix, x0: number, x1: number, y: number, ch: string): void {
  for (let x = x0; x <= x1; x++) put(m, x, y, ch)
}

// --- Тайлы (16×16, бесшовные) ---

function speckle(m: Matrix, seed: number, chs: string[]): void {
  // детерминированный «шум» внутри 2..13 — края чистые (бесшовность)
  for (let i = 0; i < 5; i++) {
    const x = 2 + ((seed * 7 + i * 29) % 12)
    const y = 2 + ((seed * 13 + i * 17) % 12)
    put(m, x, y, chs[i % chs.length]!)
  }
}

function grassTile(seed: number, base = 'g'): Matrix {
  const m = blank(16, 16).map(() => base.repeat(16)) as Matrix
  speckle(m, seed, ['G', 'H', 'G'])
  return m
}

export const GRASS_A = grassTile(1)
export const GRASS_B = grassTile(2, 'g')

/** Супер-тайл 32×32: шахматка grass-a/b (период 2 клетки — глубина без рваного шума). */
export const GRASS_SUPER: Matrix = (() => {
  const rows: Matrix = []
  for (let y = 0; y < 32; y++) {
    const left = y < 16 ? GRASS_A[y]! : GRASS_B[y - 16]!
    const right = y < 16 ? GRASS_B[y]! : GRASS_A[y - 16]!
    rows.push(left + right)
  }
  return rows
})()

export const GRASS_TUFT: Matrix = (() => {
  const m = [...GRASS_A]
  // кустики травинок: светлые пучки на тёмном стебле
  const tuft = ['H.H.H', 'HHHHH', '.HtH.', '..t..']
  return stamp(m, tuft, 5, 7)
})()

export const WILD: Matrix = (() => {
  const m = blank(16, 16).map(() => 'w'.repeat(16)) as Matrix
  speckle(m, 3, ['W', 'W', 'x'])
  return m
})()

/** Изгородь: период 4 (бугры 2×2) — бесшовна при тайлировании. */
export const HEDGE: Matrix = (() => {
  const m = blank(16, 16).map(() => 'e'.repeat(16)) as Matrix
  for (let x = 0; x < 16; x++) {
    if (x % 4 < 2) {
      put(m, x, 0, 'E')
      put(m, x, 1, 'E')
    }
    put(m, x, 15, 'F')
  }
  // лиственная фактура: светлые пятна (детерминированные)
  for (let i = 0; i < 6; i++) {
    const x = (i * 5 + 2) % 16
    const y = 2 + ((i * 7) % 12)
    put(m, x, y, y < 4 ? 'E' : 'F')
  }
  return m
})()

// --- Домик (64×48, footprint 4×3 клетки) ---

export const HOUSE: Matrix = (() => {
  const m = blank(64, 48)
  // стена
  for (let y = 22; y < 48; y++) m[y] = 'n'.repeat(64)
  hline(m, 0, 63, 22, 'N')
  hline(m, 0, 63, 46, 'N')
  hline(m, 0, 63, 47, 'N')
  vline(m, 0, 22, 47, 'N')
  vline(m, 63, 22, 47, 'N')
  // крыша-трапеция (y 0..21)
  for (let i = 0; i <= 21; i++) {
    const half = Math.round(10 + (i / 21) * 22)
    for (let x = 32 - half; x < 32 + half; x++) put(m, x, i, 'r')
    put(m, 32 - half, i, 'R')
    put(m, 32 + half - 1, i, 'R')
  }
  hline(m, 10, 53, 21, 'R')
  // дверь (10×16, по центру, до земли)
  for (let y = 32; y < 48; y++)
    for (let x = 27; x < 37; x++) {
      const edge = x === 27 || x === 36 || y === 32
      put(m, x, y, edge ? 'O' : 'd')
    }
  // окна 8×8 с отблеском
  for (const wx of [10, 46])
    for (let y = 28; y < 36; y++)
      for (let x = wx; x < wx + 8; x++) {
        const edge = y === 28 || y === 35 || x === wx || x === wx + 7
        put(m, x, y, edge ? 'N' : y === 29 && x === wx + 2 ? 'C' : 'c')
      }
  return m
})()

// --- Растения (6 видов × 3 стадии) ---
// Вид по item_key с бэка; стадии 0/1/2. anchor спрайта (0.5, 0.95).

/** Стебель с листиками (общая база цветочных видов). */
function flowerBase(w: number, h: number, topY: number): Matrix {
  const m = blank(w, h)
  const cx = Math.floor(w / 2)
  vline(m, cx, topY, h - 2, 't')
  // листья: пара эллипсов по бокам
  ellipse(m, cx - 3, h - 5, 3, 1.6, 'l')
  ellipse(m, cx + 3, h - 8, 3, 1.6, 'l')
  put(m, cx - 4, h - 6, 'k')
  put(m, cx + 4, h - 8, 'k')
  return m
}

/** Головка цветка: лепестки + сердцевина. */
function bloom(m: Matrix, cx: number, cy: number, rx: number, petal: string, core: string): void {
  ellipse(m, cx, cy, rx, Math.max(2, rx - 1), petal)
  ellipse(m, cx, cy, Math.max(1, rx - 2), Math.max(1, rx - 3), core)
  // лепестковые «выступы»
  put(m, cx, cy - Math.max(2, rx - 1) - 1, petal)
  put(m, cx, cy + Math.max(2, rx - 1), petal)
  put(m, cx - rx - 1, cy, petal)
  put(m, cx + rx + 1, cy, petal)
}

export function plantFlower(stage: number): Matrix {
  if (stage === 0) {
    const m = flowerBase(12, 12, 5)
    bloom(m, 6, 3, 2, 'p', 'y')
    return m
  }
  if (stage === 1) {
    const m = flowerBase(16, 20, 7)
    bloom(m, 8, 4, 3, 'p', 'y')
    return m
  }
  const m = flowerBase(24, 28, 10)
  bloom(m, 12, 5, 5, 'p', 'y')
  // второй бутон сбоку
  vline(m, 17, 12, 18, 't')
  bloom(m, 17, 10, 2, 'p', 'y')
  return m
}

function plantLeaf(stage: number): Matrix {
  const sizes: [number, number][] = [
    [12, 12],
    [16, 20],
    [24, 28],
  ]
  const [w, h] = sizes[stage]!
  const m = blank(w, h)
  const cx = Math.floor(w / 2)
  // пучок листьев: перекрывающиеся эллипсы от земли
  const ry = Math.floor(h * 0.45)
  ellipse(m, cx, h - ry - 2, Math.floor(w * 0.32), ry, 'l')
  ellipse(m, cx - Math.floor(w * 0.22), h - ry, Math.floor(w * 0.22), ry * 0.7, 'k')
  ellipse(m, cx + Math.floor(w * 0.22), h - ry, Math.floor(w * 0.22), ry * 0.7, 'k')
  ellipse(m, cx, h - ry - Math.floor(ry * 0.5), Math.floor(w * 0.18), ry * 0.55, 'L')
  // травинки сверху
  for (let i = 0; i <= stage * 2; i++)
    vline(m, cx - 2 + i * 2, h - ry * 2 + (i % 2), h - ry * 2 + 2 + (i % 3), 'H')
  return m
}

function plantEvergreen(stage: number): Matrix {
  const sizes: [number, number][] = [
    [12, 12],
    [16, 20],
    [24, 28],
  ]
  const [w, h] = sizes[stage]!
  const m = blank(w, h)
  const cx = Math.floor(w / 2)
  // ярусы кроны: треугольники сверху вниз
  const crownH = h - 3
  for (let tier = 0; tier <= stage; tier++) {
    const top = Math.floor((crownH / (stage + 1)) * tier)
    const bottom = Math.floor((crownH / (stage + 1)) * (tier + 1))
    const halfMax = Math.floor((w / 2 - 1) * ((tier + 1) / (stage + 1)))
    for (let y = top; y <= bottom; y++) {
      const half = Math.max(1, Math.round((halfMax * (y - top)) / (bottom - top)))
      for (let x = cx - half; x <= cx + half; x++) put(m, x, y, y === bottom ? 'k' : tier === stage ? 'l' : 'k')
      put(m, cx, y, tier === stage ? 'L' : 'l')
    }
  }
  vline(m, cx, crownH, h - 2, 'O') // ствол
  return m
}

function plantLotus(stage: number): Matrix {
  const sizes: [number, number][] = [
    [12, 12],
    [16, 20],
    [24, 28],
  ]
  const [w, h] = sizes[stage]!
  const m = blank(w, h)
  const cx = Math.floor(w / 2)
  // широкие листья-лежанки у земли
  ellipse(m, cx - 4, h - 3, 4, 1.6, 'l')
  ellipse(m, cx + 4, h - 3, 4, 1.6, 'l')
  ellipse(m, cx, h - 4, 3, 1.4, 'k')
  // лотос: заострённые лепестки веером
  const cy = h - 6 - stage * 3
  const size = 2 + stage * 2
  for (let i = -size; i <= size; i++) {
    const ph = Math.max(2, size - Math.abs(i)) // высота лепестка
    for (let dy = 0; dy < ph; dy++) put(m, cx + i, cy + dy, dy === 0 ? 'h' : dy < 2 ? 'h' : 'p')
  }
  ellipse(m, cx, cy + 1, Math.max(1, size - 2), 1, 'y')
  return m
}

function plantCactus(stage: number): Matrix {
  const sizes: [number, number][] = [
    [12, 12],
    [16, 20],
    [24, 28],
  ]
  const [w, h] = sizes[stage]!
  const m = blank(w, h)
  const cx = Math.floor(w / 2)
  const bodyH = Math.floor(h * (0.5 + stage * 0.15))
  // ствол
  ellipse(m, cx, h - bodyH / 2 - 1, Math.floor(w * 0.2), bodyH / 2, 'l')
  vline(m, cx - Math.floor(w * 0.2), h - bodyH + 2, h - 2, 'l')
  vline(m, cx + Math.floor(w * 0.2), h - bodyH + 2, h - 2, 'l')
  vline(m, cx, h - bodyH, h - 2, 'L')
  // руки
  if (stage >= 1) {
    ellipse(m, cx - Math.floor(w * 0.32), h - bodyH * 0.6, 2, 1.4, 'l')
    vline(m, cx - Math.floor(w * 0.32) + 1, h - Math.floor(bodyH * 0.75), h - Math.floor(bodyH * 0.45), 'l')
  }
  if (stage >= 2) {
    ellipse(m, cx + Math.floor(w * 0.32), h - bodyH * 0.8, 2, 1.4, 'l')
    vline(m, cx + Math.floor(w * 0.32), h - Math.floor(bodyH * 0.95), h - Math.floor(bodyH * 0.6), 'l')
  }
  // колючки
  for (let y = h - bodyH + 3; y < h - 3; y += 3) {
    put(m, cx - Math.floor(w * 0.2) - 1, y, 'H')
    put(m, cx + Math.floor(w * 0.2) + 1, y + 1, 'H')
  }
  // цветок на верхушке
  const top = h - bodyH - 2
  put(m, cx, top, 'q')
  put(m, cx - 1, top + 1, 'q')
  put(m, cx + 1, top + 1, 'q')
  if (stage >= 2) {
    put(m, cx, top - 1, 'q')
    put(m, cx, top + 1, 'y')
  }
  return m
}

function plantOak(stage: number): Matrix {
  const sizes: [number, number][] = [
    [12, 12],
    [16, 20],
    [24, 28],
  ]
  const [w, h] = sizes[stage]!
  const m = blank(w, h)
  const cx = Math.floor(w / 2)
  const trunkH = Math.floor(h * (0.25 + stage * 0.08))
  const crownR = Math.floor(w * (0.28 + stage * 0.08))
  // крона: три перекрывающихся эллипса
  ellipse(m, cx, h - trunkH - crownR * 0.6, crownR, crownR * 0.75, 'l')
  ellipse(m, cx - crownR * 0.5, h - trunkH - crownR * 0.2, crownR * 0.6, crownR * 0.5, 'k')
  ellipse(m, cx + crownR * 0.5, h - trunkH - crownR * 0.25, crownR * 0.6, crownR * 0.5, 'k')
  ellipse(m, cx - 1, h - trunkH - crownR * 0.8, crownR * 0.4, crownR * 0.4, 'L')
  put(m, cx + 2, h - trunkH - crownR, 'L')
  put(m, cx - 3, h - trunkH - crownR * 0.5, 'L')
  // ствол с корнями
  for (let y = h - trunkH; y < h; y++) {
    put(m, cx, y, 'o')
    put(m, cx + 1, y, 'O')
  }
  hline(m, cx - 2, cx + 3, h - 1, 'O')
  return m
}

export const PLANT_BUILDERS: Record<string, (stage: number) => Matrix> = {
  'ph-flower': plantFlower,
  'ph-leaf': plantLeaf,
  'ph-tree-evergreen': plantEvergreen,
  'ph-flower-lotus': plantLotus,
  'ph-cactus': plantCactus,
  'ph-tree': plantOak,
}

// --- Декорации (item_key → матрица; используются и для чипов инвентаря) ---

export const DECOR: Record<string, Matrix> = {
  fence: (() => {
    const m = blank(24, 10)
    hline(m, 0, 23, 3, 'o')
    hline(m, 0, 23, 4, 'O')
    hline(m, 0, 23, 7, 'o')
    hline(m, 0, 23, 8, 'O')
    for (const px of [1, 11, 21]) {
      vline(m, px, 0, 9, 'o')
      vline(m, px + 1, 0, 9, 'O')
      put(m, px, 0, 'O')
    }
    return m
  })(),
  flowerbed: (() => {
    const m = blank(20, 10)
    ellipse(m, 10, 7, 9, 2.5, 'b')
    ellipse(m, 10, 7, 7, 1.6, 's')
    // три цветка с лепестками и стеблями
    const flower = (cx: number, cy: number, petal: string): void => {
      vline(m, cx, cy + 2, cy + 3, 't')
      put(m, cx - 1, cy + 3, 'l')
      put(m, cx + 1, cy + 3, 'l')
      ellipse(m, cx, cy, 1.6, 1.6, petal)
      put(m, cx, cy, 'y')
    }
    flower(5, 3, 'p')
    flower(10, 2, 'P')
    flower(15, 3, 'p')
    return m
  })(),
  lantern: (() => {
    const m = blank(10, 18)
    vline(m, 4, 6, 17, 'o')
    vline(m, 5, 6, 17, 'O')
    hline(m, 2, 7, 17, 'O')
    hline(m, 2, 7, 16, 'o')
    // корпус фонаря
    for (let y = 0; y < 6; y++)
      for (let x = 2; x < 8; x++) {
        const edge = y === 0 || y === 5 || x === 2 || x === 7
        put(m, x, y, edge ? 'O' : 'C')
      }
    put(m, 4, 2, 'q')
    put(m, 5, 3, 'q')
    return m
  })(),
  bench: (() => {
    const m = blank(22, 10)
    hline(m, 0, 21, 0, 'o') // спинка
    hline(m, 0, 21, 4, 'o') // сиденье
    hline(m, 0, 21, 5, 'O')
    vline(m, 3, 6, 9, 'O')
    vline(m, 18, 6, 9, 'O')
    vline(m, 8, 1, 3, 'o')
    vline(m, 13, 1, 3, 'o')
    return m
  })(),
  birdbath: (() => {
    const m = blank(16, 12)
    ellipse(m, 8, 3, 6, 2, 'x')
    ellipse(m, 8, 3, 4, 1.2, 'u')
    put(m, 7, 3, 'U')
    vline(m, 7, 4, 9, 'x')
    vline(m, 8, 4, 9, 'x')
    ellipse(m, 8, 10, 4, 1.4, 'x')
    return m
  })(),
  pond: (() => {
    const m = blank(28, 14)
    ellipse(m, 14, 7, 13, 6, 'x')
    ellipse(m, 14, 7, 11, 4.6, 'u')
    ellipse(m, 12, 6, 5, 2, 'U')
    put(m, 18, 8, 'U')
    put(m, 9, 9, 'U')
    return m
  })(),
  gazebo: (() => {
    const m = blank(28, 24)
    // крыша-конус
    for (let y = 0; y <= 10; y++) {
      const half = Math.round(2 + (y / 10) * 12)
      for (let x = 14 - half; x < 14 + half; x++) put(m, x, y, 'r')
      put(m, 14 - half, y, 'R')
      put(m, 14 + half - 1, y, 'R')
    }
    hline(m, 2, 25, 10, 'R')
    // столбы и пол
    for (const px of [4, 23]) vline(m, px, 11, 21, 'o')
    vline(m, 14, 11, 21, 'O')
    hline(m, 2, 25, 21, 'o')
    hline(m, 2, 25, 22, 'O')
    hline(m, 5, 22, 20, 'O')
    return m
  })(),
}

// --- Вспомогательные ---

/** Кольцо редкости под растением (эллипс-кайма). */
function ring(gold: string): Matrix {
  const m = blank(18, 6)
  ellipse(m, 9, 3, 8, 2.4, gold)
  ellipse(m, 9, 3, 6, 1.6, ' ')
  return m
}

export const RING_RARE = ring('y')
export const RING_EPIC = ring('q')

export const SPARKLE: Matrix = ['.q.', 'qqq', '.q.']

/** Рамка выделенной клетки (32×32, рисуется с alpha ~0.4). */
export const SELECT_OUTLINE: Matrix = (() => {
  const m = blank(32, 32)
  hline(m, 0, 31, 0, 'h')
  hline(m, 0, 31, 31, 'h')
  vline(m, 0, 0, 31, 'h')
  vline(m, 31, 0, 31, 'h')
  return m
})()

// --- Доступ по ключам: текстуры — в textures.ts (импортирует pixi и попадает
// в async-чанк рендерера); этот файл остаётся чистым (матрицы для DOM-превью).

export { flipX }