diff --git a/v2/CLAUDE.md b/v2/CLAUDE.md index e07f908..9dd3be5 100644 --- a/v2/CLAUDE.md +++ b/v2/CLAUDE.md @@ -1061,6 +1061,24 @@ слотами (дома выглядели «мыльно»), а сиды записей подобраны так, чтобы три дома показывали три разных стиля стен. +## Раунд фидбека (2026-09-11): валуны не на весь тайл, объём домов + +- **Валуны** (`mapBoulderModel`): занимаемая площадь завязана на вариант — + крупный R=4.7 на весь тайл, средний R=3.3 (~половина площади), низкий + R=2.3 (четверть, как малый куст); высота 8/6/4. Трещина теперь только у + вариантов 1+ (на малой глыбе не читалась), мох — два верхних ряда. +- **Дома — объём**: сетка расширена 24×18 → 26×20, стены утоплены на 1 + воксель от края — нижний ряд крыши свешивается за стены (тень на фасаде + даёт глубину). Окна и дверь сделаны «с четвертью»: проём в стене, + полотно/стекло утоплены ещё на 1 воксель внутрь (откос из материала + стены закрывает щели от диагонального взгляда — иначе сквозь угол проёма + видно пустоту внутри дома), обводка выступает наружу: рамы окон на + 1 воксель перед стеной, дверь — косяки с притолокой перед стеной и + каменный порог под проёмом. Стекло получило переплёт (слот рамы крестом). + Дверь смещена на x11..14 (центр сетки 26), точка захода героя сохранена. +- Грабля: у утопленной панели откос обязателен — одна глубина проёма без + заполненного откоса даёт «дырку» мимо панели при взгляде по диагонали. + ## Рабочие привычки - Для задач из 3+ шагов веди список задач и иди автономно. diff --git a/v2/apps/game/src/areas.ts b/v2/apps/game/src/areas.ts index fa0aa04..7eb2f0f 100644 --- a/v2/apps/game/src/areas.ts +++ b/v2/apps/game/src/areas.ts @@ -125,21 +125,23 @@ /** Валун карты (редизайн): купол со скруглённым силуэтом (радиус тает * к верхушке), рваный край, трещина по боку, мох на верхушке (жизнь!); - * вариант — крупность, сид — форма. */ + * вариант — крупность (крупный на весь тайл, средний ~половина площади, + * низкий — четверть, как малый куст), сид — форма. */ export function mapBoulderModel(variant: Variant, seed: number): VoxelModel { const rng = new Rng(seed ^ 0x4b0d); - const H = 6 + (variant % 3); // 6..8 — глыба, не плитка + const R = variant === 2 ? 4.7 : variant === 1 ? 3.3 : 2.3; // занимаемая площадь + const H = variant === 2 ? 8 : variant === 1 ? 6 : 4; // глыба, не плитка const g = new VoxelGrid(10, H, 10); for (let y = 0; y < H; y++) { const t = y / (H - 1); // 0..1 по высоте - const r = 4.7 * Math.sqrt(1 - t * t * 0.9) + 0.3; // купол + const r = R * Math.sqrt(1 - t * t * 0.9) + 0.3; // купол for (let dz = -5; dz <= 4; dz++) for (let dx = -5; dx <= 4; dx++) { const d = Math.sqrt(dx * dx + dz * dz); if (d > r + (rng.next() - 0.5)) continue; // рваный край - const crack = y > 0 && y < H - 1 && dx === ((y * 3 + seed) % 5) - 2 && Math.abs(dz) < 2; - const moss = y >= H - 3 && rng.next() < 0.3; - g.set(5 + dx, y, 5 + dz, moss ? 23 : crack ? 18 : y >= H - 3 ? 7 : 6); + const crack = variant > 0 && y > 0 && y < H - 1 && dx === ((y * 3 + seed) % 5) - 2 && Math.abs(dz) < 2; + const moss = y >= H - 2 && rng.next() < 0.3; + g.set(5 + dx, y, 5 + dz, moss ? 23 : crack ? 18 : y >= H - 2 ? 7 : 6); } } return encodeModel(g, { 6: '#5b5b66', 7: '#94949e', 18: '#47474f', 23: '#5e6b4a' }); @@ -203,17 +205,18 @@ ]); /** Дом 24×18 (высокие подварианты 40): стиль стен и крыши, труба — по сиду; - * дверь на фасаде z=D−1, x10..13 сохранена (точка захода героя). Скин — - * текстурный слой: тайли кладки/досок/штукатурки/дранки на гранях стен. */ + * дверь на фасаде x11..14 сохранена (точка захода героя). Стены утоплены + * на 1 воксель от края сетки — нижний ряд крыши свешивается за стены. + * Скин — текстурный слой: тайли кладки/досок/штукатурки/дранки на гранях. */ export function houseModel(kind: number, seed: number): VoxelModel { const tall = kind === T.HOUSE_V3 || kind === T.HOUSE_V4; const rng = new Rng(seed ^ 0x1d0f); - const W = 24, H = tall ? 40 : 30, D = 18, ROOF = 5; + const W = 26, H = tall ? 40 : 30, D = 20, ROOF = 5; const wallH = H - ROOF; const style = Math.floor(rng.next() * 3); // 0 кладка, 1 доски, 2 фахверк const g = new VoxelGrid(W, H, D); houseWalls(g, W, wallH, D, style); - houseDoor(g, D); + houseDoor(g, D, style); houseWindows(g, W, wallH, D, style, tall); houseRoof(g, W, H, D, ROOF, rng.next() < 0.6); if (rng.next() < 0.6) houseChimney(g, W, H, ROOF); @@ -243,7 +246,7 @@ let id = 0; if (y >= H - ROOF) id = slot === 4 ? 4 : slot === 8 ? 3 : 0; // крыша else if (slot === baseSlot) id = 1; // стена стиля - else if (slot === 4 && (x === 0 || x === W - 1) && (z === 0 || z === D - 1)) id = 2; // углы — замковые камни + else if (slot === 4 && (x === 1 || x === W - 2) && (z === 1 || z === D - 2)) id = 2; // углы — замковые камни else if (slot === 19) id = 5; // дверное полотно else if (slot === 6) id = 6; // труба (камень) if (id !== 0) tex[x + W * (y + H * z)] = id; @@ -251,62 +254,85 @@ return { tiles, tex }; } -/** Слот стены по стилю: u — координата вдоль грани, y — высота. */ +/** Слот стены по стилю: u — координата вдоль грани, y — высота. + * Стены стоят на 1 воксель от края сетки (x=1..W−2, z=1..D−2). */ function wallSlot(x: number, y: number, z: number, W: number, D: number, wallH: number, style: number): number { - const u = x === 0 || x === W - 1 ? z : x; // вдоль грани - const corner = (x === 0 || x === W - 1) && (z === 0 || z === D - 1); + const side = x === 1 || x === W - 2; // боковая грань + const u = side ? z : x; // вдоль грани + const corner = side && (z === 1 || z === D - 2); if (corner) return style === 1 ? 4 : 7; // углы: тёмные доски / замковые камни if (style === 0) return y % 3 === 2 && (u + (y / 3 | 0) * 2) % 4 === 0 ? 7 : 6; // швы кладки if (style === 1) return y === wallH - 1 ? 4 : u % 3 === 0 ? 4 : 20; // доски + венец return y === wallH - 1 || u % 6 === 2 ? 4 : 7; // фахверк: стойки в штукатурке } -/** Коробка стен (только рёбра) с цоколем в 2 вокселя. */ +/** Коробка стен (только рёбра) с цоколем в 2 вокселя, утоплена на 1. */ function houseWalls(g: VoxelGrid, W: number, wallH: number, D: number, style: number): void { for (let y = 0; y < wallH; y++) - for (let z = 0; z < D; z++) - for (let x = 0; x < W; x++) { - const edge = x === 0 || z === 0 || x === W - 1 || z === D - 1; + for (let z = 1; z < D - 1; z++) + for (let x = 1; x < W - 1; x++) { + const edge = x === 1 || z === 1 || x === W - 2 || z === D - 2; if (!edge) continue; g.set(x, y, z, y < 2 ? 18 : wallSlot(x, y, z, W, D, wallH, style)); } } -/** Дверь на фасаде (z = D−1): тёплое полотно, тёмные косяки, ручка. */ -function houseDoor(g: VoxelGrid, D: number): void { - for (let y = 0; y <= 4; y++) for (let x = 10; x <= 13; x++) g.set(x, y, D - 1, 19); - for (let y = 0; y <= 5; y++) { g.set(9, y, D - 1, 4); g.set(14, y, D - 1, 4); } - for (let x = 9; x <= 14; x++) g.set(x, 5, D - 1, 4); // притолока - g.set(13, 3, D - 1, 4); // ручка +/** Дверь на фасаде: проём в стене (z=D−2), полотно утоплено ещё на 1 + * (четверть), косяки с притолокой выступают наружу, под дверью — + * каменный порог. */ +function houseDoor(g: VoxelGrid, D: number, style: number): void { + const zW = D - 2, zOut = D - 1, zIn = D - 3; + const wall = [6, 20, 7][style]; + for (let y = 0; y <= 4; y++) + for (let x = 11; x <= 14; x++) { + g.set(x, y, zW, 0); // проём + g.set(x, y, zIn, 19); // полотно + } + for (let y = 0; y <= 5; y++) // откос (четверть) вокруг полотна + for (let x = 10; x <= 15; x++) + if (x === 10 || x === 15 || y === 5) g.set(x, y, zIn, y < 2 ? 18 : wall); + for (let y = 0; y <= 5; y++) { g.set(10, y, zOut, 4); g.set(15, y, zOut, 4); } // косяки + for (let x = 10; x <= 15; x++) g.set(x, 5, zOut, 4); // притолока + for (let x = 11; x <= 14; x++) g.set(x, 0, zOut, 18); // порог + g.set(14, 3, zIn, 4); // ручка } -/** Окно 4×4 с рамой на плоскости стены (axis — фиксированная координата). */ -function windowOn(g: VoxelGrid, axis: 'x' | 'z', fix: number, u0: number, v0: number, frame: number): void { - for (let du = 0; du < 4; du++) - for (let dv = 0; dv < 4; dv++) { +/** Утопленное окно 4×4: рама выступает наружу (fix+out), стекло утоплено + * внутрь (fix−out) с переплётом, откос — из материала стены. */ +function windowOn(g: VoxelGrid, axis: 'x' | 'z', fix: number, out: number, + u0: number, v0: number, frame: number, wall: number): void { + for (let du = -1; du <= 4; du++) + for (let dv = -1; dv <= 4; dv++) { const u = u0 + du, v = v0 + dv; + const jamb = du === -1 || dv === -1 || du === 4 || dv === 4; // зона откоса + const set = (a: number, b: number, c: number, s: number): void => { + if (axis === 'x') g.set(a, c, b, s); else g.set(c, b, a, s); + }; + if (jamb) { set(fix - out, u, v, wall); continue; } // откос из стены const border = du === 0 || dv === 0 || du === 3 || dv === 3; - const [x, y, z] = axis === 'x' ? [fix, v, u] : [u, v, fix]; - g.set(x, y, z, border ? frame : 10); + if (border) set(fix + out, u, v, frame); // рама выступает + set(fix, u, v, 0); // проём в стене + set(fix - out, u, v, du === 2 || dv === 2 ? frame : 10); // стекло с переплётом } } /** Окна: пара на фасаде + пара на боковой стене; у высоких — второй ряд. */ function houseWindows(g: VoxelGrid, W: number, wallH: number, D: number, style: number, tall: boolean): void { const frame = style === 0 ? 7 : 4; + const wall = [6, 20, 7][style]; const y0 = Math.min(6, wallH - 6); - windowOn(g, 'z', D - 1, 2, y0, frame); - windowOn(g, 'z', D - 1, W - 6, y0, frame); - windowOn(g, 'x', 0, 4, y0, frame); - windowOn(g, 'x', W - 1, D - 8, y0, frame); + windowOn(g, 'z', D - 2, 1, 3, y0, frame, wall); + windowOn(g, 'z', D - 2, 1, W - 7, y0, frame, wall); + windowOn(g, 'x', 1, -1, 4, y0, frame, wall); + windowOn(g, 'x', W - 2, 1, D - 9, y0, frame, wall); if (tall) { - windowOn(g, 'z', D - 1, 2, y0 + 8, frame); - windowOn(g, 'z', D - 1, W - 6, y0 + 8, frame); + windowOn(g, 'z', D - 2, 1, 3, y0 + 8, frame, wall); + windowOn(g, 'z', D - 2, 1, W - 7, y0 + 8, frame, wall); } } -/** Двускатная крыша (конёк вдоль X, свес за стены): ржавый металл или - * тёмная дранка по сиду, ряд конька светлее. */ +/** Двускатная крыша (конёк вдоль X, свес за стены — стены утоплены): + * ржавый металл или тёмная дранка по сиду, ряд конька светлее. */ function houseRoof(g: VoxelGrid, W: number, H: number, D: number, ROOF: number, rust: boolean): void { const roof = rust ? 8 : 4; for (let dy = 0; dy < ROOF; dy++) {