diff --git a/game/i18n/texts.csv b/game/i18n/texts.csv index 984664a..d6d2985 100644 --- a/game/i18n/texts.csv +++ b/game/i18n/texts.csv @@ -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 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 +STYLE_WARM,Warm,Warm +STYLE_PASTEL,Pastell,Pastel +STYLE_GLOW,Glimmen,Glow diff --git a/game/scripts/art/art.gd b/game/scripts/art/art.gd new file mode 100644 index 0000000..a0a8f8c --- /dev/null +++ b/game/scripts/art/art.gd @@ -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 `/.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 diff --git a/game/scripts/art/art.gd.uid b/game/scripts/art/art.gd.uid new file mode 100644 index 0000000..19aa91d --- /dev/null +++ b/game/scripts/art/art.gd.uid @@ -0,0 +1 @@ +uid://dxmii13634y22 diff --git a/game/scripts/art/art_style.gd b/game/scripts/art/art_style.gd new file mode 100644 index 0000000..4b82fce --- /dev/null +++ b/game/scripts/art/art_style.gd @@ -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] diff --git a/game/scripts/art/art_style.gd.uid b/game/scripts/art/art_style.gd.uid new file mode 100644 index 0000000..d532adb --- /dev/null +++ b/game/scripts/art/art_style.gd.uid @@ -0,0 +1 @@ +uid://o2dqirvunvw4 diff --git a/game/scripts/art/glow_style.gd b/game/scripts/art/glow_style.gd new file mode 100644 index 0000000..7f856c6 --- /dev/null +++ b/game/scripts/art/glow_style.gd @@ -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" diff --git a/game/scripts/art/glow_style.gd.uid b/game/scripts/art/glow_style.gd.uid new file mode 100644 index 0000000..7640510 --- /dev/null +++ b/game/scripts/art/glow_style.gd.uid @@ -0,0 +1 @@ +uid://ce6e46e8mwi8f diff --git a/game/scripts/art/pastel_style.gd b/game/scripts/art/pastel_style.gd new file mode 100644 index 0000000..bf90c9b --- /dev/null +++ b/game/scripts/art/pastel_style.gd @@ -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" diff --git a/game/scripts/art/pastel_style.gd.uid b/game/scripts/art/pastel_style.gd.uid new file mode 100644 index 0000000..465743c --- /dev/null +++ b/game/scripts/art/pastel_style.gd.uid @@ -0,0 +1 @@ +uid://dr2fry8j3yai8 diff --git a/game/scripts/art/tile_shape.gd b/game/scripts/art/tile_shape.gd new file mode 100644 index 0000000..a4311a2 --- /dev/null +++ b/game/scripts/art/tile_shape.gd @@ -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) diff --git a/game/scripts/art/tile_shape.gd.uid b/game/scripts/art/tile_shape.gd.uid new file mode 100644 index 0000000..698879d --- /dev/null +++ b/game/scripts/art/tile_shape.gd.uid @@ -0,0 +1 @@ +uid://dfj1ix7h3wky2 diff --git a/game/scripts/art/warm_style.gd b/game/scripts/art/warm_style.gd new file mode 100644 index 0000000..64a3203 --- /dev/null +++ b/game/scripts/art/warm_style.gd @@ -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" diff --git a/game/scripts/art/warm_style.gd.uid b/game/scripts/art/warm_style.gd.uid new file mode 100644 index 0000000..8dd95cb --- /dev/null +++ b/game/scripts/art/warm_style.gd.uid @@ -0,0 +1 @@ +uid://7kb2jlqpvvoe diff --git a/game/tests/test_art.gd b/game/tests/test_art.gd new file mode 100644 index 0000000..76ed6c2 --- /dev/null +++ b/game/tests/test_art.gd @@ -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) diff --git a/game/tests/test_art.gd.uid b/game/tests/test_art.gd.uid new file mode 100644 index 0000000..a5e013d --- /dev/null +++ b/game/tests/test_art.gd.uid @@ -0,0 +1 @@ +uid://dmuqgq1uvws3x diff --git a/game/tests/test_art_style.gd b/game/tests/test_art_style.gd new file mode 100644 index 0000000..a6ea5fc --- /dev/null +++ b/game/tests/test_art_style.gd @@ -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") diff --git a/game/tests/test_art_style.gd.uid b/game/tests/test_art_style.gd.uid new file mode 100644 index 0000000..fa7ee1c --- /dev/null +++ b/game/tests/test_art_style.gd.uid @@ -0,0 +1 @@ +uid://cbkbq72ph6wdn diff --git a/game/tests/test_tile_shape.gd b/game/tests/test_tile_shape.gd new file mode 100644 index 0000000..10d3142 --- /dev/null +++ b/game/tests/test_tile_shape.gd @@ -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") diff --git a/game/tests/test_tile_shape.gd.uid b/game/tests/test_tile_shape.gd.uid new file mode 100644 index 0000000..ee583e3 --- /dev/null +++ b/game/tests/test_tile_shape.gd.uid @@ -0,0 +1 @@ +uid://c4ecwk4sri50q