M1d: Stilproben #12

Merged
steven merged 17 commits from m1d-stilproben into main 2026-10-02 13:01:53 +00:00
3 changed files with 387 additions and 0 deletions
Showing only changes of commit deba758e87 - Show all commits
+327
View File
@@ -14,6 +14,8 @@ const TILE := 16
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)
@@ -25,6 +27,9 @@ 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] = {}
@@ -471,3 +476,325 @@ func _figure_image(colour: int, frame: int, left: bool) -> Pixels:
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
+59
View File
@@ -0,0 +1,59 @@
extends TestCase
## The pixel art of the things of later milestones in the showcase.
## The highest row of `image` with a pixel that is not transparent.
static func _top(image: Image) -> int:
for y in image.get_height():
for x in image.get_width():
if image.get_pixel(x, y).a > 0.0:
return y
return image.get_height()
func test_a_plant_grows_taller_from_stage_to_stage() -> void:
var style := WarmStyle.new()
var tops: Array[int] = []
for stage in PixelStyle.GROWTH_STAGES:
tops.append(_top(style._plant_image(stage, 0).image))
for stage in range(1, tops.size()):
check(tops[stage] < tops[stage - 1], "stage %d is taller than stage %d: %s" % [stage, stage - 1, tops])
func test_grass_has_a_ragged_edge_only_toward_bare_ground() -> void:
var style := WarmStyle.new()
var inside := style._grass_image(255, 0).image
var lone := style._grass_image(0, 0).image
var tile := PixelStyle.TILE
var inside_gaps := 0
var lone_gaps := 0
for y in tile:
for x in tile:
inside_gaps += 1 if inside.get_pixel(x, y).a == 0.0 else 0
lone_gaps += 1 if lone.get_pixel(x, y).a == 0.0 else 0
check_eq(inside_gaps, 0, "living all around: no gaps")
check(lone_gaps > 4, "bare ground all around: a ragged, round edge (%d gaps)" % lone_gaps)
check_eq(lone.get_pixel(0, 0).a, 0.0, "round at the corner")
func test_dead_soil_is_the_bare_earth_with_cracks() -> void:
var style := WarmStyle.new()
var earth := style._earth(0, 0).image
var cracked := style._crack_soil(style._earth(0, 0), 0).image
check(cracked.get_data() != earth.get_data(), "cracks in every other tile")
check_eq(style._crack_soil(style._earth(1, 0), 1).image.get_data(), style._earth(1, 0).image.get_data(), "none in the others")
func test_an_animal_in_the_air_tucks_its_legs() -> void:
var style := WarmStyle.new()
var standing := style._grazer_image(false, false).image
var tucked := style._grazer_image(true, false).image
check(standing.get_pixel(5, 14).a > 0.0, "standing on its legs")
check_eq(tucked.get_pixel(5, 14).a, 0.0, "legs tucked in the air")
func test_the_ruin_glows_in_its_doorway() -> void:
var style := WarmStyle.new()
var light := style._ruin_glow().image
check(light.get_pixel(24, 31).a > 0.5, "bright in the middle of the doorway")
check_eq(light.get_pixel(3, 30).a, 0.0, "dark on the walls")
+1
View File
@@ -0,0 +1 @@
uid://cbm4wa0gxi4p1