// Каталог пиксель-арт спрайтов сада (ТЗ 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)
}
/** Заливка полигона сканлайнами (even-odd) — для изометрических граней. */
function fillPoly(m: Matrix, pts: [number, number][], ch: string): void {
const ys = pts.map((p) => p[1])
const y0 = Math.max(0, Math.ceil(Math.min(...ys)))
const y1 = Math.min(m.length - 1, Math.floor(Math.max(...ys)))
for (let y = y0; y <= y1; y++) {
const xs: number[] = []
for (let i = 0; i < pts.length; i++) {
const [x1, y1] = pts[i]!
const [x2, y2] = pts[(i + 1) % pts.length]!
if (y1 === y2) continue
if (y >= Math.min(y1, y2) && y < Math.max(y1, y2)) {
xs.push(x1 + ((y - y1) / (y2 - y1)) * (x2 - x1))
}
}
xs.sort((a, b) => a - b)
for (let i = 0; i + 1 < xs.length; i += 2) {
const xa = Math.ceil(xs[i]!)
const xb = Math.floor(xs[i + 1]!)
for (let x = xa; x <= xb; x++) put(m, x, y, ch)
}
}
}
/** Вертикальная грань-параллелограмм между двумя нижними точками (высота h вверх). */
function wall(m: Matrix, x1: number, y1: number, x2: number, y2: number, h: number, ch: string): void {
fillPoly(
m,
[
[x1, y1],
[x2, y2],
[x2, y2 - h],
[x1, y1 - h],
],
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
})()
// --- Домик (56×46, изометрия 2:1, footprint ~3.5×2.5 клетки) ---
// Грани строятся из мировых координат: sx = wx - wy, sy = (wx + wy)/2 - wz.
// Видимые грани: нижне-левая (тёмная N) и нижне-правая (светлая n);
// скат над нижне-левой гранью — в тени (R), над верхне-правой — свет (r).
export const HOUSE: Matrix = (() => {
const W = 56
const H = 46
const ox = 23
const oy = 20
// мировая точка → матрица
const p = (wx: number, wy: number, wz: number): [number, number] => [
Math.round(wx - wy + ox),
Math.round((wx + wy) / 2 - wz + oy),
]
const m = blank(W, H)
// основание 28×20, стены высотой 12, конёк wz=20 (wy=10), свес крыши 2
const B = p(28, 0, 0)! // правый угол основания
const C = p(28, 20, 0)! // передний
const D = p(0, 20, 0)! // левый
const B2 = p(28, 0, 12)!
const C2 = p(28, 20, 12)!
const K1 = p(0, 10, 22)! // конёк: левый конец
const K2 = p(28, 10, 22)! // конёк: правый конец
// стены: нижне-левая (тёмная) и нижне-правая (светлая)
wall(m, D[0], D[1], C[0], C[1], 12, 'N')
wall(m, C[0], C[1], B[0], B[1], 12, 'n')
// цоколь — тёмная кромка по низу стен
for (let i = 0; i <= 28; i++) {
const q1 = p(i, 20, 0)
const q2 = p(28, Math.round(20 - (i * 20) / 28), 0)
if (q1) put(m, q1[0], q1[1], 'O')
if (q2) put(m, q2[0], q2[1], 'O')
}
// скат над верхне-правой гранью AB (виден сверху, освещён)
const M1 = p(0, -2, 12)!
const M2 = p(28, -2, 12)!
fillPoly(m, [K1, K2, M2, M1], 'r')
// скат над нижне-левой гранью DC (фронтальный, в тени)
const P1 = p(0, 22, 12)!
const P2 = p(28, 22, 12)!
fillPoly(m, [K1, K2, P2, P1], 'R')
// кромки карнизов
for (const [a, b] of [
[M1, M2],
[P1, P2],
] as const)
for (let t = 0; t <= 1; t += 1 / 40)
put(m, Math.round(a[0] + (b[0] - a[0]) * t), Math.round(a[1] + (b[1] - a[1]) * t), 'O')
// конёк
for (let t = 0; t <= 1; t += 1 / 48)
put(m, Math.round(K1[0] + (K2[0] - K1[0]) * t), Math.round(K1[1] + (K2[1] - K1[1]) * t), 'r')
// фронтон над правой гранью CB (щипец)
fillPoly(m, [C2, B2, K2], 'n')
// труба на дальнем скате (низ «утоплен» в скат)
fillPoly(
m,
[
[34, 2],
[39, 2],
[39, 17],
[34, 14],
],
'n',
)
hline(m, 34, 39, 1, 'o')
hline(m, 34, 39, 2, 'O')
// дверь на светлой (правой) грани: wy 7..13, высота 10
const dr1 = p(28, 13, 0)!
const dr2 = p(28, 7, 0)!
fillPoly(m, [dr1, dr2, p(28, 7, 10)!, p(28, 13, 10)!], 'd')
// окно на тёмной (левой) грани: wx 16..22, wz 5..11
const wn1 = p(16, 20, 5)!
const wn2 = p(22, 20, 5)!
fillPoly(m, [wn1, wn2, p(22, 20, 11)!, p(16, 20, 11)!], 'c')
// отблеск на окне и двери
put(m, wn1[0] + 2, wn1[1] - 3, 'C')
put(m, dr2[0] + 2, dr2[1] - 4, '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][] = [
[14, 14],
[20, 26],
[28, 38],
]
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, 22],
[22, 30],
]
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][] = [
[16, 16],
[22, 28],
[32, 44],
]
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')
/** Мягкая овальная тень под объектом (рисуется с alpha ~0.45). */
export const SHADOW: Matrix = (() => {
const m = blank(24, 10)
ellipse(m, 12, 5, 11, 4, 'm')
return m
})()
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 }