SOURCE / PINNED RELEASE
Made of little things.
Stick Runner
- Release
- 068f2c4fd180…
- Author-recorded commit
- 2f0b589e2e75…
- License
- LICENSE
- Author’s source reference
- nostr://npub1ny7l44q0au5xkv3mjgt6zks5f0e2ydefslq4nhh2ve2xypgc2mqqat8qx2/wss%3A%2F%2Fgit.napplet.soy%2F/n-938fb0aa7b7
Archive hash verified: a0399c408781bf50…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
/** The game world is 480×270 units tall; the camera scrolls sideways. */
export const VIEW_W = 480;
export const VIEW_H = 270;
/** Anything solid the stickman can stand on or bump into. */
export interface Block {
x: number;
y: number;
w: number;
h: number;
kind: 'roof' | 'chimney' | 'vent' | 'tank' | 'spring';
}
/** Slippery patches. The stickman slides fast to the end of these and can't stop. */
export interface Ice {
x: number;
y: number;
w: number;
}
/** A clothesline tied from one building's wall to the next, sagging in the middle. */
export interface Clothesline {
x1: number;
y1: number;
x2: number;
y2: number;
sag: number;
}
/** A spinning saw blade that slides back and forth along a roof. */
export interface Saw {
/** The left and right ends of the slot its middle slides along. */
x1: number;
x2: number;
/** The roof it's on. */
roofY: number;
/** Seconds to go from one end to the other and back. */
period: number;
}
/** How big a saw blade is. Most of it sticks up above the roof. */
export const SAW_RADIUS = 16;
/** Where a saw's middle is at a moment in the level (it slows down at each end, then turns back). */
export function sawCenter(saw: Saw, time: number): { x: number; y: number } {
const t = 0.5 - 0.5 * Math.cos((time / saw.period) * Math.PI * 2);
return { x: saw.x1 + (saw.x2 - saw.x1) * t, y: saw.roofY - SAW_RADIUS * 0.55 };
}
/** A spring on a roof: jump onto it and Rog bounces way up, onto roofs too tall to jump. */
export interface Spring {
x: number;
roofY: number;
/** True for a spring at the edge of a roof, for flying over a wide gap (not up onto a tall building). */
overGap: boolean;
/** For a gap spring: where on the next roof it throws Rog. */
landX: number;
landY: number;
}
/** How big a spring is. It's solid, so you have to jump on top. */
export const SPRING_WIDTH = 40;
/** Low enough that a quick tap of jump hops you onto it. */
export const SPRING_HEIGHT = 14;
/** A patch of sharp spikes sticking up out of a roof. */
export interface Spikes {
x: number;
w: number;
/** The roof they're on. */
roofY: number;
}
/** How tall the spikes are. */
export const SPIKE_HEIGHT = 10;
/** Colours for a level's sky and faraway city. */
export interface Scenery {
sky: [string, string, string];
sun: string;
farCity: [string, string];
}
/** How a level is written down. */
export interface LevelPlan {
name: string;
/** Each roof is [gap before it, width, roof height]. The gaps are the pits. */
roofs: [number, number, number][];
/** Small things on the roofs to jump over: [roof number, distance from the roof's left edge, width, height, kind]. */
props: [number, number, number, number, Block['kind']][];
/** Ice at the end of a roof: [roof number, how long the ice is]. */
ice: [number, number][];
/** Roof numbers that have a clothesline across the (too wide to jump) gap before them. */
clotheslines: number[];
/** Moving saws: [roof number, left end of its slot, right end (both from the roof's left edge), seconds there and back]. */
saws?: [number, number, number, number][];
/** Spikes on a roof: [roof number, how wide the patch is, and where it starts from the roof's left edge (the middle if left out)]. */
spikes?: ([number, number] | [number, number, number])[];
/** Springs: [roof number, distance from the roof's left edge]. Put one before a roof that's too tall to jump. */
springs?: [number, number][];
/** A dragon that flies behind Rog and shoots fireballs at him. */
dragon?: boolean;
/** How the level ends: a checkered or gold flag, or a staircase down into the building. */
flag: 'checkered' | 'gold' | 'stairs';
scenery: Scenery;
}
/** A level ready to play. */
export interface Level {
name: string;
blocks: Block[];
ice: Ice[];
clotheslines: Clothesline[];
saws: Saw[];
spikes: Spikes[];
springs: Spring[];
dragon: boolean;
startX: number;
finishX: number;
end: number;
flag: 'checkered' | 'gold' | 'stairs';
scenery: Scenery;
}
const LEVEL_1: LevelPlan = {
name: 'Level 1',
// The map gets harder as you go: small gaps and wide roofs at first,
// then bigger gaps, narrower roofs and higher climbs near the end.
roofs: [
// Part 1: warm-up.
[0, 520, 200],
[60, 320, 200],
[60, 280, 190],
[70, 260, 190],
[70, 240, 175],
// Part 2: bigger gaps and more things in the way.
[190, 350, 195], // too wide to jump: slide on the ice before it
[80, 200, 170],
[100, 220, 185],
[90, 180, 160],
[115, 365, 200],
[85, 160, 165],
// Part 3: narrow roofs, wide pits and big climbs.
[120, 150, 200],
[90, 120, 150],
[190, 130, 175], // too wide to jump: slide on the ice before it
[95, 110, 130],
[125, 275, 180],
[100, 100, 145],
[120, 120, 165],
[100, 100, 125],
[125, 420, 175],
],
props: [
[1, 90, 22, 28, 'chimney'],
[3, 110, 40, 22, 'vent'],
[5, 90, 24, 30, 'chimney'],
[7, 60, 22, 34, 'chimney'],
[9, 80, 30, 40, 'tank'],
[19, 90, 22, 30, 'chimney'],
// More to jump over.
[1, 200, 22, 30, 'chimney'],
[2, 50, 22, 30, 'chimney'],
[4, 50, 40, 22, 'vent'],
[8, 50, 40, 22, 'vent'],
[9, 200, 22, 30, 'chimney'],
[10, 50, 22, 30, 'chimney'],
[15, 50, 30, 40, 'tank'], ],
ice: [
[2, 100],
[5, 80],
[6, 70],
[18, 60],
[4, 60],
[12, 60], ],
clotheslines: [],
flag: 'checkered',
scenery: {
sky: ['#ffb36b', '#ffd9a0', '#fff1d6'],
sun: '#fff4dc',
farCity: ['#e8a07a', '#c97c68'],
},
};
const LEVEL_2: LevelPlan = {
name: 'Level 2',
// A different city in the evening, just as long as Level 1. Short warm-up, a middle part, then a hard finish.
roofs: [
// Part 1: warm-up.
[0, 460, 170],
[70, 260, 185],
[245, 220, 165], // clothesline
[80, 220, 190],
// Part 2: the middle, with ice and clotheslines.
[100, 200, 175],
[250, 220, 195], // clothesline
[245, 180, 170], // clothesline
[100, 175, 185],
[95, 160, 160],
[240, 260, 180], // clothesline
[250, 200, 175], // clothesline
[190, 160, 160], // too wide to jump: slide on the ice before it
// Part 3: tiny roofs and big climbs.
[190, 130, 200], // too wide to jump: slide on the ice before it
[95, 110, 150],
[255, 120, 170], // clothesline
[115, 110, 135],
[190, 120, 175], // too wide to jump: slide on the ice before it
[105, 140, 150],
[120, 110, 120],
[125, 420, 165],
],
props: [
[0, 260, 24, 30, 'chimney'],
[1, 120, 30, 40, 'tank'],
[3, 100, 22, 30, 'chimney'],
[7, 60, 40, 22, 'vent'],
[19, 90, 22, 30, 'chimney'],
// More to jump over.
[4, 50, 30, 40, 'tank'],
[5, 50, 40, 22, 'vent'],
[10, 50, 22, 30, 'chimney'], ],
ice: [
[2, 80],
[6, 70],
[9, 90],
[11, 60],
[16, 50],
[10, 60],
[15, 60], ],
clotheslines: [2, 5, 6, 9, 10, 14],
flag: 'gold',
scenery: {
sky: ['#9b7fd1', '#e3a6c8', '#ffd8c9'],
sun: '#fff0f5',
farCity: ['#b98bb8', '#94709e'],
},
};
// Levels 3 to 10 get harder and harder: longer, with wider gaps, narrower roofs,
// bigger climbs, more clotheslines and ice jumps, and less room for a badly timed jump.
// Some gaps (170 or wider) are too far to jump from a run: there's always ice before
// them, so slide on it and fly across.
const LEVEL_3: LevelPlan = {
name: 'Level 3',
// Morning.
roofs: [
[0, 420, 190],
[135, 180, 215],
[125, 200, 215],
[105, 225, 180],
[115, 215, 195],
[125, 195, 210],
[245, 155, 210],
[105, 230, 170],
[245, 210, 180], // clothesline
[250, 170, 195],
[100, 185, 160],
[180, 265, 180],
[140, 205, 215],
[245, 205, 190], // clothesline
[120, 200, 195],
[130, 420, 215],
],
props: [
[8, 85, 36, 22, 'vent'],
[15, 90, 22, 30, 'chimney'],
// More to jump over.
[1, 50, 30, 40, 'tank'],
[2, 50, 40, 22, 'vent'],
[3, 50, 22, 30, 'chimney'],
[4, 50, 22, 30, 'chimney'],
[5, 50, 30, 40, 'tank'],
[6, 50, 40, 22, 'vent'],
[7, 50, 22, 30, 'chimney'],
[9, 50, 30, 40, 'tank'],
[11, 50, 22, 30, 'chimney'],
[12, 50, 22, 30, 'chimney'], ],
ice: [
[10, 100],
[13, 75],
[14, 80],
],
clotheslines: [6, 8, 9, 13],
flag: 'checkered',
scenery: {
sky: ['#8fc6ff', '#cfe8ff', '#f4fbff'],
sun: '#fffbe6',
farCity: ['#a9c3dd', '#8aa7c4'],
},
};
const LEVEL_4: LevelPlan = {
name: 'Level 4',
// Bright noon.
roofs: [
[0, 420, 190],
[135, 210, 215],
[245, 185, 200], // clothesline
[245, 205, 205],
[110, 200, 195],
[245, 200, 175],
[140, 190, 205],
[130, 180, 215],
[245, 210, 215], // clothesline
[190, 205, 215], // too wide to jump: slide on the ice before it
[125, 180, 215],
[185, 270, 215],
[110, 225, 170],
[135, 220, 215],
[125, 220, 205],
[130, 170, 215],
[110, 180, 180],
[105, 420, 135],
],
props: [
[17, 90, 22, 30, 'chimney'],
// More to jump over.
[1, 50, 22, 30, 'chimney'],
[5, 50, 22, 30, 'chimney'],
[6, 50, 30, 40, 'tank'],
[13, 50, 22, 30, 'chimney'],
[14, 50, 30, 40, 'tank'], ],
ice: [
[8, 85],
[10, 95],
[12, 70],
[14, 55],
],
clotheslines: [2, 3, 5, 8],
spikes: [[3, 40, 50], [4, 40, 50], [7, 40, 50], [11, 40, 150], [12, 40, 50], [15, 40, 50], [16, 40, 50]],
flag: 'checkered',
scenery: {
sky: ['#5ec8f2', '#a8e6ff', '#e8f9ff'],
sun: '#ffffff',
farCity: ['#8fbfd3', '#6fa0b8'],
},
};
const LEVEL_5: LevelPlan = {
name: 'Level 5',
// Autumn evening.
roofs: [
[0, 420, 190],
[115, 175, 160],
[120, 205, 140],
[250, 180, 150],
[245, 210, 155], // clothesline
[120, 160, 120],
[245, 210, 115], // clothesline
[190, 245, 125],
[110, 175, 115],
[120, 180, 125],
[180, 285, 130],
[135, 165, 130],
[110, 175, 115],
[120, 170, 115],
[245, 155, 125],
[130, 200, 125],
[140, 155, 150],
[250, 190, 155],
[140, 195, 190],
[130, 420, 175],
],
props: [
[4, 85, 30, 40, 'tank'],
[19, 90, 22, 30, 'chimney'],
// More to jump over.
[2, 50, 22, 30, 'chimney'],
[3, 50, 30, 40, 'tank'],
[10, 50, 22, 30, 'chimney'],
[11, 50, 30, 40, 'tank'],
[14, 50, 22, 30, 'chimney'],
[18, 50, 22, 30, 'chimney'], ],
ice: [
[1, 70],
[6, 90],
[9, 100],
],
clotheslines: [3, 4, 6, 14, 17],
spikes: [[7, 45], [15, 45], [5, 43, 50], [8, 43, 50], [12, 43, 50], [17, 43, 50]],
flag: 'checkered',
scenery: {
sky: ['#f08a4b', '#f7c08a', '#fde6c8'],
sun: '#fff1d0',
farCity: ['#d98a5e', '#b46a48'],
},
};
const LEVEL_6: LevelPlan = {
name: 'Level 6',
// Mint spring day.
roofs: [
[0, 420, 190],
[140, 195, 215],
[120, 185, 195],
[115, 185, 150],
[125, 195, 165],
[135, 185, 200],
[245, 155, 205],
[120, 155, 170],
[245, 180, 175], // clothesline
[180, 255, 185],
[125, 190, 165],
[250, 225, 195],
[110, 185, 150],
[190, 230, 150],
[125, 175, 145],
[245, 195, 140], // clothesline
[135, 165, 160],
[135, 180, 165],
[250, 155, 180],
[135, 135, 165],
[130, 170, 175],
[130, 420, 165],
],
props: [
[21, 90, 22, 30, 'chimney'],
// More to jump over.
[7, 50, 22, 30, 'chimney'],
[16, 50, 30, 40, 'tank'],
[20, 50, 30, 40, 'tank'], ],
ice: [
[1, 70],
[2, 90],
[8, 95],
[9, 65],
[12, 100],
[13, 75],
[15, 65],
[19, 55],
],
clotheslines: [6, 8, 11, 15, 18],
spikes: [[4, 45], [17, 45], [6, 46, 50], [9, 46, 110], [13, 46, 50], [14, 46, 50], [18, 46, 50]],
saws: [[11, 70, 185, 3.0], [3, 70, 145, 3]],
flag: 'checkered',
scenery: {
sky: ['#7fd1b9', '#c4ecd9', '#eefbf3'],
sun: '#fbfff6',
farCity: ['#8cbfa9', '#6d9f8a'],
},
};
const LEVEL_7: LevelPlan = {
name: 'Level 7',
// Stormy grey sky.
roofs: [
[0, 420, 190],
[140, 170, 215],
[245, 125, 195], // clothesline
[250, 200, 200],
[245, 125, 215], // clothesline
[135, 160, 215],
[245, 170, 210],
[135, 170, 200],
[115, 185, 175],
[120, 180, 175],
[185, 260, 200],
[140, 130, 205],
[250, 165, 215],
[125, 180, 215],
[190, 235, 215],
[140, 160, 215],
[140, 180, 215],
[190, 265, 215],
[120, 135, 180],
[255, 160, 175],
[130, 175, 150],
[140, 160, 185],
[140, 145, 215],
[140, 420, 215],
],
props: [
[23, 90, 22, 30, 'chimney'],
// More to jump over.
[1, 50, 30, 40, 'tank'],
[12, 50, 22, 30, 'chimney'],
[20, 50, 22, 30, 'chimney'],
[21, 50, 30, 40, 'tank'], ],
ice: [
[7, 70],
[9, 85],
[13, 100],
[14, 85],
[16, 100],
],
clotheslines: [2, 3, 4, 6, 12, 19],
spikes: [[3, 50], [17, 50], [6, 49, 50], [19, 49, 50]],
saws: [[10, 70, 220, 2.8], [8, 70, 145, 2.8]],
flag: 'checkered',
scenery: {
sky: ['#8a8fa8', '#bfc2d1', '#e4e5ec'],
sun: '#f4f4f8',
farCity: ['#9a9db0', '#7b7e93'],
},
};
const LEVEL_8: LevelPlan = {
name: 'Level 8',
// Snowy day, lots of ice.
roofs: [
[0, 420, 190],
[135, 130, 190],
[245, 180, 215], // clothesline
[190, 285, 215],
[245, 155, 215], // clothesline
[240, 225, 205],
[190, 145, 215], // too wide to jump: slide on the ice before it
[135, 180, 200],
[185, 245, 205],
[140, 155, 215],
[140, 165, 215],
[135, 110, 215],
[125, 180, 195],
[195, 260, 200],
[190, 150, 215], // too wide to jump: slide on the ice before it
[250, 170, 215],
[140, 120, 215],
[140, 165, 215],
[135, 165, 215],
[250, 200, 215],
[140, 150, 215],
[125, 140, 185],
[240, 200, 175],
[140, 110, 215],
[140, 145, 215],
[135, 420, 215],
],
props: [
[5, 90, 30, 40, 'tank'],
[25, 90, 22, 30, 'chimney'],
// More to jump over.
[9, 50, 22, 30, 'chimney'],
[10, 50, 30, 40, 'tank'], ],
ice: [
[2, 85],
[6, 80],
[7, 105],
[12, 90],
[17, 80],
[24, 65],
[5, 60],
[13, 60], ],
clotheslines: [2, 4, 5, 15, 19, 22],
spikes: [[3, 50], [13, 50], [22, 50], [15, 52, 50]],
saws: [[8, 70, 205, 2.6], [19, 70, 160, 2.6]],
dragon: true,
flag: 'checkered',
scenery: {
sky: ['#b8d8f0', '#e3f1fb', '#ffffff'],
sun: '#ffffff',
farCity: ['#b9cbdc', '#98aec4'],
},
};
const LEVEL_9: LevelPlan = {
name: 'Level 9',
// Desert heat.
roofs: [
[0, 420, 190],
[115, 150, 145],
[245, 115, 125], // clothesline
[250, 165, 155],
[245, 100, 125], // clothesline
[240, 225, 135],
[130, 135, 115],
[190, 100, 115], // too wide to jump: slide on the ice before it
[140, 125, 160],
[190, 180, 155], // too wide to jump: slide on the ice before it
[185, 280, 160],
[140, 160, 160],
[245, 165, 160],
[140, 110, 190],
[125, 100, 180],
[140, 140, 175],
[120, 95, 135],
[245, 210, 145],
[130, 100, 130],
[255, 185, 120],
[130, 180, 115],
[195, 250, 130],
[135, 180, 130],
[195, 250, 145],
[120, 180, 115],
[185, 250, 125],
[135, 165, 115],
[140, 420, 145],
],
props: [
[27, 90, 22, 30, 'chimney'],
// More to jump over.
[3, 50, 30, 40, 'tank'],
[19, 50, 30, 40, 'tank'],
[26, 50, 22, 30, 'chimney'], ],
ice: [
[9, 95],
[10, 65],
[20, 90],
[21, 70],
[22, 95],
[24, 85],
[25, 55],
[6, 60],
[8, 60], ],
clotheslines: [2, 3, 4, 5, 12, 17, 19],
spikes: [[5, 55], [17, 55], [12, 55, 50], [25, 55, 120]],
saws: [[23, 70, 210, 2.4], [10, 70, 175, 2.4], [21, 70, 140, 2.4]],
dragon: true,
flag: 'checkered',
scenery: {
sky: ['#f2c25b', '#f8dc95', '#fdf2d2'],
sun: '#fffbe8',
farCity: ['#d8b06a', '#b98f4f'],
},
};
const LEVEL_10: LevelPlan = {
name: 'Level 10',
// Golden finale: the longest and hardest, with four springs to fly over gaps too wide to jump
// (two of them up onto tall buildings too),
// and a staircase at the end down to the giant spider boss.
roofs: [
[0, 420, 190],
[135, 180, 190],
[190, 240, 195],
[245, 120, 190], // clothesline
[240, 225, 210],
[245, 180, 215], // clothesline
[205, 275, 215],
[140, 125, 215],
[255, 215, 210],
[135, 200, 185], // spring near the edge
[200, 120, 185], // too wide to jump: use the spring
[255, 190, 165],
[130, 200, 165], // spring near the edge
[200, 180, 140], // too wide to jump: use the spring
[195, 230, 160],
[140, 200, 200], // spring near the edge
[200, 90, 150], // too wide to jump: use the spring
[240, 200, 145],
[125, 105, 115],
[140, 200, 145], // spring near the edge
[200, 85, 140], // too wide to jump: use the spring
[135, 140, 150],
[140, 160, 180],
[255, 165, 185],
[140, 200, 205], // spring near the edge
[200, 180, 75], // too wide AND too tall to jump: use the spring
[200, 240, 205], // spring near the edge
[200, 420, 75], // too wide AND too tall to jump: use the spring
],
props: [
[4, 80, 30, 40, 'tank'],
[27, 90, 22, 30, 'chimney'],
// More to jump over.
[23, 50, 22, 30, 'chimney'], ],
ice: [
[1, 95],
[5, 90],
[13, 105],
[21, 55],
[25, 90],
],
clotheslines: [3, 4, 5, 8, 11, 17, 23],
spikes: [[2, 55], [8, 55], [17, 55]],
saws: [[6, 70, 235, 2.2], [14, 70, 190, 2.2], [11, 70, 150, 2.2]],
// Springs near the edge of a roof, to fly over a gap too wide to jump (the last two also up
// onto a building too tall to jump). There's some roof after each one, so jumping past it
// lands you on the roof to walk back and try again.
springs: [[9, 60], [12, 60], [15, 60], [19, 60], [24, 60], [26, 100]],
dragon: true,
flag: 'stairs',
scenery: {
sky: ['#e8a03c', '#f6c6a0', '#ffe8d6'],
sun: '#fff6dc',
farCity: ['#d49a6a', '#b07a55'],
},
};
/** Roofs reach down past the bottom of the screen so they look like whole buildings. */
const BUILDING_BOTTOM = VIEW_H + 40;
/** Falling this far down means the stickman is in a pit. */
export const PIT_DEPTH = VIEW_H + 30;
export function buildLevel(plan: LevelPlan): Level {
const blocks: Block[] = [];
let nextX = 0;
for (const [gap, w, y] of plan.roofs) {
blocks.push({ x: nextX + gap, y, w, h: BUILDING_BOTTOM - y, kind: 'roof' });
nextX += gap + w;
}
const roofs = blocks.slice();
for (const [index, offset, w, h, kind] of plan.props) {
const roof = roofs[index];
blocks.push({ x: roof.x + offset, y: roof.y - h, w, h, kind });
}
const ice = plan.ice.map(([index, length]) => {
const roof = roofs[index];
return { x: roof.x + roof.w - length, y: roof.y, w: length };
});
const clotheslines = plan.clotheslines.map((index) => {
const before = roofs[index - 1];
const after = roofs[index];
return { x1: before.x + before.w, y1: before.y + 8, x2: after.x, y2: after.y + 8, sag: 16 };
});
const saws = (plan.saws ?? []).map(([index, from, to, period]) => {
const roof = roofs[index];
return { x1: roof.x + from, x2: roof.x + to, roofY: roof.y, period };
});
const spikes = (plan.spikes ?? []).map(([index, w, offset]) => {
const roof = roofs[index];
return { x: offset === undefined ? roof.x + roof.w / 2 - w / 2 : roof.x + offset, w, roofY: roof.y };
});
const springs = (plan.springs ?? []).map(([index, offset]) => {
const roof = roofs[index];
const next = roofs[index + 1];
// If there's a gap after this roof, the spring is for flying over it (and up, if the next
// building is taller), and it throws Rog to a safe spot a little way onto the next roof.
const overGap = !!next && next.x > roof.x + roof.w;
return {
x: roof.x + offset,
roofY: roof.y,
overGap,
landX: next ? next.x + Math.min(45, next.w / 2) : 0,
landY: next ? next.y : 0,
};
});
for (const spring of springs) {
blocks.push({ x: spring.x, y: spring.roofY - SPRING_HEIGHT, w: SPRING_WIDTH, h: SPRING_HEIGHT, kind: 'spring' });
}
return {
name: plan.name,
blocks,
ice,
clotheslines,
saws,
spikes,
springs,
dragon: plan.dragon ?? false,
startX: 60,
finishX: nextX - 110,
end: nextX,
flag: plan.flag,
scenery: plan.scenery,
};
}
export const LEVELS: Level[] = [LEVEL_1, LEVEL_2, LEVEL_3, LEVEL_4, LEVEL_5, LEVEL_6, LEVEL_7, LEVEL_8, LEVEL_9, LEVEL_10].map(buildLevel);
/** How high the rope is at a spot along it. */
export function lineHeightAt(line: Clothesline, x: number): number {
const t = (x - line.x1) / (line.x2 - line.x1);
return line.y1 + (line.y2 - line.y1) * t + line.sag * Math.sin(Math.PI * t);
}
export function roofHeightAt(blocks: Block[], x: number): number {
const roof = blocks.find((block) => block.kind === 'roof' && x >= block.x && x <= block.x + block.w);
return roof ? roof.y : VIEW_H;
}
