// Каталог пиксель-арт спрайтов сада (ТЗ 3.13). Геометрия строится программно
// (эллипсы, стволы, крыши) — надёжнее, чем ASCII-матрицы переменной ширины.
// Всё рисуется в матрицах «нативного пикселя» (клетка = 16px, мир = ×2).
import { blank, flipX, stamp } from './pixelart'
import type { Matrix, Palette } from './pixelart'
/** Палитра сцены — тон в тон тёмной теме (фон ~#1a1f2e). */
export const PAL: Palette = {
g: '#2c4a36', // трава тёмная (холодный оттенок)
G: '#365a41', // трава
H: '#40674b', // трава светлая / травинки
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: '#b3a26b', // песок берега
s: '#4a3826', // почва тёмная
l: '#4a8f3c', // листва
L: '#5fae4b', // листва светлая
k: '#356b2c', // листва тёмная
t: '#3f7a33', // стебель
p: '#f7768e', // цветок розовый
P: '#bb9af7', // цветок лиловый
y: '#e0af68', // золото (редкость)
h: '#d6dae4', // белый
x: '#7d8496', // камень
q: '#f5d76e', // свечение (эпик)
m: '#243022', // тень на земле
i: '#c9b98a', // дорожка (гравий светлый)
I: '#9a8a5f', // дорожка тень
f: '#e8903a', // огонь / рябина
Z: '#f5b04a', // огонь светлая
v: '#7c5fd4', // звёздный/бабочий (сирень)
V: '#a48ae8', // сирень светлая
B: '#63b3e8', // голубой (роса)
A: '#8a5a3c', // янтарь (ягоды)
j: '#c9e06e', // подсолнух жёлтый
J: '#f0d75e', // подсолнух светлая
z: '#3e8f82', // стекло в тени (теплица)
}
/** Поставить пиксель (без выхода за границы). */
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,
)
}
/** Шахматная растушёвка края эллипса: переход полосы frac к цвету ch (' ' — фон). */
function feather(
m: Matrix,
cx: number,
cy: number,
rx: number,
ry: number,
frac: 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 d = ((x - cx) / rx) ** 2 + ((y - cy) / ry) ** 2
if (d > 1 - frac && d <= 1 && (x + y) % 2 === 0) 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
// фактура: светлые травинки и полутона (детерминированный «шум» в 2..13,
// края чистые — бесшовность при тайлировании)
for (let i = 0; i < 8; i++) {
const x = 2 + ((seed * 7 + i * 29) % 12)
const y = 2 + ((seed * 13 + i * 17) % 12)
put(m, x, y, i % 3 === 0 ? 'H' : 'G')
}
// редкие тёмные пятнышки почвы
for (let i = 0; i < 2; i++) {
const x = 2 + ((seed * 11 + i * 23) % 12)
const y = 2 + ((seed * 5 + i * 31) % 12)
put(m, x, y, 'k')
}
return m
}
export const GRASS_A = grassTile(1)
export const GRASS_B = grassTile(2)
export const GRASS_C = grassTile(5)
export const GRASS_D = grassTile(9)
function checker2(a: Matrix, b: Matrix): Matrix {
// 32×32: шахматка 2×2 из 16px тайлов
const rows: Matrix = []
for (let y = 0; y < 32; y++) {
const left = y < 16 ? a[y]! : b[y - 16]!
const right = y < 16 ? b[y]! : a[y - 16]!
rows.push(left + right)
}
return rows
}
/** Супер-тайл 64×64: четыре разных тайла, период 4 клетки — вариативность без рваного шума. */
export const GRASS_SUPER: Matrix = (() => {
const tl = checker2(GRASS_A, GRASS_B)
const tr = checker2(GRASS_C, GRASS_D)
const bl = checker2(GRASS_D, GRASS_C)
const br = checker2(GRASS_B, GRASS_A)
const rows: Matrix = []
for (let y = 0; y < 64; y++) {
rows.push((y < 32 ? tl[y]! : bl[y - 32]!) + (y < 32 ? tr[y]! : br[y - 32]!))
}
return rows
})()
const TUFT = ['H.H.H', 'HHHHH', '.HtH.', '..t..']
export const GRASS_TUFT: Matrix = stamp([...GRASS_A], TUFT, 5, 7)
/** Кустиик с белым цветком — второй вариант травинки для разнообразия. */
export const GRASS_TUFT_FLOWER: Matrix = stamp([...GRASS_C], ['h.h', 'hhh', '.t.'], 6, 8)
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 = 62
const H = 52
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
}
/** Роса: тонкие стебли с голубыми цветками-каплями (ур. 7). */
function plantDew(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)
// 2-4 изогнутых стебля веером
const stems = 1 + stage * 2
for (let i = 0; i < stems; i++) {
const dx = (i - (stems - 1) / 2) * Math.max(2, Math.floor(w / (stems + 2)))
const top = Math.floor(h * 0.25)
for (let y = h - 2; y >= top + Math.abs(dx); y--) put(m, cx + dx + ((h - y) % 2), y, 't')
// капля-цветок на конце стебля
const fx = cx + dx + Math.max(1, Math.floor(Math.abs(dx) / 2))
const fy = top + Math.abs(dx) - 1
put(m, fx, fy - 1, 'B')
put(m, fx - 1, fy, 'B')
put(m, fx, fy, 'B')
put(m, fx + 1, fy, 'B')
put(m, fx, fy + 1, 'h')
}
ellipse(m, cx, h - 3, Math.floor(w * 0.25), 1.6, 'l')
return m
}
/** Подсолнух: толстый стебель, крупное жёлтое соцветие (ур. 8). */
function plantSunflower(stage: number): Matrix {
const sizes: [number, number][] = [[12, 14], [16, 22], [24, 32]]
const [w, h] = sizes[stage]!
const m = blank(w, h)
const cx = Math.floor(w / 2)
const headR = 2 + stage * 2
const headY = Math.floor(h * 0.3)
vline(m, cx, headY, h - 2, 't') // стебель
vline(m, cx + 1, headY + 2, h - 2, 'k')
// листья-лопасти по стеблю
for (let i = 0; i <= stage; i++) {
const ly = headY + Math.floor((h - headY) * (0.3 + i * 0.25))
hline(m, cx - 3 - (i % 2), cx - 1, ly, 'l')
hline(m, cx + 2, cx + 3 + (i % 2), ly + 2, 'l')
}
// соцветие: жёлтые лепестки вокруг тёмной сердцевины
ellipse(m, cx, headY, headR + 1, headR, 'j')
ellipse(m, cx, headY, headR, headR - 1, 'J')
ellipse(m, cx, headY, Math.max(1, headR - 2), Math.max(1, headR - 2), 's')
put(m, cx - headR - 1, headY, 'j')
put(m, cx + headR + 1, headY, 'j')
return m
}
/** Бонсай: изогнутый ствол, плоские ярусы кроны (ур. 9). */
function plantBonsai(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 trunkH = Math.floor(h * (0.3 + stage * 0.06))
// S-изгиб ствола
for (let y = h - trunkH; y < h; y++) {
const t = (y - (h - trunkH)) / trunkH
const sway = Math.round(Math.sin(t * Math.PI) * (stage + 1) * 0.6)
put(m, cx + sway, y, 'o')
put(m, cx + sway + 1, y, 'O')
}
// крона: 2-3 плоских «подушки»
const pads = 1 + stage
const crownW = Math.floor(w * (0.28 + stage * 0.09))
for (let pad = 0; pad < pads; pad++) {
const py = h - trunkH - Math.floor((h - trunkH - 2) * (pad / pads)) - 1
const pw = Math.floor(crownW * (1 - pad * 0.25))
ellipse(m, cx, py, pw, 1.8, 'l')
ellipse(m, cx - 1, py - 1, Math.max(1, pw - 2), 1.2, 'L')
put(m, cx + pw - 1, py + 1, 'k')
}
return m
}
/** Бабочий куст: пучок стеблей с сиреневыми метёлками (ур. 10). */
function plantButterflyBush(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 spikes = 3 + stage * 2
for (let i = 0; i < spikes; i++) {
const sx = cx + Math.round((i - (spikes - 1) / 2) * (w / (spikes + 1)))
const top = Math.floor(h * 0.2) + (i % 2) * 2
vline(m, sx, top + 3, h - 2, 'k')
const spikeH = Math.max(2, h - top - 5)
for (let dy = 0; dy < spikeH; dy++) {
const half = Math.max(0, Math.round((1 - dy / spikeH) * 2))
for (let dx = -half; dx <= half; dx++) put(m, sx + dx, top + dy, dy % 2 ? 'v' : 'V')
}
put(m, sx, top - 1, 'q')
}
ellipse(m, cx, h - 3, Math.floor(w * 0.3), 1.8, 'l')
return m
}
/** Рябина: дерево с огненно-оранжевой листвой и ягодами (ур. 11, premium). */
function plantRowan(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, 'f')
ellipse(m, cx - crownR * 0.5, h - trunkH - crownR * 0.2, crownR * 0.6, crownR * 0.5, 'Z')
ellipse(m, cx + crownR * 0.5, h - trunkH - crownR * 0.25, crownR * 0.6, crownR * 0.5, 'f')
ellipse(m, cx - 1, h - trunkH - crownR * 0.8, crownR * 0.4, crownR * 0.4, 'Z')
// гроздья ягод
const clusters = 2 + stage
for (let i = 0; i < clusters; i++) {
const bx = cx - crownR + Math.floor((i * 2 * crownR) / (clusters - 1 || 1))
const by = h - trunkH - crownR * 0.4 + (i % 2) * 2
put(m, bx, by, 'A')
put(m, bx + 1, by + 1, 'A')
}
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
}
/** Звездоцвет: цветы-звёзды необычной сиреневой палитры (ур. 12, premium). */
function plantStarflower(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 stems = 1 + stage
for (let i = 0; i < stems; i++) {
const dx = (i - (stems - 1) / 2) * Math.max(3, Math.floor(w / (stems + 2)))
const top = Math.floor(h * 0.3) + (i % 2)
vline(m, cx + dx, top, h - 2, 't')
// звезда: лучи из центра
const fy = top - 2
put(m, cx + dx, fy, 'h')
put(m, cx + dx, fy - 1, 'V')
put(m, cx + dx - 1, fy, 'V')
put(m, cx + dx + 1, fy, 'V')
put(m, cx + dx - 1, fy - 1, 'v')
put(m, cx + dx + 1, fy - 1, 'v')
put(m, cx + dx, fy + 1, 'v')
put(m, cx + dx, fy, 'q')
}
ellipse(m, cx, h - 3, Math.floor(w * 0.22), 1.5, 'l')
return m
}
/** Огнецвет: цветки-язычки пламени (ур. 13, premium). */
function plantFireflower(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 stems = 1 + stage
for (let i = 0; i < stems; i++) {
const dx = (i - (stems - 1) / 2) * Math.max(3, Math.floor(w / (stems + 2)))
const top = Math.floor(h * 0.25) + (i % 2) * 2
vline(m, cx + dx, top, h - 2, 'k')
// пламя: внешние языки f, внутренние Z, ядро y
const fy = top - 2
put(m, cx + dx - 1, fy + 1, 'f')
put(m, cx + dx + 1, fy + 1, 'f')
put(m, cx + dx, fy, 'f')
put(m, cx + dx, fy - 1, 'Z')
put(m, cx + dx - 1, fy, 'Z')
put(m, cx + dx, fy + 2, 'y')
if (stage === 2) put(m, cx + dx, fy - 2, 'y')
}
ellipse(m, cx, h - 3, Math.floor(w * 0.24), 1.6, 'l')
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,
'ph-drop': plantDew,
'ph-sun': plantSunflower,
'ph-tree-structure': plantBonsai,
'ph-butterfly': plantButterflyBush,
'ph-campfire': plantRowan,
'ph-sparkle': plantStarflower,
'ph-fire': plantFireflower,
}
// --- Декорации (item_key → матрица; используются и для чипов инвентаря) ---
/** Секция забора по маске соединений: N=1, E=2, S=4, W=8.
* Столб по центру; прожилины тянутся к соседним секциям (вглубь — продолжение столба). */
export function fenceMatrix(mask: number): Matrix {
const m = blank(16, 16)
// столб: x 7-8, от верха до земли
vline(m, 7, 2, 14, 'o')
vline(m, 8, 2, 14, 'O')
put(m, 7, 2, 'O')
put(m, 8, 1, 'o')
// горизонтальные прожилины (двойные: свет + тень)
for (const ry of [4, 8]) {
if (mask & 8) {
hline(m, 0, 6, ry, 'o')
hline(m, 0, 6, ry + 1, 'O')
}
if (mask & 2) {
hline(m, 9, 15, ry, 'o')
hline(m, 9, 15, ry + 1, 'O')
}
}
// вертикальные направления: тонкая прожилина (вид с торца) между столбами
if (mask & 1) {
put(m, 7, 0, 'o')
put(m, 7, 1, 'o')
}
if (mask & 4) put(m, 7, 15, 'o')
return m
}
/** Обычный зелёный куст (декорация): плотная листва в два тона, без цветов. */
const BUSH: Matrix = (() => {
const m = blank(20, 16)
// тень-основание и крона в три слоя
ellipse(m, 10, 12, 9, 3.2, 'k')
ellipse(m, 10, 9, 8.5, 4.6, 'l')
ellipse(m, 8, 7.5, 5.5, 3.2, 'L')
// блики-листья
for (const [x, y] of [[5, 7], [7, 5], [11, 6], [13, 9], [9, 10]] as [number, number][])
put(m, x, y, 'L')
return m
})()
export const DECOR: Record<string, Matrix> = {
fence: fenceMatrix(2 | 8),
bush: BUSH,
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
})(),
flag: (() => {
// флюгер: столб с поворотной стрелкой и вымпелом
const m = blank(14, 20)
vline(m, 6, 2, 19, 'o')
vline(m, 7, 2, 19, 'O')
ellipse(m, 7, 19, 3, 1.4, 'x')
// вымпел вправо
for (let dy = 0; dy < 5; dy++)
hline(m, 8, 12 - Math.abs(dy - 2), 3 + dy, dy < 2 ? 'r' : 'R')
// стрелка-указатель
hline(m, 2, 11, 2, 'o')
put(m, 2, 1, 'o')
put(m, 1, 2, 'O')
return m
})(),
birdhouse: (() => {
// скворечник на столбике: домик с круглым летком
const m = blank(14, 16)
vline(m, 6, 8, 15, 'o')
vline(m, 7, 8, 15, 'O')
// корпус
for (let y = 4; y < 11; y++)
for (let x = 4; x < 10; x++) put(m, x, y, x === 4 || x === 9 ? 'O' : 'n')
// двускатная крыша
hline(m, 3, 10, 3, 'r')
hline(m, 4, 9, 2, 'r')
put(m, 6, 1, 'r')
put(m, 7, 1, 'R')
// леток + жёрдочка
put(m, 6, 6, 'O')
put(m, 7, 6, 'O')
hline(m, 5, 8, 11, 'o')
return m
})(),
gem: (() => {
// садовый самоцвет: кристалл на каменном постаменте
const m = blank(12, 16)
ellipse(m, 6, 13, 5, 2, 'x')
ellipse(m, 6, 12, 4, 1.6, 'x')
vline(m, 6, 13, 14, 'x')
// кристалл: гранёный ромб с бликом
fillPoly(m, [[6, 0], [9, 5], [6, 10], [3, 5]], 'd')
fillPoly(m, [[6, 0], [9, 5], [6, 10]], 'c')
put(m, 5, 3, 'h')
put(m, 4, 4, 'h')
put(m, 6, 8, 'U')
return m
})(),
pond: (() => {
// большое озеро с переходами: камни → песок → отмель → глубина,
// границы колец растушёваны шахматным краем (без резких кромок)
const W = 72
const H = 36
const cx = 36
const cy = 18
const m = blank(W, H)
ellipse(m, cx, cy, 34, 16, 'x') // каменистый берег
ellipse(m, cx, cy, 30, 13.6, 'S') // песок
ellipse(m, cx, cy, 26.5, 11.8, 'U') // отмель
ellipse(m, cx, cy, 22, 9.4, 'u') // глубина
feather(m, cx, cy, 34, 16, 0.1, ' ') // камни → трава
feather(m, cx, cy, 30, 13.6, 0.1, 'x') // песок → камни
feather(m, cx, cy, 26.5, 11.8, 0.1, 'S') // отмель → песок
feather(m, cx, cy, 22, 9.4, 0.12, 'U') // глубина → отмель
ellipse(m, 30, 15, 9, 3.5, 'U') // блик
put(m, 44, 22, 'U')
put(m, 26, 24, 'U')
// кувшинки
for (const [lx, ly] of [
[18, 17],
[30, 21],
[42, 15],
[36, 12],
[24, 13],
] as const) {
put(m, lx, ly, 'l')
put(m, lx + 1, ly, 'k')
put(m, lx, ly - 1, 'L')
}
// камыши у берега
for (const [rx, ry] of [
[7, 11],
[9, 9],
[62, 22],
[64, 20],
[12, 26],
[58, 13],
] as const) {
vline(m, rx, ry, ry + 4, 't')
put(m, rx, ry - 1, 'k')
}
return m
})(),
gazebo: (() => {
// изометрия 2:1, как у домика: настил 16×16, столбы h=14, шатёр с apex z=30,
// свес крыши 2; видимые грани — передне-левая (тень R) и передне-правая (свет r)
const W = 44
const H = 50
const ox = 22
const oy = 30
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)
// настил: ромб A(0,0) B(16,0) C(16,16) D(0,16)
const FA = p(0, 0, 0)!
const FB = p(16, 0, 0)!
const FC = p(16, 16, 0)!
const FD = p(0, 16, 0)!
fillPoly(m, [FA, FB, FC, FD], 'o')
// доски настила: линии вдоль wy
for (let wy = 3; wy < 16; wy += 3) {
const q1 = p(0, wy, 0)!
const q2 = p(16, wy, 0)!
for (let t = 0; t <= 1; t += 1 / 32)
put(m, Math.round(q1[0] + (q2[0] - q1[0]) * t), Math.round(q1[1] + (q2[1] - q1[1]) * t), 'O')
}
// перила по видимым сторонам (z=8) + вертикальные стойки столбов
const seg = (a: [number, number], b: [number, number], ch: string): void => {
for (let t = 0; t <= 1; t += 1 / 48)
put(m, Math.round(a[0] + (b[0] - a[0]) * t), Math.round(a[1] + (b[1] - a[1]) * t), ch)
}
const R_B = p(16, 0, 8)!
const R_C = p(16, 16, 8)!
const R_D = p(0, 16, 8)!
seg(R_B, R_C, 'o')
seg(R_C, R_D, 'O')
// столбы: B (право, свет), C (перед), D (лево, тень)
for (const [fx, fy, lit] of [
[16, 0, true],
[16, 16, true],
[0, 16, false],
] as const) {
const q = p(fx, fy, 0)!
vline(m, q[0], q[1] - 14, q[1], lit ? 'o' : 'O')
put(m, q[0] + (lit ? 1 : -1), q[1] - 10, lit ? 'O' : 'o')
}
// шатровая крыша: apex над центром, свес до -2..18
const apex = p(8, 8, 30)!
const EB = p(18, -2, 14)!
const EC = p(18, 18, 14)!
const ED = p(-2, 18, 14)!
fillPoly(m, [apex, EC, ED], 'R') // передне-левый скат (тень)
fillPoly(m, [apex, EB, EC], 'r') // передне-правый скат (свет)
// задний скат чуть виден над apex? нет — но добавим кромку конька
seg(ED, EC, 'O')
seg(EC, EB, 'O')
put(m, apex[0], apex[1], 'O')
return m
})(),
greenhouse: (() => {
// теплица в изометрии 2:1 (как домик): стеклянные стены с деревянным
// каркасом, двускатная стеклянная крыша
const W = 43
const H = 45
const ox = 18
const oy = 22
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)
const seg = (a: [number, number], b: [number, number], ch: string): void => {
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), ch)
}
const WX = 22 // длина по wx
const WY = 14 // глубина по wy
const WH = 9 // высота стен
const RZ = 17 // конёк
const B = p(WX, 0, 0)!
const C = p(WX, WY, 0)!
const D = p(0, WY, 0)!
const B2 = p(WX, 0, WH)!
const C2 = p(WX, WY, WH)!
const K1 = p(0, WY / 2, RZ)!
const K2 = p(WX, WY / 2, RZ)!
// стеклянные стены: передне-левая (тень) и передне-правая (свет)
wall(m, D[0], D[1], C[0], C[1], WH, 'z')
wall(m, C[0], C[1], B[0], B[1], WH, 'c')
// каркас стен: стойки каждые 4 ед. + горизонтальный пояс
for (let wx = 4; wx < WX; wx += 4) {
const q = p(wx, WY, 0)!
vline(m, q[0], q[1] - WH, q[1], 'O')
}
seg(p(0, WY, 4)!, p(WX, WY, 4)!, 'O')
for (let wy = 4; wy < WY; wy += 4) {
const q = p(WX, wy, 0)!
vline(m, q[0], q[1] - WH, q[1], 'O')
}
seg(p(WX, 0, 4)!, p(WX, WY, 4)!, 'O')
// двускатная крыша: задний скат (свет) и передний (тень), свес 2
const M1 = p(0, -2, WH)!
const M2 = p(WX, -2, WH)!
const P1 = p(0, WY + 2, WH)!
const P2 = p(WX, WY + 2, WH)!
fillPoly(m, [K1, K2, M2, M1], 'c')
fillPoly(m, [K1, K2, P2, P1], 'z')
// каркас крыши: линии по скатам каждые 4 ед. + кромки и конёк
for (let wx = 0; wx <= WX; wx += 4) {
seg(p(wx, -2, WH)!, p(wx, WY / 2, RZ)!, 'O')
seg(p(wx, WY / 2, RZ)!, p(wx, WY + 2, WH)!, 'O')
}
seg(M1, M2, 'O')
seg(P1, P2, 'O')
seg(K1, K2, 'O')
// щипец над правой гранью (стекло)
fillPoly(m, [C2, B2, K2], 'c')
seg(B2, K2, 'O')
seg(C2, K2, 'O')
// блики на стекле
put(m, 10, 16, 'h')
put(m, 30, 22, 'h')
put(m, 31, 17, 'h')
// цоколь — тёмная кромка по низу стен
for (let i = 0; i <= WX; i++) {
const q1 = p(i, WY, 0)
const q2 = p(WX, Math.round(WY - (i * WY) / WX), 0)
if (q1) put(m, q1[0], q1[1], 'O')
if (q2) put(m, q2[0], q2[1], 'O')
}
// дверь на правой грани: wy 8..12, высота 8
fillPoly(m, [p(WX, 12, 0)!, p(WX, 8, 0)!, p(WX, 8, 8)!, p(WX, 12, 8)!], 'd')
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 }