Skip to content

Commit 2b49412

Browse files
committed
Initial tree implementation
1 parent b7db04c commit 2b49412

29 files changed

Lines changed: 1363 additions & 0 deletions

‎src/trees/CHANGELOG.md‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Changelog
2+
3+
## 0.1.4
4+
5+
- Changed: the needle tip sits 3 cells (12 pixels) above the top of the trunk.
6+
7+
## 0.1.3
8+
9+
- Changed: trunk growth is faster for testing. The canopy grows one needle row at a time.
10+
11+
## 0.1.2
12+
13+
- Changed: the pine trunk is 3 cells wide.
14+
15+
## 0.1.1
16+
17+
- Changed: the pine trunk is terrain. Mine it with a shovel or drill. The rest of the tree falls as raw wood.
18+
19+
## 0.1.0
20+
21+
- Added: pine seeds grow on wet sand into a 48-cell tree with a wide needle canopy.
22+
- Added: cut the base trunk to drop raw wood. Needles drop leaf dust that composts into dirt.
23+
- Added: sealed burning raw wood turns into charcoal. Open flame still burns to ash.

‎src/trees/README.md‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Trees
2+
3+
Pine trees, harvested wood, and charcoal for Sandustry.
4+
5+
## Use
6+
7+
1. Enable the mod and load a save.
8+
2. **Debug menu:** equip the Debug tool → **Element** brush → **Pine Seed**.
9+
3. Drop a seed onto **Wet Sand**. The wet sand becomes **Sand**. A pine grows up 48 cells. The trunk is **Pine Wood terrain**, 3 cells wide.
10+
4. Needles grow as a wide cone, one row at a time. The tip sits a few cells above the trunk.
11+
5. Mine any **trunk** cell with a shovel or drill. The rest of the trunk falls as **Raw Wood**. Needles fall as **Leaf Dust**.
12+
6. Burn a sealed pile of Raw Wood (no empty cells in the 8 neighbors) to make **Charcoal**. Open flame makes vanilla burnt residue and steam.
13+
7. Leaf Dust that rests in place becomes **Dirt**.
14+
15+
Old trees from 0.1.0 are static elements. Plant a new seed after you reload.
16+
17+
## Kiln (manual)
18+
19+
1. Pile Raw Wood on a floor.
20+
2. Ignite it with a flamethrower or spark.
21+
3. Cover every neighbor, including diagonals, with sand, soil, or other solids.
22+
4. Dig the pile. Harvest Charcoal.
23+
24+
## Factory (vanilla parts)
25+
26+
No extra machines. Use conveyors, sensors, drills, pistons, and hoppers.
27+
28+
- **Tree farm:** a sensor above a mature canopy fires a drill at the trunk. Catch falling Raw Wood in a hopper.
29+
- **Kiln chute:** drop Raw Wood down a shaft. Spark at the intake. Keep the shaft sealed (no empty diagonal cells). Gate Charcoal onto a belt below.
30+
31+
## Options
32+
33+
- **Mod enabled** — turn pine trees, wood, and charcoal off.
34+
35+
## Dev
36+
37+
```bash
38+
npm run dev -- --mod trees
39+
```
40+
41+
Hard-reload Sandustry after the bundle writes.
42+
43+
## Changelog
44+
45+
See [CHANGELOG.md](CHANGELOG.md).

‎src/trees/elements/definitions.ts‎

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { ELEMENT, NAME_KEY, VANILLA_ELEMENT } from "./ids.ts";
2+
3+
const MatterType = sandkit.enums.MatterType;
4+
5+
type LiveRegister = Parameters<typeof sandkit.api.elements.register>[0] & {
6+
interactions?: readonly { kind: string; structures?: readonly string[] }[];
7+
isGrabbable?: boolean;
8+
isTransportable?: boolean;
9+
metaColor?: number;
10+
flammable?: {
11+
outputElementId?: string;
12+
outputChance?: number;
13+
};
14+
};
15+
16+
function definition(partial: LiveRegister): LiveRegister {
17+
return partial;
18+
}
19+
20+
export const ELEMENT_DEFINITIONS: LiveRegister[] = [
21+
definition({
22+
id: ELEMENT.pineSeed,
23+
nameKey: NAME_KEY.pineSeed,
24+
density: 220,
25+
matterType: MatterType.Solid,
26+
metaColor: 0x6b4226,
27+
isGrabbable: true,
28+
isTransportable: true,
29+
colors: {
30+
variants: [
31+
[107, 66, 38],
32+
[92, 54, 30],
33+
[130, 82, 48],
34+
],
35+
},
36+
}),
37+
definition({
38+
id: ELEMENT.pineShoot,
39+
nameKey: NAME_KEY.pineShoot,
40+
density: 160,
41+
matterType: MatterType.Static,
42+
metaColor: 0x5c3a21,
43+
colors: {
44+
variants: [
45+
[92, 58, 33],
46+
[74, 46, 26],
47+
[110, 70, 40],
48+
],
49+
},
50+
defaultDataFields: { field1: 0, field2: 0, field3: 1, field4: 0 },
51+
}),
52+
definition({
53+
id: ELEMENT.pineNeedle,
54+
nameKey: NAME_KEY.pineNeedle,
55+
density: 40,
56+
matterType: MatterType.Static,
57+
metaColor: 0x2f5a32,
58+
colors: {
59+
variants: [
60+
[47, 90, 50],
61+
[36, 72, 40],
62+
[62, 110, 58],
63+
],
64+
},
65+
defaultDataFields: { field1: 0, field2: 0 },
66+
}),
67+
definition({
68+
id: ELEMENT.rawWood,
69+
nameKey: NAME_KEY.rawWood,
70+
density: 120,
71+
matterType: MatterType.Solid,
72+
metaColor: 0xb07a48,
73+
isGrabbable: true,
74+
isTransportable: true,
75+
interactions: [{ kind: "flammable" }],
76+
flammable: { outputElementId: VANILLA_ELEMENT.burntResidue, outputChance: 1 },
77+
colors: {
78+
variants: [
79+
[176, 122, 72],
80+
[150, 98, 56],
81+
[196, 140, 88],
82+
],
83+
},
84+
}),
85+
definition({
86+
id: ELEMENT.charcoal,
87+
nameKey: NAME_KEY.charcoal,
88+
density: 200,
89+
matterType: MatterType.Solid,
90+
metaColor: 0x2a2a2a,
91+
isGrabbable: true,
92+
isTransportable: true,
93+
colors: {
94+
variants: [
95+
[42, 42, 42],
96+
[28, 28, 28],
97+
[58, 54, 50],
98+
],
99+
},
100+
}),
101+
definition({
102+
id: ELEMENT.leafDust,
103+
nameKey: NAME_KEY.leafDust,
104+
density: 55,
105+
matterType: MatterType.Powder,
106+
metaColor: 0x6b7a3a,
107+
isGrabbable: true,
108+
isTransportable: true,
109+
colors: {
110+
variants: [
111+
[107, 122, 58],
112+
[90, 104, 46],
113+
[128, 138, 72],
114+
],
115+
},
116+
defaultDataFields: { field1: 0 },
117+
}),
118+
];

‎src/trees/elements/ids.ts‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { modinfo } from "../modinfo.ts";
2+
3+
const root = modinfo.id;
4+
5+
export const ELEMENT = {
6+
pineSeed: `${root}:pineSeed`,
7+
pineShoot: `${root}:pineShoot`,
8+
pineNeedle: `${root}:pineNeedle`,
9+
rawWood: `${root}:rawWood`,
10+
charcoal: `${root}:charcoal`,
11+
leafDust: `${root}:leafDust`,
12+
} as const;
13+
14+
export const TERRAIN = {
15+
pineWood: `${root}:pineWood`,
16+
} as const;
17+
18+
export const NAME_KEY = {
19+
pineSeed: `${root}.pineSeed.name`,
20+
pineShoot: `${root}.pineShoot.name`,
21+
pineWood: `${root}.pineWood.name`,
22+
pineNeedle: `${root}.pineNeedle.name`,
23+
rawWood: `${root}.rawWood.name`,
24+
charcoal: `${root}.charcoal.name`,
25+
leafDust: `${root}.leafDust.name`,
26+
} as const;
27+
28+
export const VANILLA_ELEMENT = {
29+
wetSand: "wetSand",
30+
sand: "sand",
31+
burntResidue: "burntResidue",
32+
} as const;

‎src/trees/elements/register.ts‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { ELEMENT_DEFINITIONS } from "./definitions.ts";
2+
import { ELEMENT, NAME_KEY } from "./ids.ts";
3+
4+
const api = sandkit.api;
5+
6+
export function registerElements(): void {
7+
api.i18n.register("en", {
8+
[NAME_KEY.pineSeed]: "Pine Seed",
9+
[NAME_KEY.pineShoot]: "Pine Shoot",
10+
[NAME_KEY.pineWood]: "Pine Wood",
11+
[NAME_KEY.pineNeedle]: "Pine Needle",
12+
[NAME_KEY.rawWood]: "Raw Wood",
13+
[NAME_KEY.charcoal]: "Charcoal",
14+
[NAME_KEY.leafDust]: "Leaf Dust",
15+
});
16+
17+
for (const definition of ELEMENT_DEFINITIONS) {
18+
const { elementType } = api.elements.register(
19+
definition as Parameters<typeof api.elements.register>[0],
20+
);
21+
if (definition.flammable) {
22+
api.elements.addInteractionInfo(elementType, { kind: "flammable" });
23+
api.elements.updateDefinition(elementType, {
24+
flammable: definition.flammable,
25+
} as Parameters<typeof api.elements.updateDefinition>[1]);
26+
}
27+
if (definition.id !== ELEMENT.pineShoot) {
28+
api.discoveries.addElementByType(elementType);
29+
}
30+
}
31+
}

‎src/trees/main.ts‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { isEnabled } from "@modkit/utils";
2+
import { registerElements } from "./elements/register.ts";
3+
import { registerHarvest } from "./terrains/harvest.ts";
4+
import { registerTerrains } from "./terrains/register.ts";
5+
6+
const api = sandkit.api;
7+
8+
if (isEnabled(api)) {
9+
registerElements();
10+
const pineWoodType = registerTerrains();
11+
registerHarvest(pineWoodType);
12+
api.events.on("game:ready", () => {
13+
api.ui.toast("Trees — Debug → Element → Pine Seed", {});
14+
});
15+
}
16+
17+
console.log("loaded — pine trees, raw wood, charcoal");

‎src/trees/modinfo.ts‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { defineModInfo } from "@modkit/modinfo";
2+
3+
export const modinfo = defineModInfo({
4+
manifestVersion: 1,
5+
id: "irishbruse.trees",
6+
name: "Trees",
7+
version: "0.1.4",
8+
apiVersion: 1,
9+
gameVersion: { minimum: "0.5.5" },
10+
entry: "main.js",
11+
workerEntry: "worker.js",
12+
author: "IrishBruse",
13+
description:
14+
"Pine trees grow on wet sand. Mine the trunk to harvest wood. Seal burning wood to make charcoal.",
15+
dependencies: [],
16+
loadOrder: 0,
17+
configSchema: {
18+
enabled: {
19+
type: "boolean",
20+
default: true,
21+
labelKey: "Mod enabled",
22+
descriptionKey: "Turn pine trees, wood, and charcoal off.",
23+
},
24+
},
25+
});

‎src/trees/sim/air.test.ts‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import assert from "node:assert/strict";
2+
import { test } from "node:test";
3+
import { countAdjacentAir } from "./air.ts";
4+
5+
test("countAdjacentAir is 8 when every neighbor is empty", () => {
6+
assert.equal(
7+
countAdjacentAir(() => true, 10, 10),
8+
8,
9+
);
10+
});
11+
12+
test("countAdjacentAir is 0 when no neighbor is empty", () => {
13+
assert.equal(
14+
countAdjacentAir(() => false, 10, 10),
15+
0,
16+
);
17+
});
18+
19+
test("countAdjacentAir counts only empty ortho-8 neighbors", () => {
20+
const empty = new Set(["11,9", "9,11"]);
21+
const count = countAdjacentAir((x, y) => empty.has(`${x},${y}`), 10, 10);
22+
assert.equal(count, 2);
23+
});

‎src/trees/sim/air.ts‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { ORTHO8_DIRS } from "./constants.ts";
2+
3+
export function countAdjacentAir(
4+
isEmpty: (cellX: number, cellY: number) => boolean,
5+
cellX: number,
6+
cellY: number,
7+
): number {
8+
let count = 0;
9+
for (const [dx, dy] of ORTHO8_DIRS) {
10+
if (isEmpty(cellX + dx, cellY + dy)) count += 1;
11+
}
12+
return count;
13+
}

‎src/trees/sim/burn.ts‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { countAdjacentAir } from "./air.ts";
2+
import type { TreeTypes } from "./types.ts";
3+
4+
export function burnRawWood(
5+
api: WorkerSandkitApi,
6+
types: TreeTypes,
7+
cellX: number,
8+
cellY: number,
9+
context: { cancel(): void },
10+
): void {
11+
const air = countAdjacentAir((x, y) => api.grid.isCellEmptyAtCell(x, y), cellX, cellY);
12+
if (air > 0) return;
13+
context.cancel();
14+
api.elements.replaceAtCell(cellX, cellY, types.charcoal);
15+
api.grid.reportActivityAtCell(cellX, cellY);
16+
}

0 commit comments

Comments
 (0)