Files
isokit/crates/isokit-script/stdlib/nature.rhai
T
grabowski 96d472625f
Deploy web Studio / deploy (push) Successful in 1m5s
Diagonals: streets, walls, fences, hedges and house walls at any angle
Object frames turn by any angle (quarter turns stay exact, so scenes along
the axes render byte for byte as before); a box in a turned frame is a
prism with its own corners. Rotation is kept as given (normalised to
0..360), no longer snapped to quarter turns.

Depth sort: sprites whose plan outline is not their box carry their convex
outline; when boxes overlap, an outline edge that separates two sprites
orders them. A long diagonal wall sorts right against both sides.

Joints (line_meet, end_cut, cut_box in util.rhai): where a piece off the
axes is involved, corners are mitred and T-junctions slanted; fences put a
corner post or pillar in its own sprite and mitre stone bases; Y joints let
the lowest id run through. Streets: every end part is a cut e + k*w, so
side streets come in at any angle (slanted sidewalk gaps), two streets meet
in a mitre with seam patches; fixes a curve's outer sidewalk that was
skipped when its kerb and back lines had different point counts.

Studio: runs and drags keep to x/y or snap to 45 degree diagonals, Ctrl
gives any angle; a road run turning 45 degrees gets a 45 degree curve;
Rotation is a number box. Streets demo: a diagonal lane with a hedge.
MCP guidance, SCRIPTING/README/ROADMAP.

Tests: Rust 76, Studio and web checks green.
2026-10-25 13:22:59 +07:00

194 lines
8.2 KiB
Plaintext

// isokit/nature: trees, hedges, shrubs.
/// A tree with a round crown of radius r (m) standing at (x, y) of this sprite.
fn tree_at(x, y, r) {
let h = 1.4 + r * 0.6;
this.ext_box(x - r, y - r, x + r, y + r, 0.0, h + r * 1.8);
this.stem(x, y, 0.0, h, 0.7, "trunk");
let R = r * px() * 1.18;
this.circle(x, y, h + r * 0.9, R, "leaf l");
this.circle(x, y, h + r * 0.9, R * 0.58, "leaf t", -R * 0.28, -R * 0.3);
this
}
fn meta_tree() {
#{ label: "Tree", category: "Nature", icon: "🌳",
props: [ #{ key: "r", type: "float", def: 1.6, min: 0.5, max: 6.0, step: 0.1, label: "Crown radius (m)" } ] }
}
fn prefab_tree(o) {
sprite().tree_at(0.0, 0.0, o.p.r);
}
fn meta_tree_row() {
#{ label: "Row of trees", category: "Nature", icon: "🌲", shape: "line",
props: [
#{ key: "w", type: "float", def: 30.0, min: 2.0, max: 500.0, step: 1.0, label: "Length (m)" },
#{ key: "spacing", type: "float", def: 6.0, min: 2.0, max: 30.0, step: 0.5, label: "Spacing (m)" },
#{ key: "r", type: "float", def: 1.5, min: 0.5, max: 6.0, step: 0.1, label: "Crown radius (m)" },
] }
}
fn prefab_tree_row(o) {
let p = o.p;
let n = (p.w / p.spacing).floor().to_int() + 1;
let step = if n > 1 { p.w / (n - 1) } else { 0.0 };
for i in 0..n {
// one sprite per tree; small deterministic size variation
sprite().tree_at(-p.w / 2.0 + i * step, 0.0, p.r * (0.85 + 0.3 * hash(i + p.w)));
}
}
fn meta_hedge() {
#{ label: "Hedge", category: "Nature", icon: "▬", shape: "line",
props: [
#{ key: "w", type: "float", def: 10.0, min: 0.5, max: 300.0, step: 0.5, label: "Length (m)" },
#{ key: "d", type: "float", def: 0.6, min: 0.3, max: 3.0, step: 0.1, label: "Thickness (m)" },
#{ key: "h", type: "float", def: 1.4, min: 0.3, max: 4.0, step: 0.1, label: "Height (m)" },
] }
}
fn prefab_hedge(o) {
let p = o.p;
let c0 = hedge_end(o, -1.0);
let c1 = hedge_end(o, 1.0);
sprite().cut_box(c0.x, c0.k, c1.x, c1.k, -p.d / 2.0, p.d / 2.0, 0.0, p.h, "hedge");
}
/// Where hedge o ends at e: mitred or slanted where a diagonal hedge meets it.
fn hedge_end(o, e) {
let m = line_meet(o, e, ["hedge"], []);
if m.kind == "corner" || m.kind == "tee" { end_cut(o.p.w, e, m) } else { end_cut(o.p.w, e, #{ ext: 0.0, k: 0.0 }) }
}
// ---------------------------------------------------------------- pond
fn meta_pond() {
#{ label: "Pond", category: "Nature", icon: "◍", shape: "rect",
help: "A garden pond with fish swimming round under lily pads, reeds at the back and a stone or grass edge. Drag a rectangle on the map.",
props: [
#{ key: "w", type: "float", def: 6.0, min: 1.5, max: 60.0, step: 0.5, label: "Width (m)" },
#{ key: "d", type: "float", def: 4.0, min: 1.5, max: 60.0, step: 0.5, label: "Length (m)" },
#{ key: "shape", type: "choice", def: "natural", choices: ["natural", "round", "square"], label: "Shape" },
#{ key: "fish", type: "int", def: 5, min: 0, max: 16, step: 1, label: "Fish" },
#{ key: "lilies", type: "int", def: 3, min: 0, max: 12, step: 1, label: "Lily pads" },
#{ key: "reeds", type: "bool", def: true, label: "Reeds" },
#{ key: "edge", type: "choice", def: "stone", choices: ["stone", "grass"], label: "Edge" },
] }
}
/// The pond's outline inset by `ins` metres: a kidney ("natural"), an
/// ellipse ("round") or a rectangle with round corners ("square").
fn pond_outline(p, ins) {
let a = p.w / 2.0 - ins;
let b = p.d / 2.0 - ins;
let pts = [];
if p.shape == "square" {
let r = fmax(0.05, fmin(a, b) * 0.35);
for k in 0..4 {
let cx = if k == 0 || k == 3 { a - r } else { -a + r };
let cy = if k < 2 { b - r } else { -b + r };
for j in 0..7 {
let t = (k * 90 + j * 15) * PI() / 180.0;
pts.push([cx + r * t.cos(), cy + r * t.sin()]);
}
}
return pts;
}
for j in 0..48 {
let t = j * PI() / 24.0;
let k = 1.0;
if p.shape == "natural" {
// a dent at the back (+y) and a slight wobble, never past the box
let dt = (t - PI() / 2.0) / 0.6;
k = (1.0 - 0.3 * (-dt * dt).exp()) * (0.965 + 0.035 * (3.0 * t + 1.0).sin());
}
pts.push([a * k * t.cos(), b * k * t.sin()]);
}
pts
}
fn prefab_pond(o) {
let p = o.p;
let a = p.w / 2.0;
let b = p.d / 2.0;
let rim = if p.edge == "stone" { fmin(0.35, fmin(a, b) * 0.18) } else { fmin(0.2, fmin(a, b) * 0.1) };
layer("ground");
let s = sprite();
let outer = pond_outline(p, 0.0);
let inner = pond_outline(p, rim);
if p.edge == "stone" {
s.flat([outer], 0.03, "capst t");
// joints across the stone rim
let segs = [];
let step = if outer.len() > 30 { 2 } else { 1 };
for j in range(0, outer.len(), step) { segs.push([outer[j][0], outer[j][1], inner[j][0], inner[j][1]]); }
s.lines(segs, 0.031, "seam");
} else {
s.flat([outer], 0.02, "pondbank t");
}
s.flat([inner], 0.032, "pondw t");
s.flat([pond_outline(p, rim + fmin(a, b) * 0.28)], 0.033, "ponddp t");
// the water the fish keep to: an ellipse well inside, off the natural shape's dent
let wa = (a - rim) * 0.72;
let wb = (b - rim) * 0.72;
let oy = if p.shape == "natural" { -0.12 * b } else { 0.0 };
if p.shape == "natural" { wb *= 0.82; }
let n = p.fish;
for i in 0..n {
let f = 0.45 + 0.55 * ((i * 37 + 11) % 10) / 10.0;
let rx = wa * f;
let ry = wb * f;
let cx = 0.0;
// long ponds: smaller rounds spread along the long side
if rx > ry * 1.6 { cx = (((i * 53) % 9) / 8.0 - 0.5) * (rx - ry * 1.6) * 2.0; rx = ry * 1.6; }
if ry > rx * 1.6 { ry = rx * 1.6; }
let len = fmin(0.9, fmax(0.3, fmin(a, b) * 0.24)) * (0.85 + 0.1 * (i % 3));
let dur = fmax(8.0, 2.0 * PI() * (rx + ry) / 2.0 / 0.32);
s.swim(cx, oy, 0.036, [#{ rx: rx, ry: ry, dur: dur, delay: (i * 7.3) % dur, cw: i % 3 == 1, len: len,
cls: ["koi", "koiw", "koig"][i % 3] }]);
}
// lily pads over the fish, some in flower; rings spreading where a fish came up
for i in 0..p.lilies {
let t = i * 2.4 + 0.7;
let q = 0.35 + 0.4 * ((i * 29 + 3) % 10) / 10.0;
let x = wa * q * t.cos();
let y = oy + wb * q * t.sin();
s.disc(x, y, 0.04, fmin(0.4, fmax(0.18, fmin(a, b) * 0.1)), "lily t");
if i % 2 == 0 { s.disc(x + 0.08, y + 0.05, 0.05, 0.07, "lilyf t"); }
}
if n > 0 {
for i in 0..fmin(3, n).to_int() {
let t = i * 2.1 + 1.9;
s.begin("a rip", `animation-delay:-${i * 1.7}s`);
s.disc(wa * 0.5 * t.cos(), oy + wb * 0.5 * t.sin(), 0.04, fmin(0.6, fmin(a, b) * 0.15), "ripple");
s.end();
}
}
// reeds in clumps on the back edge, upright, so with the other sprites
if p.reeds {
layer("main");
for t in [1.15, 1.95, 2.55] {
let q = pond_outline(p, rim * 0.5 + 0.3);
let j = ((t / (2.0 * PI())) * q.len()).round().to_int() % q.len();
let c = q[j];
// tapered blades leaning out from the clump, cattail heads on some
let blades = [];
let heads = [];
for k in 0..7 {
let x = c[0] + ((k * 37) % 7 - 3) * 0.04;
let y = c[1] + ((k * 53) % 5 - 2) * 0.04;
let h = 0.75 + ((k * 31) % 6) * 0.09;
let lx = ((k * 17) % 7 - 3) * 0.06;
let ly = ((k * 23) % 5 - 2) * 0.06;
blades.push([[x - 0.06, y, 0.03], [x + 0.06, y, 0.03], [x + lx, y + ly, h]]);
blades.push([[x, y - 0.06, 0.03], [x, y + 0.06, 0.03], [x + lx, y + ly, h]]);
if k % 3 == 0 {
let f = 0.62;
let hx = x + lx * f;
let hy = y + ly * f;
heads.push([[hx - 0.035, hy, h * f], [hx + 0.035, hy, h * f], [hx + 0.035 + lx * 0.18, hy + ly * 0.18, h * (f + 0.18)], [hx - 0.035 + lx * 0.18, hy + ly * 0.18, h * (f + 0.18)]]);
}
}
sprite().poly(blades, "reed t").poly(heads, "cattail t");
}
}
}