M1d: Stilproben #12

Merged
steven merged 17 commits from m1d-stilproben into main 2026-10-02 13:01:53 +00:00
19 changed files with 574 additions and 0 deletions
Showing only changes of commit 9921aba63a - Show all commits
+3
View File
@@ -35,3 +35,6 @@ LOAD_ONLY_ALONE,Laden gibt es nur beim Spielen allein,Loading only works when pl
HOST_SAVES,Nur der Host speichert,Only the host saves HOST_SAVES,Nur der Host speichert,Only the host saves
KEY_UNREADABLE,Dein Spielerschlüssel lässt sich gerade nicht lesen,Your player key cannot be read right now KEY_UNREADABLE,Dein Spielerschlüssel lässt sich gerade nicht lesen,Your player key cannot be read right now
SAVE_UNREADABLE,Der Spielstand lässt sich gerade nicht lesen,The save cannot be read right now SAVE_UNREADABLE,Der Spielstand lässt sich gerade nicht lesen,The save cannot be read right now
STYLE_WARM,Warm,Warm
STYLE_PASTEL,Pastell,Pastel
STYLE_GLOW,Glimmen,Glow
1 keys de en
35 HOST_SAVES Nur der Host speichert Only the host saves
36 KEY_UNREADABLE Dein Spielerschlüssel lässt sich gerade nicht lesen Your player key cannot be read right now
37 SAVE_UNREADABLE Der Spielstand lässt sich gerade nicht lesen The save cannot be read right now
38 STYLE_WARM Warm Warm
39 STYLE_PASTEL Pastell Pastel
40 STYLE_GLOW Glimmen Glow
+105
View File
@@ -0,0 +1,105 @@
class_name Art
extends RefCounted
## Every graphic on the playing field has an id. Art draws it in the chosen
## style, or from a picture whose file name is the id, so that an artist can
## replace a graphic without changing code: res://art/terrain/ground.png
## replaces the drawing of GROUND.
## The style changed, or pictures were loaded. Everything drawn by Art has to
## be drawn again.
signal changed
const GROUND := &"terrain/ground"
const SOFT_ROCK := &"terrain/soft_rock"
const HARD_ROCK := &"terrain/hard_rock"
const BEDROCK := &"terrain/bedrock"
## The cracks of a tile that took hits, drawn over the tile.
const CRACKS := &"terrain/cracks"
## The burst where a pickaxe lands.
const HIT := &"effect/hit"
const FIGURE := &"figure"
## The outline of the tile under the mouse.
const MARKER := &"marker"
## Graphics of later milestones, shown in the showcase of the style samples.
const DEAD_SOIL := &"soil/dead"
const LIVING_SOIL := &"soil/living"
const MOSS := &"plant/moss"
const VINE := &"plant/vine"
const FIBER_PLANT := &"plant/fiber"
const STONE := &"item/stone"
const GRAZER := &"animal/grazer"
const RUIN := &"ruin"
## Every id, in the order the showcase and the checks go through them.
const IDS: Array[StringName] = [
GROUND, SOFT_ROCK, HARD_ROCK, BEDROCK, CRACKS, HIT, FIGURE, MARKER,
DEAD_SOIL, LIVING_SOIL, MOSS, VINE, FIBER_PLANT, STONE, GRAZER, RUIN,
]
## The folder where pictures that replace drawings are looked for.
const OVERRIDE_DIR := "res://art"
## The styles to choose from, in the order the game offers them.
var styles: Array[ArtStyle] = [WarmStyle.new(), PastelStyle.new(), GlowStyle.new()]
## The style everything is drawn in.
var style: ArtStyle = styles[0]
var _overrides: Dictionary[StringName, Texture2D] = {}
## The keys of the styles, as LaunchOptions and the settings name them.
static func style_keys() -> Array[StringName]:
return [WarmStyle.KEY, PastelStyle.KEY, GlowStyle.KEY]
## Draws everything in the style named `key` from now on. Returns false, and
## changes nothing, if no style has that key.
func select(key: StringName) -> bool:
for candidate in styles:
if candidate.key() == key:
if candidate != style:
style = candidate
changed.emit()
return true
return false
## Switches to the style after the current one, and from the last to the
## first.
func select_next() -> void:
style = styles[(styles.find(style) + 1) % styles.size()]
changed.emit()
## Uses the picture `<directory>/<id>.png` instead of the drawing, for every
## id that has one. Pictures loaded earlier are dropped.
func load_overrides(directory: String) -> void:
_overrides.clear()
for id in IDS:
var texture := _load_texture(directory.path_join(String(id) + ".png"))
if texture != null:
_overrides[id] = texture
changed.emit()
## True if a picture replaces the drawing of `id`.
func has_override(id: StringName) -> bool:
return _overrides.has(id)
## Draws the graphic `id` into `rect` on `canvas`. Call it only while
## `canvas` draws, from its _draw(). `state` carries what the graphic shows,
## for example how far a plant has grown; see the styles for the entries.
func draw(canvas: CanvasItem, id: StringName, rect: Rect2, state: Dictionary = {}) -> void:
var texture: Texture2D = _overrides.get(id)
if texture != null:
canvas.draw_texture_rect(texture, rect, false)
else:
style.draw(canvas, id, rect, state)
static func _load_texture(path: String) -> Texture2D:
if path.begins_with("res://"):
return load(path) as Texture2D if ResourceLoader.exists(path) else null
if not FileAccess.file_exists(path):
return null
var image := Image.load_from_file(path)
return ImageTexture.create_from_image(image) if image != null else null
+1
View File
@@ -0,0 +1 @@
uid://dxmii13634y22
+192
View File
@@ -0,0 +1,192 @@
class_name ArtStyle
extends RefCounted
## One look of the playing field. Draws every graphic that Art names, with
## code. This base class draws plain placeholders; each style overrides the
## drawing of every id.
##
## The entries of `state`, by id:
## - GROUND, SOFT_ROCK, HARD_ROCK, BEDROCK, DEAD_SOIL and LIVING_SOIL:
## `solid`, a TileShape mask of the neighbours that are rock or the edge of
## the map; `living`, for LIVING_SOIL, the mask of living neighbours;
## `variant`, from TileShape.variant
## - CRACKS: `hits`, 1 or more; `variant`
## - HIT: `age`, from 0 when the pickaxe lands to 1 when the burst is over;
## `variant`
## - FIGURE: `player`, the player's number or -1; `facing`, 1.0 to the
## right or -1.0 to the left; `step`, an angle that grows while the figure
## walks; `moving`
## - MARKER: `reachable`
## - MOSS: `variant`; `time`, in seconds
## - VINE: `points`, the path of the vine; `time`
## - FIBER_PLANT: `growth`, from 0 (a seed) to 1 (grown); `time`
## - GRAZER: `hop`, from 0 to 1 during one hop; `facing`
## - RUIN: `awake`, from 0 (asleep) to 1 (awake); `time`
## - STONE: nothing
## The name of the style on the command line and in the settings. The base
## class is no style to choose and has none.
func key() -> StringName:
return &""
## The key of the style's name in the texts, or an empty text for the base
## class.
func title_key() -> String:
return ""
## Draws the graphic `id` into `rect` on `canvas`, while `canvas` draws.
func draw(canvas: CanvasItem, id: StringName, rect: Rect2, state: Dictionary) -> void:
match id:
Art.GROUND:
_ground(canvas, rect, state)
Art.SOFT_ROCK:
_soft_rock(canvas, rect, state)
Art.HARD_ROCK:
_hard_rock(canvas, rect, state)
Art.BEDROCK:
_bedrock(canvas, rect, state)
Art.CRACKS:
_cracks(canvas, rect, state)
Art.HIT:
_hit(canvas, rect, state)
Art.FIGURE:
_figure(canvas, rect, state)
Art.MARKER:
_marker(canvas, rect, state)
Art.DEAD_SOIL:
_dead_soil(canvas, rect, state)
Art.LIVING_SOIL:
_living_soil(canvas, rect, state)
Art.MOSS:
_moss(canvas, rect, state)
Art.VINE:
_vine(canvas, rect, state)
Art.FIBER_PLANT:
_fiber_plant(canvas, rect, state)
Art.STONE:
_stone(canvas, rect, state)
Art.GRAZER:
_grazer(canvas, rect, state)
Art.RUIN:
_ruin(canvas, rect, state)
_:
push_error("no drawing for the graphic %s" % id)
## The colour of the figure of `player`. Players keep their colour; -1, a
## player not in the world yet, gets the first.
func player_color(player: int) -> Color:
return Color(0.95, 0.85, 0.6) if player <= 0 else Color(0.55, 0.8, 0.95)
func _ground(canvas: CanvasItem, rect: Rect2, _state: Dictionary) -> void:
canvas.draw_rect(rect, Color(0.42, 0.36, 0.28))
func _soft_rock(canvas: CanvasItem, rect: Rect2, _state: Dictionary) -> void:
canvas.draw_rect(rect, Color(0.56, 0.55, 0.52))
func _hard_rock(canvas: CanvasItem, rect: Rect2, _state: Dictionary) -> void:
canvas.draw_rect(rect, Color(0.33, 0.33, 0.37))
func _bedrock(canvas: CanvasItem, rect: Rect2, _state: Dictionary) -> void:
canvas.draw_rect(rect, Color(0.12, 0.12, 0.14))
func _cracks(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
var hits: int = state.get("hits", 1)
var inset := rect.grow(-3.0)
for line in hits:
var part := (line + 1.0) / (hits + 1.0)
var from := Vector2(inset.position.x, inset.position.y + inset.size.y * part)
var to := Vector2(inset.end.x, inset.position.y + inset.size.y * (1.0 - part))
canvas.draw_line(from, to, Color(0.1, 0.08, 0.06, 0.8), 1.0)
func _hit(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
var age: float = state.get("age", 0.0)
for chip in 4:
var direction := Vector2.RIGHT.rotated(TAU * chip / 4.0 + 0.4)
var at := rect.get_center() + direction * rect.size.x * 0.6 * age
canvas.draw_rect(Rect2(at - Vector2(1, 1), Vector2(2, 2)), Color(0.8, 0.8, 0.75, 1.0 - age))
func _figure(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
var radius := rect.size.x * 0.3
canvas.draw_circle(rect.get_center(), radius + 1.0, Color(0.15, 0.1, 0.05))
canvas.draw_circle(rect.get_center(), radius, player_color(state.get("player", -1)))
func _marker(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
var color := Color(0.4, 1.0, 0.4) if state.get("reachable", false) else Color(1.0, 0.4, 0.4)
canvas.draw_rect(rect, color, false, 1.0)
func _dead_soil(canvas: CanvasItem, rect: Rect2, _state: Dictionary) -> void:
canvas.draw_rect(rect, Color(0.45, 0.42, 0.38))
func _living_soil(canvas: CanvasItem, rect: Rect2, _state: Dictionary) -> void:
canvas.draw_rect(rect, Color(0.35, 0.5, 0.25))
func _moss(canvas: CanvasItem, rect: Rect2, _state: Dictionary) -> void:
canvas.draw_circle(rect.get_center(), rect.size.x * 0.3, Color(0.3, 0.65, 0.3))
func _vine(canvas: CanvasItem, _rect: Rect2, state: Dictionary) -> void:
var points: PackedVector2Array = state.get("points", PackedVector2Array())
if points.size() >= 2:
canvas.draw_polyline(points, Color(0.25, 0.6, 0.25), 2.0)
func _fiber_plant(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
var growth: float = state.get("growth", 1.0)
var foot := Vector2(rect.get_center().x, rect.end.y)
canvas.draw_line(foot, foot - Vector2(0, rect.size.y * growth), Color(0.4, 0.7, 0.3), 2.0)
func _stone(canvas: CanvasItem, rect: Rect2, _state: Dictionary) -> void:
canvas.draw_circle(rect.get_center(), rect.size.x * 0.4, Color(0.6, 0.6, 0.58))
func _grazer(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
var lift := sin(PI * float(state.get("hop", 0.0))) * rect.size.y * 0.3
canvas.draw_circle(rect.get_center() - Vector2(0, lift), rect.size.x * 0.35, Color(0.92, 0.9, 0.85))
func _ruin(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
canvas.draw_rect(rect, Color(0.5, 0.5, 0.48))
var awake: float = state.get("awake", 0.0)
canvas.draw_circle(rect.get_center(), rect.size.x * 0.2, Color(1.0, 0.85, 0.3, awake))
# Helpers for the styles.
## A share from 0 to 1 that is the same for the same `variant` and `salt`.
static func share(variant: int, salt: int) -> float:
return float(TileShape.variant(Vector2i(variant & 0xffff, salt)) % 10007) / 10007.0
## A gentle swing from -1 to 1, slow and different for every `variant`.
static func sway(time: float, variant: int, speed := 1.3) -> float:
return sin(time * speed + share(variant, 7) * TAU)
## The point at `part` of the way along `points`, from 0 to 1.
static func along(points: PackedVector2Array, part: float) -> Vector2:
var total := 0.0
for i in points.size() - 1:
total += points[i].distance_to(points[i + 1])
var left := clampf(part, 0.0, 1.0) * total
for i in points.size() - 1:
var length := points[i].distance_to(points[i + 1])
if left <= length:
return points[i].lerp(points[i + 1], left / length if length > 0.0 else 0.0)
left -= length
return points[points.size() - 1]
+1
View File
@@ -0,0 +1 @@
uid://o2dqirvunvw4
+14
View File
@@ -0,0 +1,14 @@
class_name GlowStyle
extends ArtStyle
## A dim, cool world in which everything living shines and glows.
## Until it draws pixel art, it draws the placeholders of ArtStyle.
const KEY := &"glow"
func key() -> StringName:
return KEY
func title_key() -> String:
return "STYLE_GLOW"
+1
View File
@@ -0,0 +1 @@
uid://ce6e46e8mwi8f
+14
View File
@@ -0,0 +1,14 @@
class_name PastelStyle
extends ArtStyle
## Soft, light colours with outlines in a darker shade of what they surround.
## Until it draws pixel art, it draws the placeholders of ArtStyle.
const KEY := &"pastel"
func key() -> StringName:
return KEY
func title_key() -> String:
return "STYLE_PASTEL"
+1
View File
@@ -0,0 +1 @@
uid://dr2fry8j3yai8
+49
View File
@@ -0,0 +1,49 @@
class_name TileShape
extends RefCounted
## Neighbourhoods on the tile grid, so that a style can draw each tile to fit
## the tiles around it, and a stable number per tile for small variations.
## Bits of a neighbour mask, clockwise from north.
const N := 1
const NE := 2
const E := 4
const SE := 8
const S := 16
const SW := 32
const W := 64
const NW := 128
## Where the neighbour of each bit lies, in the order of the bits.
const OFFSETS: Array[Vector2i] = [
Vector2i(0, -1), Vector2i(1, -1), Vector2i(1, 0), Vector2i(1, 1),
Vector2i(0, 1), Vector2i(-1, 1), Vector2i(-1, 0), Vector2i(-1, -1),
]
## One byte per tile of `codes`: 1 if `members` holds its code, else 0.
static func flags(codes: PackedByteArray, members: Array[int]) -> PackedByteArray:
var result := PackedByteArray()
result.resize(codes.size())
for index in codes.size():
result[index] = 1 if members.has(codes[index]) else 0
return result
## The neighbours of `tile` whose flag is 1, as a mask of the bits above, on
## a map of `width` by `height` tiles. Beyond the edge of the map, every
## neighbour counts as set if `outside_set` is true.
static func mask(tile_flags: PackedByteArray, width: int, height: int, tile: Vector2i, outside_set: bool) -> int:
var result := 0
for bit in 8:
var at := tile + OFFSETS[bit]
var inside := at.x >= 0 and at.y >= 0 and at.x < width and at.y < height
if (inside and tile_flags[at.y * width + at.x] == 1) or (not inside and outside_set):
result |= 1 << bit
return result
## A number from 0 to 2147483647 that is the same for `tile` every time and
## looks random from one tile to the next.
static func variant(tile: Vector2i) -> int:
var h := (tile.x * 374761393 + tile.y * 668265263) & 0x7fffffff
h = ((h ^ (h >> 13)) * 1274126177) & 0x7fffffff
return h ^ (h >> 16)
+1
View File
@@ -0,0 +1 @@
uid://dfj1ix7h3wky2
+14
View File
@@ -0,0 +1,14 @@
class_name WarmStyle
extends ArtStyle
## Warm, full colours and dark outlines, close to the classic farming games.
## Until it draws pixel art, it draws the placeholders of ArtStyle.
const KEY := &"warm"
func key() -> StringName:
return KEY
func title_key() -> String:
return "STYLE_WARM"
+1
View File
@@ -0,0 +1 @@
uid://7kb2jlqpvvoe
+110
View File
@@ -0,0 +1,110 @@
extends TestCase
## Draws every graphic of `art` twice, with `state` and with nothing, while
## it draws.
class Probe extends Node2D:
var art: Art
var state: Dictionary
var drawn := 0
func _draw() -> void:
for id in Art.IDS:
var rect := Rect2(0, 0, 48, 48) if id == Art.RUIN else Rect2(16, 16, 16, 16)
art.draw(self, id, rect, state)
art.draw(self, id, rect, {})
drawn += 1
## Everything a graphic can be asked to show, so that each drawing runs
## through all its parts.
static func _full_state() -> Dictionary:
return {
"solid": TileShape.N | TileShape.W | TileShape.NE, "living": TileShape.E | TileShape.S,
"variant": 123456, "hits": 2, "age": 0.5, "player": 1, "facing": -1.0, "step": 1.0,
"moving": true, "reachable": true, "time": 2.5, "growth": 0.9, "hop": 0.5, "awake": 0.7,
"points": PackedVector2Array([Vector2(0, 8), Vector2(10, 4), Vector2(24, 9)]),
}
## Lets `art` draw every graphic once and returns how many were drawn.
func _draw_everything(art: Art) -> int:
var probe := Probe.new()
probe.art = art
probe.state = _full_state()
tree.root.add_child(probe)
await tree.process_frame
await tree.process_frame
var drawn := probe.drawn
tree.root.remove_child(probe)
probe.free()
return drawn
func test_every_style_draws_every_graphic() -> void:
var art := Art.new()
for style in art.styles:
art.select(style.key())
check_eq(await _draw_everything(art), Art.IDS.size(), "%s draws them all" % style.key())
art.style = ArtStyle.new()
check_eq(await _draw_everything(art), Art.IDS.size(), "the placeholders too")
func test_the_styles_are_offered_in_a_fixed_order() -> void:
var art := Art.new()
var keys: Array[StringName] = []
for style in art.styles:
keys.append(style.key())
check_eq(keys, Art.style_keys(), "the keys in the order of the styles")
check_eq(keys, [WarmStyle.KEY, PastelStyle.KEY, GlowStyle.KEY], "warm, pastel, glow")
check_eq(art.style, art.styles[0], "the first one is chosen at first")
func test_a_style_is_chosen_by_its_key() -> void:
var art := Art.new()
var changes := [0]
art.changed.connect(func() -> void: changes[0] += 1)
check(art.select(PastelStyle.KEY), "the pastel style exists")
check_eq(art.style.key(), PastelStyle.KEY, "and is chosen")
check(art.select(PastelStyle.KEY), "choosing it again works")
check_eq(changes[0], 1, "only a real change is announced")
check(not art.select(&"watercolour"), "an unknown key is refused")
check_eq(art.style.key(), PastelStyle.KEY, "and changes nothing")
func test_the_next_style_follows_and_the_first_comes_after_the_last() -> void:
var art := Art.new()
var seen: Array[StringName] = []
for i in art.styles.size() + 1:
seen.append(art.style.key())
art.select_next()
check_eq(seen, [WarmStyle.KEY, PastelStyle.KEY, GlowStyle.KEY, WarmStyle.KEY], "round and round")
func test_every_style_has_a_name_in_every_language() -> void:
for translation in I18n.load_translations(I18n.TEXTS_FILE):
for style in Art.new().styles:
var title := String(translation.get_message(style.title_key()))
check(not title.is_empty(), "%s in %s" % [style.title_key(), translation.locale])
func test_a_picture_named_after_an_id_replaces_its_drawing() -> void:
var directory := TestFiles.temporary_dir("art")
DirAccess.make_dir_recursive_absolute(directory.path_join("terrain"))
var image := Image.create(16, 16, false, Image.FORMAT_RGBA8)
image.fill(Color.RED)
image.save_png(directory.path_join("terrain/ground.png"))
var art := Art.new()
art.load_overrides(directory)
check(art.has_override(Art.GROUND), "the ground has a picture")
check(not art.has_override(Art.SOFT_ROCK), "soft rock has none")
check_eq(await _draw_everything(art), Art.IDS.size(), "the picture is drawn like the rest")
art.load_overrides(directory.path_join("missing"))
check(not art.has_override(Art.GROUND), "loading again drops the picture")
TestFiles.remove_dir(directory)
func test_without_the_folder_of_pictures_everything_is_drawn() -> void:
var art := Art.new()
art.load_overrides(Art.OVERRIDE_DIR)
for id in Art.IDS:
check(not art.has_override(id), "no picture for %s" % id)
+1
View File
@@ -0,0 +1 @@
uid://dmuqgq1uvws3x
+26
View File
@@ -0,0 +1,26 @@
extends TestCase
## The helpers that every style draws with.
func test_a_point_can_be_found_along_a_path() -> void:
var path := PackedVector2Array([Vector2(0, 0), Vector2(10, 0), Vector2(10, 10)])
check_eq(ArtStyle.along(path, 0.0), Vector2(0, 0), "the start")
check_eq(ArtStyle.along(path, 0.5), Vector2(10, 0), "halfway is the bend")
check_eq(ArtStyle.along(path, 0.75), Vector2(10, 5), "three quarters")
check_eq(ArtStyle.along(path, 1.0), Vector2(10, 10), "the end")
check_eq(ArtStyle.along(path, 2.0), Vector2(10, 10), "never beyond the end")
func test_shares_lie_between_zero_and_one_and_stay_the_same() -> void:
for variant in [0, 1, 99, 123456, 2147483647]:
for salt in 4:
var share := ArtStyle.share(variant, salt)
check(share >= 0.0 and share < 1.0, "share of %d and %d: %f" % [variant, salt, share])
check_eq(ArtStyle.share(variant, salt), share, "the same again")
func test_a_sway_swings_gently_and_differs_between_things() -> void:
for tenth in 100:
var swing := ArtStyle.sway(tenth * 0.1, 7)
check(swing >= -1.0 and swing <= 1.0, "between left and right at %.1f s: %f" % [tenth * 0.1, swing])
check(ArtStyle.sway(0.0, 1) != ArtStyle.sway(0.0, 2), "two things do not swing alike")
+1
View File
@@ -0,0 +1 @@
uid://cbkbq72ph6wdn
+38
View File
@@ -0,0 +1,38 @@
extends TestCase
## A map of three by three tiles, row by row, with rock (1) in the middle and
## in the top right corner.
const CODES := [0, 0, 1, 0, 1, 0, 0, 0, 0]
func _flags() -> PackedByteArray:
return TileShape.flags(PackedByteArray(CODES), [1])
func test_flags_mark_the_tiles_of_the_given_codes() -> void:
check_eq(_flags(), PackedByteArray([0, 0, 1, 0, 1, 0, 0, 0, 0]), "rock is marked")
check_eq(TileShape.flags(PackedByteArray([0, 1, 2]), [1, 2]), PackedByteArray([0, 1, 1]), "several codes")
func test_a_mask_holds_the_marked_neighbours_clockwise_from_north() -> void:
check_eq(TileShape.mask(_flags(), 3, 3, Vector2i(1, 1), false), TileShape.NE, "the middle sees the corner")
check_eq(TileShape.mask(_flags(), 3, 3, Vector2i(1, 2), false), TileShape.N, "below the middle")
check_eq(TileShape.mask(_flags(), 3, 3, Vector2i(2, 1), false), TileShape.N | TileShape.W, "right of the middle")
check_eq(TileShape.mask(_flags(), 3, 3, Vector2i(0, 0), false), TileShape.SE, "the top left corner")
func test_beyond_the_edge_neighbours_count_as_marked_if_asked() -> void:
var corner := TileShape.mask(_flags(), 3, 3, Vector2i(0, 0), true)
var outside := TileShape.N | TileShape.NE | TileShape.NW | TileShape.W | TileShape.SW
check_eq(corner, outside | TileShape.SE, "five neighbours lie outside")
func test_variants_stay_the_same_and_differ_between_neighbours() -> void:
check_eq(TileShape.variant(Vector2i(4, 7)), TileShape.variant(Vector2i(4, 7)), "the same every time")
var seen := {}
for y in 4:
for x in 4:
var variant := TileShape.variant(Vector2i(x, y))
check(variant >= 0, "never negative: %d" % variant)
seen[variant] = true
check_eq(seen.size(), 16, "sixteen tiles, sixteen variants")
+1
View File
@@ -0,0 +1 @@
uid://c4ecwk4sri50q