Files
Steven Wroblewski 417cf30a9c Nachbesserungen aus der Gesamtprüfung von M1d
Das Lebensmoos bekommt links und rechts Platz für seinen Umriss, der
bei zwei von drei Formen abgeschnitten war; die Bilder der Vitrine sind
neu aufgenommen. Die Pausenanzeige kehrt zurück, wenn eine Meldung
während der Pause abläuft. Ein Test prüft die Splitter fremder
Schläge, und der Test ohne Bilderordner hängt nicht mehr davon ab, was
in res://art liegt. Der Fragebogen nennt die Felsproben und sagt, was
sofort kommt und was erst mit der Antwort des Hosts.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-10-02 14:36:43 +02:00

802 lines
31 KiB
GDScript

class_name PixelStyle
extends ArtStyle
## Pixel art in the manner of classic farming games, drawn in code. A tile
## is 16 by 16 pixels, seen a little from the front: rock shows its face
## toward open ground, and figures, animals and plants stand upright. Every
## graphic is made once as a small image, pixel by pixel from the palette,
## and kept; the camera enlarges each pixel to a crisp square.
##
## The subclasses choose the palette and the outlines.
## Pixels along the edge of a tile.
const TILE := 16
## Rows of the face that rock shows toward open ground below it.
const FACE := 6
## Images of a burst of chips, from the hit to its end.
const HIT_FRAMES := 6
## Stages of a growing plant, from the seed to the fibres.
const GROWTH_STAGES := 6
## Size of a figure: a tile wide and one and a half tiles tall.
const FIGURE_SIZE := Vector2i(16, 24)
const CLEAR := Color(0, 0, 0, 0)
## Colours by name; the subclass fills them.
var palette: Dictionary[String, Color] = {}
## The colours of the players, in the order of their numbers.
var players: Array[Color] = []
## True for outlines in a darker shade of what they surround, false for one
## dark outline colour.
var tinted_outlines := false
## How strongly living things glow, from 0 (not at all) to 1; the subclass
## chooses it.
var glow := 0.0
var _textures: Dictionary[String, Texture2D] = {}
## A small image that the drawings put their pixels on.
class Pixels:
const SIDES: Array[Vector2i] = [Vector2i(1, 0), Vector2i(-1, 0), Vector2i(0, 1), Vector2i(0, -1)]
const CLEAR := Color(0, 0, 0, 0)
var image: Image
var size: Vector2i
func _init(width: int, height: int) -> void:
size = Vector2i(width, height)
image = Image.create_empty(width, height, false, Image.FORMAT_RGBA8)
func has(x: int, y: int) -> bool:
return x >= 0 and y >= 0 and x < size.x and y < size.y
func put(x: int, y: int, color: Color) -> void:
if has(x, y):
image.set_pixel(x, y, color)
## The colour at x, y, transparent outside the image.
func at(x: int, y: int) -> Color:
return image.get_pixel(x, y) if has(x, y) else CLEAR
func filled(x: int, y: int) -> bool:
return at(x, y).a > 0.0
## Lays the translucent `color` over the pixel at x, y.
func shade(x: int, y: int, color: Color) -> void:
if has(x, y):
image.set_pixel(x, y, image.get_pixel(x, y).blend(color))
func rect(x: int, y: int, width: int, height: int, color: Color) -> void:
for row in range(y, y + height):
for column in range(x, x + width):
put(column, row, color)
## Fills the ellipse around cx, cy with the half axes rx and ry.
func ellipse(cx: float, cy: float, rx: float, ry: float, color: Color) -> void:
for row in range(floori(cy - ry), ceili(cy + ry) + 1):
for column in range(floori(cx - rx), ceili(cx + rx) + 1):
var dx := (column + 0.5 - cx) / rx
var dy := (row + 0.5 - cy) / ry
if dx * dx + dy * dy <= 1.0:
put(column, row, color)
## A line of single pixels from x0, y0 to x1, y1.
func line(x0: int, y0: int, x1: int, y1: int, color: Color) -> void:
var dx := absi(x1 - x0)
var dy := -absi(y1 - y0)
var sx := 1 if x0 < x1 else -1
var sy := 1 if y0 < y1 else -1
var error := dx + dy
var x := x0
var y := y0
while true:
put(x, y, color)
if x == x1 and y == y1:
return
var twice := 2 * error
if twice >= dy:
error += dy
x += sx
if twice <= dx:
error += dx
y += sy
## Surrounds everything solid drawn so far with a line one pixel wide: in
## `color`, or, with `tinted`, in a darker shade of the pixel beside it.
func outline(color: Color, tinted: bool) -> void:
var found: Array[Vector3i] = []
var colors: Array[Color] = []
for y in size.y:
for x in size.x:
if filled(x, y):
continue
for side in SIDES:
var beside := at(x + side.x, y + side.y)
if beside.a >= 1.0:
found.append(Vector3i(x, y, 0))
colors.append(beside.darkened(0.5) if tinted else color)
break
for index in found.size():
put(found[index].x, found[index].y, colors[index])
func player_color(player: int) -> Color:
return players[maxi(0, player) % players.size()]
func _c(name: String) -> Color:
return palette[name]
## The dark line around things, or around a thing of colour `fill` when
## outlines are tinted.
func _ink(fill: Color) -> Color:
return fill.darkened(0.5) if tinted_outlines else _c("outline")
## A number from 0 to 1 that looks random and is the same for the same
## arguments.
static func _noise(x: int, y: int, salt: int) -> float:
return float(TileShape.variant(Vector2i(x * 31 + salt * 977, y * 17 - salt * 131)) % 1000) / 1000.0
## The texture made by `make` under `key`, made only the first time.
func _image(key: String, make: Callable) -> Texture2D:
var texture: Texture2D = _textures.get(key)
if texture == null:
var pixels: Pixels = make.call()
texture = ImageTexture.create_from_image(pixels.image)
_textures[key] = texture
return texture
## Draws `texture` with its top left corner at `at`, on whole pixels.
static func _put(canvas: CanvasItem, texture: Texture2D, at: Vector2, modulate := Color.WHITE) -> void:
canvas.draw_texture(texture, at.round(), modulate)
## The neighbours of `mask` that change how a tile looks: the four sides,
## and a corner only where both sides beside it are set.
static func _shape(mask: int) -> int:
var result := mask & (TileShape.N | TileShape.E | TileShape.S | TileShape.W)
var corners := [
[TileShape.NE, TileShape.N | TileShape.E], [TileShape.SE, TileShape.S | TileShape.E],
[TileShape.SW, TileShape.S | TileShape.W], [TileShape.NW, TileShape.N | TileShape.W],
]
for corner: Array in corners:
var sides: int = corner[1]
if (mask & corner[0]) != 0 and (mask & sides) == sides:
result |= corner[0]
return result
# Terrain.
func _ground(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
var shadow: int = state.get("solid", 0) & (TileShape.N | TileShape.W | TileShape.NW)
var variant: int = state.get("variant", 0) % 8
_put(canvas, _image("ground/%d/%d" % [shadow, variant], func() -> Pixels: return _earth(variant, shadow)), rect.position)
## The bare earth of the dead world: up to two soft patches of darker earth,
## a light grain or two, now and then a pebble.
func _earth(variant: int, shadow: int) -> Pixels:
var pixels := Pixels.new(TILE, TILE)
pixels.rect(0, 0, TILE, TILE, _c("ground"))
var patches: int = [0, 1, 2, 1, 0, 1, 1, 2][variant]
for patch in patches:
var at := _spot(patch, variant, 2, 11)
for dy in 2:
for dx in range(-1, 2):
if _noise(at.x + dx, at.y + dy, variant + 40) < 0.6:
pixels.put(at.x + dx, at.y + dy, _c("ground_dark"))
for grain in 1 + variant % 2:
var at := _spot(grain + 4, variant, 1, 14)
pixels.put(at.x, at.y, _c("ground_light"))
if variant % 4 == 1:
var at := _spot(9, variant, 3, 9)
_pebble(pixels, at.x, at.y)
_cast_shadow(pixels, shadow)
return pixels
## A point in the tile that is the same for the same `index` and `variant`,
## from `low` to `low + span - 1` on both axes.
static func _spot(index: int, variant: int, low: int, span: int) -> Vector2i:
return Vector2i(low + int(_noise(index, 1, variant) * span), low + int(_noise(index, 2, variant) * span))
func _pebble(pixels: Pixels, x: int, y: int) -> void:
pixels.put(x, y, _c("pebble_light"))
pixels.put(x + 1, y, _c("pebble"))
pixels.put(x, y + 1, _c("pebble"))
pixels.put(x + 1, y + 1, _c("pebble_dark"))
## Darkens the ground where rock to the north or west casts its shadow; the
## light falls from the top left.
func _cast_shadow(pixels: Pixels, shadow: int) -> void:
var deep := Color(0, 0, 0, 0.32)
var faint := Color(0, 0, 0, 0.16)
if (shadow & TileShape.N) != 0:
for x in TILE:
pixels.shade(x, 0, deep)
pixels.shade(x, 1, deep if x % 2 == 0 else faint)
pixels.shade(x, 2, faint if x % 2 == 1 else CLEAR)
if (shadow & TileShape.W) != 0:
for y in range(1 if (shadow & TileShape.N) != 0 else 0, TILE):
pixels.shade(0, y, faint)
elif (shadow & (TileShape.N | TileShape.NW)) == TileShape.NW:
pixels.shade(0, 0, deep)
func _soft_rock(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
_rock(canvas, rect, state, "soft")
func _hard_rock(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
_rock(canvas, rect, state, "hard")
func _bedrock(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
_rock(canvas, rect, state, "bed")
## Rock of `kind`: its top, and its face where the ground below is open.
## The ground shows where the outer corners are round.
func _rock(canvas: CanvasItem, rect: Rect2, state: Dictionary, kind: String) -> void:
var mask := _shape(state.get("solid", 0))
var variant: int = state.get("variant", 0) % 4
_ground(canvas, rect, {"variant": state.get("variant", 0)})
var key := "%s/%d/%d" % [kind, mask, variant]
_put(canvas, _image(key, func() -> Pixels: return _rock_image(kind, mask, variant)), rect.position)
func _rock_image(kind: String, mask: int, variant: int) -> Pixels:
var open_n := (mask & TileShape.N) == 0
var open_e := (mask & TileShape.E) == 0
var open_s := (mask & TileShape.S) == 0
var open_w := (mask & TileShape.W) == 0
var top_rows := TILE - FACE if open_s else TILE
var ink := _c("outline")
var pixels := Pixels.new(TILE, TILE)
for y in TILE:
for x in TILE:
var color := _c(kind + "_top")
if y >= top_rows:
color = _c(kind + "_face")
if y == top_rows or ((x + variant) % 4 == 0 and y < TILE - 2):
color = _c(kind + "_face_dark")
else:
if (open_n and y == 1) or (open_w and x == 1):
color = _c(kind + "_top_light")
elif open_e and x == TILE - 2:
color = _c(kind + "_top_dark")
if open_s and y == top_rows - 1:
color = _c(kind + "_lip")
pixels.put(x, y, color)
_rock_marks(pixels, kind, variant, top_rows)
if open_n:
pixels.rect(0, 0, TILE, 1, ink)
if open_w:
pixels.rect(0, 0, 1, TILE, ink)
if open_e:
pixels.rect(TILE - 1, 0, 1, TILE, ink)
if open_s:
pixels.rect(0, TILE - 1, TILE, 1, ink)
# Inner corners, where the mass turns around open ground.
if not open_n and not open_w and (mask & TileShape.NW) == 0:
pixels.put(0, 0, ink)
if not open_n and not open_e and (mask & TileShape.NE) == 0:
pixels.put(TILE - 1, 0, ink)
if not open_s and not open_w and (mask & TileShape.SW) == 0:
pixels.rect(0, TILE - FACE, 1, FACE, ink)
if not open_s and not open_e and (mask & TileShape.SE) == 0:
pixels.rect(TILE - 1, TILE - FACE, 1, FACE, ink)
# Round outer corners.
if open_n and open_w:
_round(pixels, Vector2i(0, 0), Vector2i(1, 1), ink)
if open_n and open_e:
_round(pixels, Vector2i(TILE - 1, 0), Vector2i(-1, 1), ink)
if open_s and open_w:
_round(pixels, Vector2i(0, TILE - 1), Vector2i(1, -1), ink)
if open_s and open_e:
_round(pixels, Vector2i(TILE - 1, TILE - 1), Vector2i(-1, -1), ink)
return pixels
## Cuts the corner at `corner` and closes the outline one pixel further in,
## in the direction `inward`.
static func _round(pixels: Pixels, corner: Vector2i, inward: Vector2i, ink: Color) -> void:
pixels.put(corner.x, corner.y, CLEAR)
pixels.put(corner.x + inward.x, corner.y, CLEAR)
pixels.put(corner.x, corner.y + inward.y, CLEAR)
pixels.put(corner.x + inward.x, corner.y + inward.y, ink)
## Small details on the top of rock: pores and chips on soft rock, glinting
## crystals in hard rock, layers in bedrock.
func _rock_marks(pixels: Pixels, kind: String, variant: int, top_rows: int) -> void:
var room := maxi(1, top_rows - 6)
for pore in 2:
var spot := Vector2i(3 + int(_noise(pore, 8, variant) * 10.0), 3 + int(_noise(pore, 9, variant) * room))
pixels.put(spot.x, spot.y, _c(kind + "_top_dark"))
var at := Vector2i(4 + int(_noise(7, 8, variant) * 7.0), 3 + int(_noise(7, 9, variant) * room))
match kind:
"soft":
if variant % 2 == 0:
pixels.put(at.x, at.y, _c("soft_top_dark"))
pixels.put(at.x + 1, at.y, _c("soft_top_dark"))
pixels.put(at.x + 1, at.y - 1, _c("soft_top_light"))
pixels.put(at.x + 2, at.y + 1, _c("soft_top_dark"))
"hard":
if variant % 2 == 1:
pixels.put(at.x, at.y, _c("hard_glint"))
pixels.put(at.x + 1, at.y, _c("hard_top_light"))
pixels.put(at.x, at.y + 1, _c("hard_top_light"))
"bed":
for row in range(4, top_rows - 1, 5):
for column in range(3, TILE - 3):
if _noise(column, row, variant) < 0.5:
pixels.put(column, row, _c("bed_top_dark"))
func _cracks(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
var hits := clampi(state.get("hits", 1), 1, 3)
var variant: int = state.get("variant", 0) % 4
_put(canvas, _image("cracks/%d/%d" % [hits, variant], func() -> Pixels: return _crack_image(hits, variant)), rect.position)
func _crack_image(hits: int, variant: int) -> Pixels:
var pixels := Pixels.new(TILE, TILE)
var path: Array[Vector2i] = []
var at := Vector2i(5 + variant, 3)
for step in 2 + hits * 3:
path.append(at)
var turn := _noise(step, hits, variant)
at += Vector2i(-1 if turn < 0.3 else (1 if turn > 0.65 else 0), 1)
if hits >= 2:
var branch: Vector2i = path[path.size() >> 1]
for step in 3:
branch += Vector2i(1, 1 if step % 2 == 0 else 0)
path.append(branch)
for point in path:
pixels.put(point.x + 1, point.y, _c("soft_top_light"))
for point in path:
pixels.put(point.x, point.y, _c("outline"))
return pixels
func _hit(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
var frame := clampi(int(float(state.get("age", 0.0)) * HIT_FRAMES), 0, HIT_FRAMES - 1)
var variant: int = state.get("variant", 0) % 4
_put(canvas, _image("hit/%d/%d" % [frame, variant], func() -> Pixels: return _hit_image(frame, variant)), rect.position - Vector2(8, 8))
## Chips that fly up and fall, and a puff of dust, in a square of two tiles
## around the tile.
func _hit_image(frame: int, variant: int) -> Pixels:
var pixels := Pixels.new(TILE * 2, TILE * 2)
var t := (frame + 1.0) / HIT_FRAMES
for chip in 5:
var angle := -PI / 2.0 + (chip - 2) * 0.6 + (variant - 1.5) * 0.15
var distance := 3.0 + t * 10.0
var x := roundi(TILE + cos(angle) * distance)
var y := roundi(TILE - 1 + sin(angle) * distance + t * t * 10.0)
pixels.put(x, y, _c("soft_top_light"))
if frame < 3 and chip % 2 == 0:
pixels.put(x + 1, y, _c("soft_top"))
pixels.put(x, y + 1, _c("soft_face"))
pixels.put(x + 1, y + 1, _c("soft_face_dark"))
if frame < 3:
pixels.outline(_c("outline"), tinted_outlines)
var radius := 3.0 + frame * 2.0
for puff in 8:
var angle := TAU * puff / 8.0 + variant
var dust := _c("dust")
dust.a = 0.75 - frame * 0.2
pixels.put(roundi(TILE + cos(angle) * radius), roundi(TILE - 1 + sin(angle) * radius * 0.6), dust)
return pixels
func _marker(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
var reachable: bool = state.get("reachable", false)
_put(canvas, _image("marker/%s" % reachable, func() -> Pixels: return _marker_image(reachable)), rect.position - Vector2(1, 1))
## Four corners around the tile, with a dark rim that shows on any ground.
func _marker_image(reachable: bool) -> Pixels:
var pixels := Pixels.new(TILE + 2, TILE + 2)
var color := _c("marker_near") if reachable else _c("marker_far")
var last := TILE
for corner: Vector2i in [Vector2i(1, 1), Vector2i(last, 1), Vector2i(1, last), Vector2i(last, last)]:
var dx := 1 if corner.x == 1 else -1
var dy := 1 if corner.y == 1 else -1
for i in 4:
pixels.put(corner.x + dx * i, corner.y, color)
pixels.put(corner.x, corner.y + dy * i, color)
pixels.outline(_c("outline"), false)
return pixels
# Figures.
func _figure(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
var frame := 0
if state.get("moving", false):
frame = 1 + int(fposmod(float(state.get("step", 0.0)), TAU) / TAU * 4.0) % 4
var colour := maxi(0, state.get("player", -1)) % players.size()
var left: bool = float(state.get("facing", 1.0)) < 0.0
var foot := Vector2(rect.get_center().x, rect.end.y)
_put(canvas, _image("shadow/12", func() -> Pixels: return _shadow_image(12)), foot - Vector2(6, 2))
var key := "figure/%d/%d/%s" % [colour, frame, left]
_put(canvas, _image(key, func() -> Pixels: return _figure_image(colour, frame, left)), foot - Vector2(FIGURE_SIZE.x / 2.0, FIGURE_SIZE.y))
## A soft shadow on the ground, `width` pixels wide.
func _shadow_image(width: int) -> Pixels:
var pixels := Pixels.new(width, 3)
pixels.ellipse(width / 2.0, 1.5, width / 2.0, 1.5, Color(0, 0, 0, 0.28))
return pixels
## A figure in walking frame `frame` (0 standing, 1 to 4 walking), facing
## right or, with `left`, left. The shirt has the colour of the player.
func _figure_image(colour: int, frame: int, left: bool) -> Pixels:
var pixels := Pixels.new(FIGURE_SIZE.x, FIGURE_SIZE.y)
var shirt := players[colour]
# The body sinks a pixel while the legs are apart, so the head always keeps
# a free row above it for its outline.
var bob := 1 if frame == 1 or frame == 3 else 0
# The far leg and the near leg, by frame.
var legs: Array[Vector2i] = [Vector2i(6, 9), Vector2i(5, 10), Vector2i(7, 8), Vector2i(10, 5), Vector2i(8, 7)]
for leg in 2:
var x := legs[frame].x if leg == 0 else legs[frame].y
var lifted := 1 if frame > 0 and frame % 2 == 1 and leg == 0 else 0
pixels.rect(x, 18, 2, 4 - lifted, _c("pants_dark") if leg == 0 else _c("pants"))
pixels.rect(x, 22 - lifted, 3, 2, _c("boots"))
pixels.rect(5, 11 + bob, 6, 8, shirt)
pixels.rect(5, 11 + bob, 1, 8, shirt.darkened(0.2))
pixels.rect(5, 18 + bob, 6, 1, shirt.darkened(0.35))
var swing: int = [0, 1, 0, -1, 0][frame]
pixels.rect(8 + swing, 12 + bob, 2, 5, shirt.darkened(0.15))
pixels.rect(8 + swing, 17 + bob, 2, 1, _c("skin"))
# The head: hair behind, the face in front, bangs above the eye.
pixels.ellipse(7.8, 5.8 + bob, 5.2, 5.0, _c("hair"))
pixels.ellipse(9.6, 7.4 + bob, 3.7, 3.9, _c("skin"))
pixels.rect(9, 3 + bob, 4, 2, _c("hair"))
pixels.put(6, 2 + bob, _c("hair_light"))
pixels.put(7, 2 + bob, _c("hair_light"))
pixels.put(8, 1 + bob, _c("hair_light"))
pixels.put(11, 6 + bob, _c("eye"))
pixels.put(11, 7 + bob, _c("eye"))
pixels.put(8, 8 + bob, _c("skin_shade"))
pixels.put(12, 9 + bob, _c("skin_shade"))
pixels.outline(_ink(shirt), tinted_outlines)
if left:
pixels.image.flip_x()
return pixels
# Things of later milestones, shown in the showcase.
func _dead_soil(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
var shadow: int = state.get("solid", 0) & (TileShape.N | TileShape.W | TileShape.NW)
var variant: int = state.get("variant", 0) % 8
var key := "dead/%d/%d" % [shadow, variant]
_put(canvas, _image(key, func() -> Pixels: return _crack_soil(_earth(variant, shadow), variant)), rect.position)
## Dry cracks in every other tile of dead soil. Returns `pixels`.
func _crack_soil(pixels: Pixels, variant: int) -> Pixels:
if variant % 2 == 0:
var at := _spot(12, variant, 3, 7)
for step in 5:
pixels.put(at.x, at.y, _c("dead_crack"))
if step == 2:
pixels.put(at.x, at.y + 1, _c("dead_crack"))
pixels.put(at.x - 1, at.y + 2, _c("dead_crack"))
at += Vector2i(1, -1 if _noise(step, 5, variant) < 0.4 else 0)
return pixels
func _living_soil(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
var living := _shape(state.get("living", 0))
var variant: int = state.get("variant", 0) % 6
_ground(canvas, rect, {"variant": state.get("variant", 0)})
_put(canvas, _image("living/%d/%d" % [living, variant], func() -> Pixels: return _grass_image(living, variant)), rect.position)
## Grass with blades and a few flowers, with a soft edge toward the sides
## that are not living.
func _grass_image(living: int, variant: int) -> Pixels:
var open_n := (living & TileShape.N) == 0
var open_e := (living & TileShape.E) == 0
var open_s := (living & TileShape.S) == 0
var open_w := (living & TileShape.W) == 0
var pixels := Pixels.new(TILE, TILE)
pixels.rect(0, 0, TILE, TILE, _c("grass"))
for tuft in 6:
var at := _spot(tuft + 10, variant, 2, 12)
pixels.put(at.x, at.y, _c("grass_dark"))
pixels.put(at.x - 1, at.y - 1, _c("grass_dark"))
pixels.put(at.x + 1, at.y - 1, _c("grass_dark"))
for blade in 4:
var at := _spot(blade + 20, variant, 1, 14)
pixels.put(at.x, at.y, _c("grass_light"))
if variant % 3 == 0:
var x := 4 + variant % 7
var y := 5 + variant % 5
var petal := _c("flower_b") if variant % 2 == 0 else _c("flower_c")
for side in Pixels.SIDES:
pixels.put(x + side.x, y + side.y, petal)
pixels.put(x, y, _c("flower_a"))
if glow > 0.0:
for spark in 2:
var light := _c("spore")
light.a = glow * 0.8
pixels.put(2 + int(_noise(spark, 21, variant) * 12.0), 2 + int(_noise(spark, 22, variant) * 12.0), light)
# A ragged edge toward bare ground.
for i in TILE:
var ragged := 1 if _noise(i, 23, variant) < 0.45 else 0
if open_n:
_edge(pixels, Vector2i(i, 0), Vector2i(0, 1), ragged)
if open_s:
_edge(pixels, Vector2i(i, TILE - 1), Vector2i(0, -1), ragged)
if open_w:
_edge(pixels, Vector2i(0, i), Vector2i(1, 0), ragged)
if open_e:
_edge(pixels, Vector2i(TILE - 1, i), Vector2i(-1, 0), ragged)
if open_n and open_w:
_round(pixels, Vector2i(0, 0), Vector2i(1, 1), _c("grass_edge"))
if open_n and open_e:
_round(pixels, Vector2i(TILE - 1, 0), Vector2i(-1, 1), _c("grass_edge"))
if open_s and open_w:
_round(pixels, Vector2i(0, TILE - 1), Vector2i(1, -1), _c("grass_edge"))
if open_s and open_e:
_round(pixels, Vector2i(TILE - 1, TILE - 1), Vector2i(-1, -1), _c("grass_edge"))
return pixels
## Clears `cut` pixels at `at` and draws the darker edge of the grass behind
## them, one step further in the direction `inward`.
func _edge(pixels: Pixels, at: Vector2i, inward: Vector2i, cut: int) -> void:
for depth in cut:
var gone := at + inward * depth
pixels.put(gone.x, gone.y, CLEAR)
var edge := at + inward * cut
pixels.put(edge.x, edge.y, _c("grass_edge"))
func _moss(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
var variant: int = state.get("variant", 0) % 3
var twinkle := int(float(state.get("time", 0.0)) * 2.5) % 4
var foot := Vector2(rect.get_center().x, rect.end.y)
_put(canvas, _image("moss/%d/%d" % [variant, twinkle], func() -> Pixels: return _moss_image(variant, twinkle)), foot - Vector2(10, 13))
## A cushion of moss whose spores twinkle one after another, with room on
## both sides for its outline.
func _moss_image(variant: int, twinkle: int) -> Pixels:
var pixels := Pixels.new(TILE + 2, 13)
var shift := variant - 1
for bump: Vector4 in [Vector4(6.5 + shift, 9.5, 4, 3.2), Vector4(13, 9.2, 4.3, 3.5), Vector4(9.8 - shift, 6.8, 3.8, 3.4)]:
pixels.ellipse(bump.x, bump.y, bump.z, bump.w, _c("moss"))
for y in pixels.size.y:
for x in pixels.size.x:
if not pixels.filled(x, y):
continue
if y >= 11:
pixels.put(x, y, _c("moss_dark"))
elif not pixels.filled(x, y - 1) or not pixels.filled(x, y - 2):
pixels.put(x, y, _c("moss_light"))
pixels.outline(_ink(_c("moss")), tinted_outlines)
var spores: Array[Vector2i] = [Vector2i(6 + shift, 8), Vector2i(10 - shift, 5), Vector2i(14, 8)]
for index in spores.size():
var at := spores[index]
var bright := index == twinkle
var spore := _c("spore") if bright else _c("spore").darkened(0.2)
pixels.put(at.x, at.y, spore)
pixels.put(at.x + 1, at.y, spore)
if bright:
pixels.put(at.x, at.y - 1, _c("spore"))
if glow > 0.0:
var halo := _c("spore")
halo.a = 0.45 * glow
for side in Pixels.SIDES:
pixels.shade(at.x + side.x, at.y + side.y + 1, halo)
return pixels
func _vine(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
var points: PackedVector2Array = state.get("points", PackedVector2Array())
if points.size() < 2:
return
var origin := (rect.position - Vector2(6, 6)).floor()
var size := Vector2i(ceili(rect.size.x) + 13, ceili(rect.size.y) + 13)
var key := "vine/%d/%d/%d" % [hash(points), size.x, size.y]
_put(canvas, _image(key, func() -> Pixels: return _vine_image(points, origin, size)), origin)
## A vine two pixels thick along `points`, with leaves on alternate sides.
func _vine_image(points: PackedVector2Array, origin: Vector2, size: Vector2i) -> Pixels:
var pixels := Pixels.new(size.x, size.y)
for i in points.size() - 1:
var from := Vector2i((points[i] - origin).round())
var to := Vector2i((points[i + 1] - origin).round())
pixels.line(from.x, from.y - 1, to.x, to.y - 1, _c("leaf"))
pixels.line(from.x, from.y, to.x, to.y, _c("vine"))
pixels.line(from.x, from.y + 1, to.x, to.y + 1, _c("vine_dark"))
var length := 0.0
for i in points.size() - 1:
length += points[i].distance_to(points[i + 1])
var leaves := int(length / 9.0)
for leaf in leaves:
var at := Vector2i((along(points, (leaf + 0.5) / leaves) - origin).round())
var up := leaf % 2 == 0
var dy := -2 if up else 2
var tip := -1 if up else 1
pixels.put(at.x, at.y + dy, _c("leaf"))
pixels.put(at.x + 1, at.y + dy, _c("leaf"))
pixels.put(at.x + 1, at.y + dy + tip, _c("leaf"))
pixels.put(at.x + 2, at.y + dy + tip, _c("leaf_dark"))
pixels.outline(_ink(_c("vine")), tinted_outlines)
return pixels
func _fiber_plant(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
var stage := clampi(int(float(state.get("growth", 1.0)) * GROWTH_STAGES), 0, GROWTH_STAGES - 1)
var lean := clampi(roundi(sway(state.get("time", 0.0), 3) * 1.4), -1, 1)
var foot := Vector2(rect.get_center().x, rect.end.y)
_put(canvas, _image("plant/%d/%d" % [stage, lean], func() -> Pixels: return _plant_image(stage, lean)), foot - Vector2(8, 28))
## A fibre plant at growth stage `stage`, its top leaning by `lean` pixels:
## a seed in a small mound, a sprout, a stalk with leaves, buds and at last
## white tufts of fibre.
func _plant_image(stage: int, lean: int) -> Pixels:
var pixels := Pixels.new(TILE, 28)
pixels.rect(5, 26, 6, 1, _c("ground_dark"))
pixels.rect(4, 27, 8, 1, _c("ground_dark"))
if stage == 0:
pixels.put(7, 25, _c("seed"))
pixels.put(8, 25, _c("seed"))
pixels.outline(_ink(_c("seed")), tinted_outlines)
return pixels
var heights: Array[int] = [0, 4, 8, 13, 17, 19]
var height := heights[stage]
var top := Vector2i(8, 25)
for i in height:
var x := 8 + roundi(lean * 2.0 * i / 19.0)
top = Vector2i(x, 25 - i)
pixels.put(top.x, top.y, _c("stem"))
@warning_ignore("integer_division")
for pair in range(1, height / 3 + 1):
var y := 26 - pair * 3
var x := 8 + roundi(lean * 2.0 * (25 - y) / 19.0)
var side := 1 if pair % 2 == 0 else -1
pixels.put(x + side, y, _c("leaf"))
pixels.put(x + side * 2, y, _c("leaf"))
pixels.put(x + side * 2, y - 1, _c("leaf"))
pixels.put(x + side * 3, y - 1, _c("leaf_dark"))
pixels.put(x - side, y - 1, _c("leaf_dark"))
if stage == 1:
pixels.put(top.x - 1, top.y, _c("leaf"))
pixels.put(top.x + 1, top.y, _c("leaf"))
if stage == 4:
pixels.ellipse(top.x + 0.5, top.y - 0.5, 1.6, 1.6, _c("bud"))
if stage == 5:
for tuft: Vector3 in [Vector3(0, -2, 2.3), Vector3(-3, 1, 1.8), Vector3(3, 2, 1.8)]:
pixels.ellipse(top.x + 0.5 + tuft.x, top.y + tuft.y, tuft.z, tuft.z, _c("fibre"))
pixels.put(top.x + int(tuft.x), top.y + int(tuft.y) + 1, _c("fibre_shade"))
pixels.outline(_ink(_c("stem")), tinted_outlines)
return pixels
func _stone(canvas: CanvasItem, rect: Rect2, _state: Dictionary) -> void:
_put(canvas, _image("stone", func() -> Pixels: return _stone_image()), Vector2(rect.get_center().x - 4, rect.end.y - 8))
func _stone_image() -> Pixels:
var pixels := Pixels.new(8, 8)
pixels.ellipse(4, 4.2, 3.2, 2.7, _c("stone"))
pixels.rect(2, 6, 4, 1, _c("stone_dark"))
pixels.put(2, 3, _c("stone_light"))
pixels.put(3, 2, _c("stone_light"))
pixels.outline(_ink(_c("stone")), tinted_outlines)
return pixels
func _grazer(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
var height := roundi(sin(PI * clampf(state.get("hop", 0.0), 0.0, 1.0)) * 5.0)
var left: bool = float(state.get("facing", 1.0)) < 0.0
var foot := Vector2(rect.get_center().x, rect.end.y)
var width := 14 - height
_put(canvas, _image("shadow/%d" % width, func() -> Pixels: return _shadow_image(width)), foot - Vector2(width / 2.0, 2))
var tucked := height > 1
_put(canvas, _image("grazer/%s/%s" % [tucked, left], func() -> Pixels: return _grazer_image(tucked, left)), foot - Vector2(10, 16 + height))
## A woolly animal, facing right or, with `left`, left; in the air its legs
## are `tucked`.
func _grazer_image(tucked: bool, left: bool) -> Pixels:
var pixels := Pixels.new(20, 16)
for leg in [5, 7, 11, 13]:
pixels.rect(leg, 12, 1, 1 if tucked else 3, _c("hoof"))
for puff: Vector4 in [Vector4(9, 8, 6.5, 4.5), Vector4(5.5, 9.5, 4, 3.5), Vector4(12.5, 9.5, 4.5, 3.5), Vector4(8, 5.8, 4, 3), Vector4(11.5, 6.3, 3.5, 3)]:
pixels.ellipse(puff.x, puff.y, puff.z, puff.w, _c("wool"))
for y in pixels.size.y:
for x in pixels.size.x:
if pixels.at(x, y) == _c("wool"):
if y >= 11:
pixels.put(x, y, _c("wool_shade"))
elif not pixels.filled(x, y - 1) and _noise(x, y, 4) < 0.6:
pixels.put(x, y, _c("wool_light"))
pixels.ellipse(16.2, 8.0, 2.6, 3.2, _c("face"))
pixels.put(14, 5, _c("face"))
pixels.put(15, 5, _c("face"))
pixels.put(17, 7, _c("wool_light"))
pixels.outline(_ink(_c("wool")), tinted_outlines)
if left:
pixels.image.flip_x()
return pixels
func _ruin(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
var awake := clampf(state.get("awake", 0.0), 0.0, 1.0)
_put(canvas, _image("ruin", func() -> Pixels: return _ruin_image()), rect.position)
_put(canvas, _image("ruin_glow", func() -> Pixels: return _ruin_glow()), rect.position, Color(1, 1, 1, awake))
## A gate of old stone blocks, three tiles wide, with moss on top and a dark
## doorway.
func _ruin_image() -> Pixels:
var pixels := Pixels.new(TILE * 3, TILE * 3)
var stone := _c("ruin")
pixels.rect(3, 12, 12, 34, stone)
pixels.rect(33, 12, 12, 34, stone)
pixels.rect(1, 6, 46, 9, stone)
pixels.rect(1, 3, 46, 3, _c("ruin_light"))
pixels.rect(15, 15, 18, 31, _c("door"))
pixels.rect(9, 44, 30, 2, _c("ruin_dark"))
pixels.rect(7, 46, 34, 2, stone)
# Blocks: a joint every five rows, and between the blocks of each row.
for course in 8:
var y := 8 + course * 5
for x in range(1, 47):
if pixels.at(x, y) == stone:
pixels.put(x, y, _c("ruin_dark"))
var offset := 0 if course % 2 == 0 else 4
for x in range(3 + offset, 47, 8):
for row in range(y + 1, y + 5):
if pixels.at(x, row) == stone:
pixels.put(x, row, _c("ruin_dark"))
for x in range(1, 47):
if _noise(x, 40, 1) < 0.45:
pixels.put(x, 3, _c("ruin_moss"))
if _noise(x, 41, 1) < 0.25:
pixels.put(x, 4, _c("ruin_moss"))
for strand: Vector2i in [Vector2i(5, 4), Vector2i(12, 3), Vector2i(36, 5), Vector2i(43, 2)]:
pixels.rect(strand.x, 15, 1, strand.y, _c("ruin_moss"))
pixels.outline(_ink(stone), tinted_outlines)
return pixels
## The light in the doorway and the runes above it, shown as the ruin wakes.
func _ruin_glow() -> Pixels:
var pixels := Pixels.new(TILE * 3, TILE * 3)
var center := Vector2(24, 31)
for y in range(15, 46):
for x in range(15, 33):
var distance := Vector2(x + 0.5, y + 0.5).distance_to(center)
var light := _c("glow_light") if distance < 4.0 else _c("glow")
light.a = clampf(1.0 - distance / 13.0, 0.0, 1.0) * (0.8 + 0.2 * glow)
if light.a > 0.05:
pixels.put(x, y, light)
for rune in 3:
var x := 16 + rune * 7
for dot: Vector2i in [Vector2i(0, 0), Vector2i(1, 0), Vector2i(1, 1), Vector2i(0, 2), Vector2i(1, 2)]:
pixels.put(x + dot.x + (rune % 2), 8 + dot.y, _c("glow"))
return pixels