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(8, 13)) ## A cushion of moss whose spores twinkle one after another. func _moss_image(variant: int, twinkle: int) -> Pixels: var pixels := Pixels.new(TILE, 13) var shift := variant - 1 for bump: Vector4 in [Vector4(4.5 + shift, 9.5, 4, 3.2), Vector4(11, 9.2, 4.3, 3.5), Vector4(7.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(4 + shift, 8), Vector2i(8 - shift, 5), Vector2i(12, 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