← Medieval3D assetfogon/balista
Balista
Drag to turn it// «Balista» · gpt-6-sol (medium)
// A medieval ballista: a giant crossbow on a wooden base with four small wheels, a thick wooden bow arm with rope string drawn back, a huge bolt with an iron tip and red fletching loaded and pointing forward (-y), and a winch with a crank at the back.
// Medieval ballista, facing -y
const woodDark = "#4b3022";
const wood = "#70472b";
const woodLight = "#a47748";
const woodEdge = "#c09258";
const iron = "#303238";
const ironLight = "#777b7c";
const rope = "#d7bd87";
const red = "#a32d30";
const redLight = "#ce4a40";
const blue = "#263f77";
const gold = "#d6aa51";
// Four small, iron-bound wooden wheels. Their bottoms touch the floor.
for (const wx of [11, 37]) {
for (const wy of [15, 36]) {
const outerX = wx < 24 ? wx - 2 : wx + 2;
for (let i = 0; i < 32; i++) {
const a = i * Math.PI * 2 / 32;
const b = (i + 1) * Math.PI * 2 / 32;
for (const dx of [-1, 0, 1]) {
line(wx + dx, wy + 4 * Math.cos(a), 4 + 4 * Math.sin(a),
wx + dx, wy + 4 * Math.cos(b), 4 + 4 * Math.sin(b),
0.65, dx === 0 ? woodLight : woodDark);
}
line(outerX, wy + 4 * Math.cos(a), 4 + 4 * Math.sin(a),
outerX, wy + 4 * Math.cos(b), 4 + 4 * Math.sin(b),
0.45, iron);
}
for (let i = 0; i < 8; i++) {
const a = i * Math.PI / 4;
line(wx, wy, 4,
wx, wy + 3.7 * Math.cos(a), 4 + 3.7 * Math.sin(a),
0.7, wood);
}
cylinder(wx - 2, wy, 4, 1.4, 5, iron, "x");
set(outerX, wy, 4, ironLight);
}
}
// Axles, heavy chassis and individually visible deck planks.
for (const y of [15, 36]) {
line(10, y, 4, 38, y, 4, 1, iron);
box(14, y - 1, 6, 34, y + 1, 8, woodDark);
}
for (const x of [15, 31]) {
box(x, 12, 7, x + 2, 41, 10, woodDark);
box(x, 12, 10, x + 2, 41, 10, woodLight);
}
for (let x = 18; x <= 30; x += 3) {
box(x, 13, 8, x + 2, 40, 10, x % 2 ? wood : woodLight);
line(x, 13, 11, x, 40, 11, 0, woodDark);
}
for (const y of [13, 39]) {
box(14, y, 8, 34, y + 2, 11, woodDark);
box(15, y, 11, 33, y + 1, 11, woodEdge);
for (const x of [16, 22, 28, 33]) set(x, y, 11, ironLight);
}
// Diagonal braces and the elevated bolt trough.
for (const x of [17, 31]) {
line(x, 14, 10, x, 22, 15, 1.2, wood);
line(x, 37, 10, x, 25, 15, 1.2, woodDark);
box(x - 1, 19, 12, x + 1, 22, 15, wood);
box(x - 1, 19, 15, x + 1, 20, 15, iron);
}
box(21, 12, 12, 27, 38, 15, woodDark);
box(22, 13, 15, 26, 36, 16, woodLight);
for (const x of [21, 27]) {
line(x, 12, 16, x, 37, 16, 0.6, iron);
}
box(19, 32, 13, 29, 34, 16, woodDark); // catch for the drawn string
for (const x of [19, 29]) box(x, 32, 15, x, 34, 17, iron);
// The broad, forward-swept wooden bow.
for (const side of [-1, 1]) {
const X = d => 24 + side * d;
line(X(0), 21, 17, X(10), 19, 17, 2.1, woodDark);
line(X(10), 19, 17, X(18), 15, 17, 1.9, wood);
line(X(18), 15, 17, X(20), 13, 17, 1.35, woodDark);
line(X(2), 21, 19, X(10), 19, 19, 0.6, woodEdge);
line(X(11), 18, 19, X(17), 15, 19, 0.5, woodLight);
for (const d of [5, 11, 16]) {
const y = d < 10 ? 20 : d < 15 ? 18 : 16;
line(X(d), y - 1, 15, X(d), y + 1, 19, 0.55, iron);
set(X(d), y, 19, ironLight);
}
box(X(20) - 1, 12, 16, X(20) + 1, 14, 18, iron);
set(X(20), 13, 19, gold);
}
// Pale rope drawn taut to the rear catch, forming the crossbow's V.
line(4, 13, 18, 24, 34, 18, 0.55, rope);
line(44, 13, 18, 24, 34, 18, 0.55, rope);
sphere(24, 34, 18, 1, gold);
// Loaded siege bolt, above the trough, aimed straight at the viewer.
line(24, 10, 19, 24, 35, 19, 1.2, woodDark);
line(24, 11, 20, 24, 33, 20, 0.45, woodEdge);
for (const y of [15, 25, 32]) {
box(23, y, 18, 25, y, 19, iron);
set(24, y, 20, ironLight);
}
// Chunky tapered iron point.
box(22, 9, 18, 26, 11, 20, iron);
box(23, 7, 18, 25, 9, 20, ironLight);
box(24, 5, 19, 24, 7, 19, "#bdc0b9");
// Large red fletching, with two side vanes and an upright vane.
for (const side of [-1, 1]) {
for (let y = 29; y <= 35; y++) {
const width = y < 31 ? 2 : y < 34 ? 4 : 3;
for (let d = 2; d <= width; d++) {
set(24 + side * d, y, 19, d === width ? redLight : red);
if (d < width) set(24 + side * d, y, 20, red);
}
}
}
for (let y = 30; y <= 35; y++) {
const h = y < 32 ? 2 : 4;
for (let z = 21; z <= 20 + h; z++)
set(24, y, z, z === 20 + h ? redLight : red);
}
// Small royal-blue heraldic panels on either flank of the carriage.
for (const x of [14, 34]) {
box(x, 24, 9, x, 30, 13, gold);
box(x, 25, 10, x, 29, 12, blue);
for (let y = 26; y <= 28; y++) set(x, y, 11, red);
set(x, 27, 12, gold);
}
for (const y of [18, 27, 35]) {
for (const x of [15, 33]) set(x, y, 10, ironLight);
}
// Fixed iron bearing for the winch at the rear right.
box(32, 36, 13, 38, 40, 18, woodDark);
box(35, 37, 14, 39, 39, 17, iron);
set(39, 38, 16, ironLight);
// Only the crank wheel moves; its pivot is exactly at the axle.
part("crank", [39, 38, 16]);
for (let i = 0; i < 32; i++) {
const a = i * Math.PI * 2 / 32;
const b = (i + 1) * Math.PI * 2 / 32;
line(40, 38 + 4 * Math.cos(a), 16 + 4 * Math.sin(a),
40, 38 + 4 * Math.cos(b), 16 + 4 * Math.sin(b),
0.7, iron);
}
for (let i = 0; i < 6; i++) {
const a = i * Math.PI / 3;
line(40, 38, 16,
40, 38 + 3.8 * Math.cos(a), 16 + 3.8 * Math.sin(a),
0.6, woodLight);
}
cylinder(39, 38, 16, 1.2, 3, gold, "x");
line(41, 41, 18, 44, 41, 18, 0.8, iron);
box(43, 40, 17, 45, 42, 19, woodLight);
animate("crank", { type: "spin", axis: "x", speed: 2 });import { draw } from "./assets/voxel";
import { balista } from "./assets/balista";
// Every frame: standing on (x, y), the centre of its base.
draw(ctx, balista, x, y, { size: 64, angle: 0, t: performance.now() / 1000 });
// size: its longest side, in px
// angle: turn on the floor, in radians
// t: seconds; if it moves, that animates it# In a terminal, in the game's folder
fogon assets add balista
# With Fogón's tools (connector)
add_assets({ slug: "<your-game>", ids: ["balista"] })
# It lands in client/assets/balista.ts
# https://fogon.page/object/balista