From 298bdf8c37309becc2b1ed0a2df2d3d707893296 Mon Sep 17 00:00:00 2001 From: Steven Wroblewski Date: Fri, 2 Oct 2026 09:45:16 +0200 Subject: [PATCH 01/17] Plan M1d: Stilproben MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Acht Tasks bringen das Spielfeld in drei Stilen auf den Bildschirm, alle aus Code: Scherenschnitt, Kontur und Geometrisch. Jede Grafik hängt an einer Kennung, eine Vitrine zeigt jeden Stil mit Dingen späterer Meilensteine, F2 wechselt im Spiel, und das Spiel merkt sich die Wahl. Dazu kommen die Antworten aus dem Probelauf von M1c: Der eigene Schlag zeigt seine Splitter sofort, und die Namen über den Figuren bleiben scharf. Jede Task-Grenze wurde vorab in einer Kopie von main geprüft. Co-Authored-By: Claude Opus 5.5 --- .../plans/2026-10-02-m1d-stilproben.md | 5604 +++++++++++++++++ 1 file changed, 5604 insertions(+) create mode 100644 docs/superpowers/plans/2026-10-02-m1d-stilproben.md diff --git a/docs/superpowers/plans/2026-10-02-m1d-stilproben.md b/docs/superpowers/plans/2026-10-02-m1d-stilproben.md new file mode 100644 index 0000000..6026fe9 --- /dev/null +++ b/docs/superpowers/plans/2026-10-02-m1d-stilproben.md @@ -0,0 +1,5604 @@ +# M1d Stilproben Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Das Spielfeld lässt sich in drei Stilen zeichnen, alle aus Code: Scherenschnitt, Kontur und Geometrisch. Eine Vitrine zeigt jeden Stil zusammen mit Dingen späterer Meilensteine, F2 wechselt den Stil im Spiel, und das Spiel merkt sich die Wahl. Damit lässt sich die Prüffrage von Meilenstein 1 beantworten: „Welche der drei Stilproben soll es sein?“ + +**Architecture:** Alles auf dem Spielfeld zeichnet `Art`. Jede Grafik hat eine Kennung wie `terrain/soft_rock`. Der gewählte Stil, eine Unterklasse von `ArtStyle`, zeichnet sie mit den Zeichenbefehlen von Godot; ein Bild `game/art/.png` ersetzt die Zeichnung, ohne dass sich Code ändert. `TileShape` gibt jeder Kachel ihre acht Nachbarn, damit Felsmassen außen rund und innen geschlossen sind. `MapView` zeichnet die Karte in Blöcken und nach einem Treffer nur die Blöcke um die Kachel neu. Kern und Brücke bleiben unverändert, denn Grafik ändert nichts an der gemeinsamen Welt. + +**Tech Stack:** Godot 4.7.2 (Standard-Ausgabe, Renderer „GL Compatibility“), GDScript mit Typangaben, die Zeichenbefehle von `CanvasItem` mit Kantenglättung. Rust 1.98.1 und godot-rust 0.5.5 bleiben unverändert. + +**Spec:** `docs/KONZEPT.md`, Abschnitt 7.4 („Spielfeld aus Code“, „Austauschbar“), Abschnitt 8 (Meilenstein 1 mit der Prüffrage „Welche der drei Stilproben soll es sein?“) und Abschnitt 11 („Grafikstil“ und das Risiko „Grafik aus Code kann nüchtern wirken“). Dazu kommen die Antworten aus dem Probelauf von M1c im Kommentar zu Gitea #8 und deine Wahl der drei Richtungen und der Szene vor diesem Plan. + +## Einordnung + +| Plan | Inhalt | Stand | +|---|---|---| +| M1a Rust-Kern | Deterministischer Kern mit Karte, Spielern, Befehlen, Speicherstand, Aufzeichnung | fertig, in `main` | +| M1b Brücke und Hülle | GDExtension, Godot-Hülle, Takt, Figur, Speichern und Laden | fertig, in `main` | +| M1c Gleichschritt im Netz | Koop bis vier Spieler, Host-Welt, Prüfsummen, zwei Instanzen im Gleichschritt | fertig, in `main` | +| **M1d Stilproben** | Dieser Plan | offen | + +Danach ist Meilenstein 1 vollständig, und dein Probelauf beantwortet seine Prüffrage. + +## Was dieser Plan erledigt + +| Punkt | Task | +|---|---| +| Konzept 7.4: Jede Grafik hängt an einer Kennung; ein Artist kann sie ersetzen, ohne dass sich Code ändert | 1: `Art` mit 16 Kennungen und Bildern unter `game/art/` | +| Konzept 7.4: Spielfeld aus Code, flach und klar, Bewegung durch Programmierung, alle Kachelvarianten passen aneinander | 1 und 2: Nachbarmasken und Blöcke; 5 bis 7: die drei Stile | +| Konzept 8 und 11: drei Stilproben zur Wahl | 3: Vitrine; 4: Wechsel mit F2 und über die Befehlszeile; 8: Fragebogen mit Bildern | +| Konzept 11, Risiko „Grafik aus Code kann nüchtern wirken“ | 3: Die Vitrine zeigt lebendigen Boden, Moos, Ranke, Pflanze, Tier und Ruine in Bewegung | +| Probelauf M1c, Frage 2: Schläge als Gast sind sichtbar verzögert | 2: Der eigene Schlag zeigt seine Splitter beim Klick | +| Probelauf M1c, Frage 4: Namen sind unscharf | 2: Namen in Bildschirmgröße | +| #8: „nach höchstens `NetLink.TIMEOUT_MSEC`“ in `CLAUDE.md` | 8: Wortlaut „nach etwa“. Der Test über `get_statistic` bleibt in #8 offen. | + +## Nicht Teil dieses Plans + +- Regeln für lebendigen Boden, Moos, Ranken, Pflanzen, Tiere und Ruinen. Sie kommen ab Meilenstein 2. Die Vitrine bewegt ihre Dinge nach festen Formeln der Hülle; der Kern weiß nichts von ihnen. +- Bilder von einem Artist, Porträts, Titelbild und Ton (Konzept 7.4, Meilenstein 6) +- Das Entfernen der Stile, die du nicht wählst. Das entscheidet sich nach deinem Probelauf. +- Die übrigen Punkte aus #5 und #8 + +## Technische Festlegungen + +| Thema | Festlegung | Grund | +|---|---|---| +| Ort | Nur die Hülle ändert sich, dazu `CLAUDE.md` und `docs/`. | Grafik ändert nichts an der gemeinsamen Welt. | +| Richtungen | Scherenschnitt: Papierlagen ohne Umriss mit weichen Schatten, gedeckte Erdfarben, Lebendiges leuchtet grün und golden. Kontur: kräftige dunkle Umrisse um flache, starke Farben, gut lesbar wie ein Comic. Geometrisch: ruhige Pastellformen ohne Umriss und Schatten, Muster nach dem Raster. | Deine Wahl vor diesem Plan | +| Szene | Die Testkarte im Spiel und eine Vitrine mit Dingen späterer Meilensteine | Deine Wahl vor diesem Plan. Auf der Testkarte allein ließe sich nicht beurteilen, wie Lebendiges aussieht. | +| Kennungen | 16 Kennungen in `Art.IDS`. Für das Spiel: Boden, weicher Fels, harter Fels, Grundgestein, Risse, Splitter, Figur, Zielmarke. Für die Vitrine: toter und lebendiger Boden, Moos, Ranke, Faserpflanze, Stein, Weidetier, Ruine. | Konzept 7.4: Jede Grafik hängt an einer Kennung. | +| Austausch | `Art.load_overrides` lädt beim Start jedes Bild `game/art/.png`; es ersetzt die Zeichnung seiner Kennung. | Ein Artist kann Grafik austauschen, ohne dass sich Code ändert. | +| Stile | Ein Stil ist eine Unterklasse von `ArtStyle` und überschreibt je Kennung eine Zeichenmethode. `ArtStyle` selbst zeichnet schlichte Platzhalter. | Ein Stil steckt in einer Datei. Fehlt einer neuen Kennung die Zeichnung, fehlt nur ihr Aussehen; das Spiel läuft weiter. | +| Zustand | Was eine Grafik zeigt, steht im Wörterbuch `state`, etwa die Nachbarn einer Kachel, die Zahl der Treffer, das Alter der Splitter oder wie weit eine Pflanze gewachsen ist. Die Einträge stehen im Kopf von `art_style.gd`. | Stile und Aufrufer kennen nur Kennung und Zustand, nicht einander. | +| Kanten | Glatte Kanten kommen aus den Zeichenbefehlen mit `antialiased = true`. | Der Renderer „GL Compatibility“ kennt kein MSAA in 2D; eingeschaltet meldet es einen Engine-Fehler. | +| Kachelformen | `TileShape.mask` beschreibt die acht Nachbarn einer Kachel als Bitmaske, im Uhrzeigersinn ab Norden. Fels ist außen rund und innen geschlossen; jenseits des Kartenrands zählt alles als Fels. | Konzept 7.4: Alle Kachelvarianten passen aneinander. | +| Zeichnen der Karte | Blöcke von 16 × 16 Kacheln. Ein Treffer zeichnet nur die Blöcke um seine Kachel neu. | Die ganze Karte neu zu zeichnen dauerte 18 bis 40 ms, zu lang für jeden Treffer. Nach dem Umbau gemessen: rund 8 ms, um die Karte beim Start aufzubauen, rund 7 ms für das ganze Bild nach einem Treffer, höchstens 40 ms für das Bild nach einem Stilwechsel. | +| Eigener Schlag | Die Splitter erscheinen beim Klick. Risse und Abbau kommen weiter mit dem Takt, die Splitter anderer Spieler auch. | Probelauf M1c: Als Gast kam der Schlag erst nach der Rundreise zum Host zurück. | +| Namen | Die Namen über fremden Figuren werden in der Größe gezeichnet, die sie auf dem Bildschirm haben, und dann verkleinert. | Probelauf M1c: In 8 Pixeln gezeichnet und von der Kamera dreifach vergrößert, wirkten sie unscharf. | +| Wechsel und Wahl | F2 wechselt im Spiel. In der Vitrine wechseln die Tasten 1 bis 3, F2 und Knöpfe. `--style=NAME` wählt beim Start. Das Spiel merkt sich die Wahl in den Einstellungen, Abschnitt `display`, Schlüssel `style`. | Du vergleichst schnell und spielst im gewählten Stil weiter. | +| Esc | Esc pausiert im Spiel (`toggle_pause`) und führt in der Vitrine zurück (`back`). | Die Vitrine öffnet sich nur aus dem Menü, also nie während eines laufenden Spiels. | +| Vitrine | Eine eigene Ansicht aus dem Startmenü: 20 × 10 Kacheln, dreifach vergrößert, mit Beschriftungen. Die Pflanze wächst in 6 Sekunden, das Tier hüpft alle 1,2 Sekunden, drei Steine wandern die Ranke entlang, die Ruine erwacht und döst. | Die Prüffrage braucht Dinge, die es erst später gibt. | +| Spielerfarben | Jeder Stil hat acht Spielerfarben. Spieler n trägt Farbe n mod 8, ein Spieler, der noch nicht in der Welt ist, die erste. | Bis zu vier Spieler gleichzeitig unterscheiden sich sicher. | + +## Global Constraints + +- Arbeitsverzeichnis ist die Wurzel des Archivs: `F:/Development/_kiZeug/Grovewright`. Alle Befehle laufen dort in der Bash. +- Gearbeitet wird auf dem Branch `m1d-stilproben`, nicht auf `main`. +- Kern (`core/`) und Brücke (`bridge/`) bleiben unverändert. Dieser Plan ändert nur `game/`, `CLAUDE.md` und `docs/`. +- Spielregeln gehören in den Kern, nie in die Hülle. Die Dinge der Vitrine bewegen sich nach Formeln der Vitrine und ändern keine Welt. +- Auf dem Spielfeld zeichnet nur `Art`. Andere Skripte zeichnen dort keine eigenen Formen oder Farben; ausgenommen sind Texte wie die Namen über den Figuren. +- GDScript mit Typangaben (`var x := …`, Rückgabetypen an jeder Funktion). +- Texte, die Spieler sehen, kommen über `tr()` aus `game/i18n/texts.csv`, auf Deutsch und Englisch. Eingabeaktionen legt `game/scripts/input_setup.gd` an, nicht `project.godot`. +- Tests, die Dateien schreiben, nutzen `TestFiles.temporary_dir()`, nie die echten Dateien des Spielers. +- Code, Bezeichner und Kommentare im Code sind Englisch. Dokumente und Commit-Nachrichten sind Deutsch. +- Jede Commit-Nachricht endet mit einer Zeile `Co-Authored-By: Claude `, die das Claude-Modell nennt, das den Commit erstellt hat. Die Commit-Nachrichten in diesem Plan zeigen `Claude Opus 5.5`; wer auf einem anderen Modell läuft, setzt dessen Namen ein. +- Nach jedem Task wird committet und hochgeladen. Das Hochladen ist vom Nutzer freigegeben. +- Vor jedem Commit laufen fehlerfrei: `cargo fmt --all -- --check`, `cargo clippy --workspace --all-targets -- -D warnings`, `cargo test --workspace`, `cargo build -p grovewright-bridge`, `godot --headless --path game --import` und `godot --headless --path game -s res://tests/run_tests.gd`. +- Die GDScript-Tests geben drei bekannte Warnzeilen aus: zwei mit „save not usable“ und eine mit „settings could not be read“. Sie stammen aus Tests, die absichtlich unbrauchbare oder gesperrte Dateien lesen. Andere Warnungen oder Fehler gehören nicht in die Ausgabe eines grünen Laufs. +- Godot erzeugt beim Import zu jedem Skript eine `.uid`-Datei. Sie gehört in den Commit. Der Ordner `game/.godot/` gehört nicht hinein. +- Werkzeugstand: Rust 1.98.1, Godot 4.7.2 (Standard-Ausgabe, im Pfad als `godot`), godot-rust 0.5.5, Git 2.54.0. + +## Hinweis zur Vorabprüfung + +Der gesamte Code dieses Plans wurde am 2. Oktober 2026 vorab gebaut. Alle drei Stile wurden auf der Karte und in der Vitrine angesehen und nachgebessert. Danach wurde jede der acht Task-Grenzen einzeln in einer Kopie von `main` nachgestellt, jeweils mit dem roten und dem grünen Lauf, die der Plan beschreibt: + +- GDScript-Tests: 96 in `main`, nach den Tasks 1 bis 8: 113, 121, 128, 132, 133, 134, 135 und 135. Jeder grüne Lauf gab genau die drei bekannten Warnzeilen aus. +- Rust: 151 Tests wie in `main`; rustfmt und Clippy meldeten nichts. +- Das Bildschirmfoto-Werkzeug lieferte nach den Tasks 2 und 4 bis 8 die Bilder, die der Plan beschreibt, ohne Warnung. +- Die Anweisungen dieses Plans, der Reihe nach auf `main` angewandt, ergeben nach jedem Task genau die geprüften Dateien. + +Weicht ein Schritt ab, ist zuerst ein Übertragungsfehler zu vermuten. + +## Dateiübersicht + +| Datei | Aufgabe | +|---|---| +| `game/scripts/art/art.gd` | Kennungen, Wahl des Stils, Bilder statt Zeichnungen | +| `game/scripts/art/art_style.gd` | Grundklasse der Stile: Platzhalter und Helfer für Formen | +| `game/scripts/art/tile_shape.gd` | Nachbarmasken und eine feste Zahl je Kachel für kleine Abwandlungen | +| `game/scripts/art/papercut_style.gd`, `outline_style.gd`, `geometric_style.gd` | Die drei Stile | +| `game/scripts/map_view.gd` | Karte in Blöcken, Risse und Splitter | +| `game/scripts/avatar.gd`, `figures.gd`, `tile_marker.gd` | Eigene und fremde Figuren, Zielmarke | +| `game/scripts/showcase.gd` | Vitrine der Stilproben | +| `game/scripts/menu.gd`, `input_setup.gd` | Dazu: Knopf zur Vitrine; F2, Esc und die Tasten 1 bis 3 | +| `game/scripts/launch_options.gd`, `save_store.gd` | Dazu: `--style=NAME` und der gemerkte Stil | +| `game/scripts/main.gd` | Verbindet alles und zeigt den eigenen Schlag sofort | +| `game/i18n/texts.csv` | Namen der Stile, Texte der Vitrine, Hilfezeile | +| `game/tools/screenshot.gd` | Bildschirmfoto in einem Stil, auch von der Vitrine | +| `game/tests/test_*.gd` | Tests der Hülle | +| `CLAUDE.md`, `docs/playtests/m1d.md`, `docs/playtests/m1d/*.png` | Regeln für die Grafik, Fragebogen mit Bildern der drei Stile | + +--- + +### Task 1: Kennungen, Kachelformen und Platzhalter + +**Files:** +- Create: `game/scripts/art/tile_shape.gd`, `game/scripts/art/art_style.gd`, `game/scripts/art/art.gd` +- Create: `game/scripts/art/papercut_style.gd`, `game/scripts/art/outline_style.gd`, `game/scripts/art/geometric_style.gd` (vorläufig; die Tasks 5 bis 7 ersetzen sie) +- Modify: `game/i18n/texts.csv` (ganze Datei ersetzen) +- Test: `game/tests/test_tile_shape.gd`, `game/tests/test_art_style.gd`, `game/tests/test_art.gd` + +**Interfaces:** +- Consumes: `TestCase` mit `check(condition, message)`, `check_eq(actual, expected, message)` und `tree`; `TestFiles.temporary_dir(name)` und `TestFiles.remove_dir(path)`; `I18n.load_translations(I18n.TEXTS_FILE)` +- Produces: + - `TileShape`: die Bits `N`, `NE`, `E`, `SE`, `S`, `SW`, `W`, `NW` (1 bis 128, im Uhrzeigersinn ab Norden), `OFFSETS: Array[Vector2i]`, `static flags(codes: PackedByteArray, members: Array[int]) -> PackedByteArray`, `static mask(tile_flags: PackedByteArray, width: int, height: int, tile: Vector2i, outside_set: bool) -> int`, `static variant(tile: Vector2i) -> int` (0 bis 2147483647, für jede Kachel immer gleich) + - `ArtStyle`: `key() -> StringName`, `title_key() -> String`, `draw(canvas: CanvasItem, id: StringName, rect: Rect2, state: Dictionary) -> void`, `player_color(player: int) -> Color` und je Kennung eine Zeichenmethode `(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void`: `_ground`, `_soft_rock`, `_hard_rock`, `_bedrock`, `_cracks`, `_hit`, `_figure`, `_marker`, `_dead_soil`, `_living_soil`, `_moss`, `_vine`, `_fiber_plant`, `_stone`, `_grazer`, `_ruin`. Dazu die statischen Helfer `share(variant, salt) -> float`, `rounded_rect(rect, mask, radius, segments := 4) -> PackedVector2Array`, `inset_open_sides(rect, mask, inset) -> Rect2`, `sway(time, variant, speed := 1.3) -> float`, `regular_polygon(center, radius, corners, rotation := 0.0) -> PackedVector2Array`, `closed(points) -> PackedVector2Array`, `along(points, part) -> Vector2`, `ellipse(center, radii, segments := 14) -> PackedVector2Array`, `slit(from, to, width) -> PackedVector2Array` und `leaf(base, angle, length, width) -> PackedVector2Array` + - `Art`: Signal `changed`; die Kennungen `GROUND`, `SOFT_ROCK`, `HARD_ROCK`, `BEDROCK`, `CRACKS`, `HIT`, `FIGURE`, `MARKER`, `DEAD_SOIL`, `LIVING_SOIL`, `MOSS`, `VINE`, `FIBER_PLANT`, `STONE`, `GRAZER`, `RUIN` und `IDS: Array[StringName]`; `OVERRIDE_DIR := "res://art"`; `styles: Array[ArtStyle]` und `style: ArtStyle`; `static style_keys() -> Array[StringName]`; `select(key: StringName) -> bool`; `select_next() -> void`; `load_overrides(directory: String) -> void`; `has_override(id: StringName) -> bool`; `draw(canvas: CanvasItem, id: StringName, rect: Rect2, state: Dictionary = {}) -> void` + - `PapercutStyle`, `OutlineStyle` und `GeometricStyle` mit `KEY` (`&"papercut"`, `&"outline"`, `&"geometric"`) und den Texten `STYLE_PAPERCUT`, `STYLE_OUTLINE`, `STYLE_GEOMETRIC`, vorerst ohne eigene Zeichnungen + +- [ ] **Step 1: Auf den Branch wechseln** + +Der Branch existiert bereits. Er wurde zusammen mit diesem Plan angelegt und hochgeladen. + +```bash +git switch m1d-stilproben +``` + +Expected: `Already on 'm1d-stilproben'` oder `Switched to branch 'm1d-stilproben'` + +- [ ] **Step 2: Fehlschlagende Tests schreiben** + +Create `game/tests/test_tile_shape.gd`: + +```gdscript +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") +``` + +Create `game/tests/test_art_style.gd`: + +```gdscript +extends TestCase +## The helpers that every style draws with. + +const TILE := Rect2(0, 0, 16, 16) + + +func test_a_lone_tile_is_round_at_every_corner() -> void: + var points := ArtStyle.rounded_rect(TILE, 0, 4.0, 4) + check_eq(points.size(), 20, "five points for each of the four corners") + for point in points: + check(TILE.grow(0.001).has_point(point), "inside the tile: %s" % point) + check(not points.has(Vector2.ZERO), "the corner itself is cut off") + + +func test_a_tile_inside_a_mass_keeps_its_square_corners() -> void: + var all := 255 + check_eq(ArtStyle.rounded_rect(TILE, all, 4.0), PackedVector2Array([Vector2(0, 0), Vector2(16, 0), Vector2(16, 16), Vector2(0, 16)]), "four corners") + var top_left_open := ArtStyle.rounded_rect(TILE, TileShape.E | TileShape.S, 4.0, 4) + check_eq(top_left_open.size(), 5 + 1 + 1 + 1, "only the corner between the open sides is round") + + +func test_inner_layers_shrink_only_toward_open_sides() -> void: + var inner := ArtStyle.inset_open_sides(TILE, TileShape.N | TileShape.W, 2.0) + check_eq(inner, Rect2(0, 0, 14, 14), "kept toward the neighbours above and to the left") + check_eq(ArtStyle.inset_open_sides(TILE, 0, 2.0), Rect2(2, 2, 12, 12), "smaller on every open side") + + +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_shapes_have_their_corners() -> void: + check_eq(ArtStyle.regular_polygon(Vector2.ZERO, 2.0, 6).size(), 6, "a hexagon") + check_eq(ArtStyle.ellipse(Vector2.ZERO, Vector2(3, 1), 12).size(), 12, "an ellipse") + check_eq(ArtStyle.slit(Vector2.ZERO, Vector2(10, 0), 2.0), PackedVector2Array([Vector2(0, 0), Vector2(5, -1), Vector2(10, 0), Vector2(5, 1)]), "a cut") + check_eq(ArtStyle.leaf(Vector2.ZERO, 0.0, 10.0, 4.0)[3], Vector2(10, 0), "a leaf ends at its tip") + check_eq(ArtStyle.closed(PackedVector2Array([Vector2.ZERO, Vector2.ONE])), PackedVector2Array([Vector2.ZERO, Vector2.ONE, Vector2.ZERO]), "closed") +``` + +Create `game/tests/test_art.gd`: + +```gdscript +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, [PapercutStyle.KEY, OutlineStyle.KEY, GeometricStyle.KEY], "papercut, outline, geometric") + 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(OutlineStyle.KEY), "the outline style exists") + check_eq(art.style.key(), OutlineStyle.KEY, "and is chosen") + check(art.select(OutlineStyle.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(), OutlineStyle.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, [PapercutStyle.KEY, OutlineStyle.KEY, GeometricStyle.KEY, PapercutStyle.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) +``` + +- [ ] **Step 3: Tests ausführen und Fehlschlag sehen** + +Run: + +```bash +godot --headless --path game --import +godot --headless --path game -s res://tests/run_tests.gd +``` + +Expected: Der Testlauf endet mit Exit-Code 1 und `96 passed, 3 failed`. `test_art.gd`, `test_art_style.gd` und `test_tile_shape.gd` laden nicht; davor stehen Zeilen wie `SCRIPT ERROR: Parse Error: Identifier "TileShape" not declared in the current scope.` + +- [ ] **Step 4: Kachelformen, Stile und Kennungen schreiben** + +Lege den Ordner `game/scripts/art/` an. + +Create `game/scripts/art/tile_shape.gd`: + +```gdscript +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) +``` + +Create `game/scripts/art/art_style.gd`: + +```gdscript +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 + + +## The outline of `rect` as points clockwise, with each corner rounded by +## `radius` where neither neighbour beside the corner is set in `mask`, so +## that a mass of tiles is round only on its outside. +static func rounded_rect(rect: Rect2, mask: int, radius: float, segments := 4) -> PackedVector2Array: + var points := PackedVector2Array() + var corners := [ + [rect.position, TileShape.W | TileShape.N, PI], + [Vector2(rect.end.x, rect.position.y), TileShape.N | TileShape.E, 1.5 * PI], + [rect.end, TileShape.E | TileShape.S, 0.0], + [Vector2(rect.position.x, rect.end.y), TileShape.S | TileShape.W, 0.5 * PI], + ] + for corner: Array in corners: + var at: Vector2 = corner[0] + var sides: int = corner[1] + if mask & sides != 0 or radius <= 0.0: + points.append(at) + continue + var inward := Vector2(radius if at.x == rect.position.x else -radius, radius if at.y == rect.position.y else -radius) + var center := at + inward + var start: float = corner[2] + for i in segments + 1: + var angle := start + 0.5 * PI * i / segments + points.append(center + Vector2(cos(angle), sin(angle)) * radius) + return points + + +## `rect` made smaller by `inset` on each side that does not touch a +## neighbour set in `mask`, so that the inner layers of a mass of tiles join. +static func inset_open_sides(rect: Rect2, mask: int, inset: float) -> Rect2: + var left := 0.0 if mask & TileShape.W else inset + var top := 0.0 if mask & TileShape.N else inset + var right := 0.0 if mask & TileShape.E else inset + var bottom := 0.0 if mask & TileShape.S else inset + return Rect2(rect.position + Vector2(left, top), rect.size - Vector2(left + right, top + bottom)) + + +## 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 points of a regular polygon with `corners` corners around `center`. +static func regular_polygon(center: Vector2, radius: float, corners: int, rotation := 0.0) -> PackedVector2Array: + var points := PackedVector2Array() + for i in corners: + var angle := rotation + TAU * i / corners + points.append(center + Vector2(cos(angle), sin(angle)) * radius) + return points + + +## A closed copy of `points`, for drawing the outline of a polygon. +static func closed(points: PackedVector2Array) -> PackedVector2Array: + var result := points.duplicate() + if not points.is_empty(): + result.append(points[0]) + return result + + +## 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] + + +## The points of an ellipse with the half axes `radii` around `center`. +static func ellipse(center: Vector2, radii: Vector2, segments := 14) -> PackedVector2Array: + var points := PackedVector2Array() + for i in segments: + var angle := TAU * i / segments + points.append(center + Vector2(cos(angle) * radii.x, sin(angle) * radii.y)) + return points + + +## A narrow cut from `from` to `to`, `width` wide in the middle and pointed +## at both ends. +static func slit(from: Vector2, to: Vector2, width: float) -> PackedVector2Array: + var middle := (from + to) / 2.0 + var side := (to - from).orthogonal().normalized() * width / 2.0 + return PackedVector2Array([from, middle + side, to, middle - side]) + + +## A leaf `length` long and `width` wide that grows from `base` in the +## direction `angle`. +static func leaf(base: Vector2, angle: float, length: float, width: float) -> PackedVector2Array: + var along := Vector2.from_angle(angle) + var side := along.orthogonal() * width / 2.0 + return PackedVector2Array([ + base, + base + along * length * 0.35 + side, + base + along * length * 0.7 + side * 0.7, + base + along * length, + base + along * length * 0.7 - side * 0.7, + base + along * length * 0.35 - side, + ]) +``` + +Create `game/scripts/art/art.gd`: + +```gdscript +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] = [PapercutStyle.new(), OutlineStyle.new(), GeometricStyle.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 [PapercutStyle.KEY, OutlineStyle.KEY, GeometricStyle.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 +``` + +Die drei Stile sind vorerst dünn: Sie nennen ihren Schlüssel und ihren Namen und zeichnen die Platzhalter von `ArtStyle`. Die Tasks 5 bis 7 ersetzen sie durch die fertigen Stile. + +Create `game/scripts/art/papercut_style.gd`: + +```gdscript +class_name PapercutStyle +extends ArtStyle +## Layers of cut paper without outlines, with soft shadows. +## Until it has drawings of its own, it draws the placeholders of ArtStyle. + +const KEY := &"papercut" + + +func key() -> StringName: + return KEY + + +func title_key() -> String: + return "STYLE_PAPERCUT" +``` + +Create `game/scripts/art/outline_style.gd`: + +```gdscript +class_name OutlineStyle +extends ArtStyle +## Bold dark outlines around flat colours, easy to read like a comic. +## Until it has drawings of its own, it draws the placeholders of ArtStyle. + +const KEY := &"outline" + + +func key() -> StringName: + return KEY + + +func title_key() -> String: + return "STYLE_OUTLINE" +``` + +Create `game/scripts/art/geometric_style.gd`: + +```gdscript +class_name GeometricStyle +extends ArtStyle +## Plain geometric forms in calm pastel colours. +## Until it has drawings of its own, it draws the placeholders of ArtStyle. + +const KEY := &"geometric" + + +func key() -> StringName: + return KEY + + +func title_key() -> String: + return "STYLE_GEOMETRIC" +``` + +Ersetze den gesamten Inhalt von `game/i18n/texts.csv` durch die folgende Fassung. Neu sind die letzten drei Zeilen mit den Namen der Stile. + +```text +keys,de,en +STONE_COUNT,Stein: %d,Stone: %d +SAVED,Gespeichert,Saved +SAVE_FAILED,Speichern fehlgeschlagen,Saving failed +LOADED,Spielstand geladen,Save loaded +LOAD_FAILED,Der Spielstand ließ sich nicht laden,The save could not be loaded +INVENTORY_FULL,Die Tasche ist voll,Your bag is full +PAUSED,Pause,Paused +SIMULATION_STOPPED,Die Simulation wurde angehalten,The simulation stopped +HELP,WASD: laufen · Linke Maustaste: Stein schlagen · F5: speichern · F9: laden · Esc: Pause,WASD: walk · Left mouse button: break rock · F5: save · F9: load · Esc: pause +DEFAULT_NAME,Spieler,Player +MENU_NAME,Dein Name,Your name +MENU_SOLO,Allein spielen,Play alone +MENU_HOST,Welt öffnen,Open your world +MENU_ADDRESS,Adresse des Hosts,Address of the host +MENU_JOIN,Beitreten,Join +CONNECTING,Verbinde mit %s …,Connecting to %s … +CONNECT_FAILED,Keine Verbindung zu %s,Could not reach %s +ADDRESS_INVALID,Das ist keine Adresse: %s,That is not an address: %s +HOST_LEFT,Der Host hat die Welt geschlossen,The host closed the world +PROTOCOL_ERROR,Die Verbindung zum Host ist gestört,The connection to the host is broken +REFUSED_OTHER_PROTOCOL,Der Host hat eine andere Version des Spiels,The host has another version of the game +REFUSED_OTHER_RULES,Der Host hat andere Spielregeln. Ihr braucht denselben Stand des Spiels.,The host has other rules. You need the same version of the game. +REFUSED_WORLD_FULL,Die Welt ist voll: Vier Spieler sind schon da,The world is full: four players are in it +REFUSED_TOO_MANY_PLAYERS,Diese Welt kennt schon 16 Spieler und nimmt keine neuen mehr auf,This world knows 16 players already and takes no new ones +REFUSED_ALREADY_PLAYING,Ein Spieler mit deinem Schlüssel ist schon in der Welt,A player with your key is in the world already +STATUS_HOST,Welt offen auf Port %d · Spieler: %d,World open on port %d · players: %d +STATUS_GUEST,Gast bei %s · Spieler: %d,Guest of %s · players: %d +OPEN_FAILED,Port %d lässt sich nicht öffnen,Port %d cannot be opened +PLAYER_JOINED,%s ist da,%s joined +PLAYER_LEFT,%s ist gegangen,%s left +RESYNCED,Welt mit dem Host abgeglichen,World matched with the host again +PAUSE_ONLY_ALONE,Pause gibt es nur ohne Gäste,Pausing only works without guests +LOAD_ONLY_ALONE,Laden gibt es nur beim Spielen allein,Loading only works when playing alone +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_PAPERCUT,Scherenschnitt,Papercut +STYLE_OUTLINE,Kontur,Outline +STYLE_GEOMETRIC,Geometrisch,Geometric +``` + +- [ ] **Step 5: Tests ausführen und Erfolg sehen** + +Run: + +```bash +godot --headless --path game --import +godot --headless --path game -s res://tests/run_tests.gd +``` + +Expected: Exit-Code 0 und `113 passed, 0 failed`. Die einzigen Warnungen sind die drei bekannten Zeilen. + +- [ ] **Step 6: Alles prüfen** + +Run: + +```bash +cargo fmt --all -- --check && cargo clippy --workspace --all-targets -- -D warnings && cargo test --workspace +cargo build -p grovewright-bridge +``` + +Expected: keine Ausgabe von rustfmt, keine Warnung von Clippy, zusammen 151 bestandene Rust-Tests + +- [ ] **Step 7: Commit und Hochladen** + +```bash +git add game +git commit -F - <<'EOF' +Kennungen, Kachelformen und Platzhalter für die Grafik + +Jede Grafik auf dem Spielfeld bekommt eine Kennung. Art zeichnet sie im +gewählten Stil oder aus einem Bild gleichen Namens unter game/art. +ArtStyle zeichnet vorerst schlichte Platzhalter und bringt Helfer für +Formen mit. TileShape beschreibt die Nachbarn einer Kachel, damit +Felsmassen außen rund und innen geschlossen gezeichnet werden können. +Die drei Stile gibt es schon mit Namen, noch ohne eigene Zeichnungen. + +Co-Authored-By: Claude Opus 5.5 +EOF +git push -u origin m1d-stilproben +``` + +--- + +### Task 2: Spielfeld über Art + +**Files:** +- Modify: `game/scripts/map_view.gd`, `game/scripts/avatar.gd`, `game/scripts/figures.gd`, `game/scripts/tile_marker.gd`, `game/scripts/main.gd` (jeweils ganze Datei ersetzen) +- Test: `game/tests/test_map_view.gd` (neu); `game/tests/test_avatar.gd`, `game/tests/test_figures.gd`, `game/tests/test_main.gd` (je ein Test am Ende) + +**Interfaces:** +- Consumes: `Art` und `TileShape` aus Task 1. Aus `GwSession`: `is_running()`, `map_width()`, `map_height()`, `terrain_codes()`, `damage_codes()`, `terrain_at(x, y)`, `spawn_point()`, `local_player()`, `player_name(player)`, `dig(x, y)`, `step()` und die Konstanten `TERRAIN_GROUND`, `TERRAIN_SOFT_ROCK`, `TERRAIN_HARD_ROCK`, `TERRAIN_BEDROCK`, `DIG_COOLDOWN_TICKS`. Aus `GridMotion`: `TILE_SIZE`, `move`, `tile_center`. +- Produces: + - `MapView`: `art: Art`; `SOLID: Array[int]` (weicher Fels, harter Fels, Grundgestein), `BLOCK := 16`, `HIT_SECONDS := 0.35`; `static id_of(terrain: int) -> StringName`; `reload()` nimmt die ganze Karte neu aus der Sitzung, `redraw()` zeichnet alles neu, `refresh_tile(tile: Vector2i)` nimmt eine Kachel neu und zeichnet die Blöcke um sie, `hit(tile: Vector2i)` zeigt Splitter; für Tests `block_count() -> int`, `burst_count() -> int` und `mask_at(tile: Vector2i) -> int`. Wer die Karte neu zeigen will, ruft `reload()`; `queue_redraw()` auf `MapView` selbst zeichnet die Karte nicht mehr. + - `Avatar`: `art: Art`, `player := -1`, `facing := 1.0`, `step := 0.0`, `moving := false`, `STEP_PER_PIXEL := 0.16`, `static figure_rect(at: Vector2) -> Rect2` + - `Figures`: `art: Art`, `facing_of(player: int) -> float` + - `TileMarker`: `art: Art` + - Hauptszene: `art: Art`. `dig_at(tile)` zeigt die Splitter sofort, wenn die Kachel kein Boden ist. + +- [ ] **Step 1: Fehlschlagende Tests schreiben** + +Create `game/tests/test_map_view.gd`: + +```gdscript +extends TestCase +## The map view over a real world of the core. + +var key := PackedByteArray([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]) + + +func _session() -> GwSession: + var session := GwSession.new() + session.start_new(7) + session.join(key, "Ada") + session.step() + return session + + +func _view(session: GwSession) -> MapView: + var view := MapView.new() + view.art = Art.new() + view.session = session + tree.root.add_child(view) + view.reload() + return view + + +func _free(view: MapView) -> void: + tree.root.remove_child(view) + view.free() + + +## The first tile of the soft rock beside the spawn clearing. +func _rock(session: GwSession) -> Vector2i: + return session.spawn_point() + Vector2i(7, -2) + + +func test_the_map_is_drawn_in_blocks_that_cover_it() -> void: + var session := _session() + var view := _view(session) + var across := ceili(session.map_width() / float(MapView.BLOCK)) + var down := ceili(session.map_height() / float(MapView.BLOCK)) + check_eq(view.block_count(), across * down, "blocks over the whole map") + for style in Art.style_keys(): + view.art.select(style) + view.redraw() + await tree.process_frame + _free(view) + + +func test_each_tile_is_drawn_to_fit_the_rock_around_it() -> void: + var session := _session() + var view := _view(session) + var solid := TileShape.flags(session.terrain_codes(), MapView.SOLID) + for tile: Vector2i in [Vector2i(0, 0), session.spawn_point(), _rock(session)]: + var expected := TileShape.mask(solid, session.map_width(), session.map_height(), tile, true) + check_eq(view.mask_at(tile), expected, "the neighbours of %s" % tile) + _free(view) + + +func test_a_broken_rock_no_longer_shapes_its_neighbours() -> void: + var session := _session() + var view := _view(session) + var rock := _rock(session) + var west := rock + Vector2i(-1, 0) + check(view.mask_at(west) & TileShape.E != 0, "the rock lies east of its neighbour") + for hit in 3: + session.dig(rock.x, rock.y) + session.step() + for wait in GwSession.DIG_COOLDOWN_TICKS: + session.step() + check_eq(session.terrain_at(rock.x, rock.y), GwSession.TERRAIN_GROUND, "three hits break it") + view.refresh_tile(rock) + check(view.mask_at(west) & TileShape.E == 0, "now there is ground east of the neighbour") + await tree.process_frame + _free(view) + + +func test_a_burst_of_chips_shows_for_a_moment() -> void: + var view := _view(_session()) + view.hit(Vector2i(3, 3)) + check_eq(view.burst_count(), 1, "a burst") + view._process(MapView.HIT_SECONDS * 0.5) + check_eq(view.burst_count(), 1, "still in the air") + await tree.process_frame + view._process(MapView.HIT_SECONDS * 0.6) + check_eq(view.burst_count(), 0, "gone after its time") + _free(view) + + +func test_without_a_world_nothing_is_drawn() -> void: + var view := _view(_session()) + view.session = null + view.reload() + check_eq(view.block_count(), 0, "no blocks") + view.refresh_tile(Vector2i(1, 1)) + await tree.process_frame + _free(view) +``` + +Füge am Ende von `game/tests/test_avatar.gd` diesen Test an. Wie zwischen allen Funktionen stehen davor zwei Leerzeilen: + +```gdscript +func test_a_walking_figure_steps_and_faces_the_way_it_goes() -> void: + var avatar := Avatar.new() + avatar.is_walkable = _open_field + avatar.position = GridMotion.tile_center(Vector2i(50, 50)) + check(not avatar.moving, "standing at first") + avatar.walk(Vector2.LEFT, 0.1) + check(avatar.moving, "walking") + check_eq(avatar.facing, -1.0, "facing left") + check(avatar.step > 0.0, "the step turns") + avatar.walk(Vector2.UP, 0.1) + check_eq(avatar.facing, -1.0, "walking up keeps the way it faces") + avatar.walk(Vector2.ZERO, 0.1) + check(not avatar.moving, "standing again") + avatar.free() +``` + +Füge am Ende von `game/tests/test_figures.gd` diesen Test an, wieder nach zwei Leerzeilen: + +```gdscript +func test_a_figure_faces_the_way_it_glides() -> void: + var figures := Figures.new() + figures.place(1, Vector2(100.0, 0.0)) + check_eq(figures.facing_of(1), 1.0, "facing right at first") + figures.place(1, Vector2.ZERO) + figures._process(0.02) + check_eq(figures.facing_of(1), -1.0, "facing left while it glides to the left") + figures.free() +``` + +Füge am Ende von `game/tests/test_main.gd` diesen Test an, nach zwei Leerzeilen: + +```gdscript +func test_an_own_hit_shows_its_burst_at_once() -> void: + var directory := TestFiles.temporary_dir("burst") + var main := _start_main(directory) + main.play(false) + var rock: Vector2i = main.session.spawn_point() + Vector2i(7, -2) + check_eq(main.session.terrain_at(rock.x, rock.y), GwSession.TERRAIN_SOFT_ROCK, "rock beside the clearing") + main.dig_at(rock) + check_eq(main.map_view.burst_count(), 1, "the burst shows before the hit lands in a tick") + _clean_up([directory]) +``` + +- [ ] **Step 2: Tests ausführen und Fehlschlag sehen** + +Run: + +```bash +godot --headless --path game --import +godot --headless --path game -s res://tests/run_tests.gd +``` + +Expected: Der Testlauf endet mit Exit-Code 1 und `113 passed, 4 failed`. `test_map_view.gd` lädt nicht (`SCRIPT ERROR: Parse Error: Cannot find member "BLOCK" in base "MapView".`). Die drei angehängten Tests scheitern mit `Invalid access to property or key 'moving' on a base object of type 'Node2D (Avatar)'.`, `Invalid call. Nonexistent function 'facing_of' in base 'Node2D (Figures)'.` und `Invalid call. Nonexistent function 'burst_count' in base 'Node2D (MapView)'.` Am Ende meldet Godot liegen gebliebene Objekte (`… were leaked at exit`), weil die abgebrochenen Tests nicht aufräumen. + +- [ ] **Step 3: Karte, Figuren und Zielmarke über Art zeichnen** + +Ersetze den gesamten Inhalt von `game/scripts/map_view.gd` durch: + +```gdscript +class_name MapView +extends Node2D +## Draws the terrain of the shared world in the style of `art`: every tile to +## fit its neighbours, the cracks of the hits it took, and a short burst of +## chips where a pickaxe lands. The map is drawn in square blocks, so that a +## hit draws only the blocks around its tile again. + +## The terrain that rock masses and the edge of the map are made of. Tiles +## beside it are drawn to fit it. +const SOLID: Array[int] = [GwSession.TERRAIN_SOFT_ROCK, GwSession.TERRAIN_HARD_ROCK, GwSession.TERRAIN_BEDROCK] +## Tiles along each side of a block. +const BLOCK := 16 +## Seconds a burst of chips lasts. +const HIT_SECONDS := 0.35 + +var session: GwSession +var art: Art +var _width := 0 +var _height := 0 +var _terrain := PackedByteArray() +var _damage := PackedByteArray() +var _solid := PackedByteArray() +var _masks := PackedInt32Array() +var _variants := PackedInt32Array() +var _blocks: Array[MapBlock] = [] +var _bursts: Array[Dictionary] = [] +var _burst_layer := Node2D.new() + + +## A square part of the map that draws itself. +class MapBlock extends Node2D: + var view: MapView + var tiles: Rect2i + + func _init(owner_view: MapView, block_tiles: Rect2i) -> void: + view = owner_view + tiles = block_tiles + + func _draw() -> void: + view.draw_block(self, tiles) + + +func _ready() -> void: + _burst_layer.draw.connect(_draw_bursts) + add_child(_burst_layer) + set_process(false) + + +## The graphic of a terrain code. +static func id_of(terrain: int) -> StringName: + match terrain: + GwSession.TERRAIN_SOFT_ROCK: + return Art.SOFT_ROCK + GwSession.TERRAIN_HARD_ROCK: + return Art.HARD_ROCK + GwSession.TERRAIN_BEDROCK: + return Art.BEDROCK + return Art.GROUND + + +## Takes the whole map from `session` anew and draws it, or shows nothing if +## no world runs. Call it when a world starts or was replaced. +func reload() -> void: + for block in _blocks: + remove_child(block) + block.free() + _blocks.clear() + _bursts.clear() + _burst_layer.queue_redraw() + if session == null or not session.is_running(): + _width = 0 + _height = 0 + return + _width = session.map_width() + _height = session.map_height() + _terrain = session.terrain_codes() + _damage = session.damage_codes() + _solid = TileShape.flags(_terrain, SOLID) + _masks.resize(_width * _height) + _variants.resize(_width * _height) + for y in _height: + for x in _width: + var tile := Vector2i(x, y) + _masks[y * _width + x] = TileShape.mask(_solid, _width, _height, tile, true) + _variants[y * _width + x] = TileShape.variant(tile) + for top in range(0, _height, BLOCK): + for left in range(0, _width, BLOCK): + var block := MapBlock.new(self, Rect2i(left, top, mini(BLOCK, _width - left), mini(BLOCK, _height - top))) + _blocks.append(block) + add_child(block) + move_child(_burst_layer, -1) + + +## Draws everything again, for example in another style. +func redraw() -> void: + for block in _blocks: + block.queue_redraw() + + +## Takes `tile` anew from the session after it changed, and draws it and the +## tiles around it again, which may have to fit it differently now. +func refresh_tile(tile: Vector2i) -> void: + if _blocks.is_empty() or not Rect2i(0, 0, _width, _height).has_point(tile): + return + _terrain = session.terrain_codes() + _damage = session.damage_codes() + var index := tile.y * _width + tile.x + _solid[index] = 1 if SOLID.has(_terrain[index]) else 0 + var touched: Array[MapBlock] = [] + for y in range(tile.y - 1, tile.y + 2): + for x in range(tile.x - 1, tile.x + 2): + var around := Vector2i(x, y) + if not Rect2i(0, 0, _width, _height).has_point(around): + continue + _masks[y * _width + x] = TileShape.mask(_solid, _width, _height, around, true) + var block := _block_of(around) + if not touched.has(block): + touched.append(block) + for block in touched: + block.queue_redraw() + + +## The block that draws `tile`. +func _block_of(tile: Vector2i) -> MapBlock: + var across := ceili(_width / float(BLOCK)) + @warning_ignore("integer_division") + return _blocks[(tile.y / BLOCK) * across + tile.x / BLOCK] + + +## Shows a burst of chips on `tile`, where a pickaxe just landed. +func hit(tile: Vector2i) -> void: + _bursts.append({"tile": tile, "age": 0.0}) + set_process(true) + + +## The number of blocks the map is drawn in. Tests read it. +func block_count() -> int: + return _blocks.size() + + +## The number of bursts still shown. Tests read it. +func burst_count() -> int: + return _bursts.size() + + +## The neighbour mask a tile is drawn with. Tests read it. +func mask_at(tile: Vector2i) -> int: + return _masks[tile.y * _width + tile.x] + + +## Draws the tiles `tiles` onto `block`, while the block draws. +func draw_block(block: MapBlock, tiles: Rect2i) -> void: + if art == null: + return + var size := GridMotion.TILE_SIZE + for y in range(tiles.position.y, tiles.end.y): + for x in range(tiles.position.x, tiles.end.x): + var index := y * _width + x + var rect := Rect2(x * size, y * size, size, size) + var variant := _variants[index] + art.draw(block, id_of(_terrain[index]), rect, {"solid": _masks[index], "variant": variant}) + if _damage[index] > 0: + art.draw(block, Art.CRACKS, rect, {"hits": _damage[index], "variant": variant}) + + +func _process(delta: float) -> void: + for burst in _bursts: + burst["age"] += delta / HIT_SECONDS + _bursts = _bursts.filter(func(burst: Dictionary) -> bool: return burst["age"] < 1.0) + if _bursts.is_empty(): + set_process(false) + _burst_layer.queue_redraw() + + +func _draw_bursts() -> void: + if art == null: + return + var size := GridMotion.TILE_SIZE + for burst in _bursts: + var tile: Vector2i = burst["tile"] + var rect := Rect2(Vector2(tile) * size, Vector2(size, size)) + art.draw(_burst_layer, Art.HIT, rect, {"age": burst["age"], "variant": TileShape.variant(tile)}) +``` + +Ersetze den gesamten Inhalt von `game/scripts/avatar.gd` durch: + +```gdscript +class_name Avatar +extends Node2D +## The local player's figure. It moves outside the lockstep simulation; only +## its pickaxe hits become commands. `art` draws it, stepping while it walks +## and facing the way it went last. + +## Walking speed: five tiles per second. +const SPEED := 5.0 * GridMotion.TILE_SIZE +const RADIUS := 5.0 +## Longest frame that moves the figure in one go. Longer frames are cut, so +## that the figure cannot pass through a wall after a stall. +const MAX_STEP_SECONDS := 0.1 +## How far the step of a walking figure turns per pixel, in radians: about +## two steps per second at walking speed. +const STEP_PER_PIXEL := 0.16 + +## Callable(x: int, y: int) -> bool that says whether a tile can be walked on. +var is_walkable: Callable +var art: Art +## The number of the player the figure belongs to, for its colour, or -1 +## before the player is in the world. +var player := -1 +## 1.0 while the figure faces right, -1.0 while it faces left. +var facing := 1.0 +## Grows while the figure walks; the styles swing the steps by it. +var step := 0.0 +## True while the figure walked in the last frame. +var moving := false + + +## The square a figure standing at `at` is drawn in. +static func figure_rect(at: Vector2) -> Rect2: + var size := GridMotion.TILE_SIZE + return Rect2(at - Vector2(size, size) / 2.0, Vector2(size, size)) + + +func _process(delta: float) -> void: + walk(Input.get_vector("move_left", "move_right", "move_up", "move_down"), delta) + + +## Moves the figure in `direction` for `delta` seconds, sliding along walls. +func walk(direction: Vector2, delta: float) -> void: + var was_moving := moving + moving = false + if direction == Vector2.ZERO or not is_walkable.is_valid(): + if was_moving: + queue_redraw() + return + var motion := direction * SPEED * minf(delta, MAX_STEP_SECONDS) + var before := position + position = GridMotion.move(position, motion, RADIUS, is_walkable) + var walked := position.distance_to(before) + moving = walked > 0.0 + step += walked * STEP_PER_PIXEL + if direction.x != 0.0: + facing = signf(direction.x) + queue_redraw() + + +func _draw() -> void: + if art != null: + art.draw(self, Art.FIGURE, figure_rect(Vector2.ZERO), {"player": player, "facing": facing, "step": step, "moving": moving}) +``` + +Ersetze den gesamten Inhalt von `game/scripts/figures.gd` durch: + +```gdscript +class_name Figures +extends Node2D +## The figures of the other players, with their names, drawn by `art`. A new +## position comes over the network about ten times a second; in between, +## each figure glides toward the latest one, stepping and facing the way it +## goes. + +## How quickly a figure closes the gap to its latest position, per second. +const GLIDE := 15.0 +## Pixels per frame below which a gliding figure counts as standing. +const STANDING := 0.05 +## Size and width of the names, in pixels of the playing field. The names +## are drawn at the size they have on screen, so that they stay sharp. +const NAME_SIZE := 8 +const NAME_WIDTH := 80.0 +const NAME_OUTLINE := Color(0.05, 0.05, 0.06) + +var session: GwSession +var art: Art +var _shown: Dictionary[int, Vector2] = {} +var _latest: Dictionary[int, Vector2] = {} +var _facing: Dictionary[int, float] = {} +var _step: Dictionary[int, float] = {} +var _moving: Dictionary[int, bool] = {} + + +## Moves the figure of `player` toward `position`. A figure that was not +## shown yet appears there at once. +func place(player: int, position: Vector2) -> void: + if not _shown.has(player): + _shown[player] = position + _latest[player] = position + queue_redraw() + + +## Removes the figure of `player`. +func forget(player: int) -> void: + for figures: Dictionary in [_shown, _latest, _facing, _step, _moving]: + figures.erase(player) + queue_redraw() + + +func clear() -> void: + for figures: Dictionary in [_shown, _latest, _facing, _step, _moving]: + figures.clear() + queue_redraw() + + +## Where the figure of `player` is shown, or Vector2.INF if it is not. +func shown_at(player: int) -> Vector2: + return _shown.get(player, Vector2.INF) + + +## 1.0 if the figure of `player` faces right, -1.0 if it faces left. +func facing_of(player: int) -> float: + return _facing.get(player, 1.0) + + +func _process(delta: float) -> void: + if _shown.is_empty(): + return + var weight := minf(1.0, GLIDE * delta) + for player in _shown: + var before := _shown[player] + _shown[player] = before.lerp(_latest[player], weight) + var moved := _shown[player] - before + if absf(moved.x) > STANDING: + _facing[player] = signf(moved.x) + _step[player] = _step.get(player, 0.0) + moved.length() * Avatar.STEP_PER_PIXEL + _moving[player] = moved.length() > STANDING + queue_redraw() + + +func _draw() -> void: + if art == null: + return + var font := ThemeDB.fallback_font + # The camera enlarges the field. Text drawn at its size on screen and then + # shrunk stays sharp; text drawn small and enlarged would blur. + var zoom := maxf(1.0, get_global_transform_with_canvas().get_scale().x) + for player in _shown: + var at := _shown[player] + var state := {"player": player, "facing": facing_of(player), "step": _step.get(player, 0.0), "moving": _moving.get(player, false)} + art.draw(self, Art.FIGURE, Avatar.figure_rect(at), state) + if session != null: + var label := Vector2(at.x - NAME_WIDTH / 2.0, at.y - GridMotion.TILE_SIZE / 2.0 - 2.0) + var text := session.player_name(player) + var size := roundi(NAME_SIZE * zoom) + draw_set_transform(label, 0.0, Vector2.ONE / zoom) + draw_string_outline(font, Vector2.ZERO, text, HORIZONTAL_ALIGNMENT_CENTER, NAME_WIDTH * zoom, size, maxi(2, roundi(zoom * 1.5)), NAME_OUTLINE) + draw_string(font, Vector2.ZERO, text, HORIZONTAL_ALIGNMENT_CENTER, NAME_WIDTH * zoom, size) + draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE) +``` + +Ersetze den gesamten Inhalt von `game/scripts/tile_marker.gd` durch: + +```gdscript +class_name TileMarker +extends Node2D +## Marks the tile under the mouse, drawn by `art`: one way when the figure +## can reach it, another when it is too far away. + +var tile := Vector2i(-1, -1) +var in_reach := false +var art: Art + + +## Moves the mark to `new_tile`. Redraws only when something changed. +func show_tile(new_tile: Vector2i, reachable: bool) -> void: + if new_tile == tile and reachable == in_reach: + return + tile = new_tile + in_reach = reachable + queue_redraw() + + +func _draw() -> void: + if art == null: + return + var size := GridMotion.TILE_SIZE + art.draw(self, Art.MARKER, Rect2(Vector2(tile) * size, Vector2(size, size)), {"reachable": in_reach}) +``` + +In der Hauptszene ändert sich gegenüber `main`: + +- Sie legt `art` an, lädt die Bilder aus `Art.OVERRIDE_DIR` und gibt `art` an Karte, Zielmarke, Figuren und eigene Figur. +- Wo sie die Karte bisher mit `map_view.queue_redraw()` neu zeichnete, ruft sie `map_view.reload()`. +- Die Ereignisse `tile_hit` und `tile_changed` nehmen nur ihre Kachel neu (`refresh_tile`) und zeigen Splitter, eigene Schläge aber nicht noch einmal. +- `dig_at` zeigt die Splitter des eigenen Schlags sofort. +- Die eigene Figur kennt die Nummer ihres Spielers, damit sie dessen Farbe trägt. + +Ersetze den gesamten Inhalt von `game/scripts/main.gd` durch: + +```gdscript +extends Node2D +## Composition root. Shows the start menu, then runs a session as host or as +## guest with the map view, the figures, the HUD and the files on disk. + +## Seed of a new world until the slice ships its chosen seed. +const NEW_WORLD_SEED := 2026 +## How far the pickaxe reaches, in tiles from the figure to the tile centre. +const REACH_TILES := 2.5 +## Ticks between two automatic saves: two minutes. +const AUTOSAVE_TICKS := 2 * 60 * GwSession.TICKS_PER_SECOND +const CAMERA_ZOOM := Vector2(3.0, 3.0) +## Most ticks a guest runs in one frame. A guest that fell behind catches up +## within a few frames without freezing. +const MAX_GUEST_TICKS_PER_FRAME := 20 +## Seconds between two position updates of the own figure. +const FIGURE_SECONDS := 0.1 +## The text for every reason a host gives for turning a guest away. +const REFUSAL_TEXTS := { + GwSession.REFUSED_OTHER_PROTOCOL: "REFUSED_OTHER_PROTOCOL", + GwSession.REFUSED_OTHER_RULES: "REFUSED_OTHER_RULES", + GwSession.REFUSED_WORLD_FULL: "REFUSED_WORLD_FULL", + GwSession.REFUSED_TOO_MANY_PLAYERS: "REFUSED_TOO_MANY_PLAYERS", + GwSession.REFUSED_ALREADY_PLAYING: "REFUSED_ALREADY_PLAYING", +} + +## Folder for the player key, the save and the recordings. If it is empty, +## the command line decides; tests set a folder of their own. +@export var storage_dir := "" +## Network interface an open world listens on. Tests use the loopback one. +var listen_address := "*" +## The command line arguments after "--". Tools and tests may set their own +## before the scene enters the tree. +var command_line := OS.get_cmdline_user_args() + +var options: LaunchOptions +var store: SaveStore +## The running session, or null in the menu. A guest's session has no world +## until the host sent it. +var session: GwSession +## The network connection of an open world or a guest, or null. +var link: SessionLink +var clock: TickClock +## The style the playing field is drawn in. +var art: Art +var menu: Menu +var map_view: MapView +var marker: TileMarker +var avatar: Avatar +var figures: Figures +var hud: Hud +## How often a guest's world diverged and was replaced by the host's. +var resyncs := 0 + +var _player_key: PackedByteArray +var _next_dig_tick := 0 +## True while an unusable save could not be set aside. Saving is refused +## then, so that the unusable save is never overwritten. +var _save_blocked := false +var _figure_wait := 0.0 +## The address a guest is joining, for messages. +var _joining := "" +## The host's accepted guests when _process last looked. A new one ends a +## pause. +var _known_guests := 0 + + +func _ready() -> void: + I18n.install() + InputSetup.install() + options = LaunchOptions.parse(command_line) + for argument in options.ignored: + push_warning("ignored command line argument: %s" % argument) + if storage_dir.is_empty(): + storage_dir = options.storage_dir() + store = SaveStore.new(storage_dir) + _player_key = store.player_key() + if _player_key.is_empty(): + push_warning("the player key cannot be read: %s" % error_string(store.last_read_error)) + clock = TickClock.new(GwSession.TICKS_PER_SECOND) + art = Art.new() + art.load_overrides(Art.OVERRIDE_DIR) + map_view = MapView.new() + map_view.art = art + add_child(map_view) + marker = TileMarker.new() + marker.art = art + add_child(marker) + figures = Figures.new() + figures.art = art + add_child(figures) + avatar = Avatar.new() + avatar.art = art + add_child(avatar) + var camera := Camera2D.new() + camera.zoom = CAMERA_ZOOM + avatar.add_child(camera) + hud = Hud.new() + add_child(hud) + menu = Menu.new() + add_child(menu) + menu.name_field.text = options.player_name if not options.player_name.is_empty() else store.player_name() + menu.address_field.text = options.address if not options.address.is_empty() else store.last_address() + menu.play_chosen.connect(play) + menu.join_chosen.connect(join) + _show_menu("") + match options.start: + "solo": + play(false) + "host": + play(true) + "join": + join(options.address) + + +## Plays the saved world, or a new one, as its host. With `open`, guests may +## join. Returns false if it could not start; the menu then says why. +func play(open: bool) -> bool: + if not _can_start(): + return false + var next := GwSession.new() + var set_aside := false + if store.has_save(): + var save := store.read_save() + if store.last_read_error != OK: + _show_menu(tr("SAVE_UNREADABLE")) + return false + set_aside = not _load(next, save) + if not next.is_running(): + next.start_new(NEW_WORLD_SEED) + if open: + var net := NetLink.new() + if net.host(options.port, listen_address) != OK: + _show_menu(tr("OPEN_FAILED") % options.port) + return false + link = SessionLink.new(next, net) + session = next + session.join(_player_key, _player_name()) + _run_tick() + _show_world() + if set_aside: + hud.flash("LOAD_FAILED") + return true + + +## Starts joining the host at `address`, "host" or "host:port". The world +## arrives later; until then the menu says that it is connecting. +func join(address: String) -> void: + if not _can_start(): + return + var target := NetLink.split_address(address) + if target.is_empty(): + _show_menu(tr("ADDRESS_INVALID") % address) + return + var net := NetLink.new() + if net.join(target[0], target[1]) != OK: + _show_menu(tr("CONNECT_FAILED") % address) + return + session = GwSession.new() + session.start_guest(_player_key, _player_name()) + link = SessionLink.new(session, net) + _joining = address + store.set_last_address(address) + _show_menu(tr("CONNECTING") % address) + + +func _can_start() -> bool: + if session != null: + return false + if _player_key.is_empty(): + _show_menu(tr("KEY_UNREADABLE")) + return false + store.set_player_name(menu.player_name()) + return true + + +func _player_name() -> String: + var typed := menu.player_name() + return typed if not typed.is_empty() else tr("DEFAULT_NAME") + + +## Starts `next` from `save`. If the save is unusable, it is set aside so +## that the next save does not overwrite it, and false is returned. +func _load(next: GwSession, save: PackedByteArray) -> bool: + if next.start_loaded(save): + _save_blocked = false + return true + var aside := store.set_aside_save() + if aside.is_empty(): + _save_blocked = true + push_warning("save not usable (%s) and could not be set aside; saving is off" % next.last_error()) + else: + push_warning("save not usable (%s), kept as %s" % [next.last_error(), aside]) + return false + + +func _show_menu(status: String) -> void: + menu.show_status(status) + menu.show() + hud.hide() + for node: Node2D in [avatar, marker, figures]: + node.hide() + + +## Shows the world of the session and puts the figure on the spawn. +func _show_world() -> void: + menu.hide() + hud.show() + for node: Node2D in [avatar, marker, figures]: + node.show() + map_view.session = session + map_view.reload() + figures.session = session + avatar.player = session.local_player() + figures.clear() + avatar.is_walkable = session.is_walkable + avatar.position = GridMotion.tile_center(session.spawn_point()) + hud.show_stone(session.item_count(GwSession.ITEM_STONE)) + _update_status() + _next_dig_tick = 0 + + +## Says in the status line how the game is shared, and with how many. +func _update_status() -> void: + if link == null: + hud.show_status("") + elif session.is_host(): + hud.show_status(tr("STATUS_HOST") % [link.link.local_port(), session.online_players().size()]) + else: + hud.show_status(tr("STATUS_GUEST") % [_joining, session.online_players().size()]) + + +## Ends a guest's session and goes back to the menu with `message`. +func _leave(message: String) -> void: + if session == null: + return + _write_recording() + if link != null: + link.link.close() + link = null + session = null + map_view.session = null + map_view.reload() + avatar.is_walkable = Callable() + figures.clear() + _show_menu(message) + + +## A broken session runs no more ticks. Instead of leaving the others +## waiting, a host closes its world to guests and a guest leaves. The host's +## status line says why its world stands still. +func _stop_sharing() -> void: + if not session.is_host(): + _leave(tr("SIMULATION_STOPPED")) + return + if link != null: + link.link.close() + link = null + figures.clear() + hud.show_status(tr("SIMULATION_STOPPED")) + + +## Ends a pause once the host accepted a guest: the guest joins in the next +## tick, and a paused world would keep it waiting. A connection that has not +## greeted yet is no guest. +func _end_pause_for_new_guests() -> void: + var guests := session.guest_count() + if guests > _known_guests and clock.paused: + _set_paused(false) + _known_guests = guests + + +func _process(delta: float) -> void: + if link != null: + for notice in link.pump(delta): + _on_notice(notice) + if session != null and session.is_broken(): + _stop_sharing() + if session == null or not session.is_running() or session.is_broken(): + return + _end_pause_for_new_guests() + _update_marker() + if Input.is_action_pressed("dig") and not clock.paused: + _try_dig() + if session.is_host(): + for i in clock.advance(delta): + _run_tick() + else: + for i in mini(session.pending_ticks(), MAX_GUEST_TICKS_PER_FRAME): + _apply(session.step()) + if link != null: + link.flush() + _figure_wait -= delta + if _figure_wait <= 0.0: + _figure_wait = FIGURE_SECONDS + link.send_figure(avatar.position) + + +func _on_notice(notice: Dictionary) -> void: + # An earlier notice of the same batch may have ended the session. + if session == null: + return + match notice["type"]: + "world_received": + if notice["resync"]: + resyncs += 1 + _keep_diverged_recording() + map_view.reload() + hud.show_stone(session.item_count(GwSession.ITEM_STONE)) + hud.flash("RESYNCED") + else: + _show_world() + "refused": + _leave(tr(REFUSAL_TEXTS.get(notice["reason"], "PROTOCOL_ERROR"))) + "protocol_error": + push_warning("broken message from the host: %s" % session.last_error()) + _leave(tr("PROTOCOL_ERROR")) + "host_lost": + _leave(tr("HOST_LEFT")) + "failed": + _leave(tr("CONNECT_FAILED") % _joining) + "divergence": + push_warning("guest %d diverged at tick %d with state %s" % [notice["peer"], notice["tick"], notice["hash"]]) + "figure": + figures.place(notice["player"], notice["position"]) + + +## Keeps the recording of a guest's world that diverged from the host's, so +## that the divergence can be replayed. +func _keep_diverged_recording() -> void: + var path := store.write_recording(session.take_diverged_recording()) + push_warning("the world diverged from the host's; its recording is %s" % path) + + +func _unhandled_input(event: InputEvent) -> void: + if session == null or not session.is_running(): + return + if event.is_action_pressed("save_game"): + if session.is_host(): + hud.flash("SAVED" if save_game() else "SAVE_FAILED") + else: + hud.flash("HOST_SAVES") + elif event.is_action_pressed("load_game"): + _reload() + elif event.is_action_pressed("toggle_pause"): + # Ending a pause is always allowed, starting one only alone. + if clock.paused or (session.is_host() and session.guest_count() == 0): + _set_paused(not clock.paused) + else: + hud.flash("PAUSE_ONLY_ALONE") + + +## Goes back to the saved world. Only when playing alone: guests would no +## longer have the host's world. +func _reload() -> void: + if link != null: + hud.flash("LOAD_ONLY_ALONE") + return + var save := store.read_save() + if store.last_read_error != OK: + hud.flash("LOAD_FAILED") + return + var next := GwSession.new() + if not _load(next, save): + hud.flash("LOAD_FAILED") + return + _write_recording() + session = next + session.join(_player_key, _player_name()) + _run_tick() + _show_world() + hud.flash("LOADED") + + +func _set_paused(paused: bool) -> void: + clock.paused = paused + avatar.set_process(not paused) + hud.show_paused(paused) + + +func _notification(what: int) -> void: + if what != NOTIFICATION_WM_CLOSE_REQUEST or session == null: + return + if session.is_running(): + if session.is_host() and not save_game(): + push_warning("the game could not be saved on close") + _write_recording() + if link != null: + link.link.close() + + +func _run_tick() -> void: + _apply(session.step()) + if session.tick() % AUTOSAVE_TICKS == 0 and not save_game(): + hud.flash("SAVE_FAILED") + push_warning("the automatic save failed") + + +func _apply(events: Array[Dictionary]) -> void: + var local := session.local_player() + for event in events: + match event["type"]: + "tile_hit": + var tile := Vector2i(event["x"], event["y"]) + map_view.refresh_tile(tile) + # The own hits showed their burst when they were made. + if event["player"] != local: + map_view.hit(tile) + "tile_changed": + var tile := Vector2i(event["x"], event["y"]) + map_view.refresh_tile(tile) + map_view.hit(tile) + "item_gained": + if event["player"] == local: + hud.show_stone(session.item_count(GwSession.ITEM_STONE)) + "inventory_full": + if event["player"] == local: + hud.flash("INVENTORY_FULL") + "player_joined": + if event["player"] != local: + hud.flash("PLAYER_JOINED", [session.player_name(event["player"])]) + else: + avatar.player = local + avatar.queue_redraw() + _update_status() + "player_left": + figures.forget(event["player"]) + hud.flash("PLAYER_LEFT", [session.player_name(event["player"])]) + _update_status() + if session.is_broken(): + hud.flash("SIMULATION_STOPPED") + push_error(session.last_error()) + + +func _update_marker() -> void: + var tile := GridMotion.tile_of(get_global_mouse_position()) + var distance := avatar.position.distance_to(GridMotion.tile_center(tile)) + marker.show_tile(tile, distance <= REACH_TILES * GridMotion.TILE_SIZE) + + +func _try_dig() -> void: + if marker.in_reach: + dig_at(marker.tile) + + +## Queues a pickaxe hit on `tile`, at most once per cooldown, and shows its +## burst of chips at once: the hit itself lands with a later tick, which for a +## guest comes back from the host only after the round trip. The caller must +## check the reach: figures move outside the lockstep, so the core does not +## know where the figure is. The core checks the cooldown, the tile, its +## terrain, the tool tier and the free space in the inventory. +func dig_at(tile: Vector2i) -> void: + if session.tick() < _next_dig_tick: + return + session.dig(tile.x, tile.y) + _next_dig_tick = session.tick() + GwSession.DIG_COOLDOWN_TICKS + if session.terrain_at(tile.x, tile.y) != GwSession.TERRAIN_GROUND: + map_view.hit(tile) + + +## Writes the running world as the save. Returns false if nothing was +## written: for a guest, whose world belongs to the host, and while an +## unusable save could not be set aside. +func save_game() -> bool: + if _save_blocked or not session.is_host(): + return false + return store.write_save(session.save()) + + +func _write_recording() -> void: + store.write_recording(session.recording_bytes()) +``` + +- [ ] **Step 4: Tests ausführen und Erfolg sehen** + +Run: + +```bash +godot --headless --path game --import +godot --headless --path game -s res://tests/run_tests.gd +``` + +Expected: Exit-Code 0 und `121 passed, 0 failed`. Die einzigen Warnungen sind die drei bekannten Zeilen. + +- [ ] **Step 5: Bildschirmfoto prüfen** + +Run: `godot --path game -s res://tools/screenshot.gd -- "$TEMP/m1d-task2.png"` + +Expected: Ein Fenster öffnet sich kurz. Die Ausgabe endet mit `screenshot saved: …m1d-task2.png` und enthält weder Warnung noch Fehler. Das Bild zeigt die Karte in den Platzhaltern von `ArtStyle`, so flach wie vor diesem Plan: brauner Boden, graue Felsen mit eckigen Kanten, die Figur als heller Kreis mit dunklem Rand. Im getroffenen Fels rechts neben der Figur steht ein kurzer dunkler Strich als Riss. Oben links steht `Stein: 0`, unten die Hilfezeile. + +- [ ] **Step 6: Alles prüfen** + +Run: + +```bash +cargo fmt --all -- --check && cargo clippy --workspace --all-targets -- -D warnings && cargo test --workspace +cargo build -p grovewright-bridge +``` + +Expected: keine Ausgabe von rustfmt, keine Warnung von Clippy, zusammen 151 bestandene Rust-Tests + +- [ ] **Step 7: Commit und Hochladen** + +```bash +git add game +git commit -F - <<'EOF' +Spielfeld über Art zeichnen + +Karte, Figuren und Zielmarke zeichnet jetzt Art. Die Karte zeichnet in +Blöcken von 16 mal 16 Kacheln und nach einem Treffer nur die Blöcke um +die Kachel neu; jede Kachel kennt ihre Nachbarn. Figuren schreiten und +schauen in die Richtung, in die sie gehen. Der eigene Schlag zeigt +seine Splitter sofort, auch als Gast, und die Namen über fremden +Figuren werden in Bildschirmgröße gezeichnet und bleiben scharf. + +Co-Authored-By: Claude Opus 5.5 +EOF +git push +``` + +--- + +### Task 3: Vitrine der Stilproben + +**Files:** +- Create: `game/scripts/showcase.gd` +- Modify: `game/scripts/menu.gd`, `game/scripts/input_setup.gd`, `game/i18n/texts.csv`, `game/scripts/main.gd` (jeweils ganze Datei ersetzen) +- Test: `game/tests/test_showcase.gd` (neu); `game/tests/test_input_setup.gd` (ganze Datei ersetzen); `game/tests/test_menu.gd`, `game/tests/test_main.gd` (je ein Test am Ende) + +**Interfaces:** +- Consumes: aus Task 1 `Art` (`styles`, `style`, `select`, `select_next`, `changed`, `draw`), `ArtStyle.along`, `ArtStyle.title_key()` und `TileShape`; aus Task 2 `Avatar.figure_rect` und `MapView.redraw()`; `GridMotion.TILE_SIZE`; die Hilfen `_menu()` und `_free(menu)` in `game/tests/test_menu.gd` +- Produces: + - `Showcase` (CanvasLayer): Signal `closed`; `art: Art`, `time: float` (Sekunden seit dem Öffnen), `style_buttons: Array[Button]`, `back_button: Button`; die Konstanten `GRID`, `ZOOM`, `GROW_SECONDS`, `GROWN_SECONDS`, `HOP_EVERY`, `HOP_SECONDS`, `STONE_SPEED`, `STONES`, `VINE`, `MOSS`, `PLANT`, `GRAZER_HOME`, `RUIN`, `FIGURES`; die Bewegungen `static growth_at(seconds: float) -> float`, `static hop_at(seconds: float) -> float`, `static grazer_at(seconds: float) -> Vector2`, `static stones_at(seconds: float) -> Array[float]` und `static awake_at(seconds: float) -> float`; `caption_keys() -> PackedStringArray`; `draw_scene(canvas: CanvasItem)` + - `Menu`: Signal `showcase_chosen` und `showcase_button` + - Eingabeaktionen `next_style` (F2), `back` (Esc), `style_1`, `style_2` und `style_3` (Tasten 1 bis 3) + - Hauptszene: `showcase: Showcase`, `open_showcase()` und `close_showcase()`. Nach jedem Wechsel des Stils zeichnet `_redraw_art()` das Spielfeld neu. + - Texte `MENU_SHOWCASE`, `SHOWCASE_TITLE`, `SHOWCASE_BACK` und die Beschriftungen `SHOWCASE_ROCK`, `SHOWCASE_DEAD_SOIL`, `SHOWCASE_LIVING_SOIL`, `SHOWCASE_MOSS`, `SHOWCASE_PLANT`, `SHOWCASE_GRAZER`, `SHOWCASE_VINE`, `SHOWCASE_RUIN`, `SHOWCASE_FIGURES` + +- [ ] **Step 1: Fehlschlagende Tests schreiben** + +Create `game/tests/test_showcase.gd`: + +```gdscript +extends TestCase +## The showcase of the style samples: how its things move, and that it shows +## every style. + + +func test_the_plant_grows_from_a_seed_and_starts_again() -> void: + check_eq(Showcase.growth_at(0.0), 0.0, "a seed at first") + check(is_equal_approx(Showcase.growth_at(Showcase.GROW_SECONDS / 2.0), 0.5), "half grown halfway") + check_eq(Showcase.growth_at(Showcase.GROW_SECONDS), 1.0, "grown") + check_eq(Showcase.growth_at(Showcase.GROW_SECONDS + Showcase.GROWN_SECONDS - 0.01), 1.0, "it stays grown for a while") + check_eq(Showcase.growth_at(Showcase.GROW_SECONDS + Showcase.GROWN_SECONDS), 0.0, "then a seed again") + + +func test_the_animal_hops_then_rests_near_its_home() -> void: + check_eq(Showcase.hop_at(0.0), 0.0, "taking off") + check(is_equal_approx(Showcase.hop_at(Showcase.HOP_SECONDS / 2.0), 0.5), "in the air") + check_eq(Showcase.hop_at(Showcase.HOP_SECONDS), 1.0, "landed") + check_eq(Showcase.hop_at(Showcase.HOP_EVERY - 0.01), 1.0, "resting until the next hop") + for tenth in 100: + var at := Showcase.grazer_at(tenth * 0.1) + check(at.distance_to(Showcase.GRAZER_HOME) <= 1.01, "near home at %.1f s: %s" % [tenth * 0.1, at]) + + +func test_stones_travel_along_the_vine_one_after_another() -> void: + var start := Showcase.stones_at(0.0) + check_eq(start.size(), Showcase.STONES, "every stone") + check_eq(start[0], 0.0, "the first one starts") + for stone in start: + check(stone >= 0.0 and stone < 1.0, "on the vine: %f" % stone) + var later := Showcase.stones_at(1.0) + check(is_equal_approx(later[0], Showcase.STONE_SPEED), "it moves on") + var round_trip := Showcase.stones_at(1.0 / Showcase.STONE_SPEED) + check(is_equal_approx(round_trip[0], 0.0) or is_equal_approx(round_trip[0], 1.0), "and comes round again") + + +func test_the_ruin_wakes_and_dozes() -> void: + for tenth in 100: + var awake := Showcase.awake_at(tenth * 0.1) + check(awake >= 0.29 and awake <= 1.0, "between dozing and awake: %f" % awake) + + +func test_the_showcase_shows_every_style_and_names_its_things() -> void: + I18n.install() + InputSetup.install() + var art := Art.new() + var showcase := Showcase.new() + showcase.art = art + tree.root.add_child(showcase) + for index in art.styles.size(): + showcase.style_buttons[index].pressed.emit() + check_eq(art.style, art.styles[index], "button %d chooses its style" % (index + 1)) + check(showcase.style_buttons[index].disabled, "the chosen one cannot be pressed again") + await tree.process_frame + for key in showcase.caption_keys(): + check(tr(key) != key, "a text for %s" % key) + var closed := [0] + showcase.closed.connect(func() -> void: closed[0] += 1) + showcase.back_button.pressed.emit() + check_eq(closed[0], 1, "back leads away") + tree.root.remove_child(showcase) + showcase.free() +``` + +Ersetze den gesamten Inhalt von `game/tests/test_input_setup.gd` durch die folgende Fassung. Neu sind die fünf Aktionen am Ende der Liste. + +```gdscript +extends TestCase + + +func test_every_action_of_the_game_exists() -> void: + InputSetup.install() + for action in ["move_up", "move_down", "move_left", "move_right", "dig", "save_game", "load_game", "toggle_pause", "next_style", "back", "style_1", "style_2", "style_3"]: + check(InputMap.has_action(action), "action %s" % action) + + +func test_installing_twice_adds_no_second_set_of_keys() -> void: + InputSetup.install() + var events := InputMap.action_get_events("move_up").size() + InputSetup.install() + check_eq(InputMap.action_get_events("move_up").size(), events, "same number of keys") + check_eq(events, 2, "W and the arrow key") +``` + +Füge am Ende von `game/tests/test_menu.gd` diesen Test an, nach zwei Leerzeilen: + +```gdscript +func test_the_way_to_the_style_samples() -> void: + var menu := _menu() + var chosen := [0] + menu.showcase_chosen.connect(func() -> void: chosen[0] += 1) + check_eq(menu.showcase_button.text, tr("MENU_SHOWCASE"), "the button says where it leads") + menu.showcase_button.pressed.emit() + check_eq(chosen[0], 1, "it asks for the samples") + _free(menu) +``` + +Füge am Ende von `game/tests/test_main.gd` diesen Test an, nach zwei Leerzeilen: + +```gdscript +func test_the_style_samples_open_from_the_menu_and_lead_back_to_it() -> void: + var directory := TestFiles.temporary_dir("showcase") + var main := _start_main(directory) + main.menu.showcase_button.pressed.emit() + check(main.showcase.visible, "the samples show") + check(not main.menu.visible, "instead of the menu") + main.showcase.style_buttons[2].pressed.emit() + check_eq(main.art.style.key(), GeometricStyle.KEY, "a button there chooses the style") + await tree.process_frame + main.showcase.back_button.pressed.emit() + check(main.menu.visible, "the menu is back") + check(not main.showcase.visible, "the samples are gone") + _clean_up([directory]) +``` + +- [ ] **Step 2: Tests ausführen und Fehlschlag sehen** + +Run: + +```bash +godot --headless --path game --import +godot --headless --path game -s res://tests/run_tests.gd +``` + +Expected: Der Testlauf endet mit Exit-Code 1 und `120 passed, 4 failed`. `test_showcase.gd` lädt nicht (`SCRIPT ERROR: Parse Error: Identifier "Showcase" not declared in the current scope.`). `test_every_action_of_the_game_exists` vermisst `action next_style`, `action back` und `action style_1` bis `action style_3`. Die neuen Tests in `test_menu.gd` und `test_main.gd` scheitern mit `Invalid access to property or key 'showcase_chosen' on a base object of type 'CanvasLayer (Menu)'.` beziehungsweise `… key 'showcase_button' …`. + +- [ ] **Step 3: Vitrine, Menü, Tasten und Texte schreiben** + +Die Vitrine baut ihre Szene aus einem Raster von Buchstaben. Pflanze, Tier, Steine und Ruine bewegen sich nach Formeln der Zeit seit dem Öffnen; die Formeln sind statisch, damit die Tests sie ohne Szene prüfen. + +Create `game/scripts/showcase.gd`: + +```gdscript +class_name Showcase +extends CanvasLayer +## The showcase of the style samples: one small scene in the chosen style, +## with the terrain of the test map and things of later milestones. Dead +## soil turns living, moss glows, a vine carries stones into a waking ruin, a +## plant grows and an animal hops. Buttons switch the style. + +## The player wants to go back to the menu. +signal closed + +## The scene, one letter per tile: B edge of the map, S soft rock, H hard +## rock, . dead soil, L living soil. +const GRID: Array[String] = [ + "BBSSSHHH............", + "BSSSHHH.............", + "SS...H..............", + "..........LLLLLL....", + ".......LLLLLLLLLLLL.", + ".....LLLLLLLLLLLLLL.", + ".....LLLLLLLLLLLLLL.", + ".......LLLLLLLLLLL..", + ".........LLLLLL.....", + "....................", +] +## Pixels of the scene per pixel on screen. +const ZOOM := 3.0 +## Seconds from seed to grown plant, and the pause before it starts again. +const GROW_SECONDS := 6.0 +const GROWN_SECONDS := 2.0 +## Seconds between two hops, and how long a hop lasts. +const HOP_EVERY := 1.2 +const HOP_SECONDS := 0.5 +## How far stones travel along the vine per second, as a share of its length. +const STONE_SPEED := 0.1 +const STONES := 3 +## The path of the vine, in tiles: along the living soil into the ruin. +const VINE: Array[Vector2] = [ + Vector2(6.8, 6.9), Vector2(9.0, 7.3), Vector2(11.0, 6.9), + Vector2(13.0, 7.1), Vector2(15.0, 6.4), Vector2(17.3, 5.7), +] +## Where things stand, in tiles: the middle of their foot; for the figures +## the middle of the tile they stand on. +const MOSS: Array[Vector2] = [Vector2(8.0, 4.6), Vector2(9.8, 3.3), Vector2(14.2, 3.4), Vector2(16.8, 6.8)] +const PLANT := Vector2(10.9, 4.4) +const GRAZER_HOME := Vector2(12.8, 5.2) +## The ruin covers three by three tiles from here. +const RUIN := Vector2i(16, 3) +## The figures of two players: the own one and one of another colour. +const FIGURES: Array[Vector2] = [Vector2(3.0, 5.0), Vector2(1.6, 6.6)] + +var art: Art +## Seconds since the showcase opened. +var time := 0.0 +var style_buttons: Array[Button] = [] +var back_button := Button.new() +var _view := ShowcaseView.new() +var _title := Label.new() +var _captions: Array[Label] = [] +var _grid_codes := PackedByteArray() +var _solid := PackedByteArray() +var _living := PackedByteArray() + + +## The node that draws the scene. +class ShowcaseView extends Node2D: + var showcase: Showcase + + func _draw() -> void: + showcase.draw_scene(self) + + +func _ready() -> void: + _read_grid() + var background := ColorRect.new() + background.color = Color(0.08, 0.08, 0.09) + background.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + add_child(background) + _view.showcase = self + _view.scale = Vector2(ZOOM, ZOOM) + add_child(_view) + _title.add_theme_font_size_override("font_size", 28) + _title.position = Vector2(32, 20) + add_child(_title) + var row := HBoxContainer.new() + row.position = Vector2(32, 660) + row.add_theme_constant_override("separation", 12) + add_child(row) + for index in art.styles.size(): + var button := Button.new() + button.text = "%d %s" % [index + 1, tr(art.styles[index].title_key())] + button.pressed.connect(func() -> void: art.select(art.styles[index].key())) + style_buttons.append(button) + row.add_child(button) + back_button.text = tr("SHOWCASE_BACK") + back_button.pressed.connect(func() -> void: closed.emit()) + row.add_child(back_button) + _add_captions() + art.changed.connect(_on_style_changed) + _on_style_changed() + get_viewport().size_changed.connect(_layout) + _layout() + + +func _process(delta: float) -> void: + time += delta + _view.queue_redraw() + + +func _unhandled_input(event: InputEvent) -> void: + if not visible: + return + for index in mini(3, art.styles.size()): + if event.is_action_pressed("style_%d" % (index + 1)): + art.select(art.styles[index].key()) + if event.is_action_pressed("next_style"): + art.select_next() + elif event.is_action_pressed("back"): + closed.emit() + + +## How far the plant has grown at `seconds`, from 0 (a seed) to 1, over and +## over again. +static func growth_at(seconds: float) -> float: + return clampf(fposmod(seconds, GROW_SECONDS + GROWN_SECONDS) / GROW_SECONDS, 0.0, 1.0) + + +## How far the animal is through its hop at `seconds`: from 0 when it takes +## off to 1 when it lands, then 1 while it rests. +static func hop_at(seconds: float) -> float: + return clampf(fposmod(seconds, HOP_EVERY) / HOP_SECONDS, 0.0, 1.0) + + +## Where the animal is at `seconds`, in tiles: it hops around its home. +static func grazer_at(seconds: float) -> Vector2: + var hops := floorf(seconds / HOP_EVERY) + hop_at(seconds) + return GRAZER_HOME + Vector2(cos(hops * 0.9), sin(hops * 0.9) * 0.4) + + +## How far each stone is along the vine at `seconds`, from 0 to 1. +static func stones_at(seconds: float) -> Array[float]: + var shares: Array[float] = [] + for stone in STONES: + shares.append(fposmod(seconds * STONE_SPEED + float(stone) / STONES, 1.0)) + return shares + + +## How awake the ruin is at `seconds`: it wakes and dozes in slow breaths. +static func awake_at(seconds: float) -> float: + return 0.65 + 0.35 * sin(seconds * 1.2) + + +## The texts under the things in the scene, as keys. Tests read them. +func caption_keys() -> PackedStringArray: + var keys := PackedStringArray() + for label in _captions: + keys.append(label.get_meta("key")) + return keys + + +## Draws the whole scene onto `canvas`, while it draws. +func draw_scene(canvas: CanvasItem) -> void: + if art == null: + return + var size := GridMotion.TILE_SIZE + var width := GRID[0].length() + var height := GRID.size() + for y in height: + for x in width: + var tile := Vector2i(x, y) + var rect := Rect2(Vector2(tile) * size, Vector2(size, size)) + var code := _grid_codes[y * width + x] + var state := { + "solid": TileShape.mask(_solid, width, height, tile, false), + "living": TileShape.mask(_living, width, height, tile, false), + "variant": TileShape.variant(tile), + } + art.draw(canvas, _id_of(code), rect, state) + for at in MOSS: + art.draw(canvas, Art.MOSS, _tile_rect(at), {"variant": TileShape.variant(Vector2i(at)), "time": time}) + var path := PackedVector2Array() + for point in VINE: + path.append(point * size) + art.draw(canvas, Art.VINE, _bounds(path), {"points": path, "time": time}) + for share in stones_at(time): + var at := ArtStyle.along(path, share) + art.draw(canvas, Art.STONE, Rect2(at - Vector2(4, 5), Vector2(8, 8))) + art.draw(canvas, Art.FIBER_PLANT, _tile_rect(PLANT, Vector2(1.0, 1.8)), {"growth": growth_at(time), "time": time}) + var grazer := grazer_at(time) + var heading := grazer_at(time + 0.1) - grazer + art.draw(canvas, Art.GRAZER, _tile_rect(grazer), {"hop": hop_at(time), "facing": 1.0 if heading.x >= 0.0 else -1.0}) + var ruin := Rect2(Vector2(RUIN) * size, Vector2(3, 3) * size) + art.draw(canvas, Art.RUIN, ruin, {"awake": awake_at(time), "time": time}) + for player in FIGURES.size(): + var at := (FIGURES[player] + Vector2(0.5, 0.5)) * size + art.draw(canvas, Art.FIGURE, Avatar.figure_rect(at), {"player": player, "facing": 1.0, "step": time * 4.0 + player, "moving": true}) + + +func _read_grid() -> void: + var codes := {".": 0, "L": 1, "S": 2, "H": 3, "B": 4} + _grid_codes.clear() + for line in GRID: + for letter in line: + _grid_codes.append(codes[letter]) + _solid = TileShape.flags(_grid_codes, [2, 3, 4]) + _living = TileShape.flags(_grid_codes, [1]) + + +static func _id_of(code: int) -> StringName: + return [Art.DEAD_SOIL, Art.LIVING_SOIL, Art.SOFT_ROCK, Art.HARD_ROCK, Art.BEDROCK][code] + + +## A square of one tile, or of `size` tiles, whose bottom middle stands at +## `at` (in tiles) plus half a tile. +static func _tile_rect(at: Vector2, size := Vector2.ONE) -> Rect2: + var tile := GridMotion.TILE_SIZE + var foot := (at + Vector2(0.5, 1.0)) * tile + return Rect2(foot - Vector2(size.x * 0.5, size.y) * tile, size * tile) + + +static func _bounds(points: PackedVector2Array) -> Rect2: + var rect := Rect2(points[0], Vector2.ZERO) + for point in points: + rect = rect.expand(point) + return rect + + +func _add_captions() -> void: + var entries := [ + ["SHOWCASE_ROCK", Vector2(4.0, 3.1)], + ["SHOWCASE_DEAD_SOIL", Vector2(2.6, 8.8)], + ["SHOWCASE_LIVING_SOIL", Vector2(12.5, 9.15)], + ["SHOWCASE_MOSS", Vector2(8.4, 5.6)], + ["SHOWCASE_PLANT", Vector2(11.4, 2.3)], + ["SHOWCASE_GRAZER", Vector2(13.3, 6.3)], + ["SHOWCASE_VINE", Vector2(10.6, 7.7)], + ["SHOWCASE_RUIN", Vector2(17.5, 1.9)], + ["SHOWCASE_FIGURES", Vector2(2.6, 7.7)], + ] + for entry: Array in entries: + var label := Label.new() + label.text = tr(entry[0]) + label.set_meta("key", entry[0]) + label.set_meta("at", entry[1]) + label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + label.add_theme_font_size_override("font_size", 13) + label.add_theme_color_override("font_outline_color", Color(0.05, 0.05, 0.06)) + label.add_theme_constant_override("outline_size", 5) + _captions.append(label) + add_child(label) + + +## Puts the scene in the middle of the screen and the texts under their +## things. +func _layout() -> void: + var scene_size := Vector2(GRID[0].length(), GRID.size()) * GridMotion.TILE_SIZE * ZOOM + var screen := get_viewport().get_visible_rect().size + _view.position = ((screen - scene_size) / 2.0).floor() + for label in _captions: + var at: Vector2 = label.get_meta("at") + label.size = Vector2(160, 20) + label.position = _view.position + at * GridMotion.TILE_SIZE * ZOOM - Vector2(80, 0) + + +func _on_style_changed() -> void: + _title.text = tr("SHOWCASE_TITLE") % tr(art.style.title_key()) + for index in style_buttons.size(): + style_buttons[index].disabled = art.styles[index] == art.style + _view.queue_redraw() +``` + +Ersetze den gesamten Inhalt von `game/scripts/menu.gd` durch die folgende Fassung. Neu sind das Signal `showcase_chosen` und der Knopf `showcase_button` unter einer zweiten Trennlinie. + +```gdscript +class_name Menu +extends CanvasLayer +## The start screen: the player's name, then playing alone, opening the world +## for guests, or joining a host. Below, the way to the style samples. + +signal play_chosen(open: bool) +signal join_chosen(address: String) +signal showcase_chosen + +var name_field := LineEdit.new() +var address_field := LineEdit.new() +var solo_button := Button.new() +var host_button := Button.new() +var join_button := Button.new() +var showcase_button := Button.new() +var _status := Label.new() + + +func _ready() -> void: + var center := CenterContainer.new() + center.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + add_child(center) + var panel := PanelContainer.new() + center.add_child(panel) + var box := VBoxContainer.new() + box.custom_minimum_size = Vector2(380, 0) + box.add_theme_constant_override("separation", 10) + panel.add_child(box) + + var title := Label.new() + title.text = "Grovewright" + title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + title.add_theme_font_size_override("font_size", 32) + name_field.placeholder_text = tr("MENU_NAME") + name_field.max_length = 32 + solo_button.text = tr("MENU_SOLO") + host_button.text = tr("MENU_HOST") + var row := HBoxContainer.new() + for button in [solo_button, host_button]: + button.size_flags_horizontal = Control.SIZE_EXPAND_FILL + row.add_child(button) + address_field.placeholder_text = tr("MENU_ADDRESS") + join_button.text = tr("MENU_JOIN") + showcase_button.text = tr("MENU_SHOWCASE") + _status.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + _status.custom_minimum_size = Vector2(380, 0) + for control: Control in [title, name_field, row, HSeparator.new(), address_field, join_button, HSeparator.new(), showcase_button, _status]: + box.add_child(control) + + solo_button.pressed.connect(func() -> void: play_chosen.emit(false)) + host_button.pressed.connect(func() -> void: play_chosen.emit(true)) + join_button.pressed.connect(func() -> void: join_chosen.emit(address_field.text.strip_edges())) + address_field.text_submitted.connect(func(_text: String) -> void: join_button.pressed.emit()) + showcase_button.pressed.connect(func() -> void: showcase_chosen.emit()) + + +## Shows `text` below the buttons. +func show_status(text: String) -> void: + _status.text = text + + +## The text shown below the buttons. Tests read it. +func status() -> String: + return _status.text + + +## The name the player typed, without spaces around it. +func player_name() -> String: + return name_field.text.strip_edges() +``` + +Ersetze den gesamten Inhalt von `game/scripts/input_setup.gd` durch die folgende Fassung. Neu sind die Aktionen `next_style`, `back` und `style_1` bis `style_3`. + +```gdscript +class_name InputSetup +extends RefCounted +## Registers the game's input actions in code, so that project.godot holds no +## serialized input events. Keys are physical positions, so WASD stays in +## place on German keyboards too. + + +static func install() -> void: + _add_keys("move_up", [KEY_W, KEY_UP]) + _add_keys("move_down", [KEY_S, KEY_DOWN]) + _add_keys("move_left", [KEY_A, KEY_LEFT]) + _add_keys("move_right", [KEY_D, KEY_RIGHT]) + _add_keys("save_game", [KEY_F5]) + _add_keys("load_game", [KEY_F9]) + _add_keys("toggle_pause", [KEY_ESCAPE]) + _add_keys("next_style", [KEY_F2]) + _add_keys("back", [KEY_ESCAPE]) + _add_keys("style_1", [KEY_1]) + _add_keys("style_2", [KEY_2]) + _add_keys("style_3", [KEY_3]) + _add_mouse_button("dig", MOUSE_BUTTON_LEFT) + + +static func _add_keys(action: StringName, keys: Array[Key]) -> void: + if InputMap.has_action(action): + return + InputMap.add_action(action) + for key in keys: + var event := InputEventKey.new() + event.physical_keycode = key + InputMap.action_add_event(action, event) + + +static func _add_mouse_button(action: StringName, button: MouseButton) -> void: + if InputMap.has_action(action): + return + InputMap.add_action(action) + var event := InputEventMouseButton.new() + event.button_index = button + InputMap.action_add_event(action, event) +``` + +Ersetze den gesamten Inhalt von `game/i18n/texts.csv` durch die folgende Fassung. Neu sind die zwölf Zeilen von `MENU_SHOWCASE` bis `SHOWCASE_FIGURES` am Ende. + +```text +keys,de,en +STONE_COUNT,Stein: %d,Stone: %d +SAVED,Gespeichert,Saved +SAVE_FAILED,Speichern fehlgeschlagen,Saving failed +LOADED,Spielstand geladen,Save loaded +LOAD_FAILED,Der Spielstand ließ sich nicht laden,The save could not be loaded +INVENTORY_FULL,Die Tasche ist voll,Your bag is full +PAUSED,Pause,Paused +SIMULATION_STOPPED,Die Simulation wurde angehalten,The simulation stopped +HELP,WASD: laufen · Linke Maustaste: Stein schlagen · F5: speichern · F9: laden · Esc: Pause,WASD: walk · Left mouse button: break rock · F5: save · F9: load · Esc: pause +DEFAULT_NAME,Spieler,Player +MENU_NAME,Dein Name,Your name +MENU_SOLO,Allein spielen,Play alone +MENU_HOST,Welt öffnen,Open your world +MENU_ADDRESS,Adresse des Hosts,Address of the host +MENU_JOIN,Beitreten,Join +CONNECTING,Verbinde mit %s …,Connecting to %s … +CONNECT_FAILED,Keine Verbindung zu %s,Could not reach %s +ADDRESS_INVALID,Das ist keine Adresse: %s,That is not an address: %s +HOST_LEFT,Der Host hat die Welt geschlossen,The host closed the world +PROTOCOL_ERROR,Die Verbindung zum Host ist gestört,The connection to the host is broken +REFUSED_OTHER_PROTOCOL,Der Host hat eine andere Version des Spiels,The host has another version of the game +REFUSED_OTHER_RULES,Der Host hat andere Spielregeln. Ihr braucht denselben Stand des Spiels.,The host has other rules. You need the same version of the game. +REFUSED_WORLD_FULL,Die Welt ist voll: Vier Spieler sind schon da,The world is full: four players are in it +REFUSED_TOO_MANY_PLAYERS,Diese Welt kennt schon 16 Spieler und nimmt keine neuen mehr auf,This world knows 16 players already and takes no new ones +REFUSED_ALREADY_PLAYING,Ein Spieler mit deinem Schlüssel ist schon in der Welt,A player with your key is in the world already +STATUS_HOST,Welt offen auf Port %d · Spieler: %d,World open on port %d · players: %d +STATUS_GUEST,Gast bei %s · Spieler: %d,Guest of %s · players: %d +OPEN_FAILED,Port %d lässt sich nicht öffnen,Port %d cannot be opened +PLAYER_JOINED,%s ist da,%s joined +PLAYER_LEFT,%s ist gegangen,%s left +RESYNCED,Welt mit dem Host abgeglichen,World matched with the host again +PAUSE_ONLY_ALONE,Pause gibt es nur ohne Gäste,Pausing only works without guests +LOAD_ONLY_ALONE,Laden gibt es nur beim Spielen allein,Loading only works when playing alone +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_PAPERCUT,Scherenschnitt,Papercut +STYLE_OUTLINE,Kontur,Outline +STYLE_GEOMETRIC,Geometrisch,Geometric +MENU_SHOWCASE,Stilproben ansehen,See the style samples +SHOWCASE_TITLE,Stilprobe: %s,Style sample: %s +SHOWCASE_BACK,Zurück,Back +SHOWCASE_ROCK,Weicher und harter Fels,Soft and hard rock +SHOWCASE_DEAD_SOIL,Toter Boden,Dead soil +SHOWCASE_LIVING_SOIL,Lebendiger Boden,Living soil +SHOWCASE_MOSS,Lebensmoos,Life moss +SHOWCASE_PLANT,Faserpflanze wächst,A fibre plant grows +SHOWCASE_GRAZER,Weidetier,Grazer +SHOWCASE_VINE,Ranke trägt Steine,A vine carries stones +SHOWCASE_RUIN,Ruine erwacht,A ruin wakes +SHOWCASE_FIGURES,Spielfiguren,Player figures +``` + +In der Hauptszene kommt gegenüber Task 2 die Vitrine hinzu: Sie wird beim Start versteckt und angehalten angelegt, `open_showcase()` zeigt sie statt des Menüs, und ihr Signal `closed` führt über `close_showcase()` zurück. Weil die Knöpfe der Vitrine den Stil wechseln, zeichnet `_redraw_art()` bei jedem Signal `changed` von `art` das Spielfeld neu. + +Ersetze den gesamten Inhalt von `game/scripts/main.gd` durch: + +```gdscript +extends Node2D +## Composition root. Shows the start menu, then runs a session as host or as +## guest with the map view, the figures, the HUD and the files on disk. + +## Seed of a new world until the slice ships its chosen seed. +const NEW_WORLD_SEED := 2026 +## How far the pickaxe reaches, in tiles from the figure to the tile centre. +const REACH_TILES := 2.5 +## Ticks between two automatic saves: two minutes. +const AUTOSAVE_TICKS := 2 * 60 * GwSession.TICKS_PER_SECOND +const CAMERA_ZOOM := Vector2(3.0, 3.0) +## Most ticks a guest runs in one frame. A guest that fell behind catches up +## within a few frames without freezing. +const MAX_GUEST_TICKS_PER_FRAME := 20 +## Seconds between two position updates of the own figure. +const FIGURE_SECONDS := 0.1 +## The text for every reason a host gives for turning a guest away. +const REFUSAL_TEXTS := { + GwSession.REFUSED_OTHER_PROTOCOL: "REFUSED_OTHER_PROTOCOL", + GwSession.REFUSED_OTHER_RULES: "REFUSED_OTHER_RULES", + GwSession.REFUSED_WORLD_FULL: "REFUSED_WORLD_FULL", + GwSession.REFUSED_TOO_MANY_PLAYERS: "REFUSED_TOO_MANY_PLAYERS", + GwSession.REFUSED_ALREADY_PLAYING: "REFUSED_ALREADY_PLAYING", +} + +## Folder for the player key, the save and the recordings. If it is empty, +## the command line decides; tests set a folder of their own. +@export var storage_dir := "" +## Network interface an open world listens on. Tests use the loopback one. +var listen_address := "*" +## The command line arguments after "--". Tools and tests may set their own +## before the scene enters the tree. +var command_line := OS.get_cmdline_user_args() + +var options: LaunchOptions +var store: SaveStore +## The running session, or null in the menu. A guest's session has no world +## until the host sent it. +var session: GwSession +## The network connection of an open world or a guest, or null. +var link: SessionLink +var clock: TickClock +## The style the playing field is drawn in. +var art: Art +var menu: Menu +var showcase: Showcase +var map_view: MapView +var marker: TileMarker +var avatar: Avatar +var figures: Figures +var hud: Hud +## How often a guest's world diverged and was replaced by the host's. +var resyncs := 0 + +var _player_key: PackedByteArray +var _next_dig_tick := 0 +## True while an unusable save could not be set aside. Saving is refused +## then, so that the unusable save is never overwritten. +var _save_blocked := false +var _figure_wait := 0.0 +## The address a guest is joining, for messages. +var _joining := "" +## The host's accepted guests when _process last looked. A new one ends a +## pause. +var _known_guests := 0 + + +func _ready() -> void: + I18n.install() + InputSetup.install() + options = LaunchOptions.parse(command_line) + for argument in options.ignored: + push_warning("ignored command line argument: %s" % argument) + if storage_dir.is_empty(): + storage_dir = options.storage_dir() + store = SaveStore.new(storage_dir) + _player_key = store.player_key() + if _player_key.is_empty(): + push_warning("the player key cannot be read: %s" % error_string(store.last_read_error)) + clock = TickClock.new(GwSession.TICKS_PER_SECOND) + art = Art.new() + art.load_overrides(Art.OVERRIDE_DIR) + map_view = MapView.new() + map_view.art = art + add_child(map_view) + marker = TileMarker.new() + marker.art = art + add_child(marker) + figures = Figures.new() + figures.art = art + add_child(figures) + avatar = Avatar.new() + avatar.art = art + add_child(avatar) + var camera := Camera2D.new() + camera.zoom = CAMERA_ZOOM + avatar.add_child(camera) + hud = Hud.new() + add_child(hud) + menu = Menu.new() + add_child(menu) + menu.name_field.text = options.player_name if not options.player_name.is_empty() else store.player_name() + menu.address_field.text = options.address if not options.address.is_empty() else store.last_address() + art.changed.connect(_redraw_art) + menu.play_chosen.connect(play) + menu.join_chosen.connect(join) + menu.showcase_chosen.connect(open_showcase) + showcase = Showcase.new() + showcase.art = art + showcase.closed.connect(close_showcase) + showcase.hide() + showcase.process_mode = Node.PROCESS_MODE_DISABLED + add_child(showcase) + _show_menu("") + match options.start: + "solo": + play(false) + "host": + play(true) + "join": + join(options.address) + + +## Plays the saved world, or a new one, as its host. With `open`, guests may +## join. Returns false if it could not start; the menu then says why. +func play(open: bool) -> bool: + if not _can_start(): + return false + var next := GwSession.new() + var set_aside := false + if store.has_save(): + var save := store.read_save() + if store.last_read_error != OK: + _show_menu(tr("SAVE_UNREADABLE")) + return false + set_aside = not _load(next, save) + if not next.is_running(): + next.start_new(NEW_WORLD_SEED) + if open: + var net := NetLink.new() + if net.host(options.port, listen_address) != OK: + _show_menu(tr("OPEN_FAILED") % options.port) + return false + link = SessionLink.new(next, net) + session = next + session.join(_player_key, _player_name()) + _run_tick() + _show_world() + if set_aside: + hud.flash("LOAD_FAILED") + return true + + +## Starts joining the host at `address`, "host" or "host:port". The world +## arrives later; until then the menu says that it is connecting. +func join(address: String) -> void: + if not _can_start(): + return + var target := NetLink.split_address(address) + if target.is_empty(): + _show_menu(tr("ADDRESS_INVALID") % address) + return + var net := NetLink.new() + if net.join(target[0], target[1]) != OK: + _show_menu(tr("CONNECT_FAILED") % address) + return + session = GwSession.new() + session.start_guest(_player_key, _player_name()) + link = SessionLink.new(session, net) + _joining = address + store.set_last_address(address) + _show_menu(tr("CONNECTING") % address) + + +func _can_start() -> bool: + if session != null: + return false + if _player_key.is_empty(): + _show_menu(tr("KEY_UNREADABLE")) + return false + store.set_player_name(menu.player_name()) + return true + + +func _player_name() -> String: + var typed := menu.player_name() + return typed if not typed.is_empty() else tr("DEFAULT_NAME") + + +## Starts `next` from `save`. If the save is unusable, it is set aside so +## that the next save does not overwrite it, and false is returned. +func _load(next: GwSession, save: PackedByteArray) -> bool: + if next.start_loaded(save): + _save_blocked = false + return true + var aside := store.set_aside_save() + if aside.is_empty(): + _save_blocked = true + push_warning("save not usable (%s) and could not be set aside; saving is off" % next.last_error()) + else: + push_warning("save not usable (%s), kept as %s" % [next.last_error(), aside]) + return false + + +func _show_menu(status: String) -> void: + menu.show_status(status) + menu.show() + hud.hide() + for node: Node2D in [avatar, marker, figures]: + node.hide() + + +## Shows the world of the session and puts the figure on the spawn. +func _show_world() -> void: + menu.hide() + hud.show() + for node: Node2D in [avatar, marker, figures]: + node.show() + map_view.session = session + map_view.reload() + figures.session = session + avatar.player = session.local_player() + figures.clear() + avatar.is_walkable = session.is_walkable + avatar.position = GridMotion.tile_center(session.spawn_point()) + hud.show_stone(session.item_count(GwSession.ITEM_STONE)) + _update_status() + _next_dig_tick = 0 + + +## Shows the style samples instead of the menu. +func open_showcase() -> void: + menu.hide() + showcase.time = 0.0 + showcase.process_mode = Node.PROCESS_MODE_INHERIT + showcase.show() + + +## Goes back from the style samples to the menu. +func close_showcase() -> void: + showcase.hide() + showcase.process_mode = Node.PROCESS_MODE_DISABLED + menu.show() + + +## Draws the playing field again after the style changed. +func _redraw_art() -> void: + map_view.redraw() + for node: Node2D in [marker, figures, avatar]: + node.queue_redraw() + + +## Says in the status line how the game is shared, and with how many. +func _update_status() -> void: + if link == null: + hud.show_status("") + elif session.is_host(): + hud.show_status(tr("STATUS_HOST") % [link.link.local_port(), session.online_players().size()]) + else: + hud.show_status(tr("STATUS_GUEST") % [_joining, session.online_players().size()]) + + +## Ends a guest's session and goes back to the menu with `message`. +func _leave(message: String) -> void: + if session == null: + return + _write_recording() + if link != null: + link.link.close() + link = null + session = null + map_view.session = null + map_view.reload() + avatar.is_walkable = Callable() + figures.clear() + _show_menu(message) + + +## A broken session runs no more ticks. Instead of leaving the others +## waiting, a host closes its world to guests and a guest leaves. The host's +## status line says why its world stands still. +func _stop_sharing() -> void: + if not session.is_host(): + _leave(tr("SIMULATION_STOPPED")) + return + if link != null: + link.link.close() + link = null + figures.clear() + hud.show_status(tr("SIMULATION_STOPPED")) + + +## Ends a pause once the host accepted a guest: the guest joins in the next +## tick, and a paused world would keep it waiting. A connection that has not +## greeted yet is no guest. +func _end_pause_for_new_guests() -> void: + var guests := session.guest_count() + if guests > _known_guests and clock.paused: + _set_paused(false) + _known_guests = guests + + +func _process(delta: float) -> void: + if link != null: + for notice in link.pump(delta): + _on_notice(notice) + if session != null and session.is_broken(): + _stop_sharing() + if session == null or not session.is_running() or session.is_broken(): + return + _end_pause_for_new_guests() + _update_marker() + if Input.is_action_pressed("dig") and not clock.paused: + _try_dig() + if session.is_host(): + for i in clock.advance(delta): + _run_tick() + else: + for i in mini(session.pending_ticks(), MAX_GUEST_TICKS_PER_FRAME): + _apply(session.step()) + if link != null: + link.flush() + _figure_wait -= delta + if _figure_wait <= 0.0: + _figure_wait = FIGURE_SECONDS + link.send_figure(avatar.position) + + +func _on_notice(notice: Dictionary) -> void: + # An earlier notice of the same batch may have ended the session. + if session == null: + return + match notice["type"]: + "world_received": + if notice["resync"]: + resyncs += 1 + _keep_diverged_recording() + map_view.reload() + hud.show_stone(session.item_count(GwSession.ITEM_STONE)) + hud.flash("RESYNCED") + else: + _show_world() + "refused": + _leave(tr(REFUSAL_TEXTS.get(notice["reason"], "PROTOCOL_ERROR"))) + "protocol_error": + push_warning("broken message from the host: %s" % session.last_error()) + _leave(tr("PROTOCOL_ERROR")) + "host_lost": + _leave(tr("HOST_LEFT")) + "failed": + _leave(tr("CONNECT_FAILED") % _joining) + "divergence": + push_warning("guest %d diverged at tick %d with state %s" % [notice["peer"], notice["tick"], notice["hash"]]) + "figure": + figures.place(notice["player"], notice["position"]) + + +## Keeps the recording of a guest's world that diverged from the host's, so +## that the divergence can be replayed. +func _keep_diverged_recording() -> void: + var path := store.write_recording(session.take_diverged_recording()) + push_warning("the world diverged from the host's; its recording is %s" % path) + + +func _unhandled_input(event: InputEvent) -> void: + if session == null or not session.is_running(): + return + if event.is_action_pressed("save_game"): + if session.is_host(): + hud.flash("SAVED" if save_game() else "SAVE_FAILED") + else: + hud.flash("HOST_SAVES") + elif event.is_action_pressed("load_game"): + _reload() + elif event.is_action_pressed("toggle_pause"): + # Ending a pause is always allowed, starting one only alone. + if clock.paused or (session.is_host() and session.guest_count() == 0): + _set_paused(not clock.paused) + else: + hud.flash("PAUSE_ONLY_ALONE") + + +## Goes back to the saved world. Only when playing alone: guests would no +## longer have the host's world. +func _reload() -> void: + if link != null: + hud.flash("LOAD_ONLY_ALONE") + return + var save := store.read_save() + if store.last_read_error != OK: + hud.flash("LOAD_FAILED") + return + var next := GwSession.new() + if not _load(next, save): + hud.flash("LOAD_FAILED") + return + _write_recording() + session = next + session.join(_player_key, _player_name()) + _run_tick() + _show_world() + hud.flash("LOADED") + + +func _set_paused(paused: bool) -> void: + clock.paused = paused + avatar.set_process(not paused) + hud.show_paused(paused) + + +func _notification(what: int) -> void: + if what != NOTIFICATION_WM_CLOSE_REQUEST or session == null: + return + if session.is_running(): + if session.is_host() and not save_game(): + push_warning("the game could not be saved on close") + _write_recording() + if link != null: + link.link.close() + + +func _run_tick() -> void: + _apply(session.step()) + if session.tick() % AUTOSAVE_TICKS == 0 and not save_game(): + hud.flash("SAVE_FAILED") + push_warning("the automatic save failed") + + +func _apply(events: Array[Dictionary]) -> void: + var local := session.local_player() + for event in events: + match event["type"]: + "tile_hit": + var tile := Vector2i(event["x"], event["y"]) + map_view.refresh_tile(tile) + # The own hits showed their burst when they were made. + if event["player"] != local: + map_view.hit(tile) + "tile_changed": + var tile := Vector2i(event["x"], event["y"]) + map_view.refresh_tile(tile) + map_view.hit(tile) + "item_gained": + if event["player"] == local: + hud.show_stone(session.item_count(GwSession.ITEM_STONE)) + "inventory_full": + if event["player"] == local: + hud.flash("INVENTORY_FULL") + "player_joined": + if event["player"] != local: + hud.flash("PLAYER_JOINED", [session.player_name(event["player"])]) + else: + avatar.player = local + avatar.queue_redraw() + _update_status() + "player_left": + figures.forget(event["player"]) + hud.flash("PLAYER_LEFT", [session.player_name(event["player"])]) + _update_status() + if session.is_broken(): + hud.flash("SIMULATION_STOPPED") + push_error(session.last_error()) + + +func _update_marker() -> void: + var tile := GridMotion.tile_of(get_global_mouse_position()) + var distance := avatar.position.distance_to(GridMotion.tile_center(tile)) + marker.show_tile(tile, distance <= REACH_TILES * GridMotion.TILE_SIZE) + + +func _try_dig() -> void: + if marker.in_reach: + dig_at(marker.tile) + + +## Queues a pickaxe hit on `tile`, at most once per cooldown, and shows its +## burst of chips at once: the hit itself lands with a later tick, which for a +## guest comes back from the host only after the round trip. The caller must +## check the reach: figures move outside the lockstep, so the core does not +## know where the figure is. The core checks the cooldown, the tile, its +## terrain, the tool tier and the free space in the inventory. +func dig_at(tile: Vector2i) -> void: + if session.tick() < _next_dig_tick: + return + session.dig(tile.x, tile.y) + _next_dig_tick = session.tick() + GwSession.DIG_COOLDOWN_TICKS + if session.terrain_at(tile.x, tile.y) != GwSession.TERRAIN_GROUND: + map_view.hit(tile) + + +## Writes the running world as the save. Returns false if nothing was +## written: for a guest, whose world belongs to the host, and while an +## unusable save could not be set aside. +func save_game() -> bool: + if _save_blocked or not session.is_host(): + return false + return store.write_save(session.save()) + + +func _write_recording() -> void: + store.write_recording(session.recording_bytes()) +``` + +- [ ] **Step 4: Tests ausführen und Erfolg sehen** + +Run: + +```bash +godot --headless --path game --import +godot --headless --path game -s res://tests/run_tests.gd +``` + +Expected: Exit-Code 0 und `128 passed, 0 failed`. Die einzigen Warnungen sind die drei bekannten Zeilen. + +- [ ] **Step 5: Alles prüfen** + +Run: + +```bash +cargo fmt --all -- --check && cargo clippy --workspace --all-targets -- -D warnings && cargo test --workspace +cargo build -p grovewright-bridge +``` + +Expected: keine Ausgabe von rustfmt, keine Warnung von Clippy, zusammen 151 bestandene Rust-Tests + +- [ ] **Step 6: Commit und Hochladen** + +```bash +git add game +git commit -F - <<'EOF' +Vitrine der Stilproben + +Das Startmenü führt zu einer Vitrine, die den gewählten Stil an einer +kleinen Szene zeigt: Fels, toter und lebendiger Boden, Lebensmoos, eine +Ranke, die Steine in eine erwachende Ruine trägt, eine wachsende +Faserpflanze, ein hüpfendes Weidetier und zwei Spielfiguren. Knöpfe, +die Tasten 1 bis 3 und F2 wechseln den Stil, Esc führt zurück ins Menü. + +Co-Authored-By: Claude Opus 5.5 +EOF +git push +``` + +--- + +### Task 4: Stil wählen, merken und fotografieren + +**Files:** +- Modify: `game/scripts/launch_options.gd` (ganze Datei ersetzen), `game/scripts/save_store.gd` (zwei Funktionen einfügen), `game/scripts/main.gd` (ganze Datei ersetzen), `game/i18n/texts.csv` (ganze Datei ersetzen), `game/tools/screenshot.gd` (ganze Datei ersetzen) +- Test: `game/tests/test_launch_options.gd`, `game/tests/test_save_store.gd` (je ein Test am Ende), `game/tests/test_main.gd` (zwei Tests am Ende) + +**Interfaces:** +- Consumes: `Art.style_keys()`, `Art.select`, `Art.select_next`, `ArtStyle.key()` und `ArtStyle.title_key()` aus Task 1; die Aktion `next_style` und `_redraw_art()` aus Task 3; `SaveStore._setting(section, key)` und `SaveStore._set_setting(section, key, value)`; `Hud.flash(key: String, values: Array = [])` und `Hud.message()`; die Hilfen `_start_main`, `_stop_main`, `_clean_up`, `_press` und `_started` in `game/tests/test_main.gd` +- Produces: + - `LaunchOptions.style: StringName`: der Stil aus `--style=NAME`, leer ohne Angabe. Ein unbekannter Name landet in `ignored`. + - `SaveStore.style() -> String` und `SaveStore.set_style(key: String) -> void`, in den Einstellungen unter Abschnitt `display`, Schlüssel `style` + - Hauptszene: Beim Start gilt der Stil der Befehlszeile, sonst der gemerkte. F2 wechselt im Spiel und meldet `STYLE_SWITCHED`. Jeder Wechsel wird gemerkt. + - `res://tools/screenshot.gd`, aufgerufen mit `-- [--style=NAME] [--showcase]` + - Texte: `STYLE_SWITCHED`; die Hilfezeile `HELP` nennt F2 + +- [ ] **Step 1: Fehlschlagende Tests schreiben** + +Füge am Ende von `game/tests/test_launch_options.gd` diesen Test an, nach zwei Leerzeilen: + +```gdscript +func test_a_style_can_be_chosen() -> void: + check_eq(LaunchOptions.parse(PackedStringArray(["--style=geometric"])).style, GeometricStyle.KEY, "a known style") + check_eq(LaunchOptions.parse(PackedStringArray()).style, &"", "none without the argument") + var unknown := LaunchOptions.parse(PackedStringArray(["--style=watercolour"])) + check_eq(unknown.style, &"", "an unknown style is not taken") + check_eq(unknown.ignored, PackedStringArray(["--style=watercolour"]), "but listed as ignored") +``` + +Füge am Ende von `game/tests/test_save_store.gd` diesen Test an, nach zwei Leerzeilen: + +```gdscript +func test_the_chosen_style_is_kept() -> void: + var directory := TestFiles.temporary_dir("style") + var store := SaveStore.new(directory) + check_eq(store.style(), "", "no style at first") + store.set_style("outline") + check_eq(SaveStore.new(directory).style(), "outline", "kept for the next start") + TestFiles.remove_dir(directory) +``` + +Füge am Ende von `game/tests/test_main.gd` diese beiden Tests an, nach zwei Leerzeilen: + +```gdscript +func test_f2_switches_the_style_and_the_choice_is_kept() -> void: + var directory := TestFiles.temporary_dir("style") + var main := _start_main(directory) + main.play(false) + var first: StringName = main.art.style.key() + _press(main, "next_style") + var second: StringName = main.art.style.key() + check(second != first, "another style") + check_eq(main.hud.message(), tr("STYLE_SWITCHED") % tr(main.art.style.title_key()), "the player is told") + _stop_main(main) + + var later := _start_main(directory) + check_eq(later.art.style.key(), second, "the next start draws in it again") + _clean_up([directory]) + + +func test_the_command_line_chooses_the_style() -> void: + var directory := TestFiles.temporary_dir("style-option") + var main: Node = load("res://main.tscn").instantiate() + main.storage_dir = directory + main.command_line = PackedStringArray(["--style=outline"]) + tree.root.add_child(main) + _started.append(main) + check_eq(main.art.style.key(), OutlineStyle.KEY, "the style of the command line") + _clean_up([directory]) +``` + +- [ ] **Step 2: Tests ausführen und Fehlschlag sehen** + +Run: + +```bash +godot --headless --path game --import +godot --headless --path game -s res://tests/run_tests.gd +``` + +Expected: Der Testlauf endet mit Exit-Code 1 und `128 passed, 4 failed`: + +- `test_a_style_can_be_chosen` mit `Invalid access to property or key 'style' on a base object of type 'RefCounted (LaunchOptions)'.` +- `test_the_chosen_style_is_kept` mit `Invalid call. Nonexistent function 'style' in base 'RefCounted (SaveStore)'.` +- `test_f2_switches_the_style_and_the_choice_is_kept` mit `another style` +- `test_the_command_line_chooses_the_style` mit `the style of the command line: expected outline, got papercut` + +Dazu kommen `WARNING: ignored command line argument: --style=outline` und `ERROR: String formatting error: not all arguments converted during string formatting.`, weil es den Text `STYLE_SWITCHED` noch nicht gibt. + +- [ ] **Step 3: Befehlszeile, Einstellung, Hauptszene, Texte und Werkzeug schreiben** + +Ersetze den gesamten Inhalt von `game/scripts/launch_options.gd` durch die folgende Fassung. Neu sind die Zeilen zu `--style` in der Beschreibung, das Feld `style` und der Zweig `"--style"` in `parse`. + +```gdscript +class_name LaunchOptions +extends RefCounted +## What the command line asks for. Godot passes the arguments after "--": +## --profile=NAME keeps key, save and recordings in a folder of their own, +## so that two windows on one machine are two players +## --name=NAME the player's name; like a name typed in the menu, it is +## remembered for the next start +## --solo starts playing alone at once, without the menu +## --host opens the world for guests at once +## --port=PORT the port an open world listens on +## --join=ADDRESS joins a host at once; ADDRESS may end in ":PORT" +## --style=NAME draws the playing field in the style NAME: papercut, +## outline or geometric + +## Longest profile name. +const MAX_PROFILE_LENGTH := 32 +## Characters a profile name may contain. +const PROFILE_CHARACTERS := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_" + +var profile := "" +var player_name := "" +## How to start without the menu: "", "solo", "host" or "join". +var start := "" +var port := NetLink.DEFAULT_PORT +var address := "" +## The style to draw in, or empty for the one in the settings. +var style := &"" +## Arguments that were not understood and are ignored. +var ignored := PackedStringArray() + + +## Reads `arguments`. Unknown arguments and bad values end up in `ignored`. +static func parse(arguments: PackedStringArray) -> LaunchOptions: + var options := LaunchOptions.new() + for argument in arguments: + var value := argument.get_slice("=", 1) if argument.contains("=") else "" + match argument.get_slice("=", 0): + "--profile": + if is_valid_profile(value): + options.profile = value + else: + options.ignored.append(argument) + "--name": + options.player_name = value + "--solo": + options.start = "solo" + "--host": + options.start = "host" + "--port": + if value.is_valid_int() and value.to_int() >= 0 and value.to_int() <= 65535: + options.port = value.to_int() + else: + options.ignored.append(argument) + "--join": + options.start = "join" + options.address = value + "--style": + if Art.style_keys().has(StringName(value)): + options.style = StringName(value) + else: + options.ignored.append(argument) + _: + options.ignored.append(argument) + return options + + +## Whether `name` can name a profile folder: up to 32 of the letters A to Z, +## digits, "-" and "_". +static func is_valid_profile(name: String) -> bool: + if name.is_empty() or name.length() > MAX_PROFILE_LENGTH: + return false + for character in name: + if not PROFILE_CHARACTERS.contains(character): + return false + return true + + +## The folder for the player's files. The default profile uses the user +## folder itself, so that saves of earlier versions stay where they are. +func storage_dir() -> String: + if profile.is_empty(): + return "user://" + return "user://profiles".path_join(profile) +``` + +Füge in `game/scripts/save_store.gd` direkt vor der Zeile `func _setting(section: String, key: String) -> String:` diese beiden Funktionen ein. Danach folgen zwei Leerzeilen, wie zwischen allen Funktionen: + +```gdscript +## The key of the style the player chose last time, or an empty text. +func style() -> String: + return _setting("display", "style") + + +func set_style(key: String) -> void: + _set_setting("display", "style", key) +``` + +In der Hauptszene kommt gegenüber Task 3 hinzu: Beim Start wählt `art` den Stil der Befehlszeile und sonst den gemerkten. `_redraw_art()` merkt sich jeden Wechsel mit `store.set_style`. F2 ruft im Spiel `art.select_next()` und meldet den neuen Stil. + +Ersetze den gesamten Inhalt von `game/scripts/main.gd` durch: + +```gdscript +extends Node2D +## Composition root. Shows the start menu, then runs a session as host or as +## guest with the map view, the figures, the HUD and the files on disk. + +## Seed of a new world until the slice ships its chosen seed. +const NEW_WORLD_SEED := 2026 +## How far the pickaxe reaches, in tiles from the figure to the tile centre. +const REACH_TILES := 2.5 +## Ticks between two automatic saves: two minutes. +const AUTOSAVE_TICKS := 2 * 60 * GwSession.TICKS_PER_SECOND +const CAMERA_ZOOM := Vector2(3.0, 3.0) +## Most ticks a guest runs in one frame. A guest that fell behind catches up +## within a few frames without freezing. +const MAX_GUEST_TICKS_PER_FRAME := 20 +## Seconds between two position updates of the own figure. +const FIGURE_SECONDS := 0.1 +## The text for every reason a host gives for turning a guest away. +const REFUSAL_TEXTS := { + GwSession.REFUSED_OTHER_PROTOCOL: "REFUSED_OTHER_PROTOCOL", + GwSession.REFUSED_OTHER_RULES: "REFUSED_OTHER_RULES", + GwSession.REFUSED_WORLD_FULL: "REFUSED_WORLD_FULL", + GwSession.REFUSED_TOO_MANY_PLAYERS: "REFUSED_TOO_MANY_PLAYERS", + GwSession.REFUSED_ALREADY_PLAYING: "REFUSED_ALREADY_PLAYING", +} + +## Folder for the player key, the save and the recordings. If it is empty, +## the command line decides; tests set a folder of their own. +@export var storage_dir := "" +## Network interface an open world listens on. Tests use the loopback one. +var listen_address := "*" +## The command line arguments after "--". Tools and tests may set their own +## before the scene enters the tree. +var command_line := OS.get_cmdline_user_args() + +var options: LaunchOptions +var store: SaveStore +## The running session, or null in the menu. A guest's session has no world +## until the host sent it. +var session: GwSession +## The network connection of an open world or a guest, or null. +var link: SessionLink +var clock: TickClock +## The style the playing field is drawn in. +var art: Art +var menu: Menu +var showcase: Showcase +var map_view: MapView +var marker: TileMarker +var avatar: Avatar +var figures: Figures +var hud: Hud +## How often a guest's world diverged and was replaced by the host's. +var resyncs := 0 + +var _player_key: PackedByteArray +var _next_dig_tick := 0 +## True while an unusable save could not be set aside. Saving is refused +## then, so that the unusable save is never overwritten. +var _save_blocked := false +var _figure_wait := 0.0 +## The address a guest is joining, for messages. +var _joining := "" +## The host's accepted guests when _process last looked. A new one ends a +## pause. +var _known_guests := 0 + + +func _ready() -> void: + I18n.install() + InputSetup.install() + options = LaunchOptions.parse(command_line) + for argument in options.ignored: + push_warning("ignored command line argument: %s" % argument) + if storage_dir.is_empty(): + storage_dir = options.storage_dir() + store = SaveStore.new(storage_dir) + _player_key = store.player_key() + if _player_key.is_empty(): + push_warning("the player key cannot be read: %s" % error_string(store.last_read_error)) + clock = TickClock.new(GwSession.TICKS_PER_SECOND) + art = Art.new() + art.load_overrides(Art.OVERRIDE_DIR) + if not art.select(options.style): + art.select(StringName(store.style())) + map_view = MapView.new() + map_view.art = art + add_child(map_view) + marker = TileMarker.new() + marker.art = art + add_child(marker) + figures = Figures.new() + figures.art = art + add_child(figures) + avatar = Avatar.new() + avatar.art = art + add_child(avatar) + var camera := Camera2D.new() + camera.zoom = CAMERA_ZOOM + avatar.add_child(camera) + hud = Hud.new() + add_child(hud) + menu = Menu.new() + add_child(menu) + menu.name_field.text = options.player_name if not options.player_name.is_empty() else store.player_name() + menu.address_field.text = options.address if not options.address.is_empty() else store.last_address() + art.changed.connect(_redraw_art) + menu.play_chosen.connect(play) + menu.join_chosen.connect(join) + menu.showcase_chosen.connect(open_showcase) + showcase = Showcase.new() + showcase.art = art + showcase.closed.connect(close_showcase) + showcase.hide() + showcase.process_mode = Node.PROCESS_MODE_DISABLED + add_child(showcase) + _show_menu("") + match options.start: + "solo": + play(false) + "host": + play(true) + "join": + join(options.address) + + +## Plays the saved world, or a new one, as its host. With `open`, guests may +## join. Returns false if it could not start; the menu then says why. +func play(open: bool) -> bool: + if not _can_start(): + return false + var next := GwSession.new() + var set_aside := false + if store.has_save(): + var save := store.read_save() + if store.last_read_error != OK: + _show_menu(tr("SAVE_UNREADABLE")) + return false + set_aside = not _load(next, save) + if not next.is_running(): + next.start_new(NEW_WORLD_SEED) + if open: + var net := NetLink.new() + if net.host(options.port, listen_address) != OK: + _show_menu(tr("OPEN_FAILED") % options.port) + return false + link = SessionLink.new(next, net) + session = next + session.join(_player_key, _player_name()) + _run_tick() + _show_world() + if set_aside: + hud.flash("LOAD_FAILED") + return true + + +## Starts joining the host at `address`, "host" or "host:port". The world +## arrives later; until then the menu says that it is connecting. +func join(address: String) -> void: + if not _can_start(): + return + var target := NetLink.split_address(address) + if target.is_empty(): + _show_menu(tr("ADDRESS_INVALID") % address) + return + var net := NetLink.new() + if net.join(target[0], target[1]) != OK: + _show_menu(tr("CONNECT_FAILED") % address) + return + session = GwSession.new() + session.start_guest(_player_key, _player_name()) + link = SessionLink.new(session, net) + _joining = address + store.set_last_address(address) + _show_menu(tr("CONNECTING") % address) + + +func _can_start() -> bool: + if session != null: + return false + if _player_key.is_empty(): + _show_menu(tr("KEY_UNREADABLE")) + return false + store.set_player_name(menu.player_name()) + return true + + +func _player_name() -> String: + var typed := menu.player_name() + return typed if not typed.is_empty() else tr("DEFAULT_NAME") + + +## Starts `next` from `save`. If the save is unusable, it is set aside so +## that the next save does not overwrite it, and false is returned. +func _load(next: GwSession, save: PackedByteArray) -> bool: + if next.start_loaded(save): + _save_blocked = false + return true + var aside := store.set_aside_save() + if aside.is_empty(): + _save_blocked = true + push_warning("save not usable (%s) and could not be set aside; saving is off" % next.last_error()) + else: + push_warning("save not usable (%s), kept as %s" % [next.last_error(), aside]) + return false + + +func _show_menu(status: String) -> void: + menu.show_status(status) + menu.show() + hud.hide() + for node: Node2D in [avatar, marker, figures]: + node.hide() + + +## Shows the world of the session and puts the figure on the spawn. +func _show_world() -> void: + menu.hide() + hud.show() + for node: Node2D in [avatar, marker, figures]: + node.show() + map_view.session = session + map_view.reload() + figures.session = session + avatar.player = session.local_player() + figures.clear() + avatar.is_walkable = session.is_walkable + avatar.position = GridMotion.tile_center(session.spawn_point()) + hud.show_stone(session.item_count(GwSession.ITEM_STONE)) + _update_status() + _next_dig_tick = 0 + + +## Shows the style samples instead of the menu. +func open_showcase() -> void: + menu.hide() + showcase.time = 0.0 + showcase.process_mode = Node.PROCESS_MODE_INHERIT + showcase.show() + + +## Goes back from the style samples to the menu. +func close_showcase() -> void: + showcase.hide() + showcase.process_mode = Node.PROCESS_MODE_DISABLED + menu.show() + + +## Draws the playing field again after the style changed, and remembers the +## style for the next start. +func _redraw_art() -> void: + map_view.redraw() + for node: Node2D in [marker, figures, avatar]: + node.queue_redraw() + store.set_style(String(art.style.key())) + + +## Says in the status line how the game is shared, and with how many. +func _update_status() -> void: + if link == null: + hud.show_status("") + elif session.is_host(): + hud.show_status(tr("STATUS_HOST") % [link.link.local_port(), session.online_players().size()]) + else: + hud.show_status(tr("STATUS_GUEST") % [_joining, session.online_players().size()]) + + +## Ends a guest's session and goes back to the menu with `message`. +func _leave(message: String) -> void: + if session == null: + return + _write_recording() + if link != null: + link.link.close() + link = null + session = null + map_view.session = null + map_view.reload() + avatar.is_walkable = Callable() + figures.clear() + _show_menu(message) + + +## A broken session runs no more ticks. Instead of leaving the others +## waiting, a host closes its world to guests and a guest leaves. The host's +## status line says why its world stands still. +func _stop_sharing() -> void: + if not session.is_host(): + _leave(tr("SIMULATION_STOPPED")) + return + if link != null: + link.link.close() + link = null + figures.clear() + hud.show_status(tr("SIMULATION_STOPPED")) + + +## Ends a pause once the host accepted a guest: the guest joins in the next +## tick, and a paused world would keep it waiting. A connection that has not +## greeted yet is no guest. +func _end_pause_for_new_guests() -> void: + var guests := session.guest_count() + if guests > _known_guests and clock.paused: + _set_paused(false) + _known_guests = guests + + +func _process(delta: float) -> void: + if link != null: + for notice in link.pump(delta): + _on_notice(notice) + if session != null and session.is_broken(): + _stop_sharing() + if session == null or not session.is_running() or session.is_broken(): + return + _end_pause_for_new_guests() + _update_marker() + if Input.is_action_pressed("dig") and not clock.paused: + _try_dig() + if session.is_host(): + for i in clock.advance(delta): + _run_tick() + else: + for i in mini(session.pending_ticks(), MAX_GUEST_TICKS_PER_FRAME): + _apply(session.step()) + if link != null: + link.flush() + _figure_wait -= delta + if _figure_wait <= 0.0: + _figure_wait = FIGURE_SECONDS + link.send_figure(avatar.position) + + +func _on_notice(notice: Dictionary) -> void: + # An earlier notice of the same batch may have ended the session. + if session == null: + return + match notice["type"]: + "world_received": + if notice["resync"]: + resyncs += 1 + _keep_diverged_recording() + map_view.reload() + hud.show_stone(session.item_count(GwSession.ITEM_STONE)) + hud.flash("RESYNCED") + else: + _show_world() + "refused": + _leave(tr(REFUSAL_TEXTS.get(notice["reason"], "PROTOCOL_ERROR"))) + "protocol_error": + push_warning("broken message from the host: %s" % session.last_error()) + _leave(tr("PROTOCOL_ERROR")) + "host_lost": + _leave(tr("HOST_LEFT")) + "failed": + _leave(tr("CONNECT_FAILED") % _joining) + "divergence": + push_warning("guest %d diverged at tick %d with state %s" % [notice["peer"], notice["tick"], notice["hash"]]) + "figure": + figures.place(notice["player"], notice["position"]) + + +## Keeps the recording of a guest's world that diverged from the host's, so +## that the divergence can be replayed. +func _keep_diverged_recording() -> void: + var path := store.write_recording(session.take_diverged_recording()) + push_warning("the world diverged from the host's; its recording is %s" % path) + + +func _unhandled_input(event: InputEvent) -> void: + if session == null or not session.is_running(): + return + if event.is_action_pressed("save_game"): + if session.is_host(): + hud.flash("SAVED" if save_game() else "SAVE_FAILED") + else: + hud.flash("HOST_SAVES") + elif event.is_action_pressed("load_game"): + _reload() + elif event.is_action_pressed("toggle_pause"): + # Ending a pause is always allowed, starting one only alone. + if clock.paused or (session.is_host() and session.guest_count() == 0): + _set_paused(not clock.paused) + else: + hud.flash("PAUSE_ONLY_ALONE") + elif event.is_action_pressed("next_style"): + art.select_next() + hud.flash("STYLE_SWITCHED", [tr(art.style.title_key())]) + + +## Goes back to the saved world. Only when playing alone: guests would no +## longer have the host's world. +func _reload() -> void: + if link != null: + hud.flash("LOAD_ONLY_ALONE") + return + var save := store.read_save() + if store.last_read_error != OK: + hud.flash("LOAD_FAILED") + return + var next := GwSession.new() + if not _load(next, save): + hud.flash("LOAD_FAILED") + return + _write_recording() + session = next + session.join(_player_key, _player_name()) + _run_tick() + _show_world() + hud.flash("LOADED") + + +func _set_paused(paused: bool) -> void: + clock.paused = paused + avatar.set_process(not paused) + hud.show_paused(paused) + + +func _notification(what: int) -> void: + if what != NOTIFICATION_WM_CLOSE_REQUEST or session == null: + return + if session.is_running(): + if session.is_host() and not save_game(): + push_warning("the game could not be saved on close") + _write_recording() + if link != null: + link.link.close() + + +func _run_tick() -> void: + _apply(session.step()) + if session.tick() % AUTOSAVE_TICKS == 0 and not save_game(): + hud.flash("SAVE_FAILED") + push_warning("the automatic save failed") + + +func _apply(events: Array[Dictionary]) -> void: + var local := session.local_player() + for event in events: + match event["type"]: + "tile_hit": + var tile := Vector2i(event["x"], event["y"]) + map_view.refresh_tile(tile) + # The own hits showed their burst when they were made. + if event["player"] != local: + map_view.hit(tile) + "tile_changed": + var tile := Vector2i(event["x"], event["y"]) + map_view.refresh_tile(tile) + map_view.hit(tile) + "item_gained": + if event["player"] == local: + hud.show_stone(session.item_count(GwSession.ITEM_STONE)) + "inventory_full": + if event["player"] == local: + hud.flash("INVENTORY_FULL") + "player_joined": + if event["player"] != local: + hud.flash("PLAYER_JOINED", [session.player_name(event["player"])]) + else: + avatar.player = local + avatar.queue_redraw() + _update_status() + "player_left": + figures.forget(event["player"]) + hud.flash("PLAYER_LEFT", [session.player_name(event["player"])]) + _update_status() + if session.is_broken(): + hud.flash("SIMULATION_STOPPED") + push_error(session.last_error()) + + +func _update_marker() -> void: + var tile := GridMotion.tile_of(get_global_mouse_position()) + var distance := avatar.position.distance_to(GridMotion.tile_center(tile)) + marker.show_tile(tile, distance <= REACH_TILES * GridMotion.TILE_SIZE) + + +func _try_dig() -> void: + if marker.in_reach: + dig_at(marker.tile) + + +## Queues a pickaxe hit on `tile`, at most once per cooldown, and shows its +## burst of chips at once: the hit itself lands with a later tick, which for a +## guest comes back from the host only after the round trip. The caller must +## check the reach: figures move outside the lockstep, so the core does not +## know where the figure is. The core checks the cooldown, the tile, its +## terrain, the tool tier and the free space in the inventory. +func dig_at(tile: Vector2i) -> void: + if session.tick() < _next_dig_tick: + return + session.dig(tile.x, tile.y) + _next_dig_tick = session.tick() + GwSession.DIG_COOLDOWN_TICKS + if session.terrain_at(tile.x, tile.y) != GwSession.TERRAIN_GROUND: + map_view.hit(tile) + + +## Writes the running world as the save. Returns false if nothing was +## written: for a guest, whose world belongs to the host, and while an +## unusable save could not be set aside. +func save_game() -> bool: + if _save_blocked or not session.is_host(): + return false + return store.write_save(session.save()) + + +func _write_recording() -> void: + store.write_recording(session.recording_bytes()) +``` + +Ersetze den gesamten Inhalt von `game/i18n/texts.csv` durch die folgende Fassung. Neu sind „F2: Stil wechseln“ in der Hilfezeile `HELP` und die Zeile `STYLE_SWITCHED`. + +```text +keys,de,en +STONE_COUNT,Stein: %d,Stone: %d +SAVED,Gespeichert,Saved +SAVE_FAILED,Speichern fehlgeschlagen,Saving failed +LOADED,Spielstand geladen,Save loaded +LOAD_FAILED,Der Spielstand ließ sich nicht laden,The save could not be loaded +INVENTORY_FULL,Die Tasche ist voll,Your bag is full +PAUSED,Pause,Paused +SIMULATION_STOPPED,Die Simulation wurde angehalten,The simulation stopped +HELP,WASD: laufen · Linke Maustaste: Stein schlagen · F2: Stil wechseln · F5: speichern · F9: laden · Esc: Pause,WASD: walk · Left mouse button: break rock · F2: change the style · F5: save · F9: load · Esc: pause +DEFAULT_NAME,Spieler,Player +MENU_NAME,Dein Name,Your name +MENU_SOLO,Allein spielen,Play alone +MENU_HOST,Welt öffnen,Open your world +MENU_ADDRESS,Adresse des Hosts,Address of the host +MENU_JOIN,Beitreten,Join +CONNECTING,Verbinde mit %s …,Connecting to %s … +CONNECT_FAILED,Keine Verbindung zu %s,Could not reach %s +ADDRESS_INVALID,Das ist keine Adresse: %s,That is not an address: %s +HOST_LEFT,Der Host hat die Welt geschlossen,The host closed the world +PROTOCOL_ERROR,Die Verbindung zum Host ist gestört,The connection to the host is broken +REFUSED_OTHER_PROTOCOL,Der Host hat eine andere Version des Spiels,The host has another version of the game +REFUSED_OTHER_RULES,Der Host hat andere Spielregeln. Ihr braucht denselben Stand des Spiels.,The host has other rules. You need the same version of the game. +REFUSED_WORLD_FULL,Die Welt ist voll: Vier Spieler sind schon da,The world is full: four players are in it +REFUSED_TOO_MANY_PLAYERS,Diese Welt kennt schon 16 Spieler und nimmt keine neuen mehr auf,This world knows 16 players already and takes no new ones +REFUSED_ALREADY_PLAYING,Ein Spieler mit deinem Schlüssel ist schon in der Welt,A player with your key is in the world already +STATUS_HOST,Welt offen auf Port %d · Spieler: %d,World open on port %d · players: %d +STATUS_GUEST,Gast bei %s · Spieler: %d,Guest of %s · players: %d +OPEN_FAILED,Port %d lässt sich nicht öffnen,Port %d cannot be opened +PLAYER_JOINED,%s ist da,%s joined +PLAYER_LEFT,%s ist gegangen,%s left +RESYNCED,Welt mit dem Host abgeglichen,World matched with the host again +PAUSE_ONLY_ALONE,Pause gibt es nur ohne Gäste,Pausing only works without guests +LOAD_ONLY_ALONE,Laden gibt es nur beim Spielen allein,Loading only works when playing alone +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_PAPERCUT,Scherenschnitt,Papercut +STYLE_OUTLINE,Kontur,Outline +STYLE_GEOMETRIC,Geometrisch,Geometric +STYLE_SWITCHED,Stil: %s,Style: %s +MENU_SHOWCASE,Stilproben ansehen,See the style samples +SHOWCASE_TITLE,Stilprobe: %s,Style sample: %s +SHOWCASE_BACK,Zurück,Back +SHOWCASE_ROCK,Weicher und harter Fels,Soft and hard rock +SHOWCASE_DEAD_SOIL,Toter Boden,Dead soil +SHOWCASE_LIVING_SOIL,Lebendiger Boden,Living soil +SHOWCASE_MOSS,Lebensmoos,Life moss +SHOWCASE_PLANT,Faserpflanze wächst,A fibre plant grows +SHOWCASE_GRAZER,Weidetier,Grazer +SHOWCASE_VINE,Ranke trägt Steine,A vine carries stones +SHOWCASE_RUIN,Ruine erwacht,A ruin wakes +SHOWCASE_FIGURES,Spielfiguren,Player figures +``` + +Das Bildschirmfoto-Werkzeug lernt `--style=NAME` und `--showcase` und stellt eine zweite Figur mit Namen neben die eigene, damit die Namen auf dem Bild zu sehen sind. Das Foto entsteht jetzt acht Einzelbilder nach dem Schlag, solange die Splitter noch fliegen. + +Ersetze den gesamten Inhalt von `game/tools/screenshot.gd` durch: + +```gdscript +extends SceneTree +## Starts the game in a temporary storage folder, walks the figure toward the +## rock beside the clearing, hits it once and saves a screenshot. A second +## figure with a name stands beside it. Lets a change to the look be checked +## without playing by hand. The player's real key and save stay untouched. +## +## From the repository root (opens a window for a moment): +## godot --path game -s res://tools/screenshot.gd -- +## [--style=NAME] [--showcase] +## --style=NAME draws in that style; --showcase shows the style samples +## instead of the game. + +const STORAGE := "user://screenshot-run" +## Frames between the hit and the picture: the burst of chips is still in +## the air, and the crack is there. +const FRAMES_AFTER_HIT := 8 +## Seconds into the showcase at which the picture is taken: the plant is +## nearly grown and the animal in the air. +const SHOWCASE_SECONDS := 5.2 + + +func _initialize() -> void: + _run.call_deferred() + + +func _run() -> void: + var output := "user://screenshot.png" + var style := "" + var showcase := false + for argument in OS.get_cmdline_user_args(): + if argument.begins_with("--style="): + style = argument + elif argument == "--showcase": + showcase = true + else: + output = argument + DirAccess.make_dir_recursive_absolute(STORAGE) + var main: Node = load("res://main.tscn").instantiate() + main.storage_dir = STORAGE + main.command_line = PackedStringArray([style] if not style.is_empty() else []) + root.add_child(main) + if showcase: + main.open_showcase() + await process_frame + main.showcase.time = SHOWCASE_SECONDS + await process_frame + await process_frame + else: + await _play(main) + + var image := root.get_texture().get_image() + var error := image.save_png(output) + print("screenshot %s: %s" % ["saved" if error == OK else "failed", ProjectSettings.globalize_path(output)]) + root.remove_child(main) + main.free() + _remove(STORAGE) + quit(0 if error == OK else 1) + + +## Plays alone: walks four tiles to the right, toward the rock outcrop +## beside the clearing, and hits it once. +func _play(main: Node) -> void: + main.play(false) + await process_frame + for frame in 48: + main.avatar.walk(Vector2.RIGHT, 1.0 / 60.0) + await process_frame + var session: GwSession = main.session + main.figures.place(session.local_player(), main.avatar.position + Vector2(-40, 8)) + var rock := session.spawn_point() + Vector2i(7, 0) + main.dig_at(rock) + for frame in FRAMES_AFTER_HIT: + await process_frame + + +func _remove(path: String) -> void: + for file in DirAccess.get_files_at(path): + DirAccess.remove_absolute(path.path_join(file)) + for directory in DirAccess.get_directories_at(path): + _remove(path.path_join(directory)) + DirAccess.remove_absolute(path) +``` + +- [ ] **Step 4: Tests ausführen und Erfolg sehen** + +Run: + +```bash +godot --headless --path game --import +godot --headless --path game -s res://tests/run_tests.gd +``` + +Expected: Exit-Code 0 und `132 passed, 0 failed`. Die einzigen Warnungen sind die drei bekannten Zeilen. + +- [ ] **Step 5: Bildschirmfotos prüfen** + +Run: + +```bash +godot --path game -s res://tools/screenshot.gd -- "$TEMP/m1d-task4-spiel.png" --style=outline +godot --path game -s res://tools/screenshot.gd -- "$TEMP/m1d-task4-vitrine.png" --showcase +``` + +Expected: Beide Aufrufe enden mit `screenshot saved: …` und geben weder Warnung noch Fehler aus. Beide Bilder zeigen noch die Platzhalter, denn die Stile zeichnen erst ab Task 5 selbst. Das erste Bild zeigt das Spiel wie nach Task 2, dazu links eine zweite Figur, über der scharf „Spieler“ steht, und kleine helle Splitter am getroffenen Fels. Das zweite Bild zeigt die Vitrine: oben „Stilprobe: Scherenschnitt“, in der Mitte die Szene aus Rechtecken, Kreisen und Linien mit neun weißen Beschriftungen, unten die Knöpfe „1 Scherenschnitt“ (ausgegraut), „2 Kontur“, „3 Geometrisch“ und „Zurück“. + +- [ ] **Step 6: Alles prüfen** + +Run: + +```bash +cargo fmt --all -- --check && cargo clippy --workspace --all-targets -- -D warnings && cargo test --workspace +cargo build -p grovewright-bridge +``` + +Expected: keine Ausgabe von rustfmt, keine Warnung von Clippy, zusammen 151 bestandene Rust-Tests + +- [ ] **Step 7: Commit und Hochladen** + +```bash +git add game +git commit -F - <<'EOF' +Stil wählen, merken und fotografieren + +F2 wechselt im Spiel den Stil und meldet ihn, --style=NAME wählt ihn +beim Start, und das Spiel merkt sich die Wahl in den Einstellungen. +Das Bildschirmfoto-Werkzeug nimmt Bilder in einem bestimmten Stil und +von der Vitrine auf und zeigt eine zweite Figur mit Namen. + +Co-Authored-By: Claude Opus 5.5 +EOF +git push +``` + +--- + +### Task 5: Stil Scherenschnitt + +Lagen aus geschnittenem Papier ohne Umriss, mit weichen Schatten. Gedeckte Erdfarben; Lebendiges leuchtet grün und golden. Das Licht fällt von oben links, also wirft Fels seinen Schatten nach rechts unten auf den Boden. + +**Files:** +- Modify: `game/scripts/art/papercut_style.gd` (ganze Datei ersetzen) +- Test: `game/tests/test_papercut_style.gd` (neu) + +**Interfaces:** +- Consumes: `ArtStyle` mit seinen Zeichenmethoden und Helfern, `TileShape`, `Art.IDS`, die Einträge von `state` laut Kopf von `art_style.gd` +- Produces: `PapercutStyle` zeichnet jede Kennung selbst; `PLAYER_COLORS: Array[Color]` mit acht Farben und `player_color(player: int) -> Color` + +- [ ] **Step 1: Fehlschlagenden Test schreiben** + +Create `game/tests/test_papercut_style.gd`: + +```gdscript +extends TestCase + + +func test_four_players_have_four_colours() -> void: + var style := PapercutStyle.new() + var colours := {} + for player in 4: + colours[style.player_color(player)] = true + check_eq(colours.size(), 4, "four players, four colours") + check_eq(style.player_color(-1), style.player_color(0), "a player not in the world yet looks like the first") +``` + +- [ ] **Step 2: Test ausführen und Fehlschlag sehen** + +Run: + +```bash +godot --headless --path game --import +godot --headless --path game -s res://tests/run_tests.gd +``` + +Expected: Exit-Code 1 und `132 passed, 1 failed`; unter `FAIL test_papercut_style.gd test_four_players_have_four_colours` steht `four players, four colours: expected 4, got 2`, denn der Platzhalter kennt nur zwei Farben. + +- [ ] **Step 3: Stil schreiben** + +Ersetze den gesamten Inhalt von `game/scripts/art/papercut_style.gd` durch: + +```gdscript +class_name PapercutStyle +extends ArtStyle +## Layers of cut paper without outlines, with soft shadows. Muted earth +## colours; living things glow in green and gold. Light falls from the top +## left, so rock casts its shadow onto the ground below and to the right. + +const KEY := &"papercut" + +const GROUND := Color("9a8468") +const GROUND_FLECK := Color("8d7860") +const GROUND_FIBRE := Color("a8926f") +const SHADOW := Color(0.16, 0.11, 0.07, 0.38) +## The outer, fainter part of a shadow on the ground. +const SHADOW_FAINT := Color(0.16, 0.11, 0.07, 0.17) +const SOFT_BASE := Color("8b8275") +const SOFT_TOP := Color("bab1a2") +const SOFT_SCRAP := Color("a69d8e") +const HARD_BASE := Color("454c5b") +const HARD_TOP := Color("646c7e") +const HARD_FACET := Color("7f8899") +const BEDROCK_BASE := Color("241e1a") +const BEDROCK_TOP := Color("342b25") +const CUT := Color("2a2119") +const DEAD_CUT := Color("6f5e4a") +const LIVING_BASE := Color("587f3e") +const LIVING_TOP := Color("72a04c") +const GOLD := Color("ebc75e") +const MOSS_DARK := Color("4b7633") +const MOSS_LIGHT := Color("89bd59") +const VINE_DARK := Color("3d672b") +const VINE_LIGHT := Color("6e9d44") +const LEAF := Color("88ba57") +const STONE_BASE := Color("8f877a") +const STONE_TOP := Color("b1a999") +const STEM := Color("5a8739") +const FIBRE := Color("f1e8d2") +const SEED := Color("7a5b3a") +const WOOL := Color("eee6d4") +const WOOL_SHADE := Color("d3c7b0") +const FACE := Color("4a3a30") +const RUIN_BASE := Color("767067") +const RUIN_TOP := Color("a39c90") +const RUIN_DARK := Color("2d2722") +const SKIN := Color("e9cba4") +const HAIR := Color("4b3528") +const LEGS := Color("3a2f28") +const MARKER_NEAR := Color("f0cf6a") +const MARKER_FAR := Color("c25b4c") +const PLAYER_COLORS: Array[Color] = [ + Color("c5563f"), Color("3f7cad"), Color("d99a3d"), Color("6a9a4c"), + Color("8b5a7c"), Color("4a9a96"), Color("b07a52"), Color("6b6fa8"), +] +## How far each of the two bands of a rock shadow reaches onto the ground, +## in pixels: a darker band along the rock and a fainter one beyond it. +const SHADOW_DEPTH := 1.8 + + +func key() -> StringName: + return KEY + + +func title_key() -> String: + return "STYLE_PAPERCUT" + + +func player_color(player: int) -> Color: + return PLAYER_COLORS[maxi(0, player) % PLAYER_COLORS.size()] + + +func _ground(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: + canvas.draw_rect(rect, GROUND) + _paper_bits(canvas, rect, state.get("variant", 0), GROUND_FLECK, GROUND_FIBRE) + _shadow(canvas, rect, state.get("solid", 0)) + + +func _soft_rock(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: + var solid: int = state.get("solid", 0) + var variant: int = state.get("variant", 0) + _raised(canvas, rect, solid, SOFT_BASE, SOFT_TOP) + if share(variant, 3) < 0.4: + var at := rect.position + Vector2(4, 4) + Vector2(share(variant, 4), share(variant, 5)) * 6.0 + canvas.draw_colored_polygon(regular_polygon(at, 2.0, 5, share(variant, 6) * TAU), SOFT_SCRAP) + + +func _hard_rock(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: + var solid: int = state.get("solid", 0) + var variant: int = state.get("variant", 0) + _raised(canvas, rect, solid, HARD_BASE, HARD_TOP) + # An angled facet, as if the hard stone had split along a plane. + var c := rect.get_center() + var flip := 1.0 if share(variant, 3) < 0.5 else -1.0 + var facet := PackedVector2Array([ + c + Vector2(-5.0 * flip, -4.0), c + Vector2(4.0 * flip, -2.0), c + Vector2(-1.0 * flip, 5.0), + ]) + canvas.draw_colored_polygon(facet, HARD_FACET) + + +func _bedrock(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: + _raised(canvas, rect, state.get("solid", 0), BEDROCK_BASE, BEDROCK_TOP) + + +func _cracks(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: + var hits: int = state.get("hits", 1) + var variant: int = state.get("variant", 0) + var c := rect.get_center() + var start := c + Vector2(-5.0, -3.0 + share(variant, 8) * 2.0) + canvas.draw_colored_polygon(slit(start, c + Vector2(4.0, 3.0), 1.4), CUT) + if hits >= 2: + canvas.draw_colored_polygon(slit(c + Vector2(3.5, -5.0), c + Vector2(-1.0, 4.5), 1.2), CUT) + + +func _hit(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: + var age: float = state.get("age", 0.0) + var variant: int = state.get("variant", 0) + var c := rect.get_center() + for flake in 5: + var direction := Vector2.from_angle(TAU * flake / 5.0 + share(variant, flake) * 0.8) + var at := c + direction * (2.0 + 9.0 * age) + Vector2(0, 6.0 * age * age) + var turn := age * 5.0 + flake + var color := SOFT_TOP if flake % 2 == 0 else SOFT_BASE + color.a = 1.0 - age + canvas.draw_colored_polygon(regular_polygon(at, 1.6, 3, turn), color) + + +func _figure(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: + var facing: float = state.get("facing", 1.0) + var step: float = state.get("step", 0.0) + var moving: bool = state.get("moving", false) + var swing := sin(step) if moving else 0.0 + var lift := absf(swing) * 1.0 + var c := rect.get_center() + Vector2(0, -lift) + var feet := rect.get_center() + Vector2(0, 6.0) + canvas.draw_colored_polygon(ellipse(feet, Vector2(4.5, 1.6)), SHADOW) + for side in [-1.0, 1.0]: + var leg := Vector2(side * 1.6 + side * swing * 1.2, 4.5) + canvas.draw_rect(Rect2(c + leg - Vector2(0.9, 1.5), Vector2(1.8, 3.0)), LEGS) + var body := PackedVector2Array([ + c + Vector2(-3.6, 4.0), c + Vector2(-2.6, -1.8), c + Vector2(2.6, -1.8), c + Vector2(3.6, 4.0), + ]) + canvas.draw_colored_polygon(body, player_color(state.get("player", -1))) + var head := c + Vector2(0, -4.6) + canvas.draw_circle(head, 3.1, SKIN, true, -1.0, true) + # Hair on the back of the head, so that the figure looks the way it goes. + canvas.draw_colored_polygon(ellipse(head + Vector2(-facing * 1.2, -1.2), Vector2(2.4, 1.9)), HAIR) + canvas.draw_circle(head + Vector2(facing * 1.3, -0.2), 0.55, LEGS, true, -1.0, true) + + +func _marker(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: + var color := MARKER_NEAR if state.get("reachable", false) else MARKER_FAR + var inner := rect.grow(-1.0) + var arm := 4.0 + var thick := 1.6 + for corner in 4: + var x := inner.position.x if corner % 3 == 0 else inner.end.x + var y := inner.position.y if corner < 2 else inner.end.y + var sx := 1.0 if corner % 3 == 0 else -1.0 + var sy := 1.0 if corner < 2 else -1.0 + var bracket := PackedVector2Array([ + Vector2(x, y), Vector2(x + sx * arm, y), Vector2(x + sx * arm, y + sy * thick), + Vector2(x + sx * thick, y + sy * thick), Vector2(x + sx * thick, y + sy * arm), Vector2(x, y + sy * arm), + ]) + var shade := bracket.duplicate() + for i in shade.size(): + shade[i] += Vector2(0.6, 0.6) + canvas.draw_colored_polygon(shade, SHADOW) + canvas.draw_colored_polygon(bracket, color) + + +func _dead_soil(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: + var variant: int = state.get("variant", 0) + canvas.draw_rect(rect, GROUND) + if share(variant, 1) < 0.55: + var a := rect.position + Vector2(share(variant, 2) * 6.0 + 2.0, share(variant, 3) * 4.0 + 3.0) + var b := a + Vector2(6.0, 4.0 - share(variant, 4) * 8.0) + canvas.draw_colored_polygon(slit(a, b, 1.1), DEAD_CUT) + canvas.draw_colored_polygon(slit(b, b + Vector2(3.0, 3.0), 0.8), DEAD_CUT) + _shadow(canvas, rect, state.get("solid", 0)) + + +func _living_soil(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: + var living: int = state.get("living", 0) + var variant: int = state.get("variant", 0) + canvas.draw_rect(rect, GROUND) + canvas.draw_colored_polygon(rounded_rect(rect, living, 5.0), LIVING_BASE) + canvas.draw_colored_polygon(rounded_rect(inset_open_sides(rect, living, 1.4), living, 3.6), LIVING_TOP) + if share(variant, 1) < 0.5: + var at := rect.position + Vector2(3, 3) + Vector2(share(variant, 2), share(variant, 3)) * 10.0 + canvas.draw_circle(at, 0.8, GOLD, true, -1.0, true) + _shadow(canvas, rect, state.get("solid", 0)) + + +func _moss(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: + var variant: int = state.get("variant", 0) + var time: float = state.get("time", 0.0) + var foot := Vector2(rect.get_center().x, rect.end.y - 3.0) + var breathe := 1.0 + 0.06 * sway(time, variant, 1.6) + canvas.draw_colored_polygon(ellipse(foot + Vector2(1.0, 1.2), Vector2(6.0, 2.0)), SHADOW) + var blobs: Array[Vector3] = [Vector3(-3.5, -1.5, 3.0), Vector3(0.5, -3.0, 3.6), Vector3(3.8, -1.2, 2.7)] + for blob in blobs: + canvas.draw_circle(foot + Vector2(blob.x, blob.y) * breathe, blob.z * breathe, MOSS_DARK, true, -1.0, true) + for blob in blobs: + canvas.draw_circle(foot + Vector2(blob.x - 0.6, blob.y - 0.8) * breathe, blob.z * 0.7 * breathe, MOSS_LIGHT, true, -1.0, true) + for spark in 3: + var glow := 0.55 + 0.45 * sin(time * 2.0 + spark * 2.1 + share(variant, spark) * TAU) + var gold := GOLD + gold.a = glow + canvas.draw_circle(foot + Vector2(-3.0 + spark * 3.0, -4.0 + (spark % 2) * 2.0), 0.7, gold, true, -1.0, true) + + +func _vine(canvas: CanvasItem, _rect: Rect2, state: Dictionary) -> void: + var points: PackedVector2Array = state.get("points", PackedVector2Array()) + var time: float = state.get("time", 0.0) + if points.size() < 2: + return + var shade := points.duplicate() + for i in shade.size(): + shade[i] += Vector2(1.0, 1.4) + canvas.draw_polyline(shade, SHADOW, 3.0, true) + canvas.draw_polyline(points, VINE_DARK, 3.2, true) + var top := points.duplicate() + for i in top.size(): + top[i] -= Vector2(0.3, 0.4) + canvas.draw_polyline(top, VINE_LIGHT, 1.6, true) + var count := 9 + for i in count: + var share_along := (i + 0.5) / count + var at := along(points, share_along) + var ahead := along(points, minf(1.0, share_along + 0.02)) + var side := 1.0 if i % 2 == 0 else -1.0 + var angle := (ahead - at).angle() + side * 0.9 + 0.25 * sway(time, i, 1.4) + canvas.draw_colored_polygon(leaf(at, angle, 5.0, 2.6), LEAF) + + +func _fiber_plant(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: + var growth: float = state.get("growth", 1.0) + var time: float = state.get("time", 0.0) + var foot := Vector2(rect.get_center().x, rect.end.y - 2.0) + canvas.draw_colored_polygon(ellipse(foot + Vector2(1.0, 0.8), Vector2(4.0, 1.4)), SHADOW) + if growth < 0.15: + canvas.draw_colored_polygon(ellipse(foot + Vector2(0, -1.2), Vector2(2.2, 1.6)), SEED) + if growth > 0.08: + canvas.draw_colored_polygon(leaf(foot + Vector2(0, -2.2), -PI / 2.0, 2.5, 1.4), LEAF) + return + var grown := (growth - 0.15) / 0.85 + var height := lerpf(4.0, 20.0, grown) + canvas.draw_set_transform(foot, 0.07 * sway(time, 3, 1.1), Vector2.ONE) + canvas.draw_colored_polygon(slit(Vector2.ZERO, Vector2(0, -height), 1.6), STEM) + var leaves := 1 + int(grown * 5.0) + for i in leaves: + var at := Vector2(0, -height * (0.2 + 0.7 * i / maxf(1.0, leaves))) + var side := 1.0 if i % 2 == 0 else -1.0 + canvas.draw_colored_polygon(leaf(at, -PI / 2.0 + side * 1.0, lerpf(3.0, 6.0, grown), 2.6), LEAF) + if grown > 0.75: + var tuft := (grown - 0.75) / 0.25 + for puff in 4: + var at := Vector2(-2.0 + puff * 1.4, -height - 1.0 - (puff % 2) * 1.2) + canvas.draw_circle(at, 1.4 * tuft + 0.3, FIBRE, true, -1.0, true) + canvas.draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE) + + +func _stone(canvas: CanvasItem, rect: Rect2, _state: Dictionary) -> void: + var c := rect.get_center() + canvas.draw_colored_polygon(ellipse(c + Vector2(0.8, 2.6), Vector2(3.0, 1.0)), SHADOW) + canvas.draw_colored_polygon(regular_polygon(c, 3.0, 6, 0.3), STONE_BASE) + canvas.draw_colored_polygon(regular_polygon(c + Vector2(-0.4, -0.5), 2.2, 6, 0.3), STONE_TOP) + + +func _grazer(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: + var hop: float = state.get("hop", 1.0) + var facing: float = state.get("facing", 1.0) + var lift := sin(PI * hop) * 4.0 + var foot := Vector2(rect.get_center().x, rect.end.y - 2.0) + canvas.draw_colored_polygon(ellipse(foot + Vector2(0.8, 0.6), Vector2(5.5 - lift * 0.5, 1.6)), SHADOW) + var body := foot + Vector2(0, -6.0 - lift) + for leg in [-2.5, 2.5]: + canvas.draw_rect(Rect2(body + Vector2(leg - 0.7, 3.0), Vector2(1.4, 3.0 - lift * 0.3)), FACE) + var puffs: Array[Vector3] = [ + Vector3(-3.5, 0.5, 3.0), Vector3(0.0, -1.5, 3.4), Vector3(3.2, 0.3, 3.0), Vector3(0.0, 1.8, 3.0), + ] + for puff in puffs: + canvas.draw_circle(body + Vector2(puff.x, puff.y), puff.z, WOOL_SHADE, true, -1.0, true) + for puff in puffs: + canvas.draw_circle(body + Vector2(puff.x - 0.5, puff.y - 0.6), puff.z * 0.78, WOOL, true, -1.0, true) + var head := body + Vector2(facing * 5.0, -1.2) + canvas.draw_colored_polygon(ellipse(head, Vector2(2.4, 2.0)), FACE) + canvas.draw_colored_polygon(ellipse(head + Vector2(-facing * 1.6, -1.6), Vector2(1.2, 0.7)), FACE) + canvas.draw_circle(head + Vector2(facing * 0.9, -0.4), 0.45, WOOL, true, -1.0, true) + + +func _ruin(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: + var awake: float = state.get("awake", 0.0) + var time: float = state.get("time", 0.0) + var tile := rect.size.x / 3.0 + var floor_y := rect.end.y - 3.0 + var center := Vector2(rect.get_center().x, floor_y - tile * 1.2) + # The shadow of the ruin and the dark space under its arch. + canvas.draw_colored_polygon(ellipse(Vector2(center.x + 3.0, floor_y + 1.0), Vector2(rect.size.x * 0.48, 3.5)), SHADOW) + var opening := Rect2(rect.position.x + tile * 0.75, rect.position.y + tile * 0.9, tile * 1.5, floor_y - rect.position.y - tile * 0.9) + canvas.draw_rect(opening, RUIN_DARK) + # The glow of the sleeping grove, brighter the more it is awake. + for ring in 4: + var gold := GOLD + gold.a = awake * (0.18 + 0.12 * ring) + canvas.draw_circle(center + Vector2(0, 4), 9.0 - ring * 2.0, gold, true, -1.0, true) + var crown := center + Vector2(0, 0.5 * sin(time * 1.2)) + canvas.draw_colored_polygon(slit(Vector2(center.x, floor_y), crown + Vector2(0, 4), 2.6), SEED) + for blob in 3: + var at := crown + Vector2(-3.5 + blob * 3.5, -1.0 + (blob % 2) * -2.0) + canvas.draw_circle(at, 3.4, MOSS_DARK.lerp(GOLD, awake * 0.35), true, -1.0, true) + # Two pillars and the lintel, each of two layers of paper. + for x in [rect.position.x, rect.end.x - tile * 0.75]: + var pillar := Rect2(x, rect.position.y + tile * 0.6, tile * 0.75, floor_y - rect.position.y - tile * 0.6) + canvas.draw_colored_polygon(rounded_rect(pillar, 0, 2.0), RUIN_BASE) + canvas.draw_colored_polygon(rounded_rect(pillar.grow(-1.4), 0, 1.4), RUIN_TOP) + var vein := GOLD + vein.a = awake * 0.8 + canvas.draw_line(pillar.get_center() + Vector2(0, -6), pillar.get_center() + Vector2(1.5, 6), vein, 1.0, true) + var lintel := Rect2(rect.position.x - 1.0, rect.position.y + tile * 0.25, rect.size.x + 2.0, tile * 0.6) + canvas.draw_colored_polygon(rounded_rect(lintel, 0, 2.0), RUIN_BASE) + canvas.draw_colored_polygon(rounded_rect(lintel.grow(-1.4), 0, 1.4), RUIN_TOP) + var step := Rect2(rect.position.x + tile * 0.4, floor_y - 2.0, rect.size.x - tile * 0.8, 3.0) + canvas.draw_rect(step, RUIN_BASE) + + +## A mass of rock: ground where the corners are cut round, the darker lower +## layer of paper and the lighter upper one, inset on the open sides. +func _raised(canvas: CanvasItem, rect: Rect2, solid: int, base: Color, top: Color) -> void: + canvas.draw_rect(rect, GROUND) + _shadow(canvas, rect, solid) + canvas.draw_colored_polygon(rounded_rect(rect, solid, 4.0), base) + canvas.draw_colored_polygon(rounded_rect(inset_open_sides(rect, solid, 1.6), solid, 2.6), top) + + +## The shadow that rock above and to the left casts onto this tile: a darker +## band along the rock and a fainter one beyond, so that it fades. +func _shadow(canvas: CanvasItem, rect: Rect2, solid: int) -> void: + var above := solid & TileShape.N != 0 + var left := solid & TileShape.W != 0 + for band: Array in [[0.0, SHADOW], [SHADOW_DEPTH, SHADOW_FAINT]]: + var offset: float = band[0] + var color: Color = band[1] + if above: + canvas.draw_rect(Rect2(rect.position + Vector2(0, offset), Vector2(rect.size.x, SHADOW_DEPTH)), color) + if left: + var top := offset + SHADOW_DEPTH if above else 0.0 + canvas.draw_rect(Rect2(rect.position + Vector2(offset, top), Vector2(SHADOW_DEPTH, rect.size.y - top)), color) + if not above and not left and solid & TileShape.NW != 0: + canvas.draw_rect(Rect2(rect.position, Vector2(SHADOW_DEPTH + offset, SHADOW_DEPTH + offset)), color) + + +## Small flecks and fibres in the paper, on some tiles. +func _paper_bits(canvas: CanvasItem, rect: Rect2, variant: int, fleck: Color, fibre: Color) -> void: + if share(variant, 11) < 0.25: + var at := rect.position + Vector2(3, 3) + Vector2(share(variant, 12), share(variant, 13)) * 10.0 + canvas.draw_colored_polygon(regular_polygon(at, 1.1 + share(variant, 14), 5, share(variant, 15) * TAU), fleck) + if share(variant, 16) < 0.2: + var from := rect.position + Vector2(2, 2) + Vector2(share(variant, 17), share(variant, 18)) * 9.0 + canvas.draw_line(from, from + Vector2.from_angle(share(variant, 19) * TAU) * 3.5, fibre, 0.7, true) +``` + +- [ ] **Step 4: Tests ausführen und Erfolg sehen** + +Run: + +```bash +godot --headless --path game --import +godot --headless --path game -s res://tests/run_tests.gd +``` + +Expected: Exit-Code 0 und `133 passed, 0 failed`. Die einzigen Warnungen sind die drei bekannten Zeilen. + +- [ ] **Step 5: Bildschirmfotos ansehen** + +Run: + +```bash +godot --path game -s res://tools/screenshot.gd -- "$TEMP/m1d-scherenschnitt-karte.png" --style=papercut +godot --path game -s res://tools/screenshot.gd -- "$TEMP/m1d-scherenschnitt-vitrine.png" --style=papercut --showcase +``` + +Expected: Beide Aufrufe enden mit `screenshot saved: …` ohne Warnung. Sieh dir beide Bilder an. Nirgends steht mehr ein Platzhalter, also kein einfarbiges Rechteck und kein schlichter Kreis. + +- Spiel: Der Boden ist braunes Packpapier mit feinen Flecken und Fasern. Fels besteht aus zwei hellgrauen Papierlagen mit wenigen Flecken, außen abgerundet und innen geschlossen, und wirft nach rechts unten einen weichen Schatten. Im getroffenen Fels steht ein dunkler Schnitt, darum fliegen Papierschnipsel. Die beiden Figuren stehen auf einem Schatten: Körper in Rotbraun, heller Kopf, dunkles Haar, Beine. Über der linken steht scharf „Spieler“. +- Vitrine: Grundgestein fast schwarz, weicher Fels hell, harter Fels blaugrau mit hellen Dreiecksfacetten, alles mit Schatten. Toter Boden mit eingeschnittenen Rissen, lebendiger Boden als grüne Papierlage mit goldenen Pünktchen. Moospolster, eine Ranke als Band mit Blättern und drei Steinen, eine fast ausgewachsene Faserpflanze mit weißen Fasern, ein Wollschaf in der Luft, die Ruine als Steintor mit goldenem Leuchten, zwei Figuren in Rot und Blau. Oben „Stilprobe: Scherenschnitt“, unten ist „1 Scherenschnitt“ ausgegraut. + +- [ ] **Step 6: Alles prüfen** + +Run: + +```bash +cargo fmt --all -- --check && cargo clippy --workspace --all-targets -- -D warnings && cargo test --workspace +cargo build -p grovewright-bridge +``` + +Expected: keine Ausgabe von rustfmt, keine Warnung von Clippy, zusammen 151 bestandene Rust-Tests + +- [ ] **Step 7: Commit und Hochladen** + +```bash +git add game +git commit -F - <<'EOF' +Stil Scherenschnitt + +Lagen aus geschnittenem Papier ohne Umriss, mit weichen Schatten nach +rechts unten. Gedeckte Erdfarben; Moos, lebendiger Boden und die Ruine +leuchten grün und golden. Pflanzen wiegen sich, die Faserpflanze wächst +vom Samen bis zu den Fasern, und jeder Spieler hat eine eigene Farbe. + +Co-Authored-By: Claude Opus 5.5 +EOF +git push +``` + +--- + +### Task 6: Stil Kontur + +Kräftige dunkle Umrisse um flache, starke Farben, gut lesbar wie ein Comic. Felsmassen haben ihren Umriss nur dort, wo sie an offenen Boden grenzen, und jedes Ding hat einen dunklen Umriss um seine ganze Form. + +**Files:** +- Modify: `game/scripts/art/outline_style.gd` (ganze Datei ersetzen) +- Test: `game/tests/test_outline_style.gd` (neu) + +**Interfaces:** +- Consumes: `ArtStyle` mit seinen Zeichenmethoden und Helfern, `TileShape`, `Art.IDS`, die Einträge von `state` laut Kopf von `art_style.gd` +- Produces: `OutlineStyle` zeichnet jede Kennung selbst; `PLAYER_COLORS: Array[Color]` mit acht Farben und `player_color(player: int) -> Color` + +- [ ] **Step 1: Fehlschlagenden Test schreiben** + +Create `game/tests/test_outline_style.gd`: + +```gdscript +extends TestCase + + +func test_four_players_have_four_colours() -> void: + var style := OutlineStyle.new() + var colours := {} + for player in 4: + colours[style.player_color(player)] = true + check_eq(colours.size(), 4, "four players, four colours") + check_eq(style.player_color(-1), style.player_color(0), "a player not in the world yet looks like the first") +``` + +- [ ] **Step 2: Test ausführen und Fehlschlag sehen** + +Run: + +```bash +godot --headless --path game --import +godot --headless --path game -s res://tests/run_tests.gd +``` + +Expected: Exit-Code 1 und `133 passed, 1 failed`; unter `FAIL test_outline_style.gd test_four_players_have_four_colours` steht `four players, four colours: expected 4, got 2`. + +- [ ] **Step 3: Stil schreiben** + +Ersetze den gesamten Inhalt von `game/scripts/art/outline_style.gd` durch: + +```gdscript +class_name OutlineStyle +extends ArtStyle +## Bold dark outlines around flat colours, few and strong colours, easy to +## read like a comic. Masses of rock are outlined only where they meet open +## ground, and each thing gets one dark outline around its whole shape. + +const KEY := &"outline" + +const INK := Color("2b1d14") +## Width of the outlines, in pixels of the playing field. +const LINE := 1.1 +const GROUND := Color("d6a35a") +const GROUND_DOT := Color("bd8a43") +const SOFT_FILL := Color("c8c1d6") +const SOFT_SHADE := Color("a79fbd") +const SOFT_LIGHT := Color("eeeaf5") +const HARD_FILL := Color("4a63a6") +const HARD_SHADE := Color("374c88") +const HARD_GLINT := Color("a9c3ff") +const DEAD_CRACK := Color("9a6b31") +const LIVING := Color("5cbf48") +const LIVING_SHADE := Color("3f9a33") +const SPARK := Color("ffe14d") +const MOSS := Color("49b13a") +const MOSS_LIGHT := Color("8de06a") +const VINE := Color("3fa34a") +const LEAF := Color("6fd259") +const STONE := Color("b3aba0") +const STEM := Color("3f9a33") +const FIBRE := Color("fffaf0") +const SEED := Color("8a5a35") +const WOOL := Color("fbf8f0") +const FACE := Color("3a2a22") +const RUIN := Color("a8a49b") +const RUIN_SHADE := Color("8a857b") +const RUIN_DARK := Color("3b322b") +const GLOW := Color("ffd84a") +const SKIN := Color("f5c79a") +const MARKER_NEAR := Color("7ddc4f") +const MARKER_FAR := Color("ee5a3c") +const PLAYER_COLORS: Array[Color] = [ + Color("e8483b"), Color("2d8fe0"), Color("f5a623"), Color("33b864"), + Color("a35bd6"), Color("1fb5b5"), Color("f06aa8"), Color("6d7ce8"), +] + + +func key() -> StringName: + return KEY + + +func title_key() -> String: + return "STYLE_OUTLINE" + + +func player_color(player: int) -> Color: + return PLAYER_COLORS[maxi(0, player) % PLAYER_COLORS.size()] + + +func _ground(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: + var variant: int = state.get("variant", 0) + canvas.draw_rect(rect, GROUND) + if share(variant, 1) < 0.3: + var at := rect.position + Vector2(3, 3) + Vector2(share(variant, 2), share(variant, 3)) * 10.0 + canvas.draw_circle(at, 0.8, GROUND_DOT, true, -1.0, true) + canvas.draw_circle(at + Vector2(2.2, 1.0), 0.55, GROUND_DOT, true, -1.0, true) + + +func _soft_rock(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: + var solid: int = state.get("solid", 0) + var variant: int = state.get("variant", 0) + _mass(canvas, rect, solid, SOFT_FILL, SOFT_SHADE) + if share(variant, 3) < 0.3: + var at := rect.position + Vector2(4, 4) + Vector2(share(variant, 4), share(variant, 5)) * 5.0 + canvas.draw_line(at, at + Vector2(3.0, -1.2), SOFT_LIGHT, 0.9, true) + + +func _hard_rock(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: + var solid: int = state.get("solid", 0) + var variant: int = state.get("variant", 0) + _mass(canvas, rect, solid, HARD_FILL, HARD_SHADE) + if share(variant, 3) < 0.45: + var at := rect.position + Vector2(5, 5) + Vector2(share(variant, 4), share(variant, 5)) * 5.0 + var glint := PackedVector2Array([at + Vector2(0, -2.4), at + Vector2(1.4, 0), at + Vector2(0, 2.4), at + Vector2(-1.4, 0)]) + canvas.draw_colored_polygon(glint, HARD_GLINT) + canvas.draw_polyline(closed(glint), INK, 0.6, true) + + +func _bedrock(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: + canvas.draw_rect(rect, GROUND) + canvas.draw_colored_polygon(rounded_rect(rect, state.get("solid", 0), 4.0), INK) + + +func _cracks(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: + var hits: int = state.get("hits", 1) + var c := rect.get_center() + canvas.draw_polyline(PackedVector2Array([c + Vector2(-5, -4), c + Vector2(-1, -1), c + Vector2(-2, 2), c + Vector2(4, 5)]), INK, 1.0, true) + if hits >= 2: + canvas.draw_polyline(PackedVector2Array([c + Vector2(5, -5), c + Vector2(2, -1), c + Vector2(3, 2)]), INK, 1.0, true) + + +func _hit(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: + var age: float = state.get("age", 0.0) + var variant: int = state.get("variant", 0) + var c := rect.get_center() + for shard in 5: + var direction := Vector2.from_angle(TAU * shard / 5.0 + share(variant, shard) * 0.8) + var at := c + direction * (2.0 + 9.0 * age) + Vector2(0, 6.0 * age * age) + var points := regular_polygon(at, 1.7, 3, age * 6.0 + shard) + var fill := SOFT_FILL + var ink := INK + fill.a = 1.0 - age + ink.a = 1.0 - age + canvas.draw_colored_polygon(points, fill) + canvas.draw_polyline(closed(points), ink, 0.6, true) + + +func _figure(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: + var facing: float = state.get("facing", 1.0) + var step: float = state.get("step", 0.0) + var moving: bool = state.get("moving", false) + var swing := sin(step) if moving else 0.0 + var c := rect.get_center() + Vector2(0, -absf(swing)) + var head := c + Vector2(0, -4.2) + var feet: Array[Vector2] = [c + Vector2(-1.8 + swing * 1.2, 5.0), c + Vector2(1.8 - swing * 1.2, 5.0)] + var body := ellipse(c + Vector2(0, 1.0), Vector2(3.6, 3.6)) + # One outline around the whole figure: dark shapes first, colours on top. + for foot in feet: + canvas.draw_colored_polygon(ellipse(foot, Vector2(1.7, 1.2) + Vector2(LINE, LINE)), INK) + canvas.draw_colored_polygon(ellipse(c + Vector2(0, 1.0), Vector2(3.6 + LINE, 3.6 + LINE)), INK) + canvas.draw_circle(head, 3.0 + LINE, INK, true, -1.0, true) + for foot in feet: + canvas.draw_colored_polygon(ellipse(foot, Vector2(1.7, 1.2)), INK.lightened(0.25)) + canvas.draw_colored_polygon(body, player_color(state.get("player", -1))) + canvas.draw_circle(head, 3.0, SKIN, true, -1.0, true) + for eye in [-0.6, 0.9]: + canvas.draw_circle(head + Vector2(facing * (0.9 + eye), -0.3), 0.5, INK, true, -1.0, true) + + +func _marker(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: + var color := MARKER_NEAR if state.get("reachable", false) else MARKER_FAR + var inner := rect.grow(-1.2) + canvas.draw_rect(inner, INK, false, 2.4, true) + canvas.draw_rect(inner, color, false, 1.2, true) + + +func _dead_soil(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: + var variant: int = state.get("variant", 0) + canvas.draw_rect(rect, GROUND) + if share(variant, 1) < 0.5: + var a := rect.position + Vector2(2, 3) + Vector2(share(variant, 2), share(variant, 3)) * 6.0 + canvas.draw_polyline(PackedVector2Array([a, a + Vector2(3, 2), a + Vector2(5, 1), a + Vector2(7, 4)]), DEAD_CRACK, 0.8, true) + + +func _living_soil(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: + var living: int = state.get("living", 0) + var variant: int = state.get("variant", 0) + canvas.draw_rect(rect, GROUND) + canvas.draw_colored_polygon(rounded_rect(rect, living, 5.0), LIVING) + _edges(canvas, rect, living, 5.0, LIVING_SHADE, LINE) + if share(variant, 1) < 0.4: + var at := rect.position + Vector2(4, 6) + Vector2(share(variant, 2), share(variant, 3)) * 7.0 + canvas.draw_polyline(PackedVector2Array([at + Vector2(-1.2, -1.8), at, at + Vector2(1.2, -2.0)]), LIVING_SHADE, 0.8, true) + + +func _moss(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: + var variant: int = state.get("variant", 0) + var time: float = state.get("time", 0.0) + var foot := Vector2(rect.get_center().x, rect.end.y - 3.0) + var breathe := 1.0 + 0.06 * sway(time, variant, 1.6) + var blobs: Array[Vector3] = [Vector3(-3.5, -1.5, 3.0), Vector3(0.5, -3.0, 3.6), Vector3(3.8, -1.2, 2.7)] + for blob in blobs: + canvas.draw_circle(foot + Vector2(blob.x, blob.y) * breathe, blob.z * breathe + LINE, INK, true, -1.0, true) + for blob in blobs: + canvas.draw_circle(foot + Vector2(blob.x, blob.y) * breathe, blob.z * breathe, MOSS, true, -1.0, true) + canvas.draw_circle(foot + Vector2(blob.x - 0.8, blob.y - 0.9) * breathe, blob.z * 0.35, MOSS_LIGHT, true, -1.0, true) + for spark in 2: + var glow := 0.5 + 0.5 * sin(time * 2.4 + spark * 2.3 + share(variant, spark) * TAU) + _sparkle(canvas, foot + Vector2(-2.5 + spark * 5.0, -6.5 + spark), 1.8 * glow) + + +func _vine(canvas: CanvasItem, _rect: Rect2, state: Dictionary) -> void: + var points: PackedVector2Array = state.get("points", PackedVector2Array()) + var time: float = state.get("time", 0.0) + if points.size() < 2: + return + var leaves: Array[PackedVector2Array] = [] + var count := 9 + for i in count: + var part := (i + 0.5) / count + var at := along(points, part) + var ahead := along(points, minf(1.0, part + 0.02)) + var side := 1.0 if i % 2 == 0 else -1.0 + leaves.append(leaf(at, (ahead - at).angle() + side * 0.9 + 0.25 * sway(time, i, 1.4), 5.0, 2.8)) + for blade in leaves: + canvas.draw_polyline(closed(blade), INK, LINE * 1.6, true) + canvas.draw_polyline(points, INK, 2.2 + LINE * 2.0, true) + canvas.draw_polyline(points, VINE, 2.2, true) + for blade in leaves: + canvas.draw_colored_polygon(blade, LEAF) + + +func _fiber_plant(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: + var growth: float = state.get("growth", 1.0) + var time: float = state.get("time", 0.0) + var foot := Vector2(rect.get_center().x, rect.end.y - 2.0) + if growth < 0.15: + var seed := ellipse(foot + Vector2(0, -1.2), Vector2(2.2, 1.6)) + canvas.draw_colored_polygon(ellipse(foot + Vector2(0, -1.2), Vector2(2.2 + LINE, 1.6 + LINE)), INK) + canvas.draw_colored_polygon(seed, SEED) + return + var grown := (growth - 0.15) / 0.85 + var height := lerpf(4.0, 20.0, grown) + canvas.draw_set_transform(foot, 0.07 * sway(time, 3, 1.1), Vector2.ONE) + var leaves: Array[PackedVector2Array] = [] + var count := 1 + int(grown * 5.0) + for i in count: + var at := Vector2(0, -height * (0.2 + 0.7 * i / maxf(1.0, count))) + var side := 1.0 if i % 2 == 0 else -1.0 + leaves.append(leaf(at, -PI / 2.0 + side * 1.0, lerpf(3.0, 6.0, grown), 2.8)) + var tuft := clampf((grown - 0.75) / 0.25, 0.0, 1.0) + var puffs: Array[Vector2] = [] + if tuft > 0.0: + for puff in 4: + puffs.append(Vector2(-2.0 + puff * 1.4, -height - 1.0 - (puff % 2) * 1.2)) + for blade in leaves: + canvas.draw_polyline(closed(blade), INK, LINE * 1.6, true) + canvas.draw_line(Vector2.ZERO, Vector2(0, -height), INK, 1.6 + LINE * 2.0, true) + for puff in puffs: + canvas.draw_circle(puff, 1.4 * tuft + 0.3 + LINE, INK, true, -1.0, true) + canvas.draw_line(Vector2.ZERO, Vector2(0, -height), STEM, 1.6, true) + for blade in leaves: + canvas.draw_colored_polygon(blade, LEAF) + for puff in puffs: + canvas.draw_circle(puff, 1.4 * tuft + 0.3, FIBRE, true, -1.0, true) + canvas.draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE) + + +func _stone(canvas: CanvasItem, rect: Rect2, _state: Dictionary) -> void: + var c := rect.get_center() + var pebble := regular_polygon(c, 2.8, 6, 0.3) + canvas.draw_colored_polygon(regular_polygon(c, 2.8 + LINE, 6, 0.3), INK) + canvas.draw_colored_polygon(pebble, STONE) + canvas.draw_line(c + Vector2(-1.2, -1.0), c + Vector2(0.4, -1.6), Color.WHITE, 0.6, true) + + +func _grazer(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: + var hop: float = state.get("hop", 1.0) + var facing: float = state.get("facing", 1.0) + var lift := sin(PI * hop) * 4.0 + var foot := Vector2(rect.get_center().x, rect.end.y - 2.0) + var body := foot + Vector2(0, -6.0 - lift) + var puffs: Array[Vector3] = [ + Vector3(-3.5, 0.5, 3.0), Vector3(0.0, -1.5, 3.4), Vector3(3.2, 0.3, 3.0), Vector3(0.0, 1.8, 3.0), + ] + var head := body + Vector2(facing * 5.0, -1.2) + for leg in [-2.5, 2.5]: + canvas.draw_line(body + Vector2(leg, 3.0), body + Vector2(leg, 6.0 - lift * 0.3), INK, 1.6, true) + for puff in puffs: + canvas.draw_circle(body + Vector2(puff.x, puff.y), puff.z + LINE, INK, true, -1.0, true) + canvas.draw_colored_polygon(ellipse(head, Vector2(2.4 + LINE, 2.0 + LINE)), INK) + for puff in puffs: + canvas.draw_circle(body + Vector2(puff.x, puff.y), puff.z, WOOL, true, -1.0, true) + canvas.draw_colored_polygon(ellipse(head, Vector2(2.4, 2.0)), FACE) + canvas.draw_circle(head + Vector2(facing * 0.9, -0.4), 0.6, WOOL, true, -1.0, true) + + +func _ruin(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: + var awake: float = state.get("awake", 0.0) + var time: float = state.get("time", 0.0) + var tile := rect.size.x / 3.0 + var floor_y := rect.end.y - 3.0 + var center := Vector2(rect.get_center().x, floor_y - tile * 1.2) + var opening := Rect2(rect.position.x + tile * 0.75, rect.position.y + tile * 0.9, tile * 1.5, floor_y - rect.position.y - tile * 0.9) + canvas.draw_rect(opening, RUIN_DARK) + # Rays of the waking grove. + for ray in 8: + var direction := Vector2.from_angle(TAU * ray / 8.0 + time * 0.3) + var glow := GLOW + glow.a = awake * 0.7 + canvas.draw_line(center + direction * 4.0, center + direction * (6.0 + 3.0 * awake), glow, 1.0, true) + canvas.draw_circle(center, 3.5 + LINE, INK, true, -1.0, true) + var core := GLOW.lerp(Color.WHITE, 0.4 * awake) + canvas.draw_circle(center, 3.5, core, true, -1.0, true) + var blocks: Array[Rect2] = [ + Rect2(rect.position.x, rect.position.y + tile * 0.6, tile * 0.75, floor_y - rect.position.y - tile * 0.6), + Rect2(rect.end.x - tile * 0.75, rect.position.y + tile * 0.6, tile * 0.75, floor_y - rect.position.y - tile * 0.6), + Rect2(rect.position.x - 1.0, rect.position.y + tile * 0.25, rect.size.x + 2.0, tile * 0.6), + ] + for block in blocks: + canvas.draw_rect(block, RUIN) + canvas.draw_rect(Rect2(block.position.x, block.end.y - 2.0, block.size.x, 2.0), RUIN_SHADE) + canvas.draw_rect(block, INK, false, LINE, true) + canvas.draw_rect(Rect2(rect.position.x + tile * 0.4, floor_y - 2.0, rect.size.x - tile * 0.8, 3.0), RUIN_SHADE) + canvas.draw_rect(Rect2(rect.position.x + tile * 0.4, floor_y - 2.0, rect.size.x - tile * 0.8, 3.0), INK, false, LINE, true) + + +## A mass of rock: ground where corners are cut round, the fill, a darker +## band along its lower open edge and the outline where it meets the ground. +func _mass(canvas: CanvasItem, rect: Rect2, solid: int, fill: Color, shade: Color) -> void: + canvas.draw_rect(rect, GROUND) + canvas.draw_colored_polygon(rounded_rect(rect, solid, 4.0), fill) + if solid & TileShape.S == 0: + # The band is as high as the corners are round, so that it can take + # the round lower corners of the tile. + var band := Rect2(rect.position.x, rect.end.y - 4.0, rect.size.x, 4.0) + canvas.draw_colored_polygon(rounded_rect(band, solid | TileShape.N, 4.0), shade) + _edges(canvas, rect, solid, 4.0, INK, LINE) + + +## The outline of a mass of tiles where it meets open ground: along every +## open side, round around open corners and filled into inner corners, all +## inside `rect`, so that tiles drawn later cannot cover half of it. +func _edges(canvas: CanvasItem, rect: Rect2, mask: int, radius: float, color: Color, width: float) -> void: + var half := width / 2.0 + var inner := inset_open_sides(rect, mask, half) + var r := maxf(0.0, radius - half) + var round_tl := mask & (TileShape.N | TileShape.W) == 0 + var round_tr := mask & (TileShape.N | TileShape.E) == 0 + var round_br := mask & (TileShape.S | TileShape.E) == 0 + var round_bl := mask & (TileShape.S | TileShape.W) == 0 + if mask & TileShape.N == 0: + canvas.draw_line(Vector2(inner.position.x + (r if round_tl else 0.0), inner.position.y), Vector2(inner.end.x - (r if round_tr else 0.0), inner.position.y), color, width, true) + if mask & TileShape.S == 0: + canvas.draw_line(Vector2(inner.position.x + (r if round_bl else 0.0), inner.end.y), Vector2(inner.end.x - (r if round_br else 0.0), inner.end.y), color, width, true) + if mask & TileShape.W == 0: + canvas.draw_line(Vector2(inner.position.x, inner.position.y + (r if round_tl else 0.0)), Vector2(inner.position.x, inner.end.y - (r if round_bl else 0.0)), color, width, true) + if mask & TileShape.E == 0: + canvas.draw_line(Vector2(inner.end.x, inner.position.y + (r if round_tr else 0.0)), Vector2(inner.end.x, inner.end.y - (r if round_br else 0.0)), color, width, true) + if round_tl: + canvas.draw_arc(inner.position + Vector2(r, r), r, PI, 1.5 * PI, 6, color, width, true) + if round_tr: + canvas.draw_arc(Vector2(inner.end.x - r, inner.position.y + r), r, 1.5 * PI, TAU, 6, color, width, true) + if round_br: + canvas.draw_arc(inner.end - Vector2(r, r), r, 0.0, 0.5 * PI, 6, color, width, true) + if round_bl: + canvas.draw_arc(Vector2(inner.position.x + r, inner.end.y - r), r, 0.5 * PI, PI, 6, color, width, true) + # Inner corners: both sides join neighbours, but the tile across the + # corner is open, so the outlines of the neighbours meet here. + var corners: Array[Array] = [ + [TileShape.N | TileShape.W, TileShape.NW, rect.position], + [TileShape.N | TileShape.E, TileShape.NE, Vector2(rect.end.x - width, rect.position.y)], + [TileShape.S | TileShape.E, TileShape.SE, rect.end - Vector2(width, width)], + [TileShape.S | TileShape.W, TileShape.SW, Vector2(rect.position.x, rect.end.y - width)], + ] + for corner in corners: + var sides: int = corner[0] + var across: int = corner[1] + if mask & sides == sides and mask & across == 0: + canvas.draw_rect(Rect2(corner[2], Vector2(width, width)), color) + + +## A four-pointed sparkle of `size` around `at`. +func _sparkle(canvas: CanvasItem, at: Vector2, size: float) -> void: + if size <= 0.1: + return + var star := PackedVector2Array([ + at + Vector2(0, -size), at + Vector2(size * 0.3, -size * 0.3), at + Vector2(size, 0), + at + Vector2(size * 0.3, size * 0.3), at + Vector2(0, size), at + Vector2(-size * 0.3, size * 0.3), + at + Vector2(-size, 0), at + Vector2(-size * 0.3, -size * 0.3), + ]) + canvas.draw_colored_polygon(star, SPARK) +``` + +- [ ] **Step 4: Tests ausführen und Erfolg sehen** + +Run: + +```bash +godot --headless --path game --import +godot --headless --path game -s res://tests/run_tests.gd +``` + +Expected: Exit-Code 0 und `134 passed, 0 failed`. Die einzigen Warnungen sind die drei bekannten Zeilen. + +- [ ] **Step 5: Bildschirmfotos ansehen** + +Run: + +```bash +godot --path game -s res://tools/screenshot.gd -- "$TEMP/m1d-kontur-karte.png" --style=outline +godot --path game -s res://tools/screenshot.gd -- "$TEMP/m1d-kontur-vitrine.png" --style=outline --showcase +``` + +Expected: Beide Aufrufe enden mit `screenshot saved: …` ohne Warnung. Sieh dir beide Bilder an. Nirgends steht mehr ein Platzhalter. + +- Spiel: Ockerfarbener Boden mit feinen Punkten. Fels ist hell lila, außen abgerundet, mit dunklem Umriss nur zum Boden hin, einem dunkleren Band an der Unterkante und kleinen hellen Strichen. Im getroffenen Fels steht ein gezackter dunkler Riss mit Splittern. Die Figuren haben einen dunklen Umriss, einen roten Körper und einen hellen Kopf. Über der linken steht scharf „Spieler“. +- Vitrine: Harter Fels kräftig blau, weicher Fels hell lila, Grundgestein dunkelbraun, alles mit dunklem Umriss. Toter Boden mit kleinen Rissen, lebendiger Boden kräftig grün mit hellen Häkchen. Moos mit gelben Funken, eine Ranke mit Blättern und Steinen, die Faserpflanze, ein Schaf und die Ruine als Tor mit gelber Sonne, jedes Ding mit dunklem Umriss. Zwei Figuren in Rot und Blau. Oben „Stilprobe: Kontur“, unten ist „2 Kontur“ ausgegraut. + +- [ ] **Step 6: Alles prüfen** + +Run: + +```bash +cargo fmt --all -- --check && cargo clippy --workspace --all-targets -- -D warnings && cargo test --workspace +cargo build -p grovewright-bridge +``` + +Expected: keine Ausgabe von rustfmt, keine Warnung von Clippy, zusammen 151 bestandene Rust-Tests + +- [ ] **Step 7: Commit und Hochladen** + +```bash +git add game +git commit -F - <<'EOF' +Stil Kontur + +Kräftige dunkle Umrisse um flache, starke Farben, gut lesbar wie ein +Comic. Felsmassen haben ihren Umriss nur zum offenen Boden hin, jedes +Ding hat einen Umriss um seine ganze Form, und Lebendiges funkelt gelb. +Jeder Spieler hat eine eigene Farbe. + +Co-Authored-By: Claude Opus 5.5 +EOF +git push +``` + +--- + +### Task 7: Stil Geometrisch + +Schlichte geometrische Formen in ruhigen Pastellfarben, ordentlich und still: Quadrate, Dreiecke, Kreise und Sechsecke, ohne Umriss und ohne Schatten. Muster folgen dem Raster, das Feld wirkt geordnet. + +**Files:** +- Modify: `game/scripts/art/geometric_style.gd` (ganze Datei ersetzen) +- Test: `game/tests/test_geometric_style.gd` (neu) + +**Interfaces:** +- Consumes: `ArtStyle` mit seinen Zeichenmethoden und Helfern, `TileShape`, `Art.IDS`, die Einträge von `state` laut Kopf von `art_style.gd` +- Produces: `GeometricStyle` zeichnet jede Kennung selbst; `PLAYER_COLORS: Array[Color]` mit acht Farben und `player_color(player: int) -> Color` + +- [ ] **Step 1: Fehlschlagenden Test schreiben** + +Create `game/tests/test_geometric_style.gd`: + +```gdscript +extends TestCase + + +func test_four_players_have_four_colours() -> void: + var style := GeometricStyle.new() + var colours := {} + for player in 4: + colours[style.player_color(player)] = true + check_eq(colours.size(), 4, "four players, four colours") + check_eq(style.player_color(-1), style.player_color(0), "a player not in the world yet looks like the first") +``` + +- [ ] **Step 2: Test ausführen und Fehlschlag sehen** + +Run: + +```bash +godot --headless --path game --import +godot --headless --path game -s res://tests/run_tests.gd +``` + +Expected: Exit-Code 1 und `134 passed, 1 failed`; unter `FAIL test_geometric_style.gd test_four_players_have_four_colours` steht `four players, four colours: expected 4, got 2`. + +- [ ] **Step 3: Stil schreiben** + +Ersetze den gesamten Inhalt von `game/scripts/art/geometric_style.gd` durch: + +```gdscript +class_name GeometricStyle +extends ArtStyle +## Plain geometric forms in calm pastel colours, tidy and quiet: squares, +## triangles, circles and hexagons, without outlines or shadows. Patterns +## follow the grid, so the field looks ordered. + +const KEY := &"geometric" + +const GROUND := Color("ead9b3") +const GROUND_DOT := Color("dcc699") +const SOFT_LIGHT := Color("c6d0e2") +const SOFT_DARK := Color("aebad0") +const HARD := Color("8d94c7") +const HARD_HEX := Color("a8aedc") +const BEDROCK := Color("4a4e69") +const LINE := Color("5b6181") +const DEAD_MARK := Color("cdb78c") +const LIVING := Color("b9e4c7") +const LIVING_DOT := Color("9bd4ae") +const MOSS_DARK := Color("4f9e6c") +const MOSS_LIGHT := Color("7cc493") +const VINE := Color("4c946a") +const LEAF := Color("6fbb88") +const STONE := Color("aab6cd") +const FIBRE := Color("fffaf0") +const SEED := Color("c49b74") +const BODY := Color("f6f2e9") +const FACE := Color("6d6875") +const RUIN := Color("bab5ca") +const RUIN_DARK := Color("5f5b6f") +const GLOW := Color("f6d77e") +const SKIN := Color("f2d4bf") +const MARKER_NEAR := Color("7fca97") +const MARKER_FAR := Color("ef8b7e") +const PLAYER_COLORS: Array[Color] = [ + Color("e9665f"), Color("4f8fcc"), Color("ef9f2f"), Color("4fa870"), + Color("9a7bd8"), Color("2fa9a3"), Color("e27aae"), Color("6f7fd6"), +] + + +func key() -> StringName: + return KEY + + +func title_key() -> String: + return "STYLE_GEOMETRIC" + + +func player_color(player: int) -> Color: + return PLAYER_COLORS[maxi(0, player) % PLAYER_COLORS.size()] + + +func _ground(canvas: CanvasItem, rect: Rect2, _state: Dictionary) -> void: + canvas.draw_rect(rect, GROUND) + if _lattice(rect, 3): + canvas.draw_circle(rect.get_center(), 1.1, GROUND_DOT, true, -1.0, true) + + +func _soft_rock(canvas: CanvasItem, rect: Rect2, _state: Dictionary) -> void: + canvas.draw_rect(rect, SOFT_LIGHT) + var tile := _tile(rect) + var corner := rect.position if (tile.x + tile.y) % 2 == 0 else Vector2(rect.end.x, rect.position.y) + var opposite := rect.end if (tile.x + tile.y) % 2 == 0 else Vector2(rect.position.x, rect.end.y) + canvas.draw_colored_polygon(PackedVector2Array([corner, opposite, Vector2(corner.x, opposite.y)]), SOFT_DARK) + + +func _hard_rock(canvas: CanvasItem, rect: Rect2, _state: Dictionary) -> void: + canvas.draw_rect(rect, HARD) + canvas.draw_colored_polygon(regular_polygon(rect.get_center(), rect.size.x * 0.38, 6, PI / 6.0), HARD_HEX) + + +func _bedrock(canvas: CanvasItem, rect: Rect2, _state: Dictionary) -> void: + canvas.draw_rect(rect, BEDROCK) + + +func _cracks(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: + var hits: int = state.get("hits", 1) + var inner := rect.grow(-3.5) + canvas.draw_line(inner.position, inner.end, LINE, 1.0, true) + if hits >= 2: + canvas.draw_line(Vector2(inner.end.x, inner.position.y), Vector2(inner.position.x, inner.end.y), LINE, 1.0, true) + + +func _hit(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: + var age: float = state.get("age", 0.0) + var c := rect.get_center() + for chip in 4: + var direction := Vector2.from_angle(TAU * chip / 4.0 + PI / 4.0) + var at := c + direction * (2.0 + 8.0 * age) + var color := SOFT_DARK + color.a = 1.0 - age + canvas.draw_colored_polygon(regular_polygon(at, 1.8 * (1.0 - age * 0.5), 4, age * 4.0), color) + + +func _figure(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: + var facing: float = state.get("facing", 1.0) + var step: float = state.get("step", 0.0) + var moving: bool = state.get("moving", false) + var swing := sin(step) if moving else 0.0 + var c := rect.get_center() + Vector2(0, -absf(swing)) + for side in [-1.0, 1.0]: + var foot := c + Vector2(side * 1.8 + side * swing * 1.0, 5.2) + canvas.draw_rect(Rect2(foot - Vector2(1.0, 1.0), Vector2(2.0, 2.0)), FACE) + var body := PackedVector2Array([c + Vector2(0, -3.0), c + Vector2(4.3, 4.8), c + Vector2(-4.3, 4.8)]) + canvas.draw_colored_polygon(body, player_color(state.get("player", -1))) + var head := c + Vector2(0, -5.0) + canvas.draw_circle(head, 3.0, SKIN, true, -1.0, true) + canvas.draw_circle(head + Vector2(facing * 1.2, -0.2), 0.55, FACE, true, -1.0, true) + + +func _marker(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: + var color := MARKER_NEAR if state.get("reachable", false) else MARKER_FAR + var size := 4.0 + var corners: Array[Array] = [ + [rect.position, Vector2(1, 1)], + [Vector2(rect.end.x, rect.position.y), Vector2(-1, 1)], + [rect.end, Vector2(-1, -1)], + [Vector2(rect.position.x, rect.end.y), Vector2(1, -1)], + ] + for corner in corners: + var at: Vector2 = corner[0] + var inward: Vector2 = corner[1] + canvas.draw_colored_polygon(PackedVector2Array([at, at + Vector2(inward.x * size, 0), at + Vector2(0, inward.y * size)]), color) + + +func _dead_soil(canvas: CanvasItem, rect: Rect2, _state: Dictionary) -> void: + canvas.draw_rect(rect, GROUND) + if _lattice(rect, 4): + var c := rect.get_center() + canvas.draw_line(c - Vector2(1.6, 1.6), c + Vector2(1.6, 1.6), DEAD_MARK, 0.8, true) + canvas.draw_line(c + Vector2(1.6, -1.6), c + Vector2(-1.6, 1.6), DEAD_MARK, 0.8, true) + + +func _living_soil(canvas: CanvasItem, rect: Rect2, _state: Dictionary) -> void: + canvas.draw_rect(rect, LIVING) + if _lattice(rect, 2): + canvas.draw_circle(rect.get_center(), 1.2, LIVING_DOT, true, -1.0, true) + + +func _moss(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: + var variant: int = state.get("variant", 0) + var time: float = state.get("time", 0.0) + var foot := Vector2(rect.get_center().x, rect.end.y - 4.0) + var breathe := 1.0 + 0.08 * sway(time, variant, 1.6) + canvas.draw_circle(foot + Vector2(-3.0, 0.0), 2.6 * breathe, MOSS_DARK, true, -1.0, true) + canvas.draw_circle(foot + Vector2(2.6, 0.4), 2.2 * breathe, MOSS_DARK, true, -1.0, true) + canvas.draw_circle(foot + Vector2(0.0, -2.6), 3.0 * breathe, MOSS_LIGHT, true, -1.0, true) + + +func _vine(canvas: CanvasItem, _rect: Rect2, state: Dictionary) -> void: + var points: PackedVector2Array = state.get("points", PackedVector2Array()) + var time: float = state.get("time", 0.0) + if points.size() < 2: + return + canvas.draw_polyline(points, VINE, 1.0, true) + var beads := 24 + for i in beads + 1: + var part := float(i) / beads + var at := along(points, part) + if i % 3 == 1: + var ahead := along(points, minf(1.0, part + 0.02)) + var side := 1.0 if i % 2 == 0 else -1.0 + var angle := (ahead - at).angle() + side * (PI / 2.0) + 0.2 * sway(time, i, 1.4) + var tip := at + Vector2.from_angle(angle) * 4.0 + var base := Vector2.from_angle(angle).orthogonal() * 1.3 + canvas.draw_colored_polygon(PackedVector2Array([at + base, tip, at - base]), LEAF) + canvas.draw_circle(at, 1.4, VINE, true, -1.0, true) + + +func _fiber_plant(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: + var growth: float = state.get("growth", 1.0) + var time: float = state.get("time", 0.0) + var foot := Vector2(rect.get_center().x, rect.end.y - 2.0) + if growth < 0.15: + canvas.draw_circle(foot + Vector2(0, -1.5), 1.8, SEED, true, -1.0, true) + return + var grown := (growth - 0.15) / 0.85 + var height := lerpf(4.0, 20.0, grown) + canvas.draw_set_transform(foot, 0.06 * sway(time, 3, 1.1), Vector2.ONE) + canvas.draw_line(Vector2.ZERO, Vector2(0, -height), VINE, 1.2, true) + var leaves := 1 + int(grown * 5.0) + for i in leaves: + var at := Vector2(0, -height * (0.2 + 0.7 * i / maxf(1.0, leaves))) + var side := 1.0 if i % 2 == 0 else -1.0 + var size := lerpf(2.5, 4.5, grown) + canvas.draw_colored_polygon(PackedVector2Array([at, at + Vector2(side * size, -size * 0.6), at + Vector2(0, -size * 0.9)]), LEAF) + if grown > 0.75: + var tuft := (grown - 0.75) / 0.25 + for puff in 3: + canvas.draw_circle(Vector2(-1.6 + puff * 1.6, -height - 1.0), 1.3 * tuft + 0.2, FIBRE, true, -1.0, true) + canvas.draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE) + + +func _stone(canvas: CanvasItem, rect: Rect2, _state: Dictionary) -> void: + canvas.draw_colored_polygon(regular_polygon(rect.get_center(), 2.8, 4, PI / 4.0), STONE) + + +func _grazer(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: + var hop: float = state.get("hop", 1.0) + var facing: float = state.get("facing", 1.0) + var lift := sin(PI * hop) * 4.0 + var foot := Vector2(rect.get_center().x, rect.end.y - 2.0) + var body := foot + Vector2(0, -5.5 - lift) + for leg in [-2.0, 2.0]: + canvas.draw_line(body + Vector2(leg, 2.5), body + Vector2(leg, 5.5 - lift * 0.3), FACE, 1.2, true) + canvas.draw_circle(body, 4.4, BODY, true, -1.0, true) + canvas.draw_circle(body + Vector2(facing * 4.6, -1.6), 2.2, FACE, true, -1.0, true) + + +func _ruin(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: + var awake: float = state.get("awake", 0.0) + var tile := rect.size.x / 3.0 + var floor_y := rect.end.y - 3.0 + var center := Vector2(rect.get_center().x, floor_y - tile * 1.2) + canvas.draw_rect(Rect2(rect.position.x + tile * 0.75, rect.position.y + tile * 0.9, tile * 1.5, floor_y - rect.position.y - tile * 0.9), RUIN_DARK) + for ring in 3: + var glow := GLOW + glow.a = awake * (0.35 + 0.3 * ring) + canvas.draw_circle(center, 8.5 - ring * 2.4, glow, true, -1.0, true) + canvas.draw_rect(Rect2(rect.position.x, rect.position.y + tile * 0.6, tile * 0.75, floor_y - rect.position.y - tile * 0.6), RUIN) + canvas.draw_rect(Rect2(rect.end.x - tile * 0.75, rect.position.y + tile * 0.6, tile * 0.75, floor_y - rect.position.y - tile * 0.6), RUIN) + canvas.draw_rect(Rect2(rect.position.x - 1.0, rect.position.y + tile * 0.25, rect.size.x + 2.0, tile * 0.6), RUIN) + canvas.draw_colored_polygon(PackedVector2Array([ + Vector2(rect.position.x - 1.0, rect.position.y + tile * 0.25), Vector2(rect.get_center().x, rect.position.y - tile * 0.3), + Vector2(rect.end.x + 1.0, rect.position.y + tile * 0.25), + ]), RUIN) + + +## The tile that `rect` covers. +static func _tile(rect: Rect2) -> Vector2i: + return Vector2i(rect.position / GridMotion.TILE_SIZE) + + +## True for every `every`th tile along both diagonals, which makes an even, +## open pattern across the field. +static func _lattice(rect: Rect2, every: int) -> bool: + var tile := _tile(rect) + return posmod(tile.x + tile.y * 2, every) == 0 and posmod(tile.x - tile.y, 2) == 0 +``` + +- [ ] **Step 4: Tests ausführen und Erfolg sehen** + +Run: + +```bash +godot --headless --path game --import +godot --headless --path game -s res://tests/run_tests.gd +``` + +Expected: Exit-Code 0 und `135 passed, 0 failed`. Die einzigen Warnungen sind die drei bekannten Zeilen. + +- [ ] **Step 5: Bildschirmfotos ansehen** + +Run: + +```bash +godot --path game -s res://tools/screenshot.gd -- "$TEMP/m1d-geometrisch-karte.png" --style=geometric +godot --path game -s res://tools/screenshot.gd -- "$TEMP/m1d-geometrisch-vitrine.png" --style=geometric --showcase +``` + +Expected: Beide Aufrufe enden mit `screenshot saved: …` ohne Warnung. Sieh dir beide Bilder an. Nirgends steht mehr ein Platzhalter. + +- Spiel: Heller Sandboden mit kleinen Rauten in einem regelmäßigen Raster. Weicher Fels besteht aus hellblauen und graublauen Dreiecken, ohne Umriss und ohne Schatten. Im getroffenen Fels steht ein dünner dunkler Strich als Riss. Die Figuren sind rote Dreiecke mit rundem Kopf. Über der linken steht scharf „Spieler“. +- Vitrine: Grundgestein dunkel, weicher Fels aus Dreiecken, harter Fels lila mit hellen Sechsecken. Toter Boden mit kleinen Kreuzen im Raster, lebendiger Boden mintgrün mit Punkten. Moos aus Kreisen, die Ranke als Perlenschnur mit eckigen Steinen, die Faserpflanze, ein Schaf, die Ruine als Haus mit Giebel und goldenem Leuchten. Zwei Figuren als rotes und blaues Dreieck. Oben „Stilprobe: Geometrisch“, unten ist „3 Geometrisch“ ausgegraut. + +- [ ] **Step 6: Alles prüfen** + +Run: + +```bash +cargo fmt --all -- --check && cargo clippy --workspace --all-targets -- -D warnings && cargo test --workspace +cargo build -p grovewright-bridge +``` + +Expected: keine Ausgabe von rustfmt, keine Warnung von Clippy, zusammen 151 bestandene Rust-Tests + +- [ ] **Step 7: Commit und Hochladen** + +```bash +git add game +git commit -F - <<'EOF' +Stil Geometrisch + +Schlichte Formen in ruhigen Pastellfarben, ohne Umriss und ohne +Schatten: Dreiecke für weichen Fels, Sechsecke für harten, Kreise für +Moos und eine Perlenschnur als Ranke. Muster folgen dem Raster, und +jeder Spieler hat eine eigene Farbe. + +Co-Authored-By: Claude Opus 5.5 +EOF +git push +``` + +--- + +### Task 8: Regeln, Fragebogen und Bilder für den Probelauf + +**Files:** +- Modify: `CLAUDE.md` (ganze Datei ersetzen) +- Create: `docs/playtests/m1d.md` +- Create: `docs/playtests/m1d/scherenschnitt-karte.png`, `scherenschnitt-vitrine.png`, `kontur-karte.png`, `kontur-vitrine.png`, `geometrisch-karte.png`, `geometrisch-vitrine.png` (vom Bildschirmfoto-Werkzeug erzeugt) + +**Interfaces:** +- Consumes: das Bildschirmfoto-Werkzeug aus Task 4 und die drei Stile aus den Tasks 5 bis 7 +- Produces: der Abschnitt „Grafik“ in `CLAUDE.md`; der Fragebogen `docs/playtests/m1d.md` mit sechs Bildern + +- [ ] **Step 1: Regeln schreiben** + +Gegenüber `main` bekommt `CLAUDE.md` den Abschnitt „Grafik“. Unter „Prüfen“ stehen die Optionen des Bildschirmfoto-Werkzeugs, unter „Spielen“ das Argument `--style=NAME`. Bei der Verbindung, die als verloren gilt, heißt es jetzt „nach etwa“ statt „nach höchstens“, wie in #8 vermerkt. + +Ersetze den gesamten Inhalt von `CLAUDE.md` durch: + +````markdown +# Grovewright + +Aufbauspiel für ein bis vier Spieler, in dem die Fabrik lebt. Gebaut wird zuerst eine Vertical Slice mit 2–3 Stunden Spielzeit. + +Das verbindliche Konzept steht in [docs/KONZEPT.md](docs/KONZEPT.md). Lies es, bevor du Spielregeln, Architektur oder Umfang änderst. Was dort nicht steht, ist nicht beschlossen. + +## Aufbau + +| Ordner | Inhalt | +|---|---| +| `core/` | Rust-Kern: die gesamte gemeinsame Welt. Ohne Zugriff auf die Engine, ohne fremde Crates. | +| `bridge/` | Rust-Brücke (GDExtension): macht den Kern für Godot aufrufbar und enthält keine Spielregeln. | +| `game/` | Godot-Projekt mit der GDScript-Hülle: Darstellung, Eingabe, Oberfläche, Dateien. | +| `docs/KONZEPT.md` | Spielkonzept und Entscheidungen | +| `docs/superpowers/plans/` | Umsetzungspläne | +| `docs/playtests/` | Fragebögen für Playtests | + +Faustregel: Was im Speicherstand steht oder alle Spieler betrifft, gehört in den Kern. Die Hülle darf Gleitkommazahlen nutzen, weil sie nichts an der gemeinsamen Welt ändert; sie schickt nur Befehle. + +## Regeln für den Kern + +Alle Spieler simulieren die Welt selbst und tauschen nur Befehle aus (Gleichschritt). Das funktioniert nur, wenn der Kern auf jedem Rechner exakt gleich rechnet. Deshalb gelten für den Quelltext des Kerns in `core/src/` diese Regeln (die Tests in `core/tests/` dürfen zum Beispiel Dateien lesen): + +- Nur ganze Zahlen, keine Gleitkommazahlen. +- Keine Uhr, keine Dateien, kein Netzwerk, keine Threads, keine Umgebungsvariablen, kein globaler veränderlicher Zustand. +- Zufall nur über `rng::Rng` mit fester Startzahl. +- Keine Sammlung mit wechselnder Reihenfolge. Statt einer Hash-Tabelle nimmst du `Vec`, `BTreeMap` oder `BTreeSet`. +- Die Welt ändert sich nur durch einen `Command` in `World::step`. Ausnahmen sind das Laden eines Speicherstands mit `World::load`, das alle Spieler abmeldet, und die Übernahme der Welt des Hosts mit `World::from_snapshot`, die ein Gast beim Beitritt und nach einer Abweichung macht. +- Wer eine Sitzung aufzeichnet, ruft `Recording::step` statt `World::step` auf. So enthält die Aufzeichnung genau die ausgeführten Befehle. +- Alles, was zum Zustand der Welt gehört, steht im Snapshot. Wer ein Feld ergänzt, ergänzt es im Kodierer und im Dekodierer seiner Struktur (`snapshot.rs` für die Welt, `player.rs` für Spieler, `inventory.rs` für das Inventar, `map.rs` für die Karte), prüft es beim Dekodieren oder in `check_restorable` und erhöht `SNAPSHOT_VERSION`. +- Wo eine Rechnung im Spiel überlaufen kann, steht eine ausdrückliche Wahl: `saturating_`, `wrapping_` oder `checked_`. Taktzähler laufen praktisch nie über und werden normal addiert: Die Welt zählt in `u64`, eine Aufzeichnung in `u32`, was bei 20 Takten pro Sekunde für über sechs Jahre reicht. Takte aus geladenen Dateien begrenzt `MAX_RESTORABLE_TICK`. +- Keine fremden Crates im Kern. + +Drei Wächter prüfen das. `core/clippy.toml` sperrt Pfade wie `std::collections::HashMap` oder `std::fs::read`, auch über Aliase und gruppierte Importe. Der Test `core/tests/forbidden_apis.rs` durchsucht zusätzlich den Text nach verbotenen Begriffen; sie dürfen dort auch in Kommentaren nicht vorkommen. Gruppierte Importe aus `std` (`use std::{…}`) sind verboten, weil sie dem Text die Modulnamen verbergen: Jeder Pfad steht in einer eigenen `use`-Zeile. `core/tests/no_dependencies.rs` hält die Abhängigkeiten leer. + +## Gemeinsame Welt im Netz + +- Ein Spieler ist Host, die anderen sind Gäste. Der Host (`core/src/host.rs`) legt fest, welche Befehle in welchem Takt laufen, und schickt jeden Takt an seine Gäste. Jeder Gast (`core/src/guest.rs`) rechnet jeden Takt selbst nach. Allein spielen heißt: Host ohne Gäste. +- Ein Gast bekommt beim Beitritt die Welt des Hosts als Snapshot. Einmal pro Sekunde liegt einem Takt die Prüfsumme des Hosts bei. Weicht die Welt des Gastes ab, holt er sich die Welt des Hosts neu, und die Hülle speichert die Aufzeichnung der abgewichenen Welt. +- Die Nachrichten stehen in `core/src/protocol.rs`. Wer eine Nachricht ändert, erhöht `PROTOCOL_VERSION`. Der Anfang des Grußes und die ganze Ablehnung ändern sich dabei nie, damit Spiele jeder Version einander noch sagen können, dass sie nicht zusammenpassen; `core/tests/golden.rs` hält beides fest. +- Die Regel-Kennung (`core/src/rules.rs`) leitet sich aus den Endzuständen eingefrorener Skripte ab. Rechner mit verschiedener Regel-Kennung spielen nicht zusammen, und Aufzeichnungen anderer Regeln werden abgelehnt. Eine neue Regel braucht deshalb ein neues Skript in `SCRIPTS`, das sie benutzt; bestehende Skripte ändert niemand. +- Gäste schicken nur Aktionen, ohne Spielernummer. Der Host macht daraus Befehle für den Spieler der Verbindung und nimmt höchstens `MAX_ACTIONS_PER_TICK` pro Spieler und Takt an. +- Ein Gast zählt die Abklingzeit der Spitzhacke in seinen eigenen Takten, die denen des Hosts um die Laufzeit nachlaufen. Kommt sein Schlag an, während die Abklingzeit seines Spielers noch läuft, hält ihn der Host bis zum ersten Takt danach; kommen mehrere, gilt der neueste. Sonst ginge übers Internet etwa jeder fünfte Schlag eines Gastes verloren. +- Die Reichweite der Spitzhacke prüft nur die Hülle des eigenen Spielers. Figuren laufen außerhalb des Gleichschritts, also kennt der Kern ihre Position nicht. Ein veränderter Client könnte deshalb aus der Ferne graben. Außerdem stehen die Schlüssel aller Spieler in jedem Snapshot, den ein Gast bekommt; ein veränderter Client könnte sich damit als ein Spieler ausgeben, der gerade nicht online ist. Für ein Koop-Spiel unter Freunden nehmen wir beides hin. + +## Regeln für Brücke und Hülle + +- Die Brücke übersetzt nur. Neue Spielregeln gehören in den Kern, nie in `bridge/` oder `game/`. +- GDScript mit Typangaben schreiben (`var x := …`, Rückgabetypen an jeder Funktion). +- Eingabeaktionen legt `game/scripts/input_setup.gd` an, nicht `project.godot`. +- Alle Texte, die Spieler sehen, stehen in `game/i18n/texts.csv` (Deutsch und Englisch) und werden über `tr()` geholt. +- Tests, die Dateien schreiben, nutzen `TestFiles.temporary_dir()` (Ordner unter `user://tests`), nie die echten Dateien des Spielers. +- Godot erzeugt beim Import zu jedem Skript eine `.uid`-Datei. Sie gehört ins Archiv. Der Ordner `game/.godot/` gehört nicht hinein. +- Das Netzwerk liegt in `game/scripts/net_link.gd` (ENet, Port 24680) und `game/scripts/session_link.gd`. Nachrichten des Kerns gehen zuverlässig und in Reihenfolge über Kanal 0, Positionen der Figuren unzuverlässig, aber geordnet über Kanal 1; ENet drosselt sie nie. Eine Verbindung, deren Pakete unbeantwortet bleiben, gilt nach etwa `NetLink.TIMEOUT_MSEC` als verloren; ENet prüft die Grenze nur, wenn eine Wiederholung fällig ist. `NetLink.close_peer` schließt eine Verbindung mit `peer_disconnect_later`, damit wartende Nachrichten wie eine Ablehnung noch ankommen. +- Nur der Host speichert. Laden mit F9 geht nur ohne geöffnete Welt („Allein spielen“). +- Eine Pause beginnt nur ohne Gäste und lässt sich immer beenden. Nimmt der Host einen Gast an, endet sie; eine bloße Verbindung ohne Gruß beendet sie nicht. +- Hält der Kern mit einem internen Fehler an, schließt ein Host seine Welt für Gäste, und ein Gast kehrt ins Menü zurück. Eine angehaltene Sitzung hinterlässt keine Aufzeichnung. +- Tests mit Netzwerk lauschen nur auf `127.0.0.1` und lassen das System den Port wählen (Port 0), damit sie nie an einem belegten Port scheitern. + +## Grafik + +- Alles auf dem Spielfeld zeichnet `Art` (`game/scripts/art/art.gd`), jedes Ding unter einer Kennung wie `terrain/soft_rock`. Andere Skripte zeichnen keine eigenen Formen oder Farben aufs Spielfeld; ausgenommen sind Texte wie die Namen über den Figuren. +- Ein Stil ist eine Unterklasse von `ArtStyle` und zeichnet jede Kennung aus Code: flach, klar, Bewegung aus Programmierung (Konzept 7.4). Zur Wahl stehen Scherenschnitt, Kontur und Geometrisch. F2 wechselt im Spiel, „Stilproben ansehen“ im Menü öffnet die Vitrine, und das Spiel merkt sich die Wahl. +- Eine neue Kennung braucht eine Zeichnung in jedem Stil; sonst zeichnet `ArtStyle` nur einen Platzhalter. `game/tests/test_art.gd` lässt jeden Stil jede Kennung zeichnen. +- Ein Bild `game/art/.png` ersetzt die Zeichnung seiner Kennung, ohne dass sich Code ändert. So kann ein Artist Grafik austauschen. +- Die Karte zeichnet in Blöcken von 16 × 16 Kacheln (`MapView`); nach einem Treffer werden nur die Blöcke um die Kachel neu gezeichnet. Jede Kachel kennt ihre Nachbarn (`TileShape`), damit Felsmassen außen rund und innen geschlossen sind. +- Der Renderer der Hülle kennt kein MSAA in 2D. Glatte Kanten entstehen über die Zeichenbefehle mit `antialiased`, etwa bei Kreisen und Linien. + +## Prüfen + +Vor jedem Commit müssen alle Befehle fehlerfrei laufen: + +```bash +cargo fmt --all -- --check +cargo clippy --workspace --all-targets -- -D warnings +cargo test --workspace +cargo build -p grovewright-bridge +godot --headless --path game --import +godot --headless --path game -s res://tests/run_tests.gd +``` + +Der erste Import in einem frischen Checkout stürzt mit Godot 4.7.2 und der Rust-Erweiterung beim Beenden ab (Exit-Code 139 in der Bash). Der Import ist dann trotzdem erfolgt; ein zweiter Aufruf läuft fehlerfrei durch. `play.bat` wiederholt den Import deshalb einmal. + +Der Test `game/tests/test_two_instances.gd` startet einen zweiten Godot-Prozess als Gast (`game/tools/guest_bot.gd`) und prüft, dass beide Welten gleich bleiben. Er braucht deshalb die gebaute Brücke und den Import wie alle anderen Tests der Hülle. + +Schlägt `core/tests/golden.rs` fehl, haben sich Regeln oder ein Format geändert. Sind Speicherstände, Aufzeichnungen oder Nachrichten betroffen, erhöhst du die Versionskonstante, die die Fehlermeldung nennt (`SNAPSHOT_VERSION`, `RECORDING_VERSION` oder `PROTOCOL_VERSION`). Erst danach passt du den festgeschriebenen Wert bewusst an. + +Für Änderungen am Aussehen gibt es ein Werkzeug, das kurz ein Fenster öffnet und ein Bildschirmfoto speichert. `--style=NAME` wählt einen Stil, `--showcase` zeigt die Vitrine statt des Spiels: + +```bash +godot --path game -s res://tools/screenshot.gd -- [--style=NAME] [--showcase] +``` + +## Spielen + +`play.bat` im Hauptordner baut die Brücke und startet das Spiel. Spielstand, Spielerschlüssel und Aufzeichnungen liegen unter `%APPDATA%\Godot\app_userdata\Grovewright`. + +Ein laufendes Spiel hält `target\debug\grovewright_bridge.dll` offen. Ändern sich danach Brücke oder Kern, scheitert `cargo build` mit „Zugriff verweigert (os error 5)“, bis alle Spielfenster geschlossen sind. + +Argumente hinter `play.bat` gehen an das Spiel: + +| Argument | Wirkung | +|---|---| +| `--profile=NAME` | Eigener Ordner unter `profiles\NAME` für Schlüssel, Spielstand und Aufzeichnungen. Zwei Fenster auf einem Rechner brauchen zwei Profile, sonst sind sie derselbe Spieler. Erlaubt sind bis zu 32 Zeichen aus A bis Z, a bis z, Ziffern, `-` und `_`; einen anderen Namen übergeht das Spiel mit einer Warnung im Log und nimmt das Standardprofil. | +| `--name=NAME` | Name des Spielers | +| `--solo`, `--host` | Startet ohne Menü allein beziehungsweise mit offener Welt | +| `--port=PORT` | Port einer offenen Welt | +| `--join=ADRESSE` | Tritt ohne Menü einem Host bei; die Adresse darf auf `:PORT` enden | +| `--style=NAME` | Zeichnet im Stil `papercut`, `outline` oder `geometric` statt im zuletzt gewählten | + +`play-zweites-fenster.bat` startet ein zweites Fenster mit dem Profil `zweites-fenster`. Die Datei reicht weitere Argumente ebenfalls an das Spiel weiter, etwa `--name=Zweiter`. + +## Sprache + +- Code, Bezeichner und Kommentare im Code: Englisch. +- Dokumente, Pläne, Commit-Nachrichten, Issues und Pull Requests: Deutsch. +- Texte im Spiel: Deutsch und Englisch über `game/i18n/texts.csv`. + +## Git + +- Archiv: `https://git.race-cave.cloud/steven/Grovewright.git` (Gitea). Issues und Pull Requests sind freigegeben. +- Gearbeitet wird auf einem Branch pro Plan, nicht direkt auf `main`. +- Nach jedem abgeschlossenen Arbeitsschritt wird committet und hochgeladen. Das ist freigegeben. +- Commit-Nachrichten enden mit einer Zeile `Co-Authored-By: Claude `, die das Claude-Modell nennt, das den Commit erstellt hat. +```` + +- [ ] **Step 2: Bilder für den Fragebogen aufnehmen** + +Der Pfad muss absolut sein, denn Godot legt relative Pfade unter `game/` an. `pwd -W` gibt in der Git-Bash den Windows-Pfad aus. + +```bash +mkdir -p docs/playtests/m1d +OUT="$(pwd -W)/docs/playtests/m1d" +for pair in papercut:scherenschnitt outline:kontur geometric:geometrisch; do + godot --path game -s res://tools/screenshot.gd -- "$OUT/${pair#*:}-karte.png" --style=${pair%%:*} + godot --path game -s res://tools/screenshot.gd -- "$OUT/${pair#*:}-vitrine.png" --style=${pair%%:*} --showcase +done +ls -l docs/playtests/m1d +``` + +Expected: Jeder der sechs Aufrufe endet mit `screenshot saved: …` ohne Warnung. `ls` zeigt die sechs Dateien `geometrisch-karte.png`, `geometrisch-vitrine.png`, `kontur-karte.png`, `kontur-vitrine.png`, `scherenschnitt-karte.png` und `scherenschnitt-vitrine.png`, jede zwischen 40 und 120 KB groß. Die genauen Größen schwanken von Lauf zu Lauf um einige Bytes. + +- [ ] **Step 3: Bilder ansehen** + +Sieh dir alle sechs Bilder an. Jedes muss zu seiner Beschreibung in Step 5 der Tasks 5, 6 und 7 passen: das Spiel mit zwei Figuren und dem Namen „Spieler“, die Vitrine mit allen neun Beschriftungen und dem Titel ihres Stils. + +- [ ] **Step 4: Fragebogen schreiben** + +Create `docs/playtests/m1d.md`: + +```markdown +# Probelauf M1d: Stilproben + +M1d zeigt das Spielfeld in drei Stilen, alle aus Code gezeichnet: Scherenschnitt, Kontur und Geometrisch. Du wählst einen davon für das ganze Spiel. Das ist die Prüffrage von Meilenstein 1: „Welche der drei Stilproben soll es sein?“ Die Vitrine zeigt dazu Dinge aus späteren Meilensteinen, die es zum Spielen noch nicht gibt. + +## So siehst du die Proben + +1. Doppelklick auf `play.bat`. +2. Im Menü auf „Stilproben ansehen“. Die Vitrine zeigt + - toten und lebendigen Boden, + - Lebensmoos, + - eine Ranke, die Steine trägt, + - eine wachsende Faserpflanze, + - ein hüpfendes Weidetier, + - eine erwachende Ruine, + - zwei Spielfiguren. + + Mit 1, 2 und 3 oder den Knöpfen unten wechselst du den Stil, mit Esc oder „Zurück“ geht es ins Menü. +3. Im Spiel wechselt F2 den Stil. Lauf herum, schlag Stein und sieh dir Fels, Figur, Zielmarke und Splitter an. Das Spiel merkt sich den zuletzt gewählten Stil. +4. Zu zweit wie in M1c: Die Namen über den Figuren sind jetzt scharf, und dein eigener Schlag zeigt seine Splitter sofort, auch als Gast. + +So sehen die drei Proben aus: + +| Scherenschnitt | Kontur | Geometrisch | +|---|---|---| +| ![Scherenschnitt, Vitrine](m1d/scherenschnitt-vitrine.png) | ![Kontur, Vitrine](m1d/kontur-vitrine.png) | ![Geometrisch, Vitrine](m1d/geometrisch-vitrine.png) | +| ![Scherenschnitt, Spiel](m1d/scherenschnitt-karte.png) | ![Kontur, Spiel](m1d/kontur-karte.png) | ![Geometrisch, Spiel](m1d/geometrisch-karte.png) | + +## Fragen + +1. Welcher Stil soll es sein: Scherenschnitt, Kontur oder Geometrisch? +2. Was gefällt dir an deinem Favoriten am besten, und was stört dich daran? +3. Gibt es an den anderen beiden etwas, das du übernehmen möchtest? +4. Erkennst du auf einen Blick, was Boden, weicher Fels und harter Fels ist? Und was lebendiger Boden ist? +5. Wirkt das Lebendige lebendig: das Moos, die Ranke, die wachsende Pflanze, das Tier, die erwachende Ruine? Oder wirkt es nüchtern? +6. Ist die Bewegung angenehm: Wiegen, Wachsen, Hüpfen und die Schritte der Figur? Oder ist es zu viel oder zu wenig? +7. Findest du deine Figur sofort, auch neben anderen? +8. Wirkt ein Schlag als Gast jetzt sofort? Stört die Verzögerung noch? + +Antworte formlos im Chat oder als Kommentar in Gitea. +``` + +- [ ] **Step 5: Alles prüfen** + +Run: + +```bash +cargo fmt --all -- --check && cargo clippy --workspace --all-targets -- -D warnings && cargo test --workspace +cargo build -p grovewright-bridge +godot --headless --path game --import +godot --headless --path game -s res://tests/run_tests.gd +``` + +Expected: keine Ausgabe von rustfmt, keine Warnung von Clippy, zusammen 151 bestandene Rust-Tests; der GDScript-Lauf endet mit Exit-Code 0 und `135 passed, 0 failed` + +- [ ] **Step 6: Commit und Hochladen** + +```bash +git add CLAUDE.md docs/playtests/m1d.md docs/playtests/m1d +git commit -F - <<'EOF' +Regeln, Fragebogen und Bilder für den Probelauf M1d + +CLAUDE.md beschreibt, wie Grafik entsteht: Kennungen, Stile, Bilder, +die Zeichnungen ersetzen, Blöcke und Kachelformen. Das +Bildschirmfoto-Werkzeug und --style=NAME stehen bei ihren Befehlen. +Eine stille Verbindung gilt nach etwa NetLink.TIMEOUT_MSEC als verloren. +Ein Fragebogen mit Bildern der drei Stile begleitet den Probelauf. + +Co-Authored-By: Claude Opus 5.5 +EOF +git push +``` -- 2.52.0 From 5e9836c64ab7e482990a2b8f01e6c2e842621169 Mon Sep 17 00:00:00 2001 From: Steven Wroblewski Date: Fri, 2 Oct 2026 11:00:41 +0200 Subject: [PATCH 02/17] Plan M1d auf Pixelgrafik umgestellt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nach der ersten Vorschau soll das Spielfeld Pixelgrafik im Stil klassischer Farmspiele zeigen, aus Code gezeichnet. Konzept 7.4 und 11 sagen das jetzt. Der Plan behält Kennungen, Kartenblöcke, Vitrine und Stilwechsel und zeichnet statt der drei Vektorstile Pixelgrafik in drei Abwandlungen: Warm, Pastell und Glimmen. Warm war nach der Probe der Favorit. Jede Task-Grenze wurde erneut vorab in einer Kopie von main geprüft. Co-Authored-By: Claude Opus 5.5 --- docs/KONZEPT.md | 4 +- .../plans/2026-10-02-m1d-stilproben.md | 2518 +++++++++-------- 2 files changed, 1319 insertions(+), 1203 deletions(-) diff --git a/docs/KONZEPT.md b/docs/KONZEPT.md index 2ae4b2f..b975666 100644 --- a/docs/KONZEPT.md +++ b/docs/KONZEPT.md @@ -181,7 +181,7 @@ Die eigene Spielfigur bewegt sich sofort und läuft außerhalb des Gleichschritt ### 7.4 Grafik und Ton -- **Spielfeld aus Code.** Alle Grafiken auf dem Spielfeld sind Vektorzeichnungen in einem flachen, klaren Stil. Bewegung entsteht durch Programmierung. So passen alle Kachelvarianten garantiert aneinander. +- **Spielfeld aus Code.** Alle Grafiken auf dem Spielfeld sind Pixelgrafik im Stil klassischer Farmspiele wie Stardew Valley, aber eigenständig: 16 × 16 Pixel pro Kachel, schräg von vorn gesehen. Der Code zeichnet sie Pixel für Pixel aus einer Farbpalette; Figuren und Tiere sind kleine Pixelbilder im Quelltext. Bewegung entsteht durch Programmierung. So passen alle Kachelvarianten garantiert aneinander, und der Stil lässt sich nach jedem Playtest weiter anpassen. Bis zu den ersten Stilproben waren hier flache Vektorzeichnungen vorgesehen. - **Porträts und Titelbild als KI-Bilder** über den Skill `kie-ai`. Du wählst aus. - **Austauschbar.** Jede Grafik hängt an einer Kennung. Ein Artist kann sie später ersetzen, ohne dass sich Code ändert. - **Ton.** Lizenzfreie Geräusche und Musik im letzten Meilenstein. @@ -251,7 +251,7 @@ Offen, zu klären im jeweiligen Meilenstein: - **Wachstumsdauern.** Die Richtwerte aus 3.1 werden im Playtest eingestellt. - **Verhältnis Werkstatt zu Hain.** Wie viel Hain ein Ofen braucht, entscheidet, ob die Bodenregel reizt oder nervt. - **Andocken.** Wie Ranken, Ladetiere, Kisten und Ruinen genau aneinander anschließen, wird in den Meilensteinen 2 und 3 festgelegt. -- **Grafikstil.** Du wählst im ersten Meilenstein aus drei Stilproben. +- **Grafikstil.** Die Richtung steht: Pixelgrafik aus Code. Im ersten Meilenstein wählst du aus drei Abwandlungen (Warm, Pastell, Glimmen); nach der ersten Probe ist Warm dein Favorit. Feinheiten passen wir nach den Playtests an. - **Namen.** Alle Namen für Arten, Tiere und Waren sind Arbeitstitel. - **Titel.** Eine Websuche fand kein Spiel namens „Grovewright". Vor einem Steam-Release ist eine Markenrecherche nötig. diff --git a/docs/superpowers/plans/2026-10-02-m1d-stilproben.md b/docs/superpowers/plans/2026-10-02-m1d-stilproben.md index 6026fe9..e24577c 100644 --- a/docs/superpowers/plans/2026-10-02-m1d-stilproben.md +++ b/docs/superpowers/plans/2026-10-02-m1d-stilproben.md @@ -2,13 +2,13 @@ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. -**Goal:** Das Spielfeld lässt sich in drei Stilen zeichnen, alle aus Code: Scherenschnitt, Kontur und Geometrisch. Eine Vitrine zeigt jeden Stil zusammen mit Dingen späterer Meilensteine, F2 wechselt den Stil im Spiel, und das Spiel merkt sich die Wahl. Damit lässt sich die Prüffrage von Meilenstein 1 beantworten: „Welche der drei Stilproben soll es sein?“ +**Goal:** Das Spielfeld bekommt Pixelgrafik aus Code im Stil klassischer Farmspiele, in drei Abwandlungen: Warm, Pastell und Glimmen. Eine Vitrine zeigt jeden Stil zusammen mit Dingen späterer Meilensteine, F2 wechselt den Stil im Spiel, und das Spiel merkt sich die Wahl. Damit lässt sich die Prüffrage von Meilenstein 1 beantworten: „Welche der drei Stilproben soll es sein?“ -**Architecture:** Alles auf dem Spielfeld zeichnet `Art`. Jede Grafik hat eine Kennung wie `terrain/soft_rock`. Der gewählte Stil, eine Unterklasse von `ArtStyle`, zeichnet sie mit den Zeichenbefehlen von Godot; ein Bild `game/art/.png` ersetzt die Zeichnung, ohne dass sich Code ändert. `TileShape` gibt jeder Kachel ihre acht Nachbarn, damit Felsmassen außen rund und innen geschlossen sind. `MapView` zeichnet die Karte in Blöcken und nach einem Treffer nur die Blöcke um die Kachel neu. Kern und Brücke bleiben unverändert, denn Grafik ändert nichts an der gemeinsamen Welt. +**Architecture:** Alles auf dem Spielfeld zeichnet `Art`. Jede Grafik hat eine Kennung wie `terrain/soft_rock`; der gewählte Stil zeichnet sie, und ein Bild `game/art/.png` ersetzt die Zeichnung, ohne dass sich Code ändert. `PixelStyle` zeichnet jede Kennung Pixel für Pixel in ein kleines Bild, 16 × 16 Pixel pro Kachel, und hebt es auf; die drei Stile wählen nur Palette, Umrisse und Leuchten. `TileShape` gibt jeder Kachel ihre acht Nachbarn, damit Felsmassen außen rund und innen geschlossen sind und zum offenen Boden hin ihre Vorderkante zeigen. `MapView` zeichnet die Karte in Blöcken und nach einem Treffer nur die Blöcke um die Kachel neu. Kern und Brücke bleiben unverändert, denn Grafik ändert nichts an der gemeinsamen Welt. -**Tech Stack:** Godot 4.7.2 (Standard-Ausgabe, Renderer „GL Compatibility“), GDScript mit Typangaben, die Zeichenbefehle von `CanvasItem` mit Kantenglättung. Rust 1.98.1 und godot-rust 0.5.5 bleiben unverändert. +**Tech Stack:** Godot 4.7.2 (Standard-Ausgabe, Renderer „GL Compatibility“) mit Texturfilterung „Nearest“ und Einrasten auf ganze Pixel, GDScript mit Typangaben, `Image` und `ImageTexture` für die Pixelbilder. Rust 1.98.1 und godot-rust 0.5.5 bleiben unverändert. -**Spec:** `docs/KONZEPT.md`, Abschnitt 7.4 („Spielfeld aus Code“, „Austauschbar“), Abschnitt 8 (Meilenstein 1 mit der Prüffrage „Welche der drei Stilproben soll es sein?“) und Abschnitt 11 („Grafikstil“ und das Risiko „Grafik aus Code kann nüchtern wirken“). Dazu kommen die Antworten aus dem Probelauf von M1c im Kommentar zu Gitea #8 und deine Wahl der drei Richtungen und der Szene vor diesem Plan. +**Spec:** `docs/KONZEPT.md`, Abschnitt 7.4 („Spielfeld aus Code“ als Pixelgrafik, „Austauschbar“), Abschnitt 8 (Meilenstein 1 mit der Prüffrage „Welche der drei Stilproben soll es sein?“) und Abschnitt 11 („Grafikstil“ und das Risiko „Grafik aus Code kann nüchtern wirken“). Abschnitt 7.4 und 11 wurden mit diesem Plan geändert: Nach der ersten Vorschau hast du dir Pixelgrafik im Stil von Stardew Valley gewünscht, nur abgewandelt, aus Code und in drei Abwandlungen; nach der Pixel-Probe war Warm dein Favorit. Dazu kommen die Antworten aus dem Probelauf von M1c im Kommentar zu Gitea #8. ## Einordnung @@ -26,9 +26,9 @@ Danach ist Meilenstein 1 vollständig, und dein Probelauf beantwortet seine Prü | Punkt | Task | |---|---| | Konzept 7.4: Jede Grafik hängt an einer Kennung; ein Artist kann sie ersetzen, ohne dass sich Code ändert | 1: `Art` mit 16 Kennungen und Bildern unter `game/art/` | -| Konzept 7.4: Spielfeld aus Code, flach und klar, Bewegung durch Programmierung, alle Kachelvarianten passen aneinander | 1 und 2: Nachbarmasken und Blöcke; 5 bis 7: die drei Stile | -| Konzept 8 und 11: drei Stilproben zur Wahl | 3: Vitrine; 4: Wechsel mit F2 und über die Befehlszeile; 8: Fragebogen mit Bildern | -| Konzept 11, Risiko „Grafik aus Code kann nüchtern wirken“ | 3: Die Vitrine zeigt lebendigen Boden, Moos, Ranke, Pflanze, Tier und Ruine in Bewegung | +| Konzept 7.4: Spielfeld aus Code als Pixelgrafik, Bewegung durch Programmierung, alle Kachelvarianten passen aneinander | 1 und 2: Nachbarmasken und Blöcke; 5 und 6: Pixelgrafik | +| Konzept 8 und 11: drei Stilproben zur Wahl | 3: Vitrine; 4: Wechsel mit F2 und über die Befehlszeile; 5 bis 7: Warm, Pastell und Glimmen; 8: Fragebogen mit Bildern | +| Konzept 11, Risiko „Grafik aus Code kann nüchtern wirken“ | 3 und 6: Die Vitrine zeigt lebendigen Boden, Moos, Ranke, Pflanze, Tier und Ruine in Bewegung | | Probelauf M1c, Frage 2: Schläge als Gast sind sichtbar verzögert | 2: Der eigene Schlag zeigt seine Splitter beim Klick | | Probelauf M1c, Frage 4: Namen sind unscharf | 2: Namen in Bildschirmgröße | | #8: „nach höchstens `NetLink.TIMEOUT_MSEC`“ in `CLAUDE.md` | 8: Wortlaut „nach etwa“. Der Test über `get_statistic` bleibt in #8 offen. | @@ -37,7 +37,8 @@ Danach ist Meilenstein 1 vollständig, und dein Probelauf beantwortet seine Prü - Regeln für lebendigen Boden, Moos, Ranken, Pflanzen, Tiere und Ruinen. Sie kommen ab Meilenstein 2. Die Vitrine bewegt ihre Dinge nach festen Formeln der Hülle; der Kern weiß nichts von ihnen. - Bilder von einem Artist, Porträts, Titelbild und Ton (Konzept 7.4, Meilenstein 6) -- Das Entfernen der Stile, die du nicht wählst. Das entscheidet sich nach deinem Probelauf. +- Bilder unter `game/art/` mit mehreren Einzelbildern für Kachelränder oder Bewegung. Ein Bild ersetzt bisher nur eine feste Zeichnung. +- Das Entfernen der Stile, die du nicht wählst, und das Feinjustieren des gewählten Stils. Beides folgt nach deinem Probelauf. - Die übrigen Punkte aus #5 und #8 ## Technische Festlegungen @@ -45,15 +46,18 @@ Danach ist Meilenstein 1 vollständig, und dein Probelauf beantwortet seine Prü | Thema | Festlegung | Grund | |---|---|---| | Ort | Nur die Hülle ändert sich, dazu `CLAUDE.md` und `docs/`. | Grafik ändert nichts an der gemeinsamen Welt. | -| Richtungen | Scherenschnitt: Papierlagen ohne Umriss mit weichen Schatten, gedeckte Erdfarben, Lebendiges leuchtet grün und golden. Kontur: kräftige dunkle Umrisse um flache, starke Farben, gut lesbar wie ein Comic. Geometrisch: ruhige Pastellformen ohne Umriss und Schatten, Muster nach dem Raster. | Deine Wahl vor diesem Plan | +| Richtung | Pixelgrafik aus Code im Stil klassischer Farmspiele wie Stardew Valley, aber eigenständig. Die Grafiken von Stardew Valley werden nicht nachgebaut, nur der Charakter übernommen. | Deine Wahl nach der ersten Vorschau; Konzept 7.4 | +| Abwandlungen | Warm: warme, satte Farben und dunkle Umrisse; der Standard. Pastell: weiche, helle Farben, Umrisse in einem dunkleren Ton der Fläche. Glimmen: eine gedämpfte, kühle tote Welt, in der alles Lebendige kräftig ist und leuchtet. | Deine Wahl; nach der Probe war Warm dein Favorit | | Szene | Die Testkarte im Spiel und eine Vitrine mit Dingen späterer Meilensteine | Deine Wahl vor diesem Plan. Auf der Testkarte allein ließe sich nicht beurteilen, wie Lebendiges aussieht. | | Kennungen | 16 Kennungen in `Art.IDS`. Für das Spiel: Boden, weicher Fels, harter Fels, Grundgestein, Risse, Splitter, Figur, Zielmarke. Für die Vitrine: toter und lebendiger Boden, Moos, Ranke, Faserpflanze, Stein, Weidetier, Ruine. | Konzept 7.4: Jede Grafik hängt an einer Kennung. | | Austausch | `Art.load_overrides` lädt beim Start jedes Bild `game/art/.png`; es ersetzt die Zeichnung seiner Kennung. | Ein Artist kann Grafik austauschen, ohne dass sich Code ändert. | -| Stile | Ein Stil ist eine Unterklasse von `ArtStyle` und überschreibt je Kennung eine Zeichenmethode. `ArtStyle` selbst zeichnet schlichte Platzhalter. | Ein Stil steckt in einer Datei. Fehlt einer neuen Kennung die Zeichnung, fehlt nur ihr Aussehen; das Spiel läuft weiter. | +| Stile | `ArtStyle` zeichnet schlichte Platzhalter. `PixelStyle` überschreibt jede Zeichnung mit Pixelgrafik; `WarmStyle`, `PastelStyle` und `GlowStyle` setzen nur Palette, Spielerfarben, Art der Umrisse und Stärke des Leuchtens. | Eine neue Grafik wird einmal gezeichnet und gilt in allen Abwandlungen. Fehlt einer Kennung die Zeichnung, fehlt nur ihr Aussehen; das Spiel läuft weiter. | +| Pixel | 16 × 16 Pixel pro Kachel, dreifach vergrößert. `project.godot` stellt die Texturfilterung auf „Nearest“ und rastet 2D-Transformationen auf ganze Bildschirmpixel ein; die Stile setzen jedes Bild auf ganze Pixel. | Sonst würden Pixel unscharf oder verschieden breit. | +| Bilder | `PixelStyle` macht jede Grafik einmal als kleines Bild (`Image`, daraus `ImageTexture`) und hebt es unter einem Schlüssel aus Kennung, Nachbarn, Abwandlung und Bewegungsschritt auf. Bewegung besteht aus wenigen Bildern: vier Laufbilder, sechs Wachstumsstufen, sechs Bilder für die Splitter, vier für das Funkeln des Mooses. | Konzept 7.4: Bewegung durch Programmierung. Gemessen: Das erste Zeichnen eines Stils dauert einmalig rund 0,1 Sekunden, danach braucht ein Bild etwa 4 ms. | +| Sicht | Schräg von vorn: Fels zeigt zum offenen Boden hin eine 6 Pixel hohe Vorderkante und wirft einen Schatten auf den Boden darunter. Figuren sind 16 × 24 Pixel groß und stehen mit den Füßen knapp unter ihrem Körper. | Wie in klassischen Farmspielen. Die Zielmarke deckt weiter genau die Kachel. | | Zustand | Was eine Grafik zeigt, steht im Wörterbuch `state`, etwa die Nachbarn einer Kachel, die Zahl der Treffer, das Alter der Splitter oder wie weit eine Pflanze gewachsen ist. Die Einträge stehen im Kopf von `art_style.gd`. | Stile und Aufrufer kennen nur Kennung und Zustand, nicht einander. | -| Kanten | Glatte Kanten kommen aus den Zeichenbefehlen mit `antialiased = true`. | Der Renderer „GL Compatibility“ kennt kein MSAA in 2D; eingeschaltet meldet es einen Engine-Fehler. | -| Kachelformen | `TileShape.mask` beschreibt die acht Nachbarn einer Kachel als Bitmaske, im Uhrzeigersinn ab Norden. Fels ist außen rund und innen geschlossen; jenseits des Kartenrands zählt alles als Fels. | Konzept 7.4: Alle Kachelvarianten passen aneinander. | -| Zeichnen der Karte | Blöcke von 16 × 16 Kacheln. Ein Treffer zeichnet nur die Blöcke um seine Kachel neu. | Die ganze Karte neu zu zeichnen dauerte 18 bis 40 ms, zu lang für jeden Treffer. Nach dem Umbau gemessen: rund 8 ms, um die Karte beim Start aufzubauen, rund 7 ms für das ganze Bild nach einem Treffer, höchstens 40 ms für das Bild nach einem Stilwechsel. | +| Kachelformen | `TileShape.mask` beschreibt die acht Nachbarn einer Kachel als Bitmaske, im Uhrzeigersinn ab Norden; jenseits des Kartenrands zählt alles als Fels. `PixelStyle` beachtet eine Ecke nur, wenn beide Seiten daneben gesetzt sind, und kommt so mit höchstens 47 Formen je Felsart aus. | Konzept 7.4: Alle Kachelvarianten passen aneinander. | +| Zeichnen der Karte | Blöcke von 16 × 16 Kacheln. Ein Treffer zeichnet nur die Blöcke um seine Kachel neu. | Die ganze Karte neu zu zeichnen dauerte 18 bis 40 ms, zu lang für jeden Treffer. Gemessen nach dem Umbau: rund 9 ms für das ganze Bild mit einem Treffer, 35 bis 40 ms für das Bild nach einem Stilwechsel. | | Eigener Schlag | Die Splitter erscheinen beim Klick. Risse und Abbau kommen weiter mit dem Takt, die Splitter anderer Spieler auch. | Probelauf M1c: Als Gast kam der Schlag erst nach der Rundreise zum Host zurück. | | Namen | Die Namen über fremden Figuren werden in der Größe gezeichnet, die sie auf dem Bildschirm haben, und dann verkleinert. | Probelauf M1c: In 8 Pixeln gezeichnet und von der Kamera dreifach vergrößert, wirkten sie unscharf. | | Wechsel und Wahl | F2 wechselt im Spiel. In der Vitrine wechseln die Tasten 1 bis 3, F2 und Knöpfe. `--style=NAME` wählt beim Start. Das Spiel merkt sich die Wahl in den Einstellungen, Abschnitt `display`, Schlüssel `style`. | Du vergleichst schnell und spielst im gewählten Stil weiter. | @@ -65,7 +69,7 @@ Danach ist Meilenstein 1 vollständig, und dein Probelauf beantwortet seine Prü - Arbeitsverzeichnis ist die Wurzel des Archivs: `F:/Development/_kiZeug/Grovewright`. Alle Befehle laufen dort in der Bash. - Gearbeitet wird auf dem Branch `m1d-stilproben`, nicht auf `main`. -- Kern (`core/`) und Brücke (`bridge/`) bleiben unverändert. Dieser Plan ändert nur `game/`, `CLAUDE.md` und `docs/`. +- Kern (`core/`) und Brücke (`bridge/`) bleiben unverändert. Dieser Plan ändert nur `game/`, `CLAUDE.md` und `docs/playtests/`. - Spielregeln gehören in den Kern, nie in die Hülle. Die Dinge der Vitrine bewegen sich nach Formeln der Vitrine und ändern keine Welt. - Auf dem Spielfeld zeichnet nur `Art`. Andere Skripte zeichnen dort keine eigenen Formen oder Farben; ausgenommen sind Texte wie die Namen über den Figuren. - GDScript mit Typangaben (`var x := …`, Rückgabetypen an jeder Funktion). @@ -81,9 +85,9 @@ Danach ist Meilenstein 1 vollständig, und dein Probelauf beantwortet seine Prü ## Hinweis zur Vorabprüfung -Der gesamte Code dieses Plans wurde am 2. Oktober 2026 vorab gebaut. Alle drei Stile wurden auf der Karte und in der Vitrine angesehen und nachgebessert. Danach wurde jede der acht Task-Grenzen einzeln in einer Kopie von `main` nachgestellt, jeweils mit dem roten und dem grünen Lauf, die der Plan beschreibt: +Der gesamte Code dieses Plans wurde am 2. Oktober 2026 vorab gebaut. Du hast die drei Abwandlungen als Pixel-Probe gesehen und Warm gewählt. Danach wurde jede der acht Task-Grenzen einzeln in einer Kopie von `main` nachgestellt, jeweils mit dem roten und dem grünen Lauf, die der Plan beschreibt: -- GDScript-Tests: 96 in `main`, nach den Tasks 1 bis 8: 113, 121, 128, 132, 133, 134, 135 und 135. Jeder grüne Lauf gab genau die drei bekannten Warnzeilen aus. +- GDScript-Tests: 96 in `main`, nach den Tasks 1 bis 8: 110, 119, 126, 130, 138, 143, 147 und 147. Jeder grüne Lauf gab genau die drei bekannten Warnzeilen aus. - Rust: 151 Tests wie in `main`; rustfmt und Clippy meldeten nichts. - Das Bildschirmfoto-Werkzeug lieferte nach den Tasks 2 und 4 bis 8 die Bilder, die der Plan beschreibt, ohne Warnung. - Die Anweisungen dieses Plans, der Reihe nach auf `main` angewandt, ergeben nach jedem Task genau die geprüften Dateien. @@ -95,9 +99,11 @@ Weicht ein Schritt ab, ist zuerst ein Übertragungsfehler zu vermuten. | Datei | Aufgabe | |---|---| | `game/scripts/art/art.gd` | Kennungen, Wahl des Stils, Bilder statt Zeichnungen | -| `game/scripts/art/art_style.gd` | Grundklasse der Stile: Platzhalter und Helfer für Formen | +| `game/scripts/art/art_style.gd` | Grundklasse der Stile: Platzhalter und drei kleine Helfer | | `game/scripts/art/tile_shape.gd` | Nachbarmasken und eine feste Zahl je Kachel für kleine Abwandlungen | -| `game/scripts/art/papercut_style.gd`, `outline_style.gd`, `geometric_style.gd` | Die drei Stile | +| `game/scripts/art/pixel_style.gd` | Pixelgrafik aus Code für jede Kennung | +| `game/scripts/art/warm_style.gd`, `pastel_style.gd`, `glow_style.gd` | Die drei Abwandlungen: Palette, Spielerfarben, Umrisse, Leuchten | +| `game/project.godot` | Scharfe Pixel: Texturfilterung „Nearest“, Einrasten auf ganze Pixel | | `game/scripts/map_view.gd` | Karte in Blöcken, Risse und Splitter | | `game/scripts/avatar.gd`, `figures.gd`, `tile_marker.gd` | Eigene und fremde Figuren, Zielmarke | | `game/scripts/showcase.gd` | Vitrine der Stilproben | @@ -115,7 +121,7 @@ Weicht ein Schritt ab, ist zuerst ein Übertragungsfehler zu vermuten. **Files:** - Create: `game/scripts/art/tile_shape.gd`, `game/scripts/art/art_style.gd`, `game/scripts/art/art.gd` -- Create: `game/scripts/art/papercut_style.gd`, `game/scripts/art/outline_style.gd`, `game/scripts/art/geometric_style.gd` (vorläufig; die Tasks 5 bis 7 ersetzen sie) +- Create: `game/scripts/art/warm_style.gd`, `game/scripts/art/pastel_style.gd`, `game/scripts/art/glow_style.gd` (vorläufig; die Tasks 5 und 7 ersetzen sie) - Modify: `game/i18n/texts.csv` (ganze Datei ersetzen) - Test: `game/tests/test_tile_shape.gd`, `game/tests/test_art_style.gd`, `game/tests/test_art.gd` @@ -123,9 +129,9 @@ Weicht ein Schritt ab, ist zuerst ein Übertragungsfehler zu vermuten. - Consumes: `TestCase` mit `check(condition, message)`, `check_eq(actual, expected, message)` und `tree`; `TestFiles.temporary_dir(name)` und `TestFiles.remove_dir(path)`; `I18n.load_translations(I18n.TEXTS_FILE)` - Produces: - `TileShape`: die Bits `N`, `NE`, `E`, `SE`, `S`, `SW`, `W`, `NW` (1 bis 128, im Uhrzeigersinn ab Norden), `OFFSETS: Array[Vector2i]`, `static flags(codes: PackedByteArray, members: Array[int]) -> PackedByteArray`, `static mask(tile_flags: PackedByteArray, width: int, height: int, tile: Vector2i, outside_set: bool) -> int`, `static variant(tile: Vector2i) -> int` (0 bis 2147483647, für jede Kachel immer gleich) - - `ArtStyle`: `key() -> StringName`, `title_key() -> String`, `draw(canvas: CanvasItem, id: StringName, rect: Rect2, state: Dictionary) -> void`, `player_color(player: int) -> Color` und je Kennung eine Zeichenmethode `(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void`: `_ground`, `_soft_rock`, `_hard_rock`, `_bedrock`, `_cracks`, `_hit`, `_figure`, `_marker`, `_dead_soil`, `_living_soil`, `_moss`, `_vine`, `_fiber_plant`, `_stone`, `_grazer`, `_ruin`. Dazu die statischen Helfer `share(variant, salt) -> float`, `rounded_rect(rect, mask, radius, segments := 4) -> PackedVector2Array`, `inset_open_sides(rect, mask, inset) -> Rect2`, `sway(time, variant, speed := 1.3) -> float`, `regular_polygon(center, radius, corners, rotation := 0.0) -> PackedVector2Array`, `closed(points) -> PackedVector2Array`, `along(points, part) -> Vector2`, `ellipse(center, radii, segments := 14) -> PackedVector2Array`, `slit(from, to, width) -> PackedVector2Array` und `leaf(base, angle, length, width) -> PackedVector2Array` + - `ArtStyle`: `key() -> StringName`, `title_key() -> String`, `draw(canvas: CanvasItem, id: StringName, rect: Rect2, state: Dictionary) -> void`, `player_color(player: int) -> Color` und je Kennung eine Zeichenmethode `(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void`: `_ground`, `_soft_rock`, `_hard_rock`, `_bedrock`, `_cracks`, `_hit`, `_figure`, `_marker`, `_dead_soil`, `_living_soil`, `_moss`, `_vine`, `_fiber_plant`, `_stone`, `_grazer`, `_ruin`. Dazu die statischen Helfer `share(variant: int, salt: int) -> float`, `sway(time: float, variant: int, speed := 1.3) -> float` und `along(points: PackedVector2Array, part: float) -> Vector2`. - `Art`: Signal `changed`; die Kennungen `GROUND`, `SOFT_ROCK`, `HARD_ROCK`, `BEDROCK`, `CRACKS`, `HIT`, `FIGURE`, `MARKER`, `DEAD_SOIL`, `LIVING_SOIL`, `MOSS`, `VINE`, `FIBER_PLANT`, `STONE`, `GRAZER`, `RUIN` und `IDS: Array[StringName]`; `OVERRIDE_DIR := "res://art"`; `styles: Array[ArtStyle]` und `style: ArtStyle`; `static style_keys() -> Array[StringName]`; `select(key: StringName) -> bool`; `select_next() -> void`; `load_overrides(directory: String) -> void`; `has_override(id: StringName) -> bool`; `draw(canvas: CanvasItem, id: StringName, rect: Rect2, state: Dictionary = {}) -> void` - - `PapercutStyle`, `OutlineStyle` und `GeometricStyle` mit `KEY` (`&"papercut"`, `&"outline"`, `&"geometric"`) und den Texten `STYLE_PAPERCUT`, `STYLE_OUTLINE`, `STYLE_GEOMETRIC`, vorerst ohne eigene Zeichnungen + - `WarmStyle`, `PastelStyle` und `GlowStyle` mit `KEY` (`&"warm"`, `&"pastel"`, `&"glow"`) und den Texten `STYLE_WARM`, `STYLE_PASTEL`, `STYLE_GLOW`, vorerst ohne eigene Zeichnungen - [ ] **Step 1: Auf den Branch wechseln** @@ -188,29 +194,6 @@ Create `game/tests/test_art_style.gd`: extends TestCase ## The helpers that every style draws with. -const TILE := Rect2(0, 0, 16, 16) - - -func test_a_lone_tile_is_round_at_every_corner() -> void: - var points := ArtStyle.rounded_rect(TILE, 0, 4.0, 4) - check_eq(points.size(), 20, "five points for each of the four corners") - for point in points: - check(TILE.grow(0.001).has_point(point), "inside the tile: %s" % point) - check(not points.has(Vector2.ZERO), "the corner itself is cut off") - - -func test_a_tile_inside_a_mass_keeps_its_square_corners() -> void: - var all := 255 - check_eq(ArtStyle.rounded_rect(TILE, all, 4.0), PackedVector2Array([Vector2(0, 0), Vector2(16, 0), Vector2(16, 16), Vector2(0, 16)]), "four corners") - var top_left_open := ArtStyle.rounded_rect(TILE, TileShape.E | TileShape.S, 4.0, 4) - check_eq(top_left_open.size(), 5 + 1 + 1 + 1, "only the corner between the open sides is round") - - -func test_inner_layers_shrink_only_toward_open_sides() -> void: - var inner := ArtStyle.inset_open_sides(TILE, TileShape.N | TileShape.W, 2.0) - check_eq(inner, Rect2(0, 0, 14, 14), "kept toward the neighbours above and to the left") - check_eq(ArtStyle.inset_open_sides(TILE, 0, 2.0), Rect2(2, 2, 12, 12), "smaller on every open side") - func test_a_point_can_be_found_along_a_path() -> void: var path := PackedVector2Array([Vector2(0, 0), Vector2(10, 0), Vector2(10, 10)]) @@ -229,12 +212,11 @@ func test_shares_lie_between_zero_and_one_and_stay_the_same() -> void: check_eq(ArtStyle.share(variant, salt), share, "the same again") -func test_shapes_have_their_corners() -> void: - check_eq(ArtStyle.regular_polygon(Vector2.ZERO, 2.0, 6).size(), 6, "a hexagon") - check_eq(ArtStyle.ellipse(Vector2.ZERO, Vector2(3, 1), 12).size(), 12, "an ellipse") - check_eq(ArtStyle.slit(Vector2.ZERO, Vector2(10, 0), 2.0), PackedVector2Array([Vector2(0, 0), Vector2(5, -1), Vector2(10, 0), Vector2(5, 1)]), "a cut") - check_eq(ArtStyle.leaf(Vector2.ZERO, 0.0, 10.0, 4.0)[3], Vector2(10, 0), "a leaf ends at its tip") - check_eq(ArtStyle.closed(PackedVector2Array([Vector2.ZERO, Vector2.ONE])), PackedVector2Array([Vector2.ZERO, Vector2.ONE, Vector2.ZERO]), "closed") +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") ``` Create `game/tests/test_art.gd`: @@ -297,7 +279,7 @@ func test_the_styles_are_offered_in_a_fixed_order() -> void: 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, [PapercutStyle.KEY, OutlineStyle.KEY, GeometricStyle.KEY], "papercut, outline, geometric") + 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") @@ -305,12 +287,12 @@ 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(OutlineStyle.KEY), "the outline style exists") - check_eq(art.style.key(), OutlineStyle.KEY, "and is chosen") - check(art.select(OutlineStyle.KEY), "choosing it again works") + 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(), OutlineStyle.KEY, "and changes nothing") + check_eq(art.style.key(), PastelStyle.KEY, "and changes nothing") func test_the_next_style_follows_and_the_first_comes_after_the_last() -> void: @@ -319,7 +301,7 @@ func test_the_next_style_follows_and_the_first_comes_after_the_last() -> void: for i in art.styles.size() + 1: seen.append(art.style.key()) art.select_next() - check_eq(seen, [PapercutStyle.KEY, OutlineStyle.KEY, GeometricStyle.KEY, PapercutStyle.KEY], "round and round") + 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: @@ -599,64 +581,11 @@ static func share(variant: int, salt: int) -> float: return float(TileShape.variant(Vector2i(variant & 0xffff, salt)) % 10007) / 10007.0 -## The outline of `rect` as points clockwise, with each corner rounded by -## `radius` where neither neighbour beside the corner is set in `mask`, so -## that a mass of tiles is round only on its outside. -static func rounded_rect(rect: Rect2, mask: int, radius: float, segments := 4) -> PackedVector2Array: - var points := PackedVector2Array() - var corners := [ - [rect.position, TileShape.W | TileShape.N, PI], - [Vector2(rect.end.x, rect.position.y), TileShape.N | TileShape.E, 1.5 * PI], - [rect.end, TileShape.E | TileShape.S, 0.0], - [Vector2(rect.position.x, rect.end.y), TileShape.S | TileShape.W, 0.5 * PI], - ] - for corner: Array in corners: - var at: Vector2 = corner[0] - var sides: int = corner[1] - if mask & sides != 0 or radius <= 0.0: - points.append(at) - continue - var inward := Vector2(radius if at.x == rect.position.x else -radius, radius if at.y == rect.position.y else -radius) - var center := at + inward - var start: float = corner[2] - for i in segments + 1: - var angle := start + 0.5 * PI * i / segments - points.append(center + Vector2(cos(angle), sin(angle)) * radius) - return points - - -## `rect` made smaller by `inset` on each side that does not touch a -## neighbour set in `mask`, so that the inner layers of a mass of tiles join. -static func inset_open_sides(rect: Rect2, mask: int, inset: float) -> Rect2: - var left := 0.0 if mask & TileShape.W else inset - var top := 0.0 if mask & TileShape.N else inset - var right := 0.0 if mask & TileShape.E else inset - var bottom := 0.0 if mask & TileShape.S else inset - return Rect2(rect.position + Vector2(left, top), rect.size - Vector2(left + right, top + bottom)) - - ## 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 points of a regular polygon with `corners` corners around `center`. -static func regular_polygon(center: Vector2, radius: float, corners: int, rotation := 0.0) -> PackedVector2Array: - var points := PackedVector2Array() - for i in corners: - var angle := rotation + TAU * i / corners - points.append(center + Vector2(cos(angle), sin(angle)) * radius) - return points - - -## A closed copy of `points`, for drawing the outline of a polygon. -static func closed(points: PackedVector2Array) -> PackedVector2Array: - var result := points.duplicate() - if not points.is_empty(): - result.append(points[0]) - return result - - ## 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 @@ -669,38 +598,6 @@ static func along(points: PackedVector2Array, part: float) -> Vector2: return points[i].lerp(points[i + 1], left / length if length > 0.0 else 0.0) left -= length return points[points.size() - 1] - - -## The points of an ellipse with the half axes `radii` around `center`. -static func ellipse(center: Vector2, radii: Vector2, segments := 14) -> PackedVector2Array: - var points := PackedVector2Array() - for i in segments: - var angle := TAU * i / segments - points.append(center + Vector2(cos(angle) * radii.x, sin(angle) * radii.y)) - return points - - -## A narrow cut from `from` to `to`, `width` wide in the middle and pointed -## at both ends. -static func slit(from: Vector2, to: Vector2, width: float) -> PackedVector2Array: - var middle := (from + to) / 2.0 - var side := (to - from).orthogonal().normalized() * width / 2.0 - return PackedVector2Array([from, middle + side, to, middle - side]) - - -## A leaf `length` long and `width` wide that grows from `base` in the -## direction `angle`. -static func leaf(base: Vector2, angle: float, length: float, width: float) -> PackedVector2Array: - var along := Vector2.from_angle(angle) - var side := along.orthogonal() * width / 2.0 - return PackedVector2Array([ - base, - base + along * length * 0.35 + side, - base + along * length * 0.7 + side * 0.7, - base + along * length, - base + along * length * 0.7 - side * 0.7, - base + along * length * 0.35 - side, - ]) ``` Create `game/scripts/art/art.gd`: @@ -747,7 +644,7 @@ const IDS: Array[StringName] = [ const OVERRIDE_DIR := "res://art" ## The styles to choose from, in the order the game offers them. -var styles: Array[ArtStyle] = [PapercutStyle.new(), OutlineStyle.new(), GeometricStyle.new()] +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] = {} @@ -755,7 +652,7 @@ var _overrides: Dictionary[StringName, Texture2D] = {} ## The keys of the styles, as LaunchOptions and the settings name them. static func style_keys() -> Array[StringName]: - return [PapercutStyle.KEY, OutlineStyle.KEY, GeometricStyle.KEY] + return [WarmStyle.KEY, PastelStyle.KEY, GlowStyle.KEY] ## Draws everything in the style named `key` from now on. Returns false, and @@ -813,17 +710,17 @@ static func _load_texture(path: String) -> Texture2D: return ImageTexture.create_from_image(image) if image != null else null ``` -Die drei Stile sind vorerst dünn: Sie nennen ihren Schlüssel und ihren Namen und zeichnen die Platzhalter von `ArtStyle`. Die Tasks 5 bis 7 ersetzen sie durch die fertigen Stile. +Die drei Stile sind vorerst dünn: Sie nennen ihren Schlüssel und ihren Namen und zeichnen die Platzhalter von `ArtStyle`. Die Tasks 5 und 7 ersetzen sie durch die fertigen Abwandlungen. -Create `game/scripts/art/papercut_style.gd`: +Create `game/scripts/art/warm_style.gd`: ```gdscript -class_name PapercutStyle +class_name WarmStyle extends ArtStyle -## Layers of cut paper without outlines, with soft shadows. -## Until it has drawings of its own, it draws the placeholders of 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 := &"papercut" +const KEY := &"warm" func key() -> StringName: @@ -831,18 +728,18 @@ func key() -> StringName: func title_key() -> String: - return "STYLE_PAPERCUT" + return "STYLE_WARM" ``` -Create `game/scripts/art/outline_style.gd`: +Create `game/scripts/art/pastel_style.gd`: ```gdscript -class_name OutlineStyle +class_name PastelStyle extends ArtStyle -## Bold dark outlines around flat colours, easy to read like a comic. -## Until it has drawings of its own, it draws the placeholders of 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 := &"outline" +const KEY := &"pastel" func key() -> StringName: @@ -850,18 +747,18 @@ func key() -> StringName: func title_key() -> String: - return "STYLE_OUTLINE" + return "STYLE_PASTEL" ``` -Create `game/scripts/art/geometric_style.gd`: +Create `game/scripts/art/glow_style.gd`: ```gdscript -class_name GeometricStyle +class_name GlowStyle extends ArtStyle -## Plain geometric forms in calm pastel colours. -## Until it has drawings of its own, it draws the placeholders of 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 := &"geometric" +const KEY := &"glow" func key() -> StringName: @@ -869,7 +766,7 @@ func key() -> StringName: func title_key() -> String: - return "STYLE_GEOMETRIC" + return "STYLE_GLOW" ``` Ersetze den gesamten Inhalt von `game/i18n/texts.csv` durch die folgende Fassung. Neu sind die letzten drei Zeilen mit den Namen der Stile. @@ -912,9 +809,9 @@ 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_PAPERCUT,Scherenschnitt,Papercut -STYLE_OUTLINE,Kontur,Outline -STYLE_GEOMETRIC,Geometrisch,Geometric +STYLE_WARM,Warm,Warm +STYLE_PASTEL,Pastell,Pastel +STYLE_GLOW,Glimmen,Glow ``` - [ ] **Step 5: Tests ausführen und Erfolg sehen** @@ -926,7 +823,7 @@ godot --headless --path game --import godot --headless --path game -s res://tests/run_tests.gd ``` -Expected: Exit-Code 0 und `113 passed, 0 failed`. Die einzigen Warnungen sind die drei bekannten Zeilen. +Expected: Exit-Code 0 und `110 passed, 0 failed`. Die einzigen Warnungen sind die drei bekannten Zeilen. - [ ] **Step 6: Alles prüfen** @@ -948,10 +845,10 @@ Kennungen, Kachelformen und Platzhalter für die Grafik Jede Grafik auf dem Spielfeld bekommt eine Kennung. Art zeichnet sie im gewählten Stil oder aus einem Bild gleichen Namens unter game/art. -ArtStyle zeichnet vorerst schlichte Platzhalter und bringt Helfer für -Formen mit. TileShape beschreibt die Nachbarn einer Kachel, damit -Felsmassen außen rund und innen geschlossen gezeichnet werden können. -Die drei Stile gibt es schon mit Namen, noch ohne eigene Zeichnungen. +ArtStyle zeichnet vorerst schlichte Platzhalter. TileShape beschreibt +die Nachbarn einer Kachel, damit Felsmassen außen rund und innen +geschlossen gezeichnet werden können. Die drei Stile Warm, Pastell und +Glimmen gibt es schon mit Namen, noch ohne eigene Zeichnungen. Co-Authored-By: Claude Opus 5.5 EOF @@ -964,14 +861,14 @@ git push -u origin m1d-stilproben **Files:** - Modify: `game/scripts/map_view.gd`, `game/scripts/avatar.gd`, `game/scripts/figures.gd`, `game/scripts/tile_marker.gd`, `game/scripts/main.gd` (jeweils ganze Datei ersetzen) -- Test: `game/tests/test_map_view.gd` (neu); `game/tests/test_avatar.gd`, `game/tests/test_figures.gd`, `game/tests/test_main.gd` (je ein Test am Ende) +- Test: `game/tests/test_map_view.gd` (neu); `game/tests/test_avatar.gd` (zwei Tests am Ende); `game/tests/test_figures.gd`, `game/tests/test_main.gd` (je ein Test am Ende) **Interfaces:** - Consumes: `Art` und `TileShape` aus Task 1. Aus `GwSession`: `is_running()`, `map_width()`, `map_height()`, `terrain_codes()`, `damage_codes()`, `terrain_at(x, y)`, `spawn_point()`, `local_player()`, `player_name(player)`, `dig(x, y)`, `step()` und die Konstanten `TERRAIN_GROUND`, `TERRAIN_SOFT_ROCK`, `TERRAIN_HARD_ROCK`, `TERRAIN_BEDROCK`, `DIG_COOLDOWN_TICKS`. Aus `GridMotion`: `TILE_SIZE`, `move`, `tile_center`. - Produces: - `MapView`: `art: Art`; `SOLID: Array[int]` (weicher Fels, harter Fels, Grundgestein), `BLOCK := 16`, `HIT_SECONDS := 0.35`; `static id_of(terrain: int) -> StringName`; `reload()` nimmt die ganze Karte neu aus der Sitzung, `redraw()` zeichnet alles neu, `refresh_tile(tile: Vector2i)` nimmt eine Kachel neu und zeichnet die Blöcke um sie, `hit(tile: Vector2i)` zeigt Splitter; für Tests `block_count() -> int`, `burst_count() -> int` und `mask_at(tile: Vector2i) -> int`. Wer die Karte neu zeigen will, ruft `reload()`; `queue_redraw()` auf `MapView` selbst zeichnet die Karte nicht mehr. - - `Avatar`: `art: Art`, `player := -1`, `facing := 1.0`, `step := 0.0`, `moving := false`, `STEP_PER_PIXEL := 0.16`, `static figure_rect(at: Vector2) -> Rect2` - - `Figures`: `art: Art`, `facing_of(player: int) -> float` + - `Avatar`: `art: Art`, `player := -1`, `facing := 1.0`, `step := 0.0`, `moving := false`, `STEP_PER_PIXEL := 0.16`, `static figure_rect(at: Vector2) -> Rect2`: eine Kachel breit, anderthalb hoch, die Unterkante bei `at.y + RADIUS + 1` + - `Figures`: `art: Art`, `facing_of(player: int) -> float`; die Namen stehen über `Avatar.figure_rect` - `TileMarker`: `art: Art` - Hauptszene: `art: Art`. `dig_at(tile)` zeigt die Splitter sofort, wenn die Kachel kein Boden ist. @@ -1076,7 +973,7 @@ func test_without_a_world_nothing_is_drawn() -> void: _free(view) ``` -Füge am Ende von `game/tests/test_avatar.gd` diesen Test an. Wie zwischen allen Funktionen stehen davor zwei Leerzeilen: +Füge am Ende von `game/tests/test_avatar.gd` diese beiden Tests an. Wie zwischen allen Funktionen stehen davor zwei Leerzeilen: ```gdscript func test_a_walking_figure_steps_and_faces_the_way_it_goes() -> void: @@ -1093,6 +990,13 @@ func test_a_walking_figure_steps_and_faces_the_way_it_goes() -> void: avatar.walk(Vector2.ZERO, 0.1) check(not avatar.moving, "standing again") avatar.free() + + +func test_a_figure_stands_upright_in_its_tile() -> void: + var rect := Avatar.figure_rect(Vector2(100, 100)) + check_eq(rect.size, Vector2(GridMotion.TILE_SIZE, GridMotion.TILE_SIZE * 1.5), "a tile wide, one and a half tall") + check_eq(rect.get_center().x, 100.0, "centred on the figure") + check_eq(rect.end.y, 100.0 + Avatar.RADIUS + 1.0, "its feet just below its body") ``` Füge am Ende von `game/tests/test_figures.gd` diesen Test an, wieder nach zwei Leerzeilen: @@ -1131,7 +1035,7 @@ godot --headless --path game --import godot --headless --path game -s res://tests/run_tests.gd ``` -Expected: Der Testlauf endet mit Exit-Code 1 und `113 passed, 4 failed`. `test_map_view.gd` lädt nicht (`SCRIPT ERROR: Parse Error: Cannot find member "BLOCK" in base "MapView".`). Die drei angehängten Tests scheitern mit `Invalid access to property or key 'moving' on a base object of type 'Node2D (Avatar)'.`, `Invalid call. Nonexistent function 'facing_of' in base 'Node2D (Figures)'.` und `Invalid call. Nonexistent function 'burst_count' in base 'Node2D (MapView)'.` Am Ende meldet Godot liegen gebliebene Objekte (`… were leaked at exit`), weil die abgebrochenen Tests nicht aufräumen. +Expected: Der Testlauf endet mit Exit-Code 1 und `107 passed, 4 failed`. `test_avatar.gd` lädt nicht (`SCRIPT ERROR: Parse Error: Static function "figure_rect()" not found in base "Avatar".`), `test_map_view.gd` auch nicht (`Parse Error: Cannot find member "BLOCK" in base "MapView".`). Die angehängten Tests in `test_figures.gd` und `test_main.gd` scheitern mit `Invalid call. Nonexistent function 'facing_of' in base 'Node2D (Figures)'.` und `Invalid call. Nonexistent function 'burst_count' in base 'Node2D (MapView)'.` Am Ende meldet Godot liegen gebliebene Objekte (`… were leaked at exit`), weil die abgebrochenen Tests nicht aufräumen. - [ ] **Step 3: Karte, Figuren und Zielmarke über Art zeichnen** @@ -1355,10 +1259,12 @@ var step := 0.0 var moving := false -## The square a figure standing at `at` is drawn in. +## Where a figure standing at `at` is drawn: a tile wide and one and a half +## tiles tall, with its feet just below its body, so that it stands upright +## in its tile. static func figure_rect(at: Vector2) -> Rect2: var size := GridMotion.TILE_SIZE - return Rect2(at - Vector2(size, size) / 2.0, Vector2(size, size)) + return Rect2(at.x - size / 2.0, at.y + RADIUS + 1.0 - size * 1.5, size, size * 1.5) func _process(delta: float) -> void: @@ -1477,7 +1383,7 @@ func _draw() -> void: var state := {"player": player, "facing": facing_of(player), "step": _step.get(player, 0.0), "moving": _moving.get(player, false)} art.draw(self, Art.FIGURE, Avatar.figure_rect(at), state) if session != null: - var label := Vector2(at.x - NAME_WIDTH / 2.0, at.y - GridMotion.TILE_SIZE / 2.0 - 2.0) + var label := Vector2(at.x - NAME_WIDTH / 2.0, Avatar.figure_rect(at).position.y - 2.0) var text := session.player_name(player) var size := roundi(NAME_SIZE * zoom) draw_set_transform(label, 0.0, Vector2.ONE / zoom) @@ -2007,7 +1913,7 @@ godot --headless --path game --import godot --headless --path game -s res://tests/run_tests.gd ``` -Expected: Exit-Code 0 und `121 passed, 0 failed`. Die einzigen Warnungen sind die drei bekannten Zeilen. +Expected: Exit-Code 0 und `119 passed, 0 failed`. Die einzigen Warnungen sind die drei bekannten Zeilen. - [ ] **Step 5: Bildschirmfoto prüfen** @@ -2035,10 +1941,11 @@ Spielfeld über Art zeichnen Karte, Figuren und Zielmarke zeichnet jetzt Art. Die Karte zeichnet in Blöcken von 16 mal 16 Kacheln und nach einem Treffer nur die Blöcke um -die Kachel neu; jede Kachel kennt ihre Nachbarn. Figuren schreiten und -schauen in die Richtung, in die sie gehen. Der eigene Schlag zeigt -seine Splitter sofort, auch als Gast, und die Namen über fremden -Figuren werden in Bildschirmgröße gezeichnet und bleiben scharf. +die Kachel neu; jede Kachel kennt ihre Nachbarn. Figuren stehen +aufrecht in ihrer Kachel, schreiten und schauen in die Richtung, in die +sie gehen. Der eigene Schlag zeigt seine Splitter sofort, auch als Gast, +und die Namen über fremden Figuren werden in Bildschirmgröße gezeichnet +und bleiben scharf. Co-Authored-By: Claude Opus 5.5 EOF @@ -2174,7 +2081,7 @@ func test_the_style_samples_open_from_the_menu_and_lead_back_to_it() -> void: check(main.showcase.visible, "the samples show") check(not main.menu.visible, "instead of the menu") main.showcase.style_buttons[2].pressed.emit() - check_eq(main.art.style.key(), GeometricStyle.KEY, "a button there chooses the style") + check_eq(main.art.style.key(), GlowStyle.KEY, "a button there chooses the style") await tree.process_frame main.showcase.back_button.pressed.emit() check(main.menu.visible, "the menu is back") @@ -2191,7 +2098,7 @@ godot --headless --path game --import godot --headless --path game -s res://tests/run_tests.gd ``` -Expected: Der Testlauf endet mit Exit-Code 1 und `120 passed, 4 failed`. `test_showcase.gd` lädt nicht (`SCRIPT ERROR: Parse Error: Identifier "Showcase" not declared in the current scope.`). `test_every_action_of_the_game_exists` vermisst `action next_style`, `action back` und `action style_1` bis `action style_3`. Die neuen Tests in `test_menu.gd` und `test_main.gd` scheitern mit `Invalid access to property or key 'showcase_chosen' on a base object of type 'CanvasLayer (Menu)'.` beziehungsweise `… key 'showcase_button' …`. +Expected: Der Testlauf endet mit Exit-Code 1 und `118 passed, 4 failed`. `test_showcase.gd` lädt nicht (`SCRIPT ERROR: Parse Error: Identifier "Showcase" not declared in the current scope.`). `test_every_action_of_the_game_exists` vermisst `action next_style`, `action back` und `action style_1` bis `action style_3`. Die neuen Tests in `test_menu.gd` und `test_main.gd` scheitern mit `Invalid access to property or key 'showcase_chosen' on a base object of type 'CanvasLayer (Menu)'.` beziehungsweise `… key 'showcase_button' …`. - [ ] **Step 3: Vitrine, Menü, Tasten und Texte schreiben** @@ -2629,9 +2536,9 @@ 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_PAPERCUT,Scherenschnitt,Papercut -STYLE_OUTLINE,Kontur,Outline -STYLE_GEOMETRIC,Geometrisch,Geometric +STYLE_WARM,Warm,Warm +STYLE_PASTEL,Pastell,Pastel +STYLE_GLOW,Glimmen,Glow MENU_SHOWCASE,Stilproben ansehen,See the style samples SHOWCASE_TITLE,Stilprobe: %s,Style sample: %s SHOWCASE_BACK,Zurück,Back @@ -3163,7 +3070,7 @@ godot --headless --path game --import godot --headless --path game -s res://tests/run_tests.gd ``` -Expected: Exit-Code 0 und `128 passed, 0 failed`. Die einzigen Warnungen sind die drei bekannten Zeilen. +Expected: Exit-Code 0 und `126 passed, 0 failed`. Die einzigen Warnungen sind die drei bekannten Zeilen. - [ ] **Step 5: Alles prüfen** @@ -3207,7 +3114,7 @@ git push - Produces: - `LaunchOptions.style: StringName`: der Stil aus `--style=NAME`, leer ohne Angabe. Ein unbekannter Name landet in `ignored`. - `SaveStore.style() -> String` und `SaveStore.set_style(key: String) -> void`, in den Einstellungen unter Abschnitt `display`, Schlüssel `style` - - Hauptszene: Beim Start gilt der Stil der Befehlszeile, sonst der gemerkte. F2 wechselt im Spiel und meldet `STYLE_SWITCHED`. Jeder Wechsel wird gemerkt. + - Hauptszene: Beim Start gilt der Stil der Befehlszeile, sonst der gemerkte, sonst Warm. F2 wechselt im Spiel und meldet `STYLE_SWITCHED`. Jeder Wechsel wird gemerkt. - `res://tools/screenshot.gd`, aufgerufen mit `-- [--style=NAME] [--showcase]` - Texte: `STYLE_SWITCHED`; die Hilfezeile `HELP` nennt F2 @@ -3217,7 +3124,7 @@ Füge am Ende von `game/tests/test_launch_options.gd` diesen Test an, nach zwei ```gdscript func test_a_style_can_be_chosen() -> void: - check_eq(LaunchOptions.parse(PackedStringArray(["--style=geometric"])).style, GeometricStyle.KEY, "a known style") + check_eq(LaunchOptions.parse(PackedStringArray(["--style=glow"])).style, GlowStyle.KEY, "a known style") check_eq(LaunchOptions.parse(PackedStringArray()).style, &"", "none without the argument") var unknown := LaunchOptions.parse(PackedStringArray(["--style=watercolour"])) check_eq(unknown.style, &"", "an unknown style is not taken") @@ -3231,8 +3138,8 @@ func test_the_chosen_style_is_kept() -> void: var directory := TestFiles.temporary_dir("style") var store := SaveStore.new(directory) check_eq(store.style(), "", "no style at first") - store.set_style("outline") - check_eq(SaveStore.new(directory).style(), "outline", "kept for the next start") + store.set_style("pastel") + check_eq(SaveStore.new(directory).style(), "pastel", "kept for the next start") TestFiles.remove_dir(directory) ``` @@ -3259,10 +3166,10 @@ func test_the_command_line_chooses_the_style() -> void: var directory := TestFiles.temporary_dir("style-option") var main: Node = load("res://main.tscn").instantiate() main.storage_dir = directory - main.command_line = PackedStringArray(["--style=outline"]) + main.command_line = PackedStringArray(["--style=pastel"]) tree.root.add_child(main) _started.append(main) - check_eq(main.art.style.key(), OutlineStyle.KEY, "the style of the command line") + check_eq(main.art.style.key(), PastelStyle.KEY, "the style of the command line") _clean_up([directory]) ``` @@ -3275,14 +3182,14 @@ godot --headless --path game --import godot --headless --path game -s res://tests/run_tests.gd ``` -Expected: Der Testlauf endet mit Exit-Code 1 und `128 passed, 4 failed`: +Expected: Der Testlauf endet mit Exit-Code 1 und `126 passed, 4 failed`: - `test_a_style_can_be_chosen` mit `Invalid access to property or key 'style' on a base object of type 'RefCounted (LaunchOptions)'.` - `test_the_chosen_style_is_kept` mit `Invalid call. Nonexistent function 'style' in base 'RefCounted (SaveStore)'.` - `test_f2_switches_the_style_and_the_choice_is_kept` mit `another style` -- `test_the_command_line_chooses_the_style` mit `the style of the command line: expected outline, got papercut` +- `test_the_command_line_chooses_the_style` mit `the style of the command line: expected pastel, got warm` -Dazu kommen `WARNING: ignored command line argument: --style=outline` und `ERROR: String formatting error: not all arguments converted during string formatting.`, weil es den Text `STYLE_SWITCHED` noch nicht gibt. +Dazu kommen `WARNING: ignored command line argument: --style=pastel` und `ERROR: String formatting error: not all arguments converted during string formatting.`, weil es den Text `STYLE_SWITCHED` noch nicht gibt. - [ ] **Step 3: Befehlszeile, Einstellung, Hauptszene, Texte und Werkzeug schreiben** @@ -3300,8 +3207,8 @@ extends RefCounted ## --host opens the world for guests at once ## --port=PORT the port an open world listens on ## --join=ADDRESS joins a host at once; ADDRESS may end in ":PORT" -## --style=NAME draws the playing field in the style NAME: papercut, -## outline or geometric +## --style=NAME draws the playing field in the style NAME: warm, +## pastel or glow ## Longest profile name. const MAX_PROFILE_LENGTH := 32 @@ -3941,9 +3848,9 @@ 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_PAPERCUT,Scherenschnitt,Papercut -STYLE_OUTLINE,Kontur,Outline -STYLE_GEOMETRIC,Geometrisch,Geometric +STYLE_WARM,Warm,Warm +STYLE_PASTEL,Pastell,Pastel +STYLE_GLOW,Glimmen,Glow STYLE_SWITCHED,Stil: %s,Style: %s MENU_SHOWCASE,Stilproben ansehen,See the style samples SHOWCASE_TITLE,Stilprobe: %s,Style sample: %s @@ -4056,18 +3963,18 @@ godot --headless --path game --import godot --headless --path game -s res://tests/run_tests.gd ``` -Expected: Exit-Code 0 und `132 passed, 0 failed`. Die einzigen Warnungen sind die drei bekannten Zeilen. +Expected: Exit-Code 0 und `130 passed, 0 failed`. Die einzigen Warnungen sind die drei bekannten Zeilen. - [ ] **Step 5: Bildschirmfotos prüfen** Run: ```bash -godot --path game -s res://tools/screenshot.gd -- "$TEMP/m1d-task4-spiel.png" --style=outline +godot --path game -s res://tools/screenshot.gd -- "$TEMP/m1d-task4-spiel.png" --style=pastel godot --path game -s res://tools/screenshot.gd -- "$TEMP/m1d-task4-vitrine.png" --showcase ``` -Expected: Beide Aufrufe enden mit `screenshot saved: …` und geben weder Warnung noch Fehler aus. Beide Bilder zeigen noch die Platzhalter, denn die Stile zeichnen erst ab Task 5 selbst. Das erste Bild zeigt das Spiel wie nach Task 2, dazu links eine zweite Figur, über der scharf „Spieler“ steht, und kleine helle Splitter am getroffenen Fels. Das zweite Bild zeigt die Vitrine: oben „Stilprobe: Scherenschnitt“, in der Mitte die Szene aus Rechtecken, Kreisen und Linien mit neun weißen Beschriftungen, unten die Knöpfe „1 Scherenschnitt“ (ausgegraut), „2 Kontur“, „3 Geometrisch“ und „Zurück“. +Expected: Beide Aufrufe enden mit `screenshot saved: …` und geben weder Warnung noch Fehler aus. Beide Bilder zeigen noch die Platzhalter, denn die Pixelgrafik folgt erst ab Task 5. Das erste Bild zeigt das Spiel wie nach Task 2, dazu links eine zweite Figur, über der scharf „Spieler“ steht, und kleine helle Splitter am getroffenen Fels. Das zweite Bild zeigt die Vitrine: oben „Stilprobe: Warm“, in der Mitte die Szene aus Rechtecken, Kreisen und Linien mit neun weißen Beschriftungen, unten die Knöpfe „1 Warm“ (ausgegraut), „2 Pastell“, „3 Glimmen“ und „Zurück“. - [ ] **Step 6: Alles prüfen** @@ -4099,28 +4006,120 @@ git push --- -### Task 5: Stil Scherenschnitt +### Task 5: Pixelgrafik für Boden, Fels und Figuren -Lagen aus geschnittenem Papier ohne Umriss, mit weichen Schatten. Gedeckte Erdfarben; Lebendiges leuchtet grün und golden. Das Licht fällt von oben links, also wirft Fels seinen Schatten nach rechts unten auf den Boden. +`PixelStyle` zeichnet jede Grafik Pixel für Pixel in ein kleines Bild und hebt es auf. Dieser Task bringt den Boden, die drei Felsarten mit Vorderkante und Schatten, Risse, Splitter, die Zielmarke und die Figuren mit Laufbildern; Task 6 bringt die Dinge der Vitrine. `WarmStyle` wird die erste fertige Abwandlung und bekommt schon die ganze Palette, auch die Farben, die erst Task 6 braucht. **Files:** -- Modify: `game/scripts/art/papercut_style.gd` (ganze Datei ersetzen) -- Test: `game/tests/test_papercut_style.gd` (neu) +- Create: `game/scripts/art/pixel_style.gd` +- Modify: `game/scripts/art/warm_style.gd`, `game/project.godot` (jeweils ganze Datei ersetzen) +- Test: `game/tests/test_pixel_style.gd`, `game/tests/test_warm_style.gd` **Interfaces:** -- Consumes: `ArtStyle` mit seinen Zeichenmethoden und Helfern, `TileShape`, `Art.IDS`, die Einträge von `state` laut Kopf von `art_style.gd` -- Produces: `PapercutStyle` zeichnet jede Kennung selbst; `PLAYER_COLORS: Array[Color]` mit acht Farben und `player_color(player: int) -> Color` +- Consumes: `ArtStyle` mit seinen Zeichenmethoden und `ArtStyle.along`; `TileShape` mit seinen Bits und `TileShape.variant`; die Einträge von `state` laut Kopf von `art_style.gd`; `Avatar.figure_rect` aus Task 2, in das eine Figur gezeichnet wird +- Produces: + - `PixelStyle` (erbt von `ArtStyle`): die Konstanten `TILE := 16`, `FACE := 6`, `HIT_FRAMES := 6`, `FIGURE_SIZE := Vector2i(16, 24)` und `CLEAR`; die Felder `palette: Dictionary[String, Color]`, `players: Array[Color]` und `tinted_outlines: bool`, die eine Unterklasse füllt + - die innere Klasse `PixelStyle.Pixels` mit `image: Image`, `size: Vector2i`, `has`, `put`, `at`, `filled`, `shade`, `rect`, `ellipse`, `line` und `outline(color: Color, tinted: bool)` + - `_image(key: String, make: Callable) -> Texture2D`: macht das Bild unter `key` nur beim ersten Mal; `static _shape(mask: int) -> int`; `static _spot(index: int, variant: int, low: int, span: int) -> Vector2i`; `static _round(pixels, corner, inward, ink)`; `_shadow_image(width: int) -> Pixels` + - die Bilder `_earth(variant: int, shadow: int) -> Pixels`, `_rock_image(kind: String, mask: int, variant: int) -> Pixels` (`kind` ist `"soft"`, `"hard"` oder `"bed"`) und `_figure_image(colour: int, frame: int, left: bool) -> Pixels` (Bild 0 steht, 1 bis 4 laufen) + - die Farben der Palette mit den Namen, die `warm_style.gd` setzt; `PastelStyle` und `GlowStyle` in Task 7 setzen dieselben Namen -- [ ] **Step 1: Fehlschlagenden Test schreiben** +- [ ] **Step 1: Fehlschlagende Tests schreiben** -Create `game/tests/test_papercut_style.gd`: +Create `game/tests/test_pixel_style.gd`: + +```gdscript +extends TestCase +## The pixel art of the ground, the rock and the figures, which every pixel +## style draws. + + +## How many pixels of `image` are not transparent. +static func _opaque(image: Image) -> int: + var count := 0 + for y in image.get_height(): + for x in image.get_width(): + if image.get_pixel(x, y).a > 0.0: + count += 1 + return count + + +func test_the_field_is_drawn_in_sharp_pixels() -> void: + check_eq(ProjectSettings.get_setting("rendering/textures/canvas_textures/default_texture_filter"), 0, "each pixel a square, not blurred") + check(ProjectSettings.get_setting("rendering/2d/snap/snap_2d_transforms_to_pixel"), "on whole pixels of the screen") + + +func test_ground_fills_its_tile_and_lies_in_the_shadow_of_rock() -> void: + var style := WarmStyle.new() + var open := style._earth(3, 0).image + check_eq(open.get_size(), Vector2i(PixelStyle.TILE, PixelStyle.TILE), "a tile") + check_eq(_opaque(open), PixelStyle.TILE * PixelStyle.TILE, "no gaps") + var shaded := style._earth(3, TileShape.N).image + check(shaded.get_pixel(5, 0).get_luminance() < open.get_pixel(5, 0).get_luminance(), "darker below rock") + check_eq(shaded.get_pixel(5, 8), open.get_pixel(5, 8), "but only along the rock") + + +func test_a_lone_rock_is_round_and_shows_its_face() -> void: + var style := WarmStyle.new() + var lone := style._rock_image("soft", 0, 0).image + check_eq(lone.get_pixel(0, 0).a, 0.0, "a round corner, the ground shows there") + check_eq(lone.get_pixel(PixelStyle.TILE - 1, PixelStyle.TILE - 1).a, 0.0, "round at the bottom too") + check_eq(lone.get_pixel(8, 0), style.palette["outline"], "outlined at the top") + check_eq(lone.get_pixel(8, PixelStyle.TILE - 1), style.palette["outline"], "and where it meets the ground") + check_eq(lone.get_pixel(5, PixelStyle.TILE - 3), style.palette["soft_face"], "its face toward the open ground") + + +func test_rock_inside_a_mass_has_neither_edge_nor_face() -> void: + var style := WarmStyle.new() + var inside := style._rock_image("soft", 255, 0).image + check_eq(_opaque(inside), PixelStyle.TILE * PixelStyle.TILE, "no gaps") + for y in PixelStyle.TILE: + for x in PixelStyle.TILE: + var colour := inside.get_pixel(x, y) + check(colour != style.palette["outline"] and colour != style.palette["soft_face"], "inside at %d, %d" % [x, y]) + + +func test_only_the_corners_that_matter_tell_tiles_apart() -> void: + check_eq(PixelStyle._shape(TileShape.NE), 0, "a corner alone does not count") + check_eq(PixelStyle._shape(TileShape.N | TileShape.E | TileShape.NE), TileShape.N | TileShape.E | TileShape.NE, "between two sides it does") + check_eq(PixelStyle._shape(255), 255, "all of them inside a mass") + + +func test_each_picture_is_made_only_once() -> void: + var style := WarmStyle.new() + var made := [0] + var make := func() -> PixelStyle.Pixels: + made[0] += 1 + return PixelStyle.Pixels.new(1, 1) + var first := style._image("test/once", make) + var again := style._image("test/once", make) + check_eq(again, first, "the same picture") + check_eq(made[0], 1, "made once") + + +func test_a_figure_walks_in_four_steps_and_faces_both_ways() -> void: + var style := WarmStyle.new() + var frames: Array[PackedByteArray] = [] + for frame in 5: + var image := style._figure_image(0, frame, false).image + check_eq(image.get_size(), PixelStyle.FIGURE_SIZE, "a tile wide, one and a half tall") + check(not frames.has(image.get_data()), "frame %d differs from the others" % frame) + frames.append(image.get_data()) + var left := style._figure_image(0, 1, true).image + left.flip_x() + check_eq(left.get_data(), frames[1], "facing left mirrors facing right") + var other := style._figure_image(1, 0, false).image + check(other.get_data() != frames[0], "another player has another colour") +``` + +Create `game/tests/test_warm_style.gd`: ```gdscript extends TestCase func test_four_players_have_four_colours() -> void: - var style := PapercutStyle.new() + var style := WarmStyle.new() var colours := {} for player in 4: colours[style.player_color(player)] = true @@ -4128,7 +4127,7 @@ func test_four_players_have_four_colours() -> void: check_eq(style.player_color(-1), style.player_color(0), "a player not in the world yet looks like the first") ``` -- [ ] **Step 2: Test ausführen und Fehlschlag sehen** +- [ ] **Step 2: Tests ausführen und Fehlschlag sehen** Run: @@ -4137,68 +4136,530 @@ godot --headless --path game --import godot --headless --path game -s res://tests/run_tests.gd ``` -Expected: Exit-Code 1 und `132 passed, 1 failed`; unter `FAIL test_papercut_style.gd test_four_players_have_four_colours` steht `four players, four colours: expected 4, got 2`, denn der Platzhalter kennt nur zwei Farben. +Expected: Der Testlauf endet mit Exit-Code 1 und `130 passed, 2 failed`. `test_pixel_style.gd` lädt nicht; davor stehen Zeilen wie `SCRIPT ERROR: Parse Error: Identifier "PixelStyle" not declared in the current scope.` Unter `FAIL test_warm_style.gd test_four_players_have_four_colours` steht `four players, four colours: expected 4, got 2`, denn der Platzhalter kennt nur zwei Farben. -- [ ] **Step 3: Stil schreiben** +- [ ] **Step 3: Pixelgrafik, Palette und scharfe Pixel einbauen** -Ersetze den gesamten Inhalt von `game/scripts/art/papercut_style.gd` durch: +Create `game/scripts/art/pixel_style.gd`: ```gdscript -class_name PapercutStyle +class_name PixelStyle extends ArtStyle -## Layers of cut paper without outlines, with soft shadows. Muted earth -## colours; living things glow in green and gold. Light falls from the top -## left, so rock casts its shadow onto the ground below and to the right. +## 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. -const KEY := &"papercut" +## 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 +## 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) -const GROUND := Color("9a8468") -const GROUND_FLECK := Color("8d7860") -const GROUND_FIBRE := Color("a8926f") -const SHADOW := Color(0.16, 0.11, 0.07, 0.38) -## The outer, fainter part of a shadow on the ground. -const SHADOW_FAINT := Color(0.16, 0.11, 0.07, 0.17) -const SOFT_BASE := Color("8b8275") -const SOFT_TOP := Color("bab1a2") -const SOFT_SCRAP := Color("a69d8e") -const HARD_BASE := Color("454c5b") -const HARD_TOP := Color("646c7e") -const HARD_FACET := Color("7f8899") -const BEDROCK_BASE := Color("241e1a") -const BEDROCK_TOP := Color("342b25") -const CUT := Color("2a2119") -const DEAD_CUT := Color("6f5e4a") -const LIVING_BASE := Color("587f3e") -const LIVING_TOP := Color("72a04c") -const GOLD := Color("ebc75e") -const MOSS_DARK := Color("4b7633") -const MOSS_LIGHT := Color("89bd59") -const VINE_DARK := Color("3d672b") -const VINE_LIGHT := Color("6e9d44") -const LEAF := Color("88ba57") -const STONE_BASE := Color("8f877a") -const STONE_TOP := Color("b1a999") -const STEM := Color("5a8739") -const FIBRE := Color("f1e8d2") -const SEED := Color("7a5b3a") -const WOOL := Color("eee6d4") -const WOOL_SHADE := Color("d3c7b0") -const FACE := Color("4a3a30") -const RUIN_BASE := Color("767067") -const RUIN_TOP := Color("a39c90") -const RUIN_DARK := Color("2d2722") -const SKIN := Color("e9cba4") -const HAIR := Color("4b3528") -const LEGS := Color("3a2f28") -const MARKER_NEAR := Color("f0cf6a") -const MARKER_FAR := Color("c25b4c") -const PLAYER_COLORS: Array[Color] = [ - Color("c5563f"), Color("3f7cad"), Color("d99a3d"), Color("6a9a4c"), - Color("8b5a7c"), Color("4a9a96"), Color("b07a52"), Color("6b6fa8"), -] -## How far each of the two bands of a rock shadow reaches onto the ground, -## in pixels: a darker band along the rock and a fainter one beyond it. -const SHADOW_DEPTH := 1.8 +## 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 +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] + var bob := -1 if frame == 2 or frame == 4 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 +``` + +Ersetze den gesamten Inhalt von `game/scripts/art/warm_style.gd` durch: + +```gdscript +class_name WarmStyle +extends PixelStyle +## Warm, full colours and dark outlines around figures and things, close to +## the classic farming games. + +const KEY := &"warm" + + +func _init() -> void: + palette = { + "outline": Color("3a2820"), "dust": Color("e9dcc6"), + "ground": Color("b98a5e"), "ground_dark": Color("a07650"), "ground_light": Color("cba077"), + "pebble": Color("8e8072"), "pebble_light": Color("b8ab9a"), "pebble_dark": Color("6b5f54"), + "dead_crack": Color("8a6646"), + "soft_top": Color("bcb0a0"), "soft_top_dark": Color("a69a8a"), "soft_top_light": Color("d6ccbd"), + "soft_face": Color("8f8170"), "soft_face_dark": Color("776a5b"), "soft_lip": Color("e4dccf"), + "hard_top": Color("7c88a2"), "hard_top_dark": Color("68738c"), "hard_top_light": Color("9aa6c0"), + "hard_face": Color("596380"), "hard_face_dark": Color("454d66"), "hard_lip": Color("b6c2da"), + "hard_glint": Color("dff4ff"), + "bed_top": Color("51453e"), "bed_top_dark": Color("433832"), "bed_top_light": Color("62554c"), + "bed_face": Color("382e29"), "bed_face_dark": Color("2b231f"), "bed_lip": Color("6e6157"), + "grass": Color("64a83f"), "grass_dark": Color("4e8f33"), "grass_light": Color("8acb52"), + "grass_edge": Color("3f7a2c"), + "flower_a": Color("f7e25c"), "flower_b": Color("fbf6ea"), "flower_c": Color("ef7fa6"), + "moss": Color("4e9a3a"), "moss_dark": Color("3b7b2e"), "moss_light": Color("7ec552"), "spore": Color("ffe57e"), + "vine": Color("4f9439"), "vine_dark": Color("38702a"), "leaf": Color("68b545"), "leaf_dark": Color("4c8d35"), + "stem": Color("5c9c3c"), "bud": Color("acd97a"), "fibre": Color("f8f3e6"), "fibre_shade": Color("dcd3c0"), + "seed": Color("7b5433"), + "stone": Color("a29a8f"), "stone_light": Color("c8c1b6"), "stone_dark": Color("7c756d"), + "wool": Color("f5f0e6"), "wool_shade": Color("dbd2c2"), "wool_light": Color("ffffff"), + "face": Color("4b3a31"), "hoof": Color("35281f"), + "ruin": Color("aaa395"), "ruin_dark": Color("8a8376"), "ruin_light": Color("c6bfb2"), + "ruin_moss": Color("5f9f43"), "door": Color("2b231f"), "glow": Color("ffd560"), "glow_light": Color("fff4c6"), + "skin": Color("f4cba5"), "skin_shade": Color("dba67f"), "hair": Color("6e4128"), "hair_light": Color("8f5a37"), + "eye": Color("2a1b15"), "pants": Color("4b5b8d"), "pants_dark": Color("3a4672"), "boots": Color("5b3a26"), + "marker_near": Color("fff6c2"), "marker_far": Color("ff705c"), + } + players = [ + Color("d9573f"), Color("3f7fd1"), Color("e9b23c"), Color("4ea84a"), + Color("9a62c9"), Color("39a9a0"), Color("e271a1"), Color("e8873b"), + ] func key() -> StringName: @@ -4206,302 +4667,35 @@ func key() -> StringName: func title_key() -> String: - return "STYLE_PAPERCUT" + return "STYLE_WARM" +``` +Ersetze den gesamten Inhalt von `game/project.godot` durch die folgende Fassung. Neu sind die letzten beiden Zeilen im Abschnitt `[rendering]`: Texturfilterung „Nearest“ und Einrasten auf ganze Pixel. -func player_color(player: int) -> Color: - return PLAYER_COLORS[maxi(0, player) % PLAYER_COLORS.size()] +```ini +; Engine configuration file. +; Input actions are registered in code (scripts/input_setup.gd), so this file +; holds no serialized input events. +config_version=5 -func _ground(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: - canvas.draw_rect(rect, GROUND) - _paper_bits(canvas, rect, state.get("variant", 0), GROUND_FLECK, GROUND_FIBRE) - _shadow(canvas, rect, state.get("solid", 0)) +[application] +config/name="Grovewright" +run/main_scene="res://main.tscn" +config/features=PackedStringArray("4.7", "GL Compatibility") -func _soft_rock(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: - var solid: int = state.get("solid", 0) - var variant: int = state.get("variant", 0) - _raised(canvas, rect, solid, SOFT_BASE, SOFT_TOP) - if share(variant, 3) < 0.4: - var at := rect.position + Vector2(4, 4) + Vector2(share(variant, 4), share(variant, 5)) * 6.0 - canvas.draw_colored_polygon(regular_polygon(at, 2.0, 5, share(variant, 6) * TAU), SOFT_SCRAP) +[display] +window/size/viewport_width=1280 +window/size/viewport_height=720 -func _hard_rock(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: - var solid: int = state.get("solid", 0) - var variant: int = state.get("variant", 0) - _raised(canvas, rect, solid, HARD_BASE, HARD_TOP) - # An angled facet, as if the hard stone had split along a plane. - var c := rect.get_center() - var flip := 1.0 if share(variant, 3) < 0.5 else -1.0 - var facet := PackedVector2Array([ - c + Vector2(-5.0 * flip, -4.0), c + Vector2(4.0 * flip, -2.0), c + Vector2(-1.0 * flip, 5.0), - ]) - canvas.draw_colored_polygon(facet, HARD_FACET) +[rendering] - -func _bedrock(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: - _raised(canvas, rect, state.get("solid", 0), BEDROCK_BASE, BEDROCK_TOP) - - -func _cracks(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: - var hits: int = state.get("hits", 1) - var variant: int = state.get("variant", 0) - var c := rect.get_center() - var start := c + Vector2(-5.0, -3.0 + share(variant, 8) * 2.0) - canvas.draw_colored_polygon(slit(start, c + Vector2(4.0, 3.0), 1.4), CUT) - if hits >= 2: - canvas.draw_colored_polygon(slit(c + Vector2(3.5, -5.0), c + Vector2(-1.0, 4.5), 1.2), CUT) - - -func _hit(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: - var age: float = state.get("age", 0.0) - var variant: int = state.get("variant", 0) - var c := rect.get_center() - for flake in 5: - var direction := Vector2.from_angle(TAU * flake / 5.0 + share(variant, flake) * 0.8) - var at := c + direction * (2.0 + 9.0 * age) + Vector2(0, 6.0 * age * age) - var turn := age * 5.0 + flake - var color := SOFT_TOP if flake % 2 == 0 else SOFT_BASE - color.a = 1.0 - age - canvas.draw_colored_polygon(regular_polygon(at, 1.6, 3, turn), color) - - -func _figure(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: - var facing: float = state.get("facing", 1.0) - var step: float = state.get("step", 0.0) - var moving: bool = state.get("moving", false) - var swing := sin(step) if moving else 0.0 - var lift := absf(swing) * 1.0 - var c := rect.get_center() + Vector2(0, -lift) - var feet := rect.get_center() + Vector2(0, 6.0) - canvas.draw_colored_polygon(ellipse(feet, Vector2(4.5, 1.6)), SHADOW) - for side in [-1.0, 1.0]: - var leg := Vector2(side * 1.6 + side * swing * 1.2, 4.5) - canvas.draw_rect(Rect2(c + leg - Vector2(0.9, 1.5), Vector2(1.8, 3.0)), LEGS) - var body := PackedVector2Array([ - c + Vector2(-3.6, 4.0), c + Vector2(-2.6, -1.8), c + Vector2(2.6, -1.8), c + Vector2(3.6, 4.0), - ]) - canvas.draw_colored_polygon(body, player_color(state.get("player", -1))) - var head := c + Vector2(0, -4.6) - canvas.draw_circle(head, 3.1, SKIN, true, -1.0, true) - # Hair on the back of the head, so that the figure looks the way it goes. - canvas.draw_colored_polygon(ellipse(head + Vector2(-facing * 1.2, -1.2), Vector2(2.4, 1.9)), HAIR) - canvas.draw_circle(head + Vector2(facing * 1.3, -0.2), 0.55, LEGS, true, -1.0, true) - - -func _marker(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: - var color := MARKER_NEAR if state.get("reachable", false) else MARKER_FAR - var inner := rect.grow(-1.0) - var arm := 4.0 - var thick := 1.6 - for corner in 4: - var x := inner.position.x if corner % 3 == 0 else inner.end.x - var y := inner.position.y if corner < 2 else inner.end.y - var sx := 1.0 if corner % 3 == 0 else -1.0 - var sy := 1.0 if corner < 2 else -1.0 - var bracket := PackedVector2Array([ - Vector2(x, y), Vector2(x + sx * arm, y), Vector2(x + sx * arm, y + sy * thick), - Vector2(x + sx * thick, y + sy * thick), Vector2(x + sx * thick, y + sy * arm), Vector2(x, y + sy * arm), - ]) - var shade := bracket.duplicate() - for i in shade.size(): - shade[i] += Vector2(0.6, 0.6) - canvas.draw_colored_polygon(shade, SHADOW) - canvas.draw_colored_polygon(bracket, color) - - -func _dead_soil(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: - var variant: int = state.get("variant", 0) - canvas.draw_rect(rect, GROUND) - if share(variant, 1) < 0.55: - var a := rect.position + Vector2(share(variant, 2) * 6.0 + 2.0, share(variant, 3) * 4.0 + 3.0) - var b := a + Vector2(6.0, 4.0 - share(variant, 4) * 8.0) - canvas.draw_colored_polygon(slit(a, b, 1.1), DEAD_CUT) - canvas.draw_colored_polygon(slit(b, b + Vector2(3.0, 3.0), 0.8), DEAD_CUT) - _shadow(canvas, rect, state.get("solid", 0)) - - -func _living_soil(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: - var living: int = state.get("living", 0) - var variant: int = state.get("variant", 0) - canvas.draw_rect(rect, GROUND) - canvas.draw_colored_polygon(rounded_rect(rect, living, 5.0), LIVING_BASE) - canvas.draw_colored_polygon(rounded_rect(inset_open_sides(rect, living, 1.4), living, 3.6), LIVING_TOP) - if share(variant, 1) < 0.5: - var at := rect.position + Vector2(3, 3) + Vector2(share(variant, 2), share(variant, 3)) * 10.0 - canvas.draw_circle(at, 0.8, GOLD, true, -1.0, true) - _shadow(canvas, rect, state.get("solid", 0)) - - -func _moss(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: - var variant: int = state.get("variant", 0) - var time: float = state.get("time", 0.0) - var foot := Vector2(rect.get_center().x, rect.end.y - 3.0) - var breathe := 1.0 + 0.06 * sway(time, variant, 1.6) - canvas.draw_colored_polygon(ellipse(foot + Vector2(1.0, 1.2), Vector2(6.0, 2.0)), SHADOW) - var blobs: Array[Vector3] = [Vector3(-3.5, -1.5, 3.0), Vector3(0.5, -3.0, 3.6), Vector3(3.8, -1.2, 2.7)] - for blob in blobs: - canvas.draw_circle(foot + Vector2(blob.x, blob.y) * breathe, blob.z * breathe, MOSS_DARK, true, -1.0, true) - for blob in blobs: - canvas.draw_circle(foot + Vector2(blob.x - 0.6, blob.y - 0.8) * breathe, blob.z * 0.7 * breathe, MOSS_LIGHT, true, -1.0, true) - for spark in 3: - var glow := 0.55 + 0.45 * sin(time * 2.0 + spark * 2.1 + share(variant, spark) * TAU) - var gold := GOLD - gold.a = glow - canvas.draw_circle(foot + Vector2(-3.0 + spark * 3.0, -4.0 + (spark % 2) * 2.0), 0.7, gold, true, -1.0, true) - - -func _vine(canvas: CanvasItem, _rect: Rect2, state: Dictionary) -> void: - var points: PackedVector2Array = state.get("points", PackedVector2Array()) - var time: float = state.get("time", 0.0) - if points.size() < 2: - return - var shade := points.duplicate() - for i in shade.size(): - shade[i] += Vector2(1.0, 1.4) - canvas.draw_polyline(shade, SHADOW, 3.0, true) - canvas.draw_polyline(points, VINE_DARK, 3.2, true) - var top := points.duplicate() - for i in top.size(): - top[i] -= Vector2(0.3, 0.4) - canvas.draw_polyline(top, VINE_LIGHT, 1.6, true) - var count := 9 - for i in count: - var share_along := (i + 0.5) / count - var at := along(points, share_along) - var ahead := along(points, minf(1.0, share_along + 0.02)) - var side := 1.0 if i % 2 == 0 else -1.0 - var angle := (ahead - at).angle() + side * 0.9 + 0.25 * sway(time, i, 1.4) - canvas.draw_colored_polygon(leaf(at, angle, 5.0, 2.6), LEAF) - - -func _fiber_plant(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: - var growth: float = state.get("growth", 1.0) - var time: float = state.get("time", 0.0) - var foot := Vector2(rect.get_center().x, rect.end.y - 2.0) - canvas.draw_colored_polygon(ellipse(foot + Vector2(1.0, 0.8), Vector2(4.0, 1.4)), SHADOW) - if growth < 0.15: - canvas.draw_colored_polygon(ellipse(foot + Vector2(0, -1.2), Vector2(2.2, 1.6)), SEED) - if growth > 0.08: - canvas.draw_colored_polygon(leaf(foot + Vector2(0, -2.2), -PI / 2.0, 2.5, 1.4), LEAF) - return - var grown := (growth - 0.15) / 0.85 - var height := lerpf(4.0, 20.0, grown) - canvas.draw_set_transform(foot, 0.07 * sway(time, 3, 1.1), Vector2.ONE) - canvas.draw_colored_polygon(slit(Vector2.ZERO, Vector2(0, -height), 1.6), STEM) - var leaves := 1 + int(grown * 5.0) - for i in leaves: - var at := Vector2(0, -height * (0.2 + 0.7 * i / maxf(1.0, leaves))) - var side := 1.0 if i % 2 == 0 else -1.0 - canvas.draw_colored_polygon(leaf(at, -PI / 2.0 + side * 1.0, lerpf(3.0, 6.0, grown), 2.6), LEAF) - if grown > 0.75: - var tuft := (grown - 0.75) / 0.25 - for puff in 4: - var at := Vector2(-2.0 + puff * 1.4, -height - 1.0 - (puff % 2) * 1.2) - canvas.draw_circle(at, 1.4 * tuft + 0.3, FIBRE, true, -1.0, true) - canvas.draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE) - - -func _stone(canvas: CanvasItem, rect: Rect2, _state: Dictionary) -> void: - var c := rect.get_center() - canvas.draw_colored_polygon(ellipse(c + Vector2(0.8, 2.6), Vector2(3.0, 1.0)), SHADOW) - canvas.draw_colored_polygon(regular_polygon(c, 3.0, 6, 0.3), STONE_BASE) - canvas.draw_colored_polygon(regular_polygon(c + Vector2(-0.4, -0.5), 2.2, 6, 0.3), STONE_TOP) - - -func _grazer(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: - var hop: float = state.get("hop", 1.0) - var facing: float = state.get("facing", 1.0) - var lift := sin(PI * hop) * 4.0 - var foot := Vector2(rect.get_center().x, rect.end.y - 2.0) - canvas.draw_colored_polygon(ellipse(foot + Vector2(0.8, 0.6), Vector2(5.5 - lift * 0.5, 1.6)), SHADOW) - var body := foot + Vector2(0, -6.0 - lift) - for leg in [-2.5, 2.5]: - canvas.draw_rect(Rect2(body + Vector2(leg - 0.7, 3.0), Vector2(1.4, 3.0 - lift * 0.3)), FACE) - var puffs: Array[Vector3] = [ - Vector3(-3.5, 0.5, 3.0), Vector3(0.0, -1.5, 3.4), Vector3(3.2, 0.3, 3.0), Vector3(0.0, 1.8, 3.0), - ] - for puff in puffs: - canvas.draw_circle(body + Vector2(puff.x, puff.y), puff.z, WOOL_SHADE, true, -1.0, true) - for puff in puffs: - canvas.draw_circle(body + Vector2(puff.x - 0.5, puff.y - 0.6), puff.z * 0.78, WOOL, true, -1.0, true) - var head := body + Vector2(facing * 5.0, -1.2) - canvas.draw_colored_polygon(ellipse(head, Vector2(2.4, 2.0)), FACE) - canvas.draw_colored_polygon(ellipse(head + Vector2(-facing * 1.6, -1.6), Vector2(1.2, 0.7)), FACE) - canvas.draw_circle(head + Vector2(facing * 0.9, -0.4), 0.45, WOOL, true, -1.0, true) - - -func _ruin(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: - var awake: float = state.get("awake", 0.0) - var time: float = state.get("time", 0.0) - var tile := rect.size.x / 3.0 - var floor_y := rect.end.y - 3.0 - var center := Vector2(rect.get_center().x, floor_y - tile * 1.2) - # The shadow of the ruin and the dark space under its arch. - canvas.draw_colored_polygon(ellipse(Vector2(center.x + 3.0, floor_y + 1.0), Vector2(rect.size.x * 0.48, 3.5)), SHADOW) - var opening := Rect2(rect.position.x + tile * 0.75, rect.position.y + tile * 0.9, tile * 1.5, floor_y - rect.position.y - tile * 0.9) - canvas.draw_rect(opening, RUIN_DARK) - # The glow of the sleeping grove, brighter the more it is awake. - for ring in 4: - var gold := GOLD - gold.a = awake * (0.18 + 0.12 * ring) - canvas.draw_circle(center + Vector2(0, 4), 9.0 - ring * 2.0, gold, true, -1.0, true) - var crown := center + Vector2(0, 0.5 * sin(time * 1.2)) - canvas.draw_colored_polygon(slit(Vector2(center.x, floor_y), crown + Vector2(0, 4), 2.6), SEED) - for blob in 3: - var at := crown + Vector2(-3.5 + blob * 3.5, -1.0 + (blob % 2) * -2.0) - canvas.draw_circle(at, 3.4, MOSS_DARK.lerp(GOLD, awake * 0.35), true, -1.0, true) - # Two pillars and the lintel, each of two layers of paper. - for x in [rect.position.x, rect.end.x - tile * 0.75]: - var pillar := Rect2(x, rect.position.y + tile * 0.6, tile * 0.75, floor_y - rect.position.y - tile * 0.6) - canvas.draw_colored_polygon(rounded_rect(pillar, 0, 2.0), RUIN_BASE) - canvas.draw_colored_polygon(rounded_rect(pillar.grow(-1.4), 0, 1.4), RUIN_TOP) - var vein := GOLD - vein.a = awake * 0.8 - canvas.draw_line(pillar.get_center() + Vector2(0, -6), pillar.get_center() + Vector2(1.5, 6), vein, 1.0, true) - var lintel := Rect2(rect.position.x - 1.0, rect.position.y + tile * 0.25, rect.size.x + 2.0, tile * 0.6) - canvas.draw_colored_polygon(rounded_rect(lintel, 0, 2.0), RUIN_BASE) - canvas.draw_colored_polygon(rounded_rect(lintel.grow(-1.4), 0, 1.4), RUIN_TOP) - var step := Rect2(rect.position.x + tile * 0.4, floor_y - 2.0, rect.size.x - tile * 0.8, 3.0) - canvas.draw_rect(step, RUIN_BASE) - - -## A mass of rock: ground where the corners are cut round, the darker lower -## layer of paper and the lighter upper one, inset on the open sides. -func _raised(canvas: CanvasItem, rect: Rect2, solid: int, base: Color, top: Color) -> void: - canvas.draw_rect(rect, GROUND) - _shadow(canvas, rect, solid) - canvas.draw_colored_polygon(rounded_rect(rect, solid, 4.0), base) - canvas.draw_colored_polygon(rounded_rect(inset_open_sides(rect, solid, 1.6), solid, 2.6), top) - - -## The shadow that rock above and to the left casts onto this tile: a darker -## band along the rock and a fainter one beyond, so that it fades. -func _shadow(canvas: CanvasItem, rect: Rect2, solid: int) -> void: - var above := solid & TileShape.N != 0 - var left := solid & TileShape.W != 0 - for band: Array in [[0.0, SHADOW], [SHADOW_DEPTH, SHADOW_FAINT]]: - var offset: float = band[0] - var color: Color = band[1] - if above: - canvas.draw_rect(Rect2(rect.position + Vector2(0, offset), Vector2(rect.size.x, SHADOW_DEPTH)), color) - if left: - var top := offset + SHADOW_DEPTH if above else 0.0 - canvas.draw_rect(Rect2(rect.position + Vector2(offset, top), Vector2(SHADOW_DEPTH, rect.size.y - top)), color) - if not above and not left and solid & TileShape.NW != 0: - canvas.draw_rect(Rect2(rect.position, Vector2(SHADOW_DEPTH + offset, SHADOW_DEPTH + offset)), color) - - -## Small flecks and fibres in the paper, on some tiles. -func _paper_bits(canvas: CanvasItem, rect: Rect2, variant: int, fleck: Color, fibre: Color) -> void: - if share(variant, 11) < 0.25: - var at := rect.position + Vector2(3, 3) + Vector2(share(variant, 12), share(variant, 13)) * 10.0 - canvas.draw_colored_polygon(regular_polygon(at, 1.1 + share(variant, 14), 5, share(variant, 15) * TAU), fleck) - if share(variant, 16) < 0.2: - var from := rect.position + Vector2(2, 2) + Vector2(share(variant, 17), share(variant, 18)) * 9.0 - canvas.draw_line(from, from + Vector2.from_angle(share(variant, 19) * TAU) * 3.5, fibre, 0.7, true) +renderer/rendering_method="gl_compatibility" +renderer/rendering_method.mobile="gl_compatibility" +textures/canvas_textures/default_texture_filter=0 +2d/snap/snap_2d_transforms_to_pixel=true ``` - [ ] **Step 4: Tests ausführen und Erfolg sehen** @@ -4513,21 +4707,21 @@ godot --headless --path game --import godot --headless --path game -s res://tests/run_tests.gd ``` -Expected: Exit-Code 0 und `133 passed, 0 failed`. Die einzigen Warnungen sind die drei bekannten Zeilen. +Expected: Exit-Code 0 und `138 passed, 0 failed`. Die einzigen Warnungen sind die drei bekannten Zeilen. - [ ] **Step 5: Bildschirmfotos ansehen** Run: ```bash -godot --path game -s res://tools/screenshot.gd -- "$TEMP/m1d-scherenschnitt-karte.png" --style=papercut -godot --path game -s res://tools/screenshot.gd -- "$TEMP/m1d-scherenschnitt-vitrine.png" --style=papercut --showcase +godot --path game -s res://tools/screenshot.gd -- "$TEMP/m1d-warm-karte.png" --style=warm +godot --path game -s res://tools/screenshot.gd -- "$TEMP/m1d-warm-vitrine.png" --style=warm --showcase ``` -Expected: Beide Aufrufe enden mit `screenshot saved: …` ohne Warnung. Sieh dir beide Bilder an. Nirgends steht mehr ein Platzhalter, also kein einfarbiges Rechteck und kein schlichter Kreis. +Expected: Beide Aufrufe enden mit `screenshot saved: …` ohne Warnung. Sieh dir beide Bilder an. Alle Pixel sind scharfe Quadrate, nichts ist verschwommen. -- Spiel: Der Boden ist braunes Packpapier mit feinen Flecken und Fasern. Fels besteht aus zwei hellgrauen Papierlagen mit wenigen Flecken, außen abgerundet und innen geschlossen, und wirft nach rechts unten einen weichen Schatten. Im getroffenen Fels steht ein dunkler Schnitt, darum fliegen Papierschnipsel. Die beiden Figuren stehen auf einem Schatten: Körper in Rotbraun, heller Kopf, dunkles Haar, Beine. Über der linken steht scharf „Spieler“. -- Vitrine: Grundgestein fast schwarz, weicher Fels hell, harter Fels blaugrau mit hellen Dreiecksfacetten, alles mit Schatten. Toter Boden mit eingeschnittenen Rissen, lebendiger Boden als grüne Papierlage mit goldenen Pünktchen. Moospolster, eine Ranke als Band mit Blättern und drei Steinen, eine fast ausgewachsene Faserpflanze mit weißen Fasern, ein Wollschaf in der Luft, die Ruine als Steintor mit goldenem Leuchten, zwei Figuren in Rot und Blau. Oben „Stilprobe: Scherenschnitt“, unten ist „1 Scherenschnitt“ ausgegraut. +- Spiel: Warmer, brauner Erdboden mit wenigen dunkleren Flecken, hellen Körnern und kleinen Kieseln. Weicher Fels ist ein hellgraues Plateau mit dunklem Umriss, außen abgerundet und innen geschlossen. Zum offenen Boden hin zeigt er unten eine dunklere Vorderkante mit senkrechten Streifen und wirft einen Schatten auf den Boden darunter. Im getroffenen Fels steht ein dunkler Riss mit Splittern. Die beiden Figuren stehen auf einem kleinen Schatten: braune Haare, helles Gesicht mit Auge, rotes Hemd, blaue Hose und Stiefel. Über der linken steht scharf „Spieler“. +- Vitrine: Oben steht „Stilprobe: Warm“. Oben links liegen Grundgestein (fast schwarzbraun mit Schichten), weicher Fels (hell) und harter Fels (blaugrau mit hellen Glanzpunkten) als Pixelgrafik mit Vorderkanten. Die beiden Figuren in Rot und Blau sind Pixelfiguren. Alles andere zeigt noch die Platzhalter von `ArtStyle`: graubraunen und grünen Boden, Kreise für Moos, Tier und Stein, einen Strich als Pflanze, eine Linie als Ranke, ein graues Feld mit gelbem Kreis als Ruine. An den runden Ecken des Felses blitzt der braune Boden der Karte durch; das gleicht Task 6 aus, weil toter Boden dann dieselbe Erde ist. - [ ] **Step 6: Alles prüfen** @@ -4545,12 +4739,14 @@ Expected: keine Ausgabe von rustfmt, keine Warnung von Clippy, zusammen 151 best ```bash git add game git commit -F - <<'EOF' -Stil Scherenschnitt +Pixelgrafik für Boden, Fels und Figuren -Lagen aus geschnittenem Papier ohne Umriss, mit weichen Schatten nach -rechts unten. Gedeckte Erdfarben; Moos, lebendiger Boden und die Ruine -leuchten grün und golden. Pflanzen wiegen sich, die Faserpflanze wächst -vom Samen bis zu den Fasern, und jeder Spieler hat eine eigene Farbe. +PixelStyle zeichnet jede Grafik Pixel für Pixel in ein kleines Bild, +16 mal 16 Pixel pro Kachel, und hebt es auf. Fels ist außen rund, innen +geschlossen und zeigt zum offenen Boden hin seine Vorderkante; Figuren +stehen aufrecht und haben vier Laufbilder. Warm bekommt als erster Stil +seine Palette und acht Spielerfarben. Die Texturfilterung steht auf +Nearest, damit die Pixel scharf bleiben. Co-Authored-By: Claude Opus 5.5 EOF @@ -4559,33 +4755,85 @@ git push --- -### Task 6: Stil Kontur +### Task 6: Pixelgrafik für die Dinge der Vitrine -Kräftige dunkle Umrisse um flache, starke Farben, gut lesbar wie ein Comic. Felsmassen haben ihren Umriss nur dort, wo sie an offenen Boden grenzen, und jedes Ding hat einen dunklen Umriss um seine ganze Form. +Der letzte Abschnitt von `PixelStyle` zeichnet, was die Vitrine aus späteren Meilensteinen zeigt: toten Boden mit Rissen, lebendigen Boden mit Gras, Blumen und unregelmäßigem Rand, Moos mit funkelnden Sporen, die Ranke, die wachsende Faserpflanze, den Stein, das Weidetier und die Ruine mit ihrem Licht. **Files:** -- Modify: `game/scripts/art/outline_style.gd` (ganze Datei ersetzen) -- Test: `game/tests/test_outline_style.gd` (neu) +- Modify: `game/scripts/art/pixel_style.gd` (zwei Stellen einfügen, einen Abschnitt anhängen) +- Test: `game/tests/test_pixel_things.gd` **Interfaces:** -- Consumes: `ArtStyle` mit seinen Zeichenmethoden und Helfern, `TileShape`, `Art.IDS`, die Einträge von `state` laut Kopf von `art_style.gd` -- Produces: `OutlineStyle` zeichnet jede Kennung selbst; `PLAYER_COLORS: Array[Color]` mit acht Farben und `player_color(player: int) -> Color` +- Consumes: alles aus Task 5, dazu `ArtStyle.sway` und `ArtStyle.along` +- Produces: + - `PixelStyle.GROWTH_STAGES := 6` und das Feld `glow: float`, das eine Unterklasse setzt + - die Zeichnungen für `DEAD_SOIL`, `LIVING_SOIL`, `MOSS`, `VINE`, `FIBER_PLANT`, `STONE`, `GRAZER` und `RUIN` + - die Bilder `_crack_soil(pixels: Pixels, variant: int) -> Pixels`, `_grass_image(living: int, variant: int) -> Pixels`, `_moss_image(variant: int, twinkle: int) -> Pixels`, `_vine_image(points: PackedVector2Array, origin: Vector2, size: Vector2i) -> Pixels`, `_plant_image(stage: int, lean: int) -> Pixels`, `_stone_image() -> Pixels`, `_grazer_image(tucked: bool, left: bool) -> Pixels`, `_ruin_image() -> Pixels` und `_ruin_glow() -> Pixels` - [ ] **Step 1: Fehlschlagenden Test schreiben** -Create `game/tests/test_outline_style.gd`: +Create `game/tests/test_pixel_things.gd`: ```gdscript extends TestCase +## The pixel art of the things of later milestones in the showcase. -func test_four_players_have_four_colours() -> void: - var style := OutlineStyle.new() - var colours := {} - for player in 4: - colours[style.player_color(player)] = true - check_eq(colours.size(), 4, "four players, four colours") - check_eq(style.player_color(-1), style.player_color(0), "a player not in the world yet looks like the first") +## 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") ``` - [ ] **Step 2: Test ausführen und Fehlschlag sehen** @@ -4597,374 +4845,348 @@ godot --headless --path game --import godot --headless --path game -s res://tests/run_tests.gd ``` -Expected: Exit-Code 1 und `133 passed, 1 failed`; unter `FAIL test_outline_style.gd test_four_players_have_four_colours` steht `four players, four colours: expected 4, got 2`. +Expected: Der Testlauf endet mit Exit-Code 1 und `138 passed, 1 failed`. `test_pixel_things.gd` lädt nicht; davor steht `SCRIPT ERROR: Parse Error: Cannot find member "GROWTH_STAGES" in base "PixelStyle".` -- [ ] **Step 3: Stil schreiben** +- [ ] **Step 3: Die Dinge der Vitrine zeichnen** -Ersetze den gesamten Inhalt von `game/scripts/art/outline_style.gd` durch: +Füge in `game/scripts/art/pixel_style.gd` direkt vor der Zeile `## Size of a figure: a tile wide and one and a half tiles tall.` diese Zeilen ein: ```gdscript -class_name OutlineStyle -extends ArtStyle -## Bold dark outlines around flat colours, few and strong colours, easy to -## read like a comic. Masses of rock are outlined only where they meet open -## ground, and each thing gets one dark outline around its whole shape. +## Stages of a growing plant, from the seed to the fibres. +const GROWTH_STAGES := 6 +``` -const KEY := &"outline" +Füge in `game/scripts/art/pixel_style.gd` direkt vor der Zeile `var _textures: Dictionary[String, Texture2D] = {}` diese Zeilen ein: -const INK := Color("2b1d14") -## Width of the outlines, in pixels of the playing field. -const LINE := 1.1 -const GROUND := Color("d6a35a") -const GROUND_DOT := Color("bd8a43") -const SOFT_FILL := Color("c8c1d6") -const SOFT_SHADE := Color("a79fbd") -const SOFT_LIGHT := Color("eeeaf5") -const HARD_FILL := Color("4a63a6") -const HARD_SHADE := Color("374c88") -const HARD_GLINT := Color("a9c3ff") -const DEAD_CRACK := Color("9a6b31") -const LIVING := Color("5cbf48") -const LIVING_SHADE := Color("3f9a33") -const SPARK := Color("ffe14d") -const MOSS := Color("49b13a") -const MOSS_LIGHT := Color("8de06a") -const VINE := Color("3fa34a") -const LEAF := Color("6fd259") -const STONE := Color("b3aba0") -const STEM := Color("3f9a33") -const FIBRE := Color("fffaf0") -const SEED := Color("8a5a35") -const WOOL := Color("fbf8f0") -const FACE := Color("3a2a22") -const RUIN := Color("a8a49b") -const RUIN_SHADE := Color("8a857b") -const RUIN_DARK := Color("3b322b") -const GLOW := Color("ffd84a") -const SKIN := Color("f5c79a") -const MARKER_NEAR := Color("7ddc4f") -const MARKER_FAR := Color("ee5a3c") -const PLAYER_COLORS: Array[Color] = [ - Color("e8483b"), Color("2d8fe0"), Color("f5a623"), Color("33b864"), - Color("a35bd6"), Color("1fb5b5"), Color("f06aa8"), Color("6d7ce8"), -] +```gdscript +## How strongly living things glow, from 0 (not at all) to 1; the subclass +## chooses it. +var glow := 0.0 +``` +Füge am Ende von `game/scripts/art/pixel_style.gd` diesen Abschnitt an. Wie zwischen allen Funktionen stehen davor zwei Leerzeilen: -func key() -> StringName: - return KEY - - -func title_key() -> String: - return "STYLE_OUTLINE" - - -func player_color(player: int) -> Color: - return PLAYER_COLORS[maxi(0, player) % PLAYER_COLORS.size()] - - -func _ground(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: - var variant: int = state.get("variant", 0) - canvas.draw_rect(rect, GROUND) - if share(variant, 1) < 0.3: - var at := rect.position + Vector2(3, 3) + Vector2(share(variant, 2), share(variant, 3)) * 10.0 - canvas.draw_circle(at, 0.8, GROUND_DOT, true, -1.0, true) - canvas.draw_circle(at + Vector2(2.2, 1.0), 0.55, GROUND_DOT, true, -1.0, true) - - -func _soft_rock(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: - var solid: int = state.get("solid", 0) - var variant: int = state.get("variant", 0) - _mass(canvas, rect, solid, SOFT_FILL, SOFT_SHADE) - if share(variant, 3) < 0.3: - var at := rect.position + Vector2(4, 4) + Vector2(share(variant, 4), share(variant, 5)) * 5.0 - canvas.draw_line(at, at + Vector2(3.0, -1.2), SOFT_LIGHT, 0.9, true) - - -func _hard_rock(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: - var solid: int = state.get("solid", 0) - var variant: int = state.get("variant", 0) - _mass(canvas, rect, solid, HARD_FILL, HARD_SHADE) - if share(variant, 3) < 0.45: - var at := rect.position + Vector2(5, 5) + Vector2(share(variant, 4), share(variant, 5)) * 5.0 - var glint := PackedVector2Array([at + Vector2(0, -2.4), at + Vector2(1.4, 0), at + Vector2(0, 2.4), at + Vector2(-1.4, 0)]) - canvas.draw_colored_polygon(glint, HARD_GLINT) - canvas.draw_polyline(closed(glint), INK, 0.6, true) - - -func _bedrock(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: - canvas.draw_rect(rect, GROUND) - canvas.draw_colored_polygon(rounded_rect(rect, state.get("solid", 0), 4.0), INK) - - -func _cracks(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: - var hits: int = state.get("hits", 1) - var c := rect.get_center() - canvas.draw_polyline(PackedVector2Array([c + Vector2(-5, -4), c + Vector2(-1, -1), c + Vector2(-2, 2), c + Vector2(4, 5)]), INK, 1.0, true) - if hits >= 2: - canvas.draw_polyline(PackedVector2Array([c + Vector2(5, -5), c + Vector2(2, -1), c + Vector2(3, 2)]), INK, 1.0, true) - - -func _hit(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: - var age: float = state.get("age", 0.0) - var variant: int = state.get("variant", 0) - var c := rect.get_center() - for shard in 5: - var direction := Vector2.from_angle(TAU * shard / 5.0 + share(variant, shard) * 0.8) - var at := c + direction * (2.0 + 9.0 * age) + Vector2(0, 6.0 * age * age) - var points := regular_polygon(at, 1.7, 3, age * 6.0 + shard) - var fill := SOFT_FILL - var ink := INK - fill.a = 1.0 - age - ink.a = 1.0 - age - canvas.draw_colored_polygon(points, fill) - canvas.draw_polyline(closed(points), ink, 0.6, true) - - -func _figure(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: - var facing: float = state.get("facing", 1.0) - var step: float = state.get("step", 0.0) - var moving: bool = state.get("moving", false) - var swing := sin(step) if moving else 0.0 - var c := rect.get_center() + Vector2(0, -absf(swing)) - var head := c + Vector2(0, -4.2) - var feet: Array[Vector2] = [c + Vector2(-1.8 + swing * 1.2, 5.0), c + Vector2(1.8 - swing * 1.2, 5.0)] - var body := ellipse(c + Vector2(0, 1.0), Vector2(3.6, 3.6)) - # One outline around the whole figure: dark shapes first, colours on top. - for foot in feet: - canvas.draw_colored_polygon(ellipse(foot, Vector2(1.7, 1.2) + Vector2(LINE, LINE)), INK) - canvas.draw_colored_polygon(ellipse(c + Vector2(0, 1.0), Vector2(3.6 + LINE, 3.6 + LINE)), INK) - canvas.draw_circle(head, 3.0 + LINE, INK, true, -1.0, true) - for foot in feet: - canvas.draw_colored_polygon(ellipse(foot, Vector2(1.7, 1.2)), INK.lightened(0.25)) - canvas.draw_colored_polygon(body, player_color(state.get("player", -1))) - canvas.draw_circle(head, 3.0, SKIN, true, -1.0, true) - for eye in [-0.6, 0.9]: - canvas.draw_circle(head + Vector2(facing * (0.9 + eye), -0.3), 0.5, INK, true, -1.0, true) - - -func _marker(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: - var color := MARKER_NEAR if state.get("reachable", false) else MARKER_FAR - var inner := rect.grow(-1.2) - canvas.draw_rect(inner, INK, false, 2.4, true) - canvas.draw_rect(inner, color, false, 1.2, true) +```gdscript +# Things of later milestones, shown in the showcase. func _dead_soil(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: - var variant: int = state.get("variant", 0) - canvas.draw_rect(rect, GROUND) - if share(variant, 1) < 0.5: - var a := rect.position + Vector2(2, 3) + Vector2(share(variant, 2), share(variant, 3)) * 6.0 - canvas.draw_polyline(PackedVector2Array([a, a + Vector2(3, 2), a + Vector2(5, 1), a + Vector2(7, 4)]), DEAD_CRACK, 0.8, true) + 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: int = state.get("living", 0) - var variant: int = state.get("variant", 0) - canvas.draw_rect(rect, GROUND) - canvas.draw_colored_polygon(rounded_rect(rect, living, 5.0), LIVING) - _edges(canvas, rect, living, 5.0, LIVING_SHADE, LINE) - if share(variant, 1) < 0.4: - var at := rect.position + Vector2(4, 6) + Vector2(share(variant, 2), share(variant, 3)) * 7.0 - canvas.draw_polyline(PackedVector2Array([at + Vector2(-1.2, -1.8), at, at + Vector2(1.2, -2.0)]), LIVING_SHADE, 0.8, true) + 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) - var time: float = state.get("time", 0.0) - var foot := Vector2(rect.get_center().x, rect.end.y - 3.0) - var breathe := 1.0 + 0.06 * sway(time, variant, 1.6) - var blobs: Array[Vector3] = [Vector3(-3.5, -1.5, 3.0), Vector3(0.5, -3.0, 3.6), Vector3(3.8, -1.2, 2.7)] - for blob in blobs: - canvas.draw_circle(foot + Vector2(blob.x, blob.y) * breathe, blob.z * breathe + LINE, INK, true, -1.0, true) - for blob in blobs: - canvas.draw_circle(foot + Vector2(blob.x, blob.y) * breathe, blob.z * breathe, MOSS, true, -1.0, true) - canvas.draw_circle(foot + Vector2(blob.x - 0.8, blob.y - 0.9) * breathe, blob.z * 0.35, MOSS_LIGHT, true, -1.0, true) - for spark in 2: - var glow := 0.5 + 0.5 * sin(time * 2.4 + spark * 2.3 + share(variant, spark) * TAU) - _sparkle(canvas, foot + Vector2(-2.5 + spark * 5.0, -6.5 + spark), 1.8 * glow) + 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)) -func _vine(canvas: CanvasItem, _rect: Rect2, state: Dictionary) -> void: +## 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()) - var time: float = state.get("time", 0.0) if points.size() < 2: return - var leaves: Array[PackedVector2Array] = [] - var count := 9 - for i in count: - var part := (i + 0.5) / count - var at := along(points, part) - var ahead := along(points, minf(1.0, part + 0.02)) - var side := 1.0 if i % 2 == 0 else -1.0 - leaves.append(leaf(at, (ahead - at).angle() + side * 0.9 + 0.25 * sway(time, i, 1.4), 5.0, 2.8)) - for blade in leaves: - canvas.draw_polyline(closed(blade), INK, LINE * 1.6, true) - canvas.draw_polyline(points, INK, 2.2 + LINE * 2.0, true) - canvas.draw_polyline(points, VINE, 2.2, true) - for blade in leaves: - canvas.draw_colored_polygon(blade, LEAF) + 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 growth: float = state.get("growth", 1.0) - var time: float = state.get("time", 0.0) - var foot := Vector2(rect.get_center().x, rect.end.y - 2.0) - if growth < 0.15: - var seed := ellipse(foot + Vector2(0, -1.2), Vector2(2.2, 1.6)) - canvas.draw_colored_polygon(ellipse(foot + Vector2(0, -1.2), Vector2(2.2 + LINE, 1.6 + LINE)), INK) - canvas.draw_colored_polygon(seed, SEED) - return - var grown := (growth - 0.15) / 0.85 - var height := lerpf(4.0, 20.0, grown) - canvas.draw_set_transform(foot, 0.07 * sway(time, 3, 1.1), Vector2.ONE) - var leaves: Array[PackedVector2Array] = [] - var count := 1 + int(grown * 5.0) - for i in count: - var at := Vector2(0, -height * (0.2 + 0.7 * i / maxf(1.0, count))) - var side := 1.0 if i % 2 == 0 else -1.0 - leaves.append(leaf(at, -PI / 2.0 + side * 1.0, lerpf(3.0, 6.0, grown), 2.8)) - var tuft := clampf((grown - 0.75) / 0.25, 0.0, 1.0) - var puffs: Array[Vector2] = [] - if tuft > 0.0: - for puff in 4: - puffs.append(Vector2(-2.0 + puff * 1.4, -height - 1.0 - (puff % 2) * 1.2)) - for blade in leaves: - canvas.draw_polyline(closed(blade), INK, LINE * 1.6, true) - canvas.draw_line(Vector2.ZERO, Vector2(0, -height), INK, 1.6 + LINE * 2.0, true) - for puff in puffs: - canvas.draw_circle(puff, 1.4 * tuft + 0.3 + LINE, INK, true, -1.0, true) - canvas.draw_line(Vector2.ZERO, Vector2(0, -height), STEM, 1.6, true) - for blade in leaves: - canvas.draw_colored_polygon(blade, LEAF) - for puff in puffs: - canvas.draw_circle(puff, 1.4 * tuft + 0.3, FIBRE, true, -1.0, true) - canvas.draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE) + 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: - var c := rect.get_center() - var pebble := regular_polygon(c, 2.8, 6, 0.3) - canvas.draw_colored_polygon(regular_polygon(c, 2.8 + LINE, 6, 0.3), INK) - canvas.draw_colored_polygon(pebble, STONE) - canvas.draw_line(c + Vector2(-1.2, -1.0), c + Vector2(0.4, -1.6), Color.WHITE, 0.6, true) + _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 hop: float = state.get("hop", 1.0) - var facing: float = state.get("facing", 1.0) - var lift := sin(PI * hop) * 4.0 - var foot := Vector2(rect.get_center().x, rect.end.y - 2.0) - var body := foot + Vector2(0, -6.0 - lift) - var puffs: Array[Vector3] = [ - Vector3(-3.5, 0.5, 3.0), Vector3(0.0, -1.5, 3.4), Vector3(3.2, 0.3, 3.0), Vector3(0.0, 1.8, 3.0), - ] - var head := body + Vector2(facing * 5.0, -1.2) - for leg in [-2.5, 2.5]: - canvas.draw_line(body + Vector2(leg, 3.0), body + Vector2(leg, 6.0 - lift * 0.3), INK, 1.6, true) - for puff in puffs: - canvas.draw_circle(body + Vector2(puff.x, puff.y), puff.z + LINE, INK, true, -1.0, true) - canvas.draw_colored_polygon(ellipse(head, Vector2(2.4 + LINE, 2.0 + LINE)), INK) - for puff in puffs: - canvas.draw_circle(body + Vector2(puff.x, puff.y), puff.z, WOOL, true, -1.0, true) - canvas.draw_colored_polygon(ellipse(head, Vector2(2.4, 2.0)), FACE) - canvas.draw_circle(head + Vector2(facing * 0.9, -0.4), 0.6, WOOL, true, -1.0, true) + 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: float = state.get("awake", 0.0) - var time: float = state.get("time", 0.0) - var tile := rect.size.x / 3.0 - var floor_y := rect.end.y - 3.0 - var center := Vector2(rect.get_center().x, floor_y - tile * 1.2) - var opening := Rect2(rect.position.x + tile * 0.75, rect.position.y + tile * 0.9, tile * 1.5, floor_y - rect.position.y - tile * 0.9) - canvas.draw_rect(opening, RUIN_DARK) - # Rays of the waking grove. - for ray in 8: - var direction := Vector2.from_angle(TAU * ray / 8.0 + time * 0.3) - var glow := GLOW - glow.a = awake * 0.7 - canvas.draw_line(center + direction * 4.0, center + direction * (6.0 + 3.0 * awake), glow, 1.0, true) - canvas.draw_circle(center, 3.5 + LINE, INK, true, -1.0, true) - var core := GLOW.lerp(Color.WHITE, 0.4 * awake) - canvas.draw_circle(center, 3.5, core, true, -1.0, true) - var blocks: Array[Rect2] = [ - Rect2(rect.position.x, rect.position.y + tile * 0.6, tile * 0.75, floor_y - rect.position.y - tile * 0.6), - Rect2(rect.end.x - tile * 0.75, rect.position.y + tile * 0.6, tile * 0.75, floor_y - rect.position.y - tile * 0.6), - Rect2(rect.position.x - 1.0, rect.position.y + tile * 0.25, rect.size.x + 2.0, tile * 0.6), - ] - for block in blocks: - canvas.draw_rect(block, RUIN) - canvas.draw_rect(Rect2(block.position.x, block.end.y - 2.0, block.size.x, 2.0), RUIN_SHADE) - canvas.draw_rect(block, INK, false, LINE, true) - canvas.draw_rect(Rect2(rect.position.x + tile * 0.4, floor_y - 2.0, rect.size.x - tile * 0.8, 3.0), RUIN_SHADE) - canvas.draw_rect(Rect2(rect.position.x + tile * 0.4, floor_y - 2.0, rect.size.x - tile * 0.8, 3.0), INK, false, LINE, true) + 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 mass of rock: ground where corners are cut round, the fill, a darker -## band along its lower open edge and the outline where it meets the ground. -func _mass(canvas: CanvasItem, rect: Rect2, solid: int, fill: Color, shade: Color) -> void: - canvas.draw_rect(rect, GROUND) - canvas.draw_colored_polygon(rounded_rect(rect, solid, 4.0), fill) - if solid & TileShape.S == 0: - # The band is as high as the corners are round, so that it can take - # the round lower corners of the tile. - var band := Rect2(rect.position.x, rect.end.y - 4.0, rect.size.x, 4.0) - canvas.draw_colored_polygon(rounded_rect(band, solid | TileShape.N, 4.0), shade) - _edges(canvas, rect, solid, 4.0, INK, LINE) +## 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 outline of a mass of tiles where it meets open ground: along every -## open side, round around open corners and filled into inner corners, all -## inside `rect`, so that tiles drawn later cannot cover half of it. -func _edges(canvas: CanvasItem, rect: Rect2, mask: int, radius: float, color: Color, width: float) -> void: - var half := width / 2.0 - var inner := inset_open_sides(rect, mask, half) - var r := maxf(0.0, radius - half) - var round_tl := mask & (TileShape.N | TileShape.W) == 0 - var round_tr := mask & (TileShape.N | TileShape.E) == 0 - var round_br := mask & (TileShape.S | TileShape.E) == 0 - var round_bl := mask & (TileShape.S | TileShape.W) == 0 - if mask & TileShape.N == 0: - canvas.draw_line(Vector2(inner.position.x + (r if round_tl else 0.0), inner.position.y), Vector2(inner.end.x - (r if round_tr else 0.0), inner.position.y), color, width, true) - if mask & TileShape.S == 0: - canvas.draw_line(Vector2(inner.position.x + (r if round_bl else 0.0), inner.end.y), Vector2(inner.end.x - (r if round_br else 0.0), inner.end.y), color, width, true) - if mask & TileShape.W == 0: - canvas.draw_line(Vector2(inner.position.x, inner.position.y + (r if round_tl else 0.0)), Vector2(inner.position.x, inner.end.y - (r if round_bl else 0.0)), color, width, true) - if mask & TileShape.E == 0: - canvas.draw_line(Vector2(inner.end.x, inner.position.y + (r if round_tr else 0.0)), Vector2(inner.end.x, inner.end.y - (r if round_br else 0.0)), color, width, true) - if round_tl: - canvas.draw_arc(inner.position + Vector2(r, r), r, PI, 1.5 * PI, 6, color, width, true) - if round_tr: - canvas.draw_arc(Vector2(inner.end.x - r, inner.position.y + r), r, 1.5 * PI, TAU, 6, color, width, true) - if round_br: - canvas.draw_arc(inner.end - Vector2(r, r), r, 0.0, 0.5 * PI, 6, color, width, true) - if round_bl: - canvas.draw_arc(Vector2(inner.position.x + r, inner.end.y - r), r, 0.5 * PI, PI, 6, color, width, true) - # Inner corners: both sides join neighbours, but the tile across the - # corner is open, so the outlines of the neighbours meet here. - var corners: Array[Array] = [ - [TileShape.N | TileShape.W, TileShape.NW, rect.position], - [TileShape.N | TileShape.E, TileShape.NE, Vector2(rect.end.x - width, rect.position.y)], - [TileShape.S | TileShape.E, TileShape.SE, rect.end - Vector2(width, width)], - [TileShape.S | TileShape.W, TileShape.SW, Vector2(rect.position.x, rect.end.y - width)], - ] - for corner in corners: - var sides: int = corner[0] - var across: int = corner[1] - if mask & sides == sides and mask & across == 0: - canvas.draw_rect(Rect2(corner[2], Vector2(width, width)), color) - - -## A four-pointed sparkle of `size` around `at`. -func _sparkle(canvas: CanvasItem, at: Vector2, size: float) -> void: - if size <= 0.1: - return - var star := PackedVector2Array([ - at + Vector2(0, -size), at + Vector2(size * 0.3, -size * 0.3), at + Vector2(size, 0), - at + Vector2(size * 0.3, size * 0.3), at + Vector2(0, size), at + Vector2(-size * 0.3, size * 0.3), - at + Vector2(-size, 0), at + Vector2(-size * 0.3, -size * 0.3), - ]) - canvas.draw_colored_polygon(star, SPARK) +## 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 ``` - [ ] **Step 4: Tests ausführen und Erfolg sehen** @@ -4976,21 +5198,13 @@ godot --headless --path game --import godot --headless --path game -s res://tests/run_tests.gd ``` -Expected: Exit-Code 0 und `134 passed, 0 failed`. Die einzigen Warnungen sind die drei bekannten Zeilen. +Expected: Exit-Code 0 und `143 passed, 0 failed`. Die einzigen Warnungen sind die drei bekannten Zeilen. -- [ ] **Step 5: Bildschirmfotos ansehen** +- [ ] **Step 5: Bildschirmfoto der Vitrine ansehen** -Run: +Run: `godot --path game -s res://tools/screenshot.gd -- "$TEMP/m1d-warm-vitrine.png" --style=warm --showcase` -```bash -godot --path game -s res://tools/screenshot.gd -- "$TEMP/m1d-kontur-karte.png" --style=outline -godot --path game -s res://tools/screenshot.gd -- "$TEMP/m1d-kontur-vitrine.png" --style=outline --showcase -``` - -Expected: Beide Aufrufe enden mit `screenshot saved: …` ohne Warnung. Sieh dir beide Bilder an. Nirgends steht mehr ein Platzhalter. - -- Spiel: Ockerfarbener Boden mit feinen Punkten. Fels ist hell lila, außen abgerundet, mit dunklem Umriss nur zum Boden hin, einem dunkleren Band an der Unterkante und kleinen hellen Strichen. Im getroffenen Fels steht ein gezackter dunkler Riss mit Splittern. Die Figuren haben einen dunklen Umriss, einen roten Körper und einen hellen Kopf. Über der linken steht scharf „Spieler“. -- Vitrine: Harter Fels kräftig blau, weicher Fels hell lila, Grundgestein dunkelbraun, alles mit dunklem Umriss. Toter Boden mit kleinen Rissen, lebendiger Boden kräftig grün mit hellen Häkchen. Moos mit gelben Funken, eine Ranke mit Blättern und Steinen, die Faserpflanze, ein Schaf und die Ruine als Tor mit gelber Sonne, jedes Ding mit dunklem Umriss. Zwei Figuren in Rot und Blau. Oben „Stilprobe: Kontur“, unten ist „2 Kontur“ ausgegraut. +Expected: Der Aufruf endet mit `screenshot saved: …` ohne Warnung. Sieh dir das Bild an. Jetzt ist die ganze Vitrine Pixelgrafik, nirgends steht mehr ein Platzhalter. Toter Boden ist dieselbe braune Erde wie im Spiel, in jeder zweiten Kachel mit einem kleinen Riss. Lebendiger Boden ist sattgrünes Gras mit dunklen Grasbüscheln, hellen Halmen und einzelnen Blumen in Gelb, Weiß und Rosa; sein Rand ist unregelmäßig und dunkler. Dazu kommen Moospolster mit gelben Sporen, die Ranke als grünes Band mit Blättern und drei grauen Steinen, die Faserpflanze mit weißen Faserbüscheln, ein Schaf mit dunklem Gesicht in der Luft und die Ruine als Steintor aus Blöcken mit Moos, Runen und goldenem Licht im Tor. - [ ] **Step 6: Alles prüfen** @@ -5008,12 +5222,13 @@ Expected: keine Ausgabe von rustfmt, keine Warnung von Clippy, zusammen 151 best ```bash git add game git commit -F - <<'EOF' -Stil Kontur +Pixelgrafik für die Dinge der Vitrine -Kräftige dunkle Umrisse um flache, starke Farben, gut lesbar wie ein -Comic. Felsmassen haben ihren Umriss nur zum offenen Boden hin, jedes -Ding hat einen Umriss um seine ganze Form, und Lebendiges funkelt gelb. -Jeder Spieler hat eine eigene Farbe. +Toter Boden hat Risse, lebendiger Boden Gras, Blumen und einen +unregelmäßigen Rand. Moos funkelt, die Ranke trägt Blätter, die +Faserpflanze wächst in sechs Stufen bis zu den Fasern und wiegt sich, +das Weidetier zieht im Sprung die Beine an, und die Ruine leuchtet, +wenn sie erwacht. Co-Authored-By: Claude Opus 5.5 EOF @@ -5022,36 +5237,67 @@ git push --- -### Task 7: Stil Geometrisch +### Task 7: Abwandlungen Pastell und Glimmen -Schlichte geometrische Formen in ruhigen Pastellfarben, ordentlich und still: Quadrate, Dreiecke, Kreise und Sechsecke, ohne Umriss und ohne Schatten. Muster folgen dem Raster, das Feld wirkt geordnet. +Pastell: weiche, helle Farben und Umrisse in einem dunkleren Ton der Fläche. Glimmen: eine gedämpfte, kühle tote Welt in Grau und Schiefer, in der alles Lebendige kräftig ist und leuchtet. Beide setzen dieselben Farbnamen wie Warm. **Files:** -- Modify: `game/scripts/art/geometric_style.gd` (ganze Datei ersetzen) -- Test: `game/tests/test_geometric_style.gd` (neu) +- Modify: `game/scripts/art/pastel_style.gd`, `game/scripts/art/glow_style.gd` (jeweils ganze Datei ersetzen) +- Test: `game/tests/test_pastel_style.gd`, `game/tests/test_glow_style.gd` **Interfaces:** -- Consumes: `ArtStyle` mit seinen Zeichenmethoden und Helfern, `TileShape`, `Art.IDS`, die Einträge von `state` laut Kopf von `art_style.gd` -- Produces: `GeometricStyle` zeichnet jede Kennung selbst; `PLAYER_COLORS: Array[Color]` mit acht Farben und `player_color(player: int) -> Color` +- Consumes: `PixelStyle` aus den Tasks 5 und 6 mit `palette`, `players`, `tinted_outlines` und `glow`; die Farbnamen von `WarmStyle` +- Produces: `PastelStyle` (`tinted_outlines = true`) und `GlowStyle` (`glow = 1.0`) als fertige Abwandlungen von `PixelStyle`, je mit acht Spielerfarben -- [ ] **Step 1: Fehlschlagenden Test schreiben** +- [ ] **Step 1: Fehlschlagende Tests schreiben** -Create `game/tests/test_geometric_style.gd`: +Create `game/tests/test_pastel_style.gd`: ```gdscript extends TestCase func test_four_players_have_four_colours() -> void: - var style := GeometricStyle.new() + var style := PastelStyle.new() var colours := {} for player in 4: colours[style.player_color(player)] = true check_eq(colours.size(), 4, "four players, four colours") check_eq(style.player_color(-1), style.player_color(0), "a player not in the world yet looks like the first") + + +func test_it_has_every_colour_the_warm_style_has() -> void: + var names := PastelStyle.new().palette.keys() + var warm := WarmStyle.new().palette.keys() + names.sort() + warm.sort() + check_eq(names, warm, "the same colours by name") ``` -- [ ] **Step 2: Test ausführen und Fehlschlag sehen** +Create `game/tests/test_glow_style.gd`: + +```gdscript +extends TestCase + + +func test_four_players_have_four_colours() -> void: + var style := GlowStyle.new() + var colours := {} + for player in 4: + colours[style.player_color(player)] = true + check_eq(colours.size(), 4, "four players, four colours") + check_eq(style.player_color(-1), style.player_color(0), "a player not in the world yet looks like the first") + + +func test_it_has_every_colour_the_warm_style_has() -> void: + var names := GlowStyle.new().palette.keys() + var warm := WarmStyle.new().palette.keys() + names.sort() + warm.sort() + check_eq(names, warm, "the same colours by name") +``` + +- [ ] **Step 2: Tests ausführen und Fehlschlag sehen** Run: @@ -5060,51 +5306,55 @@ godot --headless --path game --import godot --headless --path game -s res://tests/run_tests.gd ``` -Expected: Exit-Code 1 und `134 passed, 1 failed`; unter `FAIL test_geometric_style.gd test_four_players_have_four_colours` steht `four players, four colours: expected 4, got 2`. +Expected: Der Testlauf endet mit Exit-Code 1 und `143 passed, 2 failed`. `test_pastel_style.gd` und `test_glow_style.gd` laden nicht; davor steht je `SCRIPT ERROR: Parse Error: Cannot infer the type of "names" variable because the value doesn't have a set type.`, denn die vorläufigen Stile haben noch keine Palette. -- [ ] **Step 3: Stil schreiben** +- [ ] **Step 3: Die beiden Abwandlungen schreiben** -Ersetze den gesamten Inhalt von `game/scripts/art/geometric_style.gd` durch: +Ersetze den gesamten Inhalt von `game/scripts/art/pastel_style.gd` durch: ```gdscript -class_name GeometricStyle -extends ArtStyle -## Plain geometric forms in calm pastel colours, tidy and quiet: squares, -## triangles, circles and hexagons, without outlines or shadows. Patterns -## follow the grid, so the field looks ordered. +class_name PastelStyle +extends PixelStyle +## Soft, light colours, and outlines in a darker shade of what they +## surround instead of one dark line. -const KEY := &"geometric" +const KEY := &"pastel" -const GROUND := Color("ead9b3") -const GROUND_DOT := Color("dcc699") -const SOFT_LIGHT := Color("c6d0e2") -const SOFT_DARK := Color("aebad0") -const HARD := Color("8d94c7") -const HARD_HEX := Color("a8aedc") -const BEDROCK := Color("4a4e69") -const LINE := Color("5b6181") -const DEAD_MARK := Color("cdb78c") -const LIVING := Color("b9e4c7") -const LIVING_DOT := Color("9bd4ae") -const MOSS_DARK := Color("4f9e6c") -const MOSS_LIGHT := Color("7cc493") -const VINE := Color("4c946a") -const LEAF := Color("6fbb88") -const STONE := Color("aab6cd") -const FIBRE := Color("fffaf0") -const SEED := Color("c49b74") -const BODY := Color("f6f2e9") -const FACE := Color("6d6875") -const RUIN := Color("bab5ca") -const RUIN_DARK := Color("5f5b6f") -const GLOW := Color("f6d77e") -const SKIN := Color("f2d4bf") -const MARKER_NEAR := Color("7fca97") -const MARKER_FAR := Color("ef8b7e") -const PLAYER_COLORS: Array[Color] = [ - Color("e9665f"), Color("4f8fcc"), Color("ef9f2f"), Color("4fa870"), - Color("9a7bd8"), Color("2fa9a3"), Color("e27aae"), Color("6f7fd6"), -] + +func _init() -> void: + tinted_outlines = true + palette = { + "outline": Color("7a6670"), "dust": Color("fff6ea"), + "ground": Color("e2c7a6"), "ground_dark": Color("d2b591"), "ground_light": Color("eedac0"), + "pebble": Color("c2b6aa"), "pebble_light": Color("ddd4ca"), "pebble_dark": Color("a3978c"), + "dead_crack": Color("c3a585"), + "soft_top": Color("dcd5cf"), "soft_top_dark": Color("cbc3bc"), "soft_top_light": Color("eeeae6"), + "soft_face": Color("bab1ab"), "soft_face_dark": Color("a89f99"), "soft_lip": Color("f6f3f1"), + "hard_top": Color("abb6da"), "hard_top_dark": Color("99a4c9"), "hard_top_light": Color("c5ceea"), + "hard_face": Color("8e98bf"), "hard_face_dark": Color("7c86ad"), "hard_lip": Color("dde4f6"), + "hard_glint": Color("ffffff"), + "bed_top": Color("8d8292"), "bed_top_dark": Color("7e7383"), "bed_top_light": Color("9e93a3"), + "bed_face": Color("6f6574"), "bed_face_dark": Color("615866"), "bed_lip": Color("b0a6b5"), + "grass": Color("9dd28b"), "grass_dark": Color("86c177"), "grass_light": Color("bae4a7"), + "grass_edge": Color("74ae67"), + "flower_a": Color("fff2a8"), "flower_b": Color("ffffff"), "flower_c": Color("ffb6cf"), + "moss": Color("88c57b"), "moss_dark": Color("72ae67"), "moss_light": Color("abde9a"), "spore": Color("fff0a8"), + "vine": Color("80c077"), "vine_dark": Color("67a460"), "leaf": Color("94d086"), "leaf_dark": Color("76b16a"), + "stem": Color("88c37b"), "bud": Color("cae9b2"), "fibre": Color("fffdf6"), "fibre_shade": Color("ede7db"), + "seed": Color("b38d6c"), + "stone": Color("c6bfb8"), "stone_light": Color("ded9d4"), "stone_dark": Color("aaa39c"), + "wool": Color("fffaf2"), "wool_shade": Color("ede5d9"), "wool_light": Color("ffffff"), + "face": Color("8c7c76"), "hoof": Color("71635d"), + "ruin": Color("cfc8d2"), "ruin_dark": Color("b7b0bd"), "ruin_light": Color("e2dde5"), + "ruin_moss": Color("94c888"), "door": Color("62596d"), "glow": Color("ffe7a2"), "glow_light": Color("fffbe9"), + "skin": Color("fcddc3"), "skin_shade": Color("efc3a4"), "hair": Color("b27c5c"), "hair_light": Color("cb987c"), + "eye": Color("5c4852"), "pants": Color("91a2d2"), "pants_dark": Color("7c8dbf"), "boots": Color("a28068"), + "marker_near": Color("ffffff"), "marker_far": Color("ff9c8c"), + } + players = [ + Color("f08c7c"), Color("7cb0ea"), Color("f2ce6c"), Color("88cb7c"), + Color("ba98e2"), Color("76d1c8"), Color("f2a2c6"), Color("f4af7a"), + ] func key() -> StringName: @@ -5112,199 +5362,62 @@ func key() -> StringName: func title_key() -> String: - return "STYLE_GEOMETRIC" + return "STYLE_PASTEL" +``` + +Ersetze den gesamten Inhalt von `game/scripts/art/glow_style.gd` durch: + +```gdscript +class_name GlowStyle +extends PixelStyle +## A dim, cool world in grey and slate, in which everything living shines in +## strong colours and glows: the dead and the living apart at a glance. + +const KEY := &"glow" -func player_color(player: int) -> Color: - return PLAYER_COLORS[maxi(0, player) % PLAYER_COLORS.size()] - - -func _ground(canvas: CanvasItem, rect: Rect2, _state: Dictionary) -> void: - canvas.draw_rect(rect, GROUND) - if _lattice(rect, 3): - canvas.draw_circle(rect.get_center(), 1.1, GROUND_DOT, true, -1.0, true) - - -func _soft_rock(canvas: CanvasItem, rect: Rect2, _state: Dictionary) -> void: - canvas.draw_rect(rect, SOFT_LIGHT) - var tile := _tile(rect) - var corner := rect.position if (tile.x + tile.y) % 2 == 0 else Vector2(rect.end.x, rect.position.y) - var opposite := rect.end if (tile.x + tile.y) % 2 == 0 else Vector2(rect.position.x, rect.end.y) - canvas.draw_colored_polygon(PackedVector2Array([corner, opposite, Vector2(corner.x, opposite.y)]), SOFT_DARK) - - -func _hard_rock(canvas: CanvasItem, rect: Rect2, _state: Dictionary) -> void: - canvas.draw_rect(rect, HARD) - canvas.draw_colored_polygon(regular_polygon(rect.get_center(), rect.size.x * 0.38, 6, PI / 6.0), HARD_HEX) - - -func _bedrock(canvas: CanvasItem, rect: Rect2, _state: Dictionary) -> void: - canvas.draw_rect(rect, BEDROCK) - - -func _cracks(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: - var hits: int = state.get("hits", 1) - var inner := rect.grow(-3.5) - canvas.draw_line(inner.position, inner.end, LINE, 1.0, true) - if hits >= 2: - canvas.draw_line(Vector2(inner.end.x, inner.position.y), Vector2(inner.position.x, inner.end.y), LINE, 1.0, true) - - -func _hit(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: - var age: float = state.get("age", 0.0) - var c := rect.get_center() - for chip in 4: - var direction := Vector2.from_angle(TAU * chip / 4.0 + PI / 4.0) - var at := c + direction * (2.0 + 8.0 * age) - var color := SOFT_DARK - color.a = 1.0 - age - canvas.draw_colored_polygon(regular_polygon(at, 1.8 * (1.0 - age * 0.5), 4, age * 4.0), color) - - -func _figure(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: - var facing: float = state.get("facing", 1.0) - var step: float = state.get("step", 0.0) - var moving: bool = state.get("moving", false) - var swing := sin(step) if moving else 0.0 - var c := rect.get_center() + Vector2(0, -absf(swing)) - for side in [-1.0, 1.0]: - var foot := c + Vector2(side * 1.8 + side * swing * 1.0, 5.2) - canvas.draw_rect(Rect2(foot - Vector2(1.0, 1.0), Vector2(2.0, 2.0)), FACE) - var body := PackedVector2Array([c + Vector2(0, -3.0), c + Vector2(4.3, 4.8), c + Vector2(-4.3, 4.8)]) - canvas.draw_colored_polygon(body, player_color(state.get("player", -1))) - var head := c + Vector2(0, -5.0) - canvas.draw_circle(head, 3.0, SKIN, true, -1.0, true) - canvas.draw_circle(head + Vector2(facing * 1.2, -0.2), 0.55, FACE, true, -1.0, true) - - -func _marker(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: - var color := MARKER_NEAR if state.get("reachable", false) else MARKER_FAR - var size := 4.0 - var corners: Array[Array] = [ - [rect.position, Vector2(1, 1)], - [Vector2(rect.end.x, rect.position.y), Vector2(-1, 1)], - [rect.end, Vector2(-1, -1)], - [Vector2(rect.position.x, rect.end.y), Vector2(1, -1)], +func _init() -> void: + glow = 1.0 + palette = { + "outline": Color("15171f"), "dust": Color("b9b5af"), + "ground": Color("6f6a68"), "ground_dark": Color("5f5a59"), "ground_light": Color("7f7a77"), + "pebble": Color("57534f"), "pebble_light": Color("8a8580"), "pebble_dark": Color("45413e"), + "dead_crack": Color("4c4846"), + "soft_top": Color("8d8a87"), "soft_top_dark": Color("7c7976"), "soft_top_light": Color("a3a09c"), + "soft_face": Color("636160"), "soft_face_dark": Color("514f4f"), "soft_lip": Color("b5b2ae"), + "hard_top": Color("4f5a75"), "hard_top_dark": Color("434d66"), "hard_top_light": Color("62708f"), + "hard_face": Color("39425a"), "hard_face_dark": Color("2e354a"), "hard_lip": Color("7a89aa"), + "hard_glint": Color("8ff0ff"), + "bed_top": Color("2e2b30"), "bed_top_dark": Color("262328"), "bed_top_light": Color("3a363c"), + "bed_face": Color("1e1b20"), "bed_face_dark": Color("171519"), "bed_lip": Color("48434a"), + "grass": Color("3fbf5a"), "grass_dark": Color("2e9c48"), "grass_light": Color("6ee887"), + "grass_edge": Color("1f7a3a"), + "flower_a": Color("fff27a"), "flower_b": Color("e8fff0"), "flower_c": Color("ff8ad0"), + "moss": Color("35c46a"), "moss_dark": Color("25a052"), "moss_light": Color("7cf5a5"), "spore": Color("fff59a"), + "vine": Color("3dbb63"), "vine_dark": Color("2a8f4a"), "leaf": Color("52d47a"), "leaf_dark": Color("34a85c"), + "stem": Color("45c068"), "bud": Color("a0f5b8"), "fibre": Color("f0fff6"), "fibre_shade": Color("c8e8d6"), + "seed": Color("8a6a4a"), + "stone": Color("8e8b88"), "stone_light": Color("aeaaa6"), "stone_dark": Color("6c6967"), + "wool": Color("eef2f0"), "wool_shade": Color("cfd6d3"), "wool_light": Color("ffffff"), + "face": Color("3a3438"), "hoof": Color("2a2629"), + "ruin": Color("7d7a80"), "ruin_dark": Color("68656b"), "ruin_light": Color("94919a"), + "ruin_moss": Color("3fbf6a"), "door": Color("121014"), "glow": Color("ffe066"), "glow_light": Color("fffbd0"), + "skin": Color("f0c4a0"), "skin_shade": Color("d39e7a"), "hair": Color("3d2a24"), "hair_light": Color("5a3e34"), + "eye": Color("1a1416"), "pants": Color("3b4466"), "pants_dark": Color("2e3552"), "boots": Color("3a2a22"), + "marker_near": Color("c8fff0"), "marker_far": Color("ff6a5a"), + } + players = [ + Color("ff5f4f"), Color("4f9fff"), Color("ffc847"), Color("4fd06a"), + Color("b77aff"), Color("3fd8c8"), Color("ff7ac0"), Color("ff9a4a"), ] - for corner in corners: - var at: Vector2 = corner[0] - var inward: Vector2 = corner[1] - canvas.draw_colored_polygon(PackedVector2Array([at, at + Vector2(inward.x * size, 0), at + Vector2(0, inward.y * size)]), color) -func _dead_soil(canvas: CanvasItem, rect: Rect2, _state: Dictionary) -> void: - canvas.draw_rect(rect, GROUND) - if _lattice(rect, 4): - var c := rect.get_center() - canvas.draw_line(c - Vector2(1.6, 1.6), c + Vector2(1.6, 1.6), DEAD_MARK, 0.8, true) - canvas.draw_line(c + Vector2(1.6, -1.6), c + Vector2(-1.6, 1.6), DEAD_MARK, 0.8, true) +func key() -> StringName: + return KEY -func _living_soil(canvas: CanvasItem, rect: Rect2, _state: Dictionary) -> void: - canvas.draw_rect(rect, LIVING) - if _lattice(rect, 2): - canvas.draw_circle(rect.get_center(), 1.2, LIVING_DOT, true, -1.0, true) - - -func _moss(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: - var variant: int = state.get("variant", 0) - var time: float = state.get("time", 0.0) - var foot := Vector2(rect.get_center().x, rect.end.y - 4.0) - var breathe := 1.0 + 0.08 * sway(time, variant, 1.6) - canvas.draw_circle(foot + Vector2(-3.0, 0.0), 2.6 * breathe, MOSS_DARK, true, -1.0, true) - canvas.draw_circle(foot + Vector2(2.6, 0.4), 2.2 * breathe, MOSS_DARK, true, -1.0, true) - canvas.draw_circle(foot + Vector2(0.0, -2.6), 3.0 * breathe, MOSS_LIGHT, true, -1.0, true) - - -func _vine(canvas: CanvasItem, _rect: Rect2, state: Dictionary) -> void: - var points: PackedVector2Array = state.get("points", PackedVector2Array()) - var time: float = state.get("time", 0.0) - if points.size() < 2: - return - canvas.draw_polyline(points, VINE, 1.0, true) - var beads := 24 - for i in beads + 1: - var part := float(i) / beads - var at := along(points, part) - if i % 3 == 1: - var ahead := along(points, minf(1.0, part + 0.02)) - var side := 1.0 if i % 2 == 0 else -1.0 - var angle := (ahead - at).angle() + side * (PI / 2.0) + 0.2 * sway(time, i, 1.4) - var tip := at + Vector2.from_angle(angle) * 4.0 - var base := Vector2.from_angle(angle).orthogonal() * 1.3 - canvas.draw_colored_polygon(PackedVector2Array([at + base, tip, at - base]), LEAF) - canvas.draw_circle(at, 1.4, VINE, true, -1.0, true) - - -func _fiber_plant(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: - var growth: float = state.get("growth", 1.0) - var time: float = state.get("time", 0.0) - var foot := Vector2(rect.get_center().x, rect.end.y - 2.0) - if growth < 0.15: - canvas.draw_circle(foot + Vector2(0, -1.5), 1.8, SEED, true, -1.0, true) - return - var grown := (growth - 0.15) / 0.85 - var height := lerpf(4.0, 20.0, grown) - canvas.draw_set_transform(foot, 0.06 * sway(time, 3, 1.1), Vector2.ONE) - canvas.draw_line(Vector2.ZERO, Vector2(0, -height), VINE, 1.2, true) - var leaves := 1 + int(grown * 5.0) - for i in leaves: - var at := Vector2(0, -height * (0.2 + 0.7 * i / maxf(1.0, leaves))) - var side := 1.0 if i % 2 == 0 else -1.0 - var size := lerpf(2.5, 4.5, grown) - canvas.draw_colored_polygon(PackedVector2Array([at, at + Vector2(side * size, -size * 0.6), at + Vector2(0, -size * 0.9)]), LEAF) - if grown > 0.75: - var tuft := (grown - 0.75) / 0.25 - for puff in 3: - canvas.draw_circle(Vector2(-1.6 + puff * 1.6, -height - 1.0), 1.3 * tuft + 0.2, FIBRE, true, -1.0, true) - canvas.draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE) - - -func _stone(canvas: CanvasItem, rect: Rect2, _state: Dictionary) -> void: - canvas.draw_colored_polygon(regular_polygon(rect.get_center(), 2.8, 4, PI / 4.0), STONE) - - -func _grazer(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: - var hop: float = state.get("hop", 1.0) - var facing: float = state.get("facing", 1.0) - var lift := sin(PI * hop) * 4.0 - var foot := Vector2(rect.get_center().x, rect.end.y - 2.0) - var body := foot + Vector2(0, -5.5 - lift) - for leg in [-2.0, 2.0]: - canvas.draw_line(body + Vector2(leg, 2.5), body + Vector2(leg, 5.5 - lift * 0.3), FACE, 1.2, true) - canvas.draw_circle(body, 4.4, BODY, true, -1.0, true) - canvas.draw_circle(body + Vector2(facing * 4.6, -1.6), 2.2, FACE, true, -1.0, true) - - -func _ruin(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void: - var awake: float = state.get("awake", 0.0) - var tile := rect.size.x / 3.0 - var floor_y := rect.end.y - 3.0 - var center := Vector2(rect.get_center().x, floor_y - tile * 1.2) - canvas.draw_rect(Rect2(rect.position.x + tile * 0.75, rect.position.y + tile * 0.9, tile * 1.5, floor_y - rect.position.y - tile * 0.9), RUIN_DARK) - for ring in 3: - var glow := GLOW - glow.a = awake * (0.35 + 0.3 * ring) - canvas.draw_circle(center, 8.5 - ring * 2.4, glow, true, -1.0, true) - canvas.draw_rect(Rect2(rect.position.x, rect.position.y + tile * 0.6, tile * 0.75, floor_y - rect.position.y - tile * 0.6), RUIN) - canvas.draw_rect(Rect2(rect.end.x - tile * 0.75, rect.position.y + tile * 0.6, tile * 0.75, floor_y - rect.position.y - tile * 0.6), RUIN) - canvas.draw_rect(Rect2(rect.position.x - 1.0, rect.position.y + tile * 0.25, rect.size.x + 2.0, tile * 0.6), RUIN) - canvas.draw_colored_polygon(PackedVector2Array([ - Vector2(rect.position.x - 1.0, rect.position.y + tile * 0.25), Vector2(rect.get_center().x, rect.position.y - tile * 0.3), - Vector2(rect.end.x + 1.0, rect.position.y + tile * 0.25), - ]), RUIN) - - -## The tile that `rect` covers. -static func _tile(rect: Rect2) -> Vector2i: - return Vector2i(rect.position / GridMotion.TILE_SIZE) - - -## True for every `every`th tile along both diagonals, which makes an even, -## open pattern across the field. -static func _lattice(rect: Rect2, every: int) -> bool: - var tile := _tile(rect) - return posmod(tile.x + tile.y * 2, every) == 0 and posmod(tile.x - tile.y, 2) == 0 +func title_key() -> String: + return "STYLE_GLOW" ``` - [ ] **Step 4: Tests ausführen und Erfolg sehen** @@ -5316,21 +5429,23 @@ godot --headless --path game --import godot --headless --path game -s res://tests/run_tests.gd ``` -Expected: Exit-Code 0 und `135 passed, 0 failed`. Die einzigen Warnungen sind die drei bekannten Zeilen. +Expected: Exit-Code 0 und `147 passed, 0 failed`. Die einzigen Warnungen sind die drei bekannten Zeilen. - [ ] **Step 5: Bildschirmfotos ansehen** Run: ```bash -godot --path game -s res://tools/screenshot.gd -- "$TEMP/m1d-geometrisch-karte.png" --style=geometric -godot --path game -s res://tools/screenshot.gd -- "$TEMP/m1d-geometrisch-vitrine.png" --style=geometric --showcase +for key in pastel glow; do + godot --path game -s res://tools/screenshot.gd -- "$TEMP/m1d-$key-karte.png" --style=$key + godot --path game -s res://tools/screenshot.gd -- "$TEMP/m1d-$key-vitrine.png" --style=$key --showcase +done ``` -Expected: Beide Aufrufe enden mit `screenshot saved: …` ohne Warnung. Sieh dir beide Bilder an. Nirgends steht mehr ein Platzhalter. +Expected: Alle vier Aufrufe enden mit `screenshot saved: …` ohne Warnung. Sieh dir die Bilder an. Nirgends steht ein Platzhalter. -- Spiel: Heller Sandboden mit kleinen Rauten in einem regelmäßigen Raster. Weicher Fels besteht aus hellblauen und graublauen Dreiecken, ohne Umriss und ohne Schatten. Im getroffenen Fels steht ein dünner dunkler Strich als Riss. Die Figuren sind rote Dreiecke mit rundem Kopf. Über der linken steht scharf „Spieler“. -- Vitrine: Grundgestein dunkel, weicher Fels aus Dreiecken, harter Fels lila mit hellen Sechsecken. Toter Boden mit kleinen Kreuzen im Raster, lebendiger Boden mintgrün mit Punkten. Moos aus Kreisen, die Ranke als Perlenschnur mit eckigen Steinen, die Faserpflanze, ein Schaf, die Ruine als Haus mit Giebel und goldenem Leuchten. Zwei Figuren als rotes und blaues Dreieck. Oben „Stilprobe: Geometrisch“, unten ist „3 Geometrisch“ ausgegraut. +- Pastell: dieselben Formen wie Warm, aber in weichen, hellen Farben: heller Sandboden, hellgrauer Fels, lavendelblauer harter Fels, zartgrünes Gras. Die Umrisse sind kein Schwarzbraun, sondern ein dunklerer Ton der Fläche. Die Vitrine hat oben „Stilprobe: Pastell“, unten ist „2 Pastell“ ausgegraut. +- Glimmen: eine graue, gedämpfte tote Welt mit grauem Boden, grauem Fels, dunklem schieferblauem harten Fels, fast schwarzem Grundgestein und dunklen Umrissen. Das Lebendige ist kräftig: leuchtend grünes Gras mit hellen Funken, Moos mit leuchtenden Sporen, die Ruine mit hellem Licht. Die Vitrine hat oben „Stilprobe: Glimmen“, unten ist „3 Glimmen“ ausgegraut. - [ ] **Step 6: Alles prüfen** @@ -5348,12 +5463,12 @@ Expected: keine Ausgabe von rustfmt, keine Warnung von Clippy, zusammen 151 best ```bash git add game git commit -F - <<'EOF' -Stil Geometrisch +Abwandlungen Pastell und Glimmen -Schlichte Formen in ruhigen Pastellfarben, ohne Umriss und ohne -Schatten: Dreiecke für weichen Fels, Sechsecke für harten, Kreise für -Moos und eine Perlenschnur als Ranke. Muster folgen dem Raster, und -jeder Spieler hat eine eigene Farbe. +Pastell zeichnet mit weichen, hellen Farben und Umrissen im dunkleren +Ton der Fläche. Glimmen zeigt eine gedämpfte, kühle tote Welt, in der +alles Lebendige kräftig ist und leuchtet. Beide haben dieselben +Farbnamen wie Warm und je acht Spielerfarben. Co-Authored-By: Claude Opus 5.5 EOF @@ -5367,7 +5482,7 @@ git push **Files:** - Modify: `CLAUDE.md` (ganze Datei ersetzen) - Create: `docs/playtests/m1d.md` -- Create: `docs/playtests/m1d/scherenschnitt-karte.png`, `scherenschnitt-vitrine.png`, `kontur-karte.png`, `kontur-vitrine.png`, `geometrisch-karte.png`, `geometrisch-vitrine.png` (vom Bildschirmfoto-Werkzeug erzeugt) +- Create: `docs/playtests/m1d/warm-karte.png`, `warm-vitrine.png`, `pastell-karte.png`, `pastell-vitrine.png`, `glimmen-karte.png`, `glimmen-vitrine.png` (vom Bildschirmfoto-Werkzeug erzeugt) **Interfaces:** - Consumes: das Bildschirmfoto-Werkzeug aus Task 4 und die drei Stile aus den Tasks 5 bis 7 @@ -5442,11 +5557,11 @@ Drei Wächter prüfen das. `core/clippy.toml` sperrt Pfade wie `std::collections ## Grafik - Alles auf dem Spielfeld zeichnet `Art` (`game/scripts/art/art.gd`), jedes Ding unter einer Kennung wie `terrain/soft_rock`. Andere Skripte zeichnen keine eigenen Formen oder Farben aufs Spielfeld; ausgenommen sind Texte wie die Namen über den Figuren. -- Ein Stil ist eine Unterklasse von `ArtStyle` und zeichnet jede Kennung aus Code: flach, klar, Bewegung aus Programmierung (Konzept 7.4). Zur Wahl stehen Scherenschnitt, Kontur und Geometrisch. F2 wechselt im Spiel, „Stilproben ansehen“ im Menü öffnet die Vitrine, und das Spiel merkt sich die Wahl. -- Eine neue Kennung braucht eine Zeichnung in jedem Stil; sonst zeichnet `ArtStyle` nur einen Platzhalter. `game/tests/test_art.gd` lässt jeden Stil jede Kennung zeichnen. -- Ein Bild `game/art/.png` ersetzt die Zeichnung seiner Kennung, ohne dass sich Code ändert. So kann ein Artist Grafik austauschen. -- Die Karte zeichnet in Blöcken von 16 × 16 Kacheln (`MapView`); nach einem Treffer werden nur die Blöcke um die Kachel neu gezeichnet. Jede Kachel kennt ihre Nachbarn (`TileShape`), damit Felsmassen außen rund und innen geschlossen sind. -- Der Renderer der Hülle kennt kein MSAA in 2D. Glatte Kanten entstehen über die Zeichenbefehle mit `antialiased`, etwa bei Kreisen und Linien. +- Die Grafik ist Pixelgrafik aus Code (Konzept 7.4): 16 × 16 Pixel pro Kachel, schräg von vorn gesehen. `PixelStyle` (`game/scripts/art/pixel_style.gd`) zeichnet jede Kennung Pixel für Pixel in ein kleines Bild und hebt es auf. Die Stile `WarmStyle`, `PastelStyle` und `GlowStyle` wählen nur Palette, Umrisse und Leuchten; Warm ist der Standard. F2 wechselt im Spiel, „Stilproben ansehen“ im Menü öffnet die Vitrine, und das Spiel merkt sich die Wahl. +- Eine neue Kennung braucht eine Zeichnung in `PixelStyle` und ihre Farben in jeder Palette; sonst zeichnet `ArtStyle` nur einen Platzhalter. `game/tests/test_art.gd` lässt jeden Stil jede Kennung zeichnen. +- Damit die Pixel scharf bleiben, stellt `project.godot` die Texturfilterung auf „Nearest“ und rastet 2D-Transformationen auf ganze Bildschirmpixel ein. Die Kamera vergrößert ganzzahlig (`CAMERA_ZOOM` in `main.gd`), und die Stile setzen ihre Bilder auf ganze Pixel. +- Ein Bild `game/art/.png` ersetzt die Zeichnung seiner Kennung, ohne dass sich Code ändert. So kann ein Artist Grafik austauschen. Ein solches Bild ist ein einzelnes festes Bild; Kachelränder und Bewegung zeichnet bisher nur der Code. +- Die Karte zeichnet in Blöcken von 16 × 16 Kacheln (`MapView`); nach einem Treffer werden nur die Blöcke um die Kachel neu gezeichnet. Jede Kachel kennt ihre Nachbarn (`TileShape`), damit Felsmassen außen rund und innen geschlossen sind und zum offenen Boden hin ihre Vorderkante zeigen. ## Prüfen @@ -5488,7 +5603,7 @@ Argumente hinter `play.bat` gehen an das Spiel: | `--solo`, `--host` | Startet ohne Menü allein beziehungsweise mit offener Welt | | `--port=PORT` | Port einer offenen Welt | | `--join=ADRESSE` | Tritt ohne Menü einem Host bei; die Adresse darf auf `:PORT` enden | -| `--style=NAME` | Zeichnet im Stil `papercut`, `outline` oder `geometric` statt im zuletzt gewählten | +| `--style=NAME` | Zeichnet im Stil `warm`, `pastel` oder `glow` statt im zuletzt gewählten | `play-zweites-fenster.bat` startet ein zweites Fenster mit dem Profil `zweites-fenster`. Die Datei reicht weitere Argumente ebenfalls an das Spiel weiter, etwa `--name=Zweiter`. @@ -5513,18 +5628,18 @@ Der Pfad muss absolut sein, denn Godot legt relative Pfade unter `game/` an. `pw ```bash mkdir -p docs/playtests/m1d OUT="$(pwd -W)/docs/playtests/m1d" -for pair in papercut:scherenschnitt outline:kontur geometric:geometrisch; do +for pair in warm:warm pastel:pastell glow:glimmen; do godot --path game -s res://tools/screenshot.gd -- "$OUT/${pair#*:}-karte.png" --style=${pair%%:*} godot --path game -s res://tools/screenshot.gd -- "$OUT/${pair#*:}-vitrine.png" --style=${pair%%:*} --showcase done ls -l docs/playtests/m1d ``` -Expected: Jeder der sechs Aufrufe endet mit `screenshot saved: …` ohne Warnung. `ls` zeigt die sechs Dateien `geometrisch-karte.png`, `geometrisch-vitrine.png`, `kontur-karte.png`, `kontur-vitrine.png`, `scherenschnitt-karte.png` und `scherenschnitt-vitrine.png`, jede zwischen 40 und 120 KB groß. Die genauen Größen schwanken von Lauf zu Lauf um einige Bytes. +Expected: Jeder der sechs Aufrufe endet mit `screenshot saved: …` ohne Warnung. `ls` zeigt die sechs Dateien `glimmen-karte.png`, `glimmen-vitrine.png`, `pastell-karte.png`, `pastell-vitrine.png`, `warm-karte.png` und `warm-vitrine.png`, jede zwischen 35 und 75 KB groß. Die genauen Größen schwanken von Lauf zu Lauf um einige Bytes. - [ ] **Step 3: Bilder ansehen** -Sieh dir alle sechs Bilder an. Jedes muss zu seiner Beschreibung in Step 5 der Tasks 5, 6 und 7 passen: das Spiel mit zwei Figuren und dem Namen „Spieler“, die Vitrine mit allen neun Beschriftungen und dem Titel ihres Stils. +Sieh dir alle sechs Bilder an. Jedes muss zu seiner Beschreibung aus den Tasks 5 bis 7 passen: das Spiel mit zwei Figuren und dem Namen „Spieler“, die Vitrine mit allen neun Beschriftungen und dem Titel ihres Stils. - [ ] **Step 4: Fragebogen schreiben** @@ -5533,7 +5648,7 @@ Create `docs/playtests/m1d.md`: ```markdown # Probelauf M1d: Stilproben -M1d zeigt das Spielfeld in drei Stilen, alle aus Code gezeichnet: Scherenschnitt, Kontur und Geometrisch. Du wählst einen davon für das ganze Spiel. Das ist die Prüffrage von Meilenstein 1: „Welche der drei Stilproben soll es sein?“ Die Vitrine zeigt dazu Dinge aus späteren Meilensteinen, die es zum Spielen noch nicht gibt. +M1d zeichnet das Spielfeld als Pixelgrafik aus Code, im Stil klassischer Farmspiele, aber eigenständig. Es gibt drei Abwandlungen: Warm, Pastell und Glimmen. Nach der ersten Probe war Warm dein Favorit; jetzt siehst du die Stile im Spiel und in Bewegung. Das beantwortet die Prüffrage von Meilenstein 1: „Welche der drei Stilproben soll es sein?“ Die Vitrine zeigt dazu Dinge aus späteren Meilensteinen, die es zum Spielen noch nicht gibt. ## So siehst du die Proben @@ -5553,21 +5668,21 @@ M1d zeigt das Spielfeld in drei Stilen, alle aus Code gezeichnet: Scherenschnitt So sehen die drei Proben aus: -| Scherenschnitt | Kontur | Geometrisch | +| Warm | Pastell | Glimmen | |---|---|---| -| ![Scherenschnitt, Vitrine](m1d/scherenschnitt-vitrine.png) | ![Kontur, Vitrine](m1d/kontur-vitrine.png) | ![Geometrisch, Vitrine](m1d/geometrisch-vitrine.png) | -| ![Scherenschnitt, Spiel](m1d/scherenschnitt-karte.png) | ![Kontur, Spiel](m1d/kontur-karte.png) | ![Geometrisch, Spiel](m1d/geometrisch-karte.png) | +| ![Warm, Vitrine](m1d/warm-vitrine.png) | ![Pastell, Vitrine](m1d/pastell-vitrine.png) | ![Glimmen, Vitrine](m1d/glimmen-vitrine.png) | +| ![Warm, Spiel](m1d/warm-karte.png) | ![Pastell, Spiel](m1d/pastell-karte.png) | ![Glimmen, Spiel](m1d/glimmen-karte.png) | ## Fragen -1. Welcher Stil soll es sein: Scherenschnitt, Kontur oder Geometrisch? +1. Bleibt es bei Warm, oder gefällt dir im Spiel ein anderer Stil besser? 2. Was gefällt dir an deinem Favoriten am besten, und was stört dich daran? -3. Gibt es an den anderen beiden etwas, das du übernehmen möchtest? +3. Gibt es an den anderen beiden etwas, das du übernehmen möchtest, etwa das Leuchten des Lebendigen aus Glimmen? 4. Erkennst du auf einen Blick, was Boden, weicher Fels und harter Fels ist? Und was lebendiger Boden ist? 5. Wirkt das Lebendige lebendig: das Moos, die Ranke, die wachsende Pflanze, das Tier, die erwachende Ruine? Oder wirkt es nüchtern? -6. Ist die Bewegung angenehm: Wiegen, Wachsen, Hüpfen und die Schritte der Figur? Oder ist es zu viel oder zu wenig? -7. Findest du deine Figur sofort, auch neben anderen? -8. Wirkt ein Schlag als Gast jetzt sofort? Stört die Verzögerung noch? +6. Ist die Bewegung angenehm: Laufen, Wiegen, Wachsen und Hüpfen? Oder ist es zu viel oder zu wenig? +7. Findest du deine Figur sofort, auch neben anderen? Sind Figuren und Dinge groß genug? +8. Wirkt ein Schlag als Gast jetzt sofort, und sind die Namen über den Figuren scharf? Antworte formlos im Chat oder als Kommentar in Gitea. ``` @@ -5583,7 +5698,7 @@ godot --headless --path game --import godot --headless --path game -s res://tests/run_tests.gd ``` -Expected: keine Ausgabe von rustfmt, keine Warnung von Clippy, zusammen 151 bestandene Rust-Tests; der GDScript-Lauf endet mit Exit-Code 0 und `135 passed, 0 failed` +Expected: keine Ausgabe von rustfmt, keine Warnung von Clippy, zusammen 151 bestandene Rust-Tests; der GDScript-Lauf endet mit Exit-Code 0 und `147 passed, 0 failed` - [ ] **Step 6: Commit und Hochladen** @@ -5592,11 +5707,12 @@ git add CLAUDE.md docs/playtests/m1d.md docs/playtests/m1d git commit -F - <<'EOF' Regeln, Fragebogen und Bilder für den Probelauf M1d -CLAUDE.md beschreibt, wie Grafik entsteht: Kennungen, Stile, Bilder, -die Zeichnungen ersetzen, Blöcke und Kachelformen. Das -Bildschirmfoto-Werkzeug und --style=NAME stehen bei ihren Befehlen. -Eine stille Verbindung gilt nach etwa NetLink.TIMEOUT_MSEC als verloren. -Ein Fragebogen mit Bildern der drei Stile begleitet den Probelauf. +CLAUDE.md beschreibt, wie die Grafik entsteht: Kennungen, Pixelgrafik +aus Code, scharfe Pixel, Bilder, die Zeichnungen ersetzen, Blöcke und +Kachelformen. Das Bildschirmfoto-Werkzeug und --style=NAME stehen bei +ihren Befehlen. Eine stille Verbindung gilt nach etwa +NetLink.TIMEOUT_MSEC als verloren. Ein Fragebogen mit Bildern der drei +Stile begleitet den Probelauf. Co-Authored-By: Claude Opus 5.5 EOF -- 2.52.0 From 9921aba63a1c901c76021e0af66c739ee49ff434 Mon Sep 17 00:00:00 2001 From: Steven Wroblewski Date: Fri, 2 Oct 2026 11:24:45 +0200 Subject: [PATCH 03/17] =?UTF-8?q?Kennungen,=20Kachelformen=20und=20Platzha?= =?UTF-8?q?lter=20f=C3=BCr=20die=20Grafik?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Jede Grafik auf dem Spielfeld bekommt eine Kennung. Art zeichnet sie im gewählten Stil oder aus einem Bild gleichen Namens unter game/art. ArtStyle zeichnet vorerst schlichte Platzhalter. TileShape beschreibt die Nachbarn einer Kachel, damit Felsmassen außen rund und innen geschlossen gezeichnet werden können. Die drei Stile Warm, Pastell und Glimmen gibt es schon mit Namen, noch ohne eigene Zeichnungen. Co-Authored-By: Claude Haiku 4.5 --- game/i18n/texts.csv | 3 + game/scripts/art/art.gd | 105 +++++++++++++++ game/scripts/art/art.gd.uid | 1 + game/scripts/art/art_style.gd | 192 +++++++++++++++++++++++++++ game/scripts/art/art_style.gd.uid | 1 + game/scripts/art/glow_style.gd | 14 ++ game/scripts/art/glow_style.gd.uid | 1 + game/scripts/art/pastel_style.gd | 14 ++ game/scripts/art/pastel_style.gd.uid | 1 + game/scripts/art/tile_shape.gd | 49 +++++++ game/scripts/art/tile_shape.gd.uid | 1 + game/scripts/art/warm_style.gd | 14 ++ game/scripts/art/warm_style.gd.uid | 1 + game/tests/test_art.gd | 110 +++++++++++++++ game/tests/test_art.gd.uid | 1 + game/tests/test_art_style.gd | 26 ++++ game/tests/test_art_style.gd.uid | 1 + game/tests/test_tile_shape.gd | 38 ++++++ game/tests/test_tile_shape.gd.uid | 1 + 19 files changed, 574 insertions(+) create mode 100644 game/scripts/art/art.gd create mode 100644 game/scripts/art/art.gd.uid create mode 100644 game/scripts/art/art_style.gd create mode 100644 game/scripts/art/art_style.gd.uid create mode 100644 game/scripts/art/glow_style.gd create mode 100644 game/scripts/art/glow_style.gd.uid create mode 100644 game/scripts/art/pastel_style.gd create mode 100644 game/scripts/art/pastel_style.gd.uid create mode 100644 game/scripts/art/tile_shape.gd create mode 100644 game/scripts/art/tile_shape.gd.uid create mode 100644 game/scripts/art/warm_style.gd create mode 100644 game/scripts/art/warm_style.gd.uid create mode 100644 game/tests/test_art.gd create mode 100644 game/tests/test_art.gd.uid create mode 100644 game/tests/test_art_style.gd create mode 100644 game/tests/test_art_style.gd.uid create mode 100644 game/tests/test_tile_shape.gd create mode 100644 game/tests/test_tile_shape.gd.uid 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 -- 2.52.0 From d2a79730226c83826d0b7c1bbb746c7dbe5675aa Mon Sep 17 00:00:00 2001 From: Steven Wroblewski Date: Fri, 2 Oct 2026 11:49:18 +0200 Subject: [PATCH 04/17] =?UTF-8?q?Spielfeld=20=C3=BCber=20Art=20zeichnen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Karte, Figuren und Zielmarke zeichnet jetzt Art. Die Karte zeichnet in Blöcken von 16 mal 16 Kacheln und nach einem Treffer nur die Blöcke um die Kachel neu; jede Kachel kennt ihre Nachbarn. Figuren stehen aufrecht in ihrer Kachel, schreiten und schauen in die Richtung, in die sie gehen. Der eigene Schlag zeigt seine Splitter sofort, auch als Gast, und die Namen über fremden Figuren werden in Bildschirmgröße gezeichnet und bleiben scharf. Co-Authored-By: Claude Haiku 4.5 --- game/scripts/avatar.gd | 39 ++++++- game/scripts/figures.gd | 57 ++++++--- game/scripts/main.gd | 36 +++++- game/scripts/map_view.gd | 200 ++++++++++++++++++++++++++------ game/scripts/tile_marker.gd | 15 ++- game/tests/test_avatar.gd | 23 ++++ game/tests/test_figures.gd | 10 ++ game/tests/test_main.gd | 11 ++ game/tests/test_map_view.gd | 94 +++++++++++++++ game/tests/test_map_view.gd.uid | 1 + 10 files changed, 422 insertions(+), 64 deletions(-) create mode 100644 game/tests/test_map_view.gd create mode 100644 game/tests/test_map_view.gd.uid diff --git a/game/scripts/avatar.gd b/game/scripts/avatar.gd index cde223c..f0ec1e3 100644 --- a/game/scripts/avatar.gd +++ b/game/scripts/avatar.gd @@ -1,7 +1,8 @@ class_name Avatar extends Node2D ## The local player's figure. It moves outside the lockstep simulation; only -## its pickaxe hits become commands. +## its pickaxe hits become commands. `art` draws it, stepping while it walks +## and facing the way it went last. ## Walking speed: five tiles per second. const SPEED := 5.0 * GridMotion.TILE_SIZE @@ -9,9 +10,30 @@ const RADIUS := 5.0 ## Longest frame that moves the figure in one go. Longer frames are cut, so ## that the figure cannot pass through a wall after a stall. const MAX_STEP_SECONDS := 0.1 +## How far the step of a walking figure turns per pixel, in radians: about +## two steps per second at walking speed. +const STEP_PER_PIXEL := 0.16 ## Callable(x: int, y: int) -> bool that says whether a tile can be walked on. var is_walkable: Callable +var art: Art +## The number of the player the figure belongs to, for its colour, or -1 +## before the player is in the world. +var player := -1 +## 1.0 while the figure faces right, -1.0 while it faces left. +var facing := 1.0 +## Grows while the figure walks; the styles swing the steps by it. +var step := 0.0 +## True while the figure walked in the last frame. +var moving := false + + +## Where a figure standing at `at` is drawn: a tile wide and one and a half +## tiles tall, with its feet just below its body, so that it stands upright +## in its tile. +static func figure_rect(at: Vector2) -> Rect2: + var size := GridMotion.TILE_SIZE + return Rect2(at.x - size / 2.0, at.y + RADIUS + 1.0 - size * 1.5, size, size * 1.5) func _process(delta: float) -> void: @@ -20,12 +42,23 @@ func _process(delta: float) -> void: ## Moves the figure in `direction` for `delta` seconds, sliding along walls. func walk(direction: Vector2, delta: float) -> void: + var was_moving := moving + moving = false if direction == Vector2.ZERO or not is_walkable.is_valid(): + if was_moving: + queue_redraw() return var motion := direction * SPEED * minf(delta, MAX_STEP_SECONDS) + var before := position position = GridMotion.move(position, motion, RADIUS, is_walkable) + var walked := position.distance_to(before) + moving = walked > 0.0 + step += walked * STEP_PER_PIXEL + if direction.x != 0.0: + facing = signf(direction.x) + queue_redraw() func _draw() -> void: - draw_circle(Vector2.ZERO, RADIUS + 1.0, Color(0.15, 0.1, 0.05)) - draw_circle(Vector2.ZERO, RADIUS, Color(0.95, 0.85, 0.6)) + if art != null: + art.draw(self, Art.FIGURE, figure_rect(Vector2.ZERO), {"player": player, "facing": facing, "step": step, "moving": moving}) diff --git a/game/scripts/figures.gd b/game/scripts/figures.gd index 0809836..5b8eea1 100644 --- a/game/scripts/figures.gd +++ b/game/scripts/figures.gd @@ -1,19 +1,27 @@ class_name Figures extends Node2D -## The figures of the other players, with their names. A new position comes -## over the network about ten times a second; in between, each figure glides -## toward the latest one. +## The figures of the other players, with their names, drawn by `art`. A new +## position comes over the network about ten times a second; in between, +## each figure glides toward the latest one, stepping and facing the way it +## goes. ## How quickly a figure closes the gap to its latest position, per second. const GLIDE := 15.0 -const COLOR := Color(0.55, 0.8, 0.95) -const OUTLINE := Color(0.05, 0.1, 0.15) +## Pixels per frame below which a gliding figure counts as standing. +const STANDING := 0.05 +## Size and width of the names, in pixels of the playing field. The names +## are drawn at the size they have on screen, so that they stay sharp. const NAME_SIZE := 8 const NAME_WIDTH := 80.0 +const NAME_OUTLINE := Color(0.05, 0.05, 0.06) var session: GwSession +var art: Art var _shown: Dictionary[int, Vector2] = {} var _latest: Dictionary[int, Vector2] = {} +var _facing: Dictionary[int, float] = {} +var _step: Dictionary[int, float] = {} +var _moving: Dictionary[int, bool] = {} ## Moves the figure of `player` toward `position`. A figure that was not @@ -27,14 +35,14 @@ func place(player: int, position: Vector2) -> void: ## Removes the figure of `player`. func forget(player: int) -> void: - _shown.erase(player) - _latest.erase(player) + for figures: Dictionary in [_shown, _latest, _facing, _step, _moving]: + figures.erase(player) queue_redraw() func clear() -> void: - _shown.clear() - _latest.clear() + for figures: Dictionary in [_shown, _latest, _facing, _step, _moving]: + figures.clear() queue_redraw() @@ -43,21 +51,42 @@ func shown_at(player: int) -> Vector2: return _shown.get(player, Vector2.INF) +## 1.0 if the figure of `player` faces right, -1.0 if it faces left. +func facing_of(player: int) -> float: + return _facing.get(player, 1.0) + + func _process(delta: float) -> void: if _shown.is_empty(): return var weight := minf(1.0, GLIDE * delta) for player in _shown: - _shown[player] = _shown[player].lerp(_latest[player], weight) + var before := _shown[player] + _shown[player] = before.lerp(_latest[player], weight) + var moved := _shown[player] - before + if absf(moved.x) > STANDING: + _facing[player] = signf(moved.x) + _step[player] = _step.get(player, 0.0) + moved.length() * Avatar.STEP_PER_PIXEL + _moving[player] = moved.length() > STANDING queue_redraw() func _draw() -> void: + if art == null: + return var font := ThemeDB.fallback_font + # The camera enlarges the field. Text drawn at its size on screen and then + # shrunk stays sharp; text drawn small and enlarged would blur. + var zoom := maxf(1.0, get_global_transform_with_canvas().get_scale().x) for player in _shown: var at := _shown[player] - draw_circle(at, Avatar.RADIUS + 1.0, OUTLINE) - draw_circle(at, Avatar.RADIUS, COLOR) + var state := {"player": player, "facing": facing_of(player), "step": _step.get(player, 0.0), "moving": _moving.get(player, false)} + art.draw(self, Art.FIGURE, Avatar.figure_rect(at), state) if session != null: - var label := Vector2(at.x - NAME_WIDTH / 2.0, at.y - Avatar.RADIUS - 3.0) - draw_string(font, label, session.player_name(player), HORIZONTAL_ALIGNMENT_CENTER, NAME_WIDTH, NAME_SIZE) + var label := Vector2(at.x - NAME_WIDTH / 2.0, Avatar.figure_rect(at).position.y - 2.0) + var text := session.player_name(player) + var size := roundi(NAME_SIZE * zoom) + draw_set_transform(label, 0.0, Vector2.ONE / zoom) + draw_string_outline(font, Vector2.ZERO, text, HORIZONTAL_ALIGNMENT_CENTER, NAME_WIDTH * zoom, size, maxi(2, roundi(zoom * 1.5)), NAME_OUTLINE) + draw_string(font, Vector2.ZERO, text, HORIZONTAL_ALIGNMENT_CENTER, NAME_WIDTH * zoom, size) + draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE) diff --git a/game/scripts/main.gd b/game/scripts/main.gd index d8c8b85..2f64871 100644 --- a/game/scripts/main.gd +++ b/game/scripts/main.gd @@ -40,6 +40,8 @@ var session: GwSession ## The network connection of an open world or a guest, or null. var link: SessionLink var clock: TickClock +## The style the playing field is drawn in. +var art: Art var menu: Menu var map_view: MapView var marker: TileMarker @@ -75,13 +77,19 @@ func _ready() -> void: if _player_key.is_empty(): push_warning("the player key cannot be read: %s" % error_string(store.last_read_error)) clock = TickClock.new(GwSession.TICKS_PER_SECOND) + art = Art.new() + art.load_overrides(Art.OVERRIDE_DIR) map_view = MapView.new() + map_view.art = art add_child(map_view) marker = TileMarker.new() + marker.art = art add_child(marker) figures = Figures.new() + figures.art = art add_child(figures) avatar = Avatar.new() + avatar.art = art add_child(avatar) var camera := Camera2D.new() camera.zoom = CAMERA_ZOOM @@ -200,8 +208,9 @@ func _show_world() -> void: for node: Node2D in [avatar, marker, figures]: node.show() map_view.session = session - map_view.queue_redraw() + map_view.reload() figures.session = session + avatar.player = session.local_player() figures.clear() avatar.is_walkable = session.is_walkable avatar.position = GridMotion.tile_center(session.spawn_point()) @@ -230,7 +239,7 @@ func _leave(message: String) -> void: link = null session = null map_view.session = null - map_view.queue_redraw() + map_view.reload() avatar.is_walkable = Callable() figures.clear() _show_menu(message) @@ -295,7 +304,7 @@ func _on_notice(notice: Dictionary) -> void: if notice["resync"]: resyncs += 1 _keep_diverged_recording() - map_view.queue_redraw() + map_view.reload() hud.show_stone(session.item_count(GwSession.ITEM_STONE)) hud.flash("RESYNCED") else: @@ -390,8 +399,16 @@ func _apply(events: Array[Dictionary]) -> void: var local := session.local_player() for event in events: match event["type"]: - "tile_hit", "tile_changed": - map_view.queue_redraw() + "tile_hit": + var tile := Vector2i(event["x"], event["y"]) + map_view.refresh_tile(tile) + # The own hits showed their burst when they were made. + if event["player"] != local: + map_view.hit(tile) + "tile_changed": + var tile := Vector2i(event["x"], event["y"]) + map_view.refresh_tile(tile) + map_view.hit(tile) "item_gained": if event["player"] == local: hud.show_stone(session.item_count(GwSession.ITEM_STONE)) @@ -401,6 +418,9 @@ func _apply(events: Array[Dictionary]) -> void: "player_joined": if event["player"] != local: hud.flash("PLAYER_JOINED", [session.player_name(event["player"])]) + else: + avatar.player = local + avatar.queue_redraw() _update_status() "player_left": figures.forget(event["player"]) @@ -422,7 +442,9 @@ func _try_dig() -> void: dig_at(marker.tile) -## Queues a pickaxe hit on `tile`, at most once per cooldown. The caller must +## Queues a pickaxe hit on `tile`, at most once per cooldown, and shows its +## burst of chips at once: the hit itself lands with a later tick, which for a +## guest comes back from the host only after the round trip. The caller must ## check the reach: figures move outside the lockstep, so the core does not ## know where the figure is. The core checks the cooldown, the tile, its ## terrain, the tool tier and the free space in the inventory. @@ -431,6 +453,8 @@ func dig_at(tile: Vector2i) -> void: return session.dig(tile.x, tile.y) _next_dig_tick = session.tick() + GwSession.DIG_COOLDOWN_TICKS + if session.terrain_at(tile.x, tile.y) != GwSession.TERRAIN_GROUND: + map_view.hit(tile) ## Writes the running world as the save. Returns false if nothing was diff --git a/game/scripts/map_view.gd b/game/scripts/map_view.gd index f9913d0..4f47146 100644 --- a/game/scripts/map_view.gd +++ b/game/scripts/map_view.gd @@ -1,48 +1,182 @@ class_name MapView extends Node2D -## Draws the terrain of the shared world. The colours are placeholders until -## a style is chosen from the samples of plan M1d. +## Draws the terrain of the shared world in the style of `art`: every tile to +## fit its neighbours, the cracks of the hits it took, and a short burst of +## chips where a pickaxe lands. The map is drawn in square blocks, so that a +## hit draws only the blocks around its tile again. -const CRACK_COLOR := Color(0.1, 0.08, 0.06, 0.8) +## The terrain that rock masses and the edge of the map are made of. Tiles +## beside it are drawn to fit it. +const SOLID: Array[int] = [GwSession.TERRAIN_SOFT_ROCK, GwSession.TERRAIN_HARD_ROCK, GwSession.TERRAIN_BEDROCK] +## Tiles along each side of a block. +const BLOCK := 16 +## Seconds a burst of chips lasts. +const HIT_SECONDS := 0.35 var session: GwSession +var art: Art +var _width := 0 +var _height := 0 +var _terrain := PackedByteArray() +var _damage := PackedByteArray() +var _solid := PackedByteArray() +var _masks := PackedInt32Array() +var _variants := PackedInt32Array() +var _blocks: Array[MapBlock] = [] +var _bursts: Array[Dictionary] = [] +var _burst_layer := Node2D.new() -func _draw() -> void: +## A square part of the map that draws itself. +class MapBlock extends Node2D: + var view: MapView + var tiles: Rect2i + + func _init(owner_view: MapView, block_tiles: Rect2i) -> void: + view = owner_view + tiles = block_tiles + + func _draw() -> void: + view.draw_block(self, tiles) + + +func _ready() -> void: + _burst_layer.draw.connect(_draw_bursts) + add_child(_burst_layer) + set_process(false) + + +## The graphic of a terrain code. +static func id_of(terrain: int) -> StringName: + match terrain: + GwSession.TERRAIN_SOFT_ROCK: + return Art.SOFT_ROCK + GwSession.TERRAIN_HARD_ROCK: + return Art.HARD_ROCK + GwSession.TERRAIN_BEDROCK: + return Art.BEDROCK + return Art.GROUND + + +## Takes the whole map from `session` anew and draws it, or shows nothing if +## no world runs. Call it when a world starts or was replaced. +func reload() -> void: + for block in _blocks: + remove_child(block) + block.free() + _blocks.clear() + _bursts.clear() + _burst_layer.queue_redraw() if session == null or not session.is_running(): + _width = 0 + _height = 0 + return + _width = session.map_width() + _height = session.map_height() + _terrain = session.terrain_codes() + _damage = session.damage_codes() + _solid = TileShape.flags(_terrain, SOLID) + _masks.resize(_width * _height) + _variants.resize(_width * _height) + for y in _height: + for x in _width: + var tile := Vector2i(x, y) + _masks[y * _width + x] = TileShape.mask(_solid, _width, _height, tile, true) + _variants[y * _width + x] = TileShape.variant(tile) + for top in range(0, _height, BLOCK): + for left in range(0, _width, BLOCK): + var block := MapBlock.new(self, Rect2i(left, top, mini(BLOCK, _width - left), mini(BLOCK, _height - top))) + _blocks.append(block) + add_child(block) + move_child(_burst_layer, -1) + + +## Draws everything again, for example in another style. +func redraw() -> void: + for block in _blocks: + block.queue_redraw() + + +## Takes `tile` anew from the session after it changed, and draws it and the +## tiles around it again, which may have to fit it differently now. +func refresh_tile(tile: Vector2i) -> void: + if _blocks.is_empty() or not Rect2i(0, 0, _width, _height).has_point(tile): + return + _terrain = session.terrain_codes() + _damage = session.damage_codes() + var index := tile.y * _width + tile.x + _solid[index] = 1 if SOLID.has(_terrain[index]) else 0 + var touched: Array[MapBlock] = [] + for y in range(tile.y - 1, tile.y + 2): + for x in range(tile.x - 1, tile.x + 2): + var around := Vector2i(x, y) + if not Rect2i(0, 0, _width, _height).has_point(around): + continue + _masks[y * _width + x] = TileShape.mask(_solid, _width, _height, around, true) + var block := _block_of(around) + if not touched.has(block): + touched.append(block) + for block in touched: + block.queue_redraw() + + +## The block that draws `tile`. +func _block_of(tile: Vector2i) -> MapBlock: + var across := ceili(_width / float(BLOCK)) + @warning_ignore("integer_division") + return _blocks[(tile.y / BLOCK) * across + tile.x / BLOCK] + + +## Shows a burst of chips on `tile`, where a pickaxe just landed. +func hit(tile: Vector2i) -> void: + _bursts.append({"tile": tile, "age": 0.0}) + set_process(true) + + +## The number of blocks the map is drawn in. Tests read it. +func block_count() -> int: + return _blocks.size() + + +## The number of bursts still shown. Tests read it. +func burst_count() -> int: + return _bursts.size() + + +## The neighbour mask a tile is drawn with. Tests read it. +func mask_at(tile: Vector2i) -> int: + return _masks[tile.y * _width + tile.x] + + +## Draws the tiles `tiles` onto `block`, while the block draws. +func draw_block(block: MapBlock, tiles: Rect2i) -> void: + if art == null: return - var width := session.map_width() - var height := session.map_height() - var terrain := session.terrain_codes() - var damage := session.damage_codes() var size := GridMotion.TILE_SIZE - for y in height: - for x in width: - var index := y * width + x + for y in range(tiles.position.y, tiles.end.y): + for x in range(tiles.position.x, tiles.end.x): + var index := y * _width + x var rect := Rect2(x * size, y * size, size, size) - draw_rect(rect, color_of(terrain[index])) - if damage[index] > 0: - _draw_cracks(rect, damage[index]) + var variant := _variants[index] + art.draw(block, id_of(_terrain[index]), rect, {"solid": _masks[index], "variant": variant}) + if _damage[index] > 0: + art.draw(block, Art.CRACKS, rect, {"hits": _damage[index], "variant": variant}) -## The placeholder colour of a terrain code. -static func color_of(terrain: int) -> Color: - if terrain == GwSession.TERRAIN_GROUND: - return Color(0.42, 0.36, 0.28) - if terrain == GwSession.TERRAIN_SOFT_ROCK: - return Color(0.56, 0.55, 0.52) - if terrain == GwSession.TERRAIN_HARD_ROCK: - return Color(0.33, 0.33, 0.37) - if terrain == GwSession.TERRAIN_BEDROCK: - return Color(0.12, 0.12, 0.14) - return Color.MAGENTA +func _process(delta: float) -> void: + for burst in _bursts: + burst["age"] += delta / HIT_SECONDS + _bursts = _bursts.filter(func(burst: Dictionary) -> bool: return burst["age"] < 1.0) + if _bursts.is_empty(): + set_process(false) + _burst_layer.queue_redraw() -## One crack line for every hit the tile has taken. -func _draw_cracks(rect: Rect2, hits: int) -> void: - var inset := rect.grow(-3.0) - for line in hits: - var share := (line + 1.0) / (hits + 1.0) - var from := Vector2(inset.position.x, inset.position.y + inset.size.y * share) - var to := Vector2(inset.end.x, inset.position.y + inset.size.y * (1.0 - share)) - draw_line(from, to, CRACK_COLOR, 1.0) +func _draw_bursts() -> void: + if art == null: + return + var size := GridMotion.TILE_SIZE + for burst in _bursts: + var tile: Vector2i = burst["tile"] + var rect := Rect2(Vector2(tile) * size, Vector2(size, size)) + art.draw(_burst_layer, Art.HIT, rect, {"age": burst["age"], "variant": TileShape.variant(tile)}) diff --git a/game/scripts/tile_marker.gd b/game/scripts/tile_marker.gd index 123fc9b..ee2cb81 100644 --- a/game/scripts/tile_marker.gd +++ b/game/scripts/tile_marker.gd @@ -1,16 +1,14 @@ class_name TileMarker extends Node2D -## Outlines the tile under the mouse: green when the figure can reach it, -## red when it is too far away. - -const REACHABLE_COLOR := Color(0.4, 1.0, 0.4) -const UNREACHABLE_COLOR := Color(1.0, 0.4, 0.4) +## Marks the tile under the mouse, drawn by `art`: one way when the figure +## can reach it, another when it is too far away. var tile := Vector2i(-1, -1) var in_reach := false +var art: Art -## Moves the outline to `new_tile`. Redraws only when something changed. +## Moves the mark to `new_tile`. Redraws only when something changed. func show_tile(new_tile: Vector2i, reachable: bool) -> void: if new_tile == tile and reachable == in_reach: return @@ -20,6 +18,7 @@ func show_tile(new_tile: Vector2i, reachable: bool) -> void: func _draw() -> void: + if art == null: + return var size := GridMotion.TILE_SIZE - var color := REACHABLE_COLOR if in_reach else UNREACHABLE_COLOR - draw_rect(Rect2(Vector2(tile) * size, Vector2(size, size)), color, false, 1.0) + art.draw(self, Art.MARKER, Rect2(Vector2(tile) * size, Vector2(size, size)), {"reachable": in_reach}) diff --git a/game/tests/test_avatar.gd b/game/tests/test_avatar.gd index 87dbfa8..5716486 100644 --- a/game/tests/test_avatar.gd +++ b/game/tests/test_avatar.gd @@ -31,3 +31,26 @@ func test_without_a_map_the_figure_stays() -> void: avatar.walk(Vector2.LEFT, 0.1) check_eq(avatar.position, Vector2.ZERO, "no walkability callable") avatar.free() + + +func test_a_walking_figure_steps_and_faces_the_way_it_goes() -> void: + var avatar := Avatar.new() + avatar.is_walkable = _open_field + avatar.position = GridMotion.tile_center(Vector2i(50, 50)) + check(not avatar.moving, "standing at first") + avatar.walk(Vector2.LEFT, 0.1) + check(avatar.moving, "walking") + check_eq(avatar.facing, -1.0, "facing left") + check(avatar.step > 0.0, "the step turns") + avatar.walk(Vector2.UP, 0.1) + check_eq(avatar.facing, -1.0, "walking up keeps the way it faces") + avatar.walk(Vector2.ZERO, 0.1) + check(not avatar.moving, "standing again") + avatar.free() + + +func test_a_figure_stands_upright_in_its_tile() -> void: + var rect := Avatar.figure_rect(Vector2(100, 100)) + check_eq(rect.size, Vector2(GridMotion.TILE_SIZE, GridMotion.TILE_SIZE * 1.5), "a tile wide, one and a half tall") + check_eq(rect.get_center().x, 100.0, "centred on the figure") + check_eq(rect.end.y, 100.0 + Avatar.RADIUS + 1.0, "its feet just below its body") diff --git a/game/tests/test_figures.gd b/game/tests/test_figures.gd index 31cf682..522e7fa 100644 --- a/game/tests/test_figures.gd +++ b/game/tests/test_figures.gd @@ -32,3 +32,13 @@ func test_figures_can_be_forgotten() -> void: figures.clear() check_eq(figures.shown_at(3), Vector2.INF, "all are gone") figures.free() + + +func test_a_figure_faces_the_way_it_glides() -> void: + var figures := Figures.new() + figures.place(1, Vector2(100.0, 0.0)) + check_eq(figures.facing_of(1), 1.0, "facing right at first") + figures.place(1, Vector2.ZERO) + figures._process(0.02) + check_eq(figures.facing_of(1), -1.0, "facing left while it glides to the left") + figures.free() diff --git a/game/tests/test_main.gd b/game/tests/test_main.gd index 58653f6..5cf7f4e 100644 --- a/game/tests/test_main.gd +++ b/game/tests/test_main.gd @@ -381,3 +381,14 @@ func test_a_mistyped_address_is_reported() -> void: check(main.session == null, "nothing starts") check_eq(main.menu.status(), tr("ADDRESS_INVALID") % "host:port", "the menu says why") _clean_up([directory]) + + +func test_an_own_hit_shows_its_burst_at_once() -> void: + var directory := TestFiles.temporary_dir("burst") + var main := _start_main(directory) + main.play(false) + var rock: Vector2i = main.session.spawn_point() + Vector2i(7, -2) + check_eq(main.session.terrain_at(rock.x, rock.y), GwSession.TERRAIN_SOFT_ROCK, "rock beside the clearing") + main.dig_at(rock) + check_eq(main.map_view.burst_count(), 1, "the burst shows before the hit lands in a tick") + _clean_up([directory]) diff --git a/game/tests/test_map_view.gd b/game/tests/test_map_view.gd new file mode 100644 index 0000000..da61fc7 --- /dev/null +++ b/game/tests/test_map_view.gd @@ -0,0 +1,94 @@ +extends TestCase +## The map view over a real world of the core. + +var key := PackedByteArray([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]) + + +func _session() -> GwSession: + var session := GwSession.new() + session.start_new(7) + session.join(key, "Ada") + session.step() + return session + + +func _view(session: GwSession) -> MapView: + var view := MapView.new() + view.art = Art.new() + view.session = session + tree.root.add_child(view) + view.reload() + return view + + +func _free(view: MapView) -> void: + tree.root.remove_child(view) + view.free() + + +## The first tile of the soft rock beside the spawn clearing. +func _rock(session: GwSession) -> Vector2i: + return session.spawn_point() + Vector2i(7, -2) + + +func test_the_map_is_drawn_in_blocks_that_cover_it() -> void: + var session := _session() + var view := _view(session) + var across := ceili(session.map_width() / float(MapView.BLOCK)) + var down := ceili(session.map_height() / float(MapView.BLOCK)) + check_eq(view.block_count(), across * down, "blocks over the whole map") + for style in Art.style_keys(): + view.art.select(style) + view.redraw() + await tree.process_frame + _free(view) + + +func test_each_tile_is_drawn_to_fit_the_rock_around_it() -> void: + var session := _session() + var view := _view(session) + var solid := TileShape.flags(session.terrain_codes(), MapView.SOLID) + for tile: Vector2i in [Vector2i(0, 0), session.spawn_point(), _rock(session)]: + var expected := TileShape.mask(solid, session.map_width(), session.map_height(), tile, true) + check_eq(view.mask_at(tile), expected, "the neighbours of %s" % tile) + _free(view) + + +func test_a_broken_rock_no_longer_shapes_its_neighbours() -> void: + var session := _session() + var view := _view(session) + var rock := _rock(session) + var west := rock + Vector2i(-1, 0) + check(view.mask_at(west) & TileShape.E != 0, "the rock lies east of its neighbour") + for hit in 3: + session.dig(rock.x, rock.y) + session.step() + for wait in GwSession.DIG_COOLDOWN_TICKS: + session.step() + check_eq(session.terrain_at(rock.x, rock.y), GwSession.TERRAIN_GROUND, "three hits break it") + view.refresh_tile(rock) + check(view.mask_at(west) & TileShape.E == 0, "now there is ground east of the neighbour") + await tree.process_frame + _free(view) + + +func test_a_burst_of_chips_shows_for_a_moment() -> void: + var view := _view(_session()) + view.hit(Vector2i(3, 3)) + check_eq(view.burst_count(), 1, "a burst") + view._process(MapView.HIT_SECONDS * 0.5) + check_eq(view.burst_count(), 1, "still in the air") + await tree.process_frame + view._process(MapView.HIT_SECONDS * 0.6) + check_eq(view.burst_count(), 0, "gone after its time") + _free(view) + + +func test_without_a_world_nothing_is_drawn() -> void: + var view := _view(_session()) + view.session = null + view.reload() + check_eq(view.block_count(), 0, "no blocks") + view.refresh_tile(Vector2i(1, 1)) + await tree.process_frame + _free(view) diff --git a/game/tests/test_map_view.gd.uid b/game/tests/test_map_view.gd.uid new file mode 100644 index 0000000..03c1808 --- /dev/null +++ b/game/tests/test_map_view.gd.uid @@ -0,0 +1 @@ +uid://bp0vlwfwtlh7h -- 2.52.0 From 55c09b0f1fe32e96a5dcc2b037cd197f15c7f170 Mon Sep 17 00:00:00 2001 From: Steven Wroblewski Date: Fri, 2 Oct 2026 12:04:56 +0200 Subject: [PATCH 05/17] Eigener Schlag zeigt beim Brechen keine zweiten Splitter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Der Kern meldet einen Schlag, der Fels bricht, nur als geänderte Kachel ohne Spieler. Die Hauptszene merkt sich deshalb die Kacheln ihrer eigenen Schläge, bis der Kern sie meldet, und zeigt dann keinen zweiten Splitterwurf. Als Gast kam der zweite eine Rundreise später und sah wieder verzögert aus. Splitter gibt es nur noch auf Fels, nicht mehr außerhalb der Karte. Co-Authored-By: Claude Haiku 4.5 --- game/scripts/main.gd | 17 ++++++++++++++--- game/tests/test_main.gd | 14 ++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/game/scripts/main.gd b/game/scripts/main.gd index 2f64871..39e6aed 100644 --- a/game/scripts/main.gd +++ b/game/scripts/main.gd @@ -62,6 +62,9 @@ var _joining := "" ## The host's accepted guests when _process last looked. A new one ends a ## pause. var _known_guests := 0 +## Tiles whose burst of chips the own figure showed when it hit them, until +## the core reports that hit. Their burst is not shown a second time. +var _own_hits: Dictionary[Vector2i, bool] = {} func _ready() -> void: @@ -217,6 +220,7 @@ func _show_world() -> void: hud.show_stone(session.item_count(GwSession.ITEM_STONE)) _update_status() _next_dig_tick = 0 + _own_hits.clear() ## Says in the status line how the game is shared, and with how many. @@ -242,6 +246,7 @@ func _leave(message: String) -> void: map_view.reload() avatar.is_walkable = Callable() figures.clear() + _own_hits.clear() _show_menu(message) @@ -403,12 +408,17 @@ func _apply(events: Array[Dictionary]) -> void: var tile := Vector2i(event["x"], event["y"]) map_view.refresh_tile(tile) # The own hits showed their burst when they were made. - if event["player"] != local: + if event["player"] == local: + _own_hits.erase(tile) + else: map_view.hit(tile) "tile_changed": var tile := Vector2i(event["x"], event["y"]) map_view.refresh_tile(tile) - map_view.hit(tile) + # The core does not say whose hit broke the tile; an own hit + # showed its burst already. + if not _own_hits.erase(tile): + map_view.hit(tile) "item_gained": if event["player"] == local: hud.show_stone(session.item_count(GwSession.ITEM_STONE)) @@ -453,8 +463,9 @@ func dig_at(tile: Vector2i) -> void: return session.dig(tile.x, tile.y) _next_dig_tick = session.tick() + GwSession.DIG_COOLDOWN_TICKS - if session.terrain_at(tile.x, tile.y) != GwSession.TERRAIN_GROUND: + if MapView.SOLID.has(session.terrain_at(tile.x, tile.y)): map_view.hit(tile) + _own_hits[tile] = true ## Writes the running world as the save. Returns false if nothing was diff --git a/game/tests/test_main.gd b/game/tests/test_main.gd index 5cf7f4e..45c51c9 100644 --- a/game/tests/test_main.gd +++ b/game/tests/test_main.gd @@ -392,3 +392,17 @@ func test_an_own_hit_shows_its_burst_at_once() -> void: main.dig_at(rock) check_eq(main.map_view.burst_count(), 1, "the burst shows before the hit lands in a tick") _clean_up([directory]) + + +func test_an_own_hit_that_breaks_the_rock_shows_its_burst_only_once() -> void: + var directory := TestFiles.temporary_dir("break") + var main := _start_main(directory) + main.play(false) + var rock: Vector2i = main.session.spawn_point() + Vector2i(7, -2) + for blow in 3: + main.dig_at(rock) + for tick in GwSession.DIG_COOLDOWN_TICKS + 1: + main._run_tick() + check_eq(main.session.terrain_at(rock.x, rock.y), GwSession.TERRAIN_GROUND, "three own hits break the rock") + check_eq(main.map_view.burst_count(), 3, "one burst per own hit, none more when the rock breaks") + _clean_up([directory]) -- 2.52.0 From 69b642cc60a611279c1903425fbc09bd8abfd29d Mon Sep 17 00:00:00 2001 From: Steven Wroblewski Date: Fri, 2 Oct 2026 12:19:01 +0200 Subject: [PATCH 06/17] =?UTF-8?q?Eigene=20Schl=C3=A4ge=20pro=20Kachel=20z?= =?UTF-8?q?=C3=A4hlen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Als Gast kann die Meldung eines Schlags erst nach dem nächsten Schlag auf dieselbe Kachel ankommen. Ein Merker pro Kachel ging dabei verloren, und der brechende Schlag zeigte seine Splitter doch zweimal. Die Hauptszene zählt ihre noch nicht gemeldeten Schläge deshalb pro Kachel und vergisst sie, wenn die Welt neu abgeglichen wird. Co-Authored-By: Claude Haiku 4.5 --- game/scripts/main.gd | 24 ++++++++++++++++++------ game/tests/test_main.gd | 19 +++++++++++++++++++ 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/game/scripts/main.gd b/game/scripts/main.gd index 39e6aed..67bd0bc 100644 --- a/game/scripts/main.gd +++ b/game/scripts/main.gd @@ -62,9 +62,9 @@ var _joining := "" ## The host's accepted guests when _process last looked. A new one ends a ## pause. var _known_guests := 0 -## Tiles whose burst of chips the own figure showed when it hit them, until -## the core reports that hit. Their burst is not shown a second time. -var _own_hits: Dictionary[Vector2i, bool] = {} +## How many own hits on each tile showed their burst of chips at once and +## are not reported by the core yet. Their burst is not shown a second time. +var _own_hits: Dictionary[Vector2i, int] = {} func _ready() -> void: @@ -310,6 +310,7 @@ func _on_notice(notice: Dictionary) -> void: resyncs += 1 _keep_diverged_recording() map_view.reload() + _own_hits.clear() hud.show_stone(session.item_count(GwSession.ITEM_STONE)) hud.flash("RESYNCED") else: @@ -409,7 +410,7 @@ func _apply(events: Array[Dictionary]) -> void: map_view.refresh_tile(tile) # The own hits showed their burst when they were made. if event["player"] == local: - _own_hits.erase(tile) + _own_hit_reported(tile) else: map_view.hit(tile) "tile_changed": @@ -417,7 +418,9 @@ func _apply(events: Array[Dictionary]) -> void: map_view.refresh_tile(tile) # The core does not say whose hit broke the tile; an own hit # showed its burst already. - if not _own_hits.erase(tile): + if _own_hits.has(tile): + _own_hits.erase(tile) + else: map_view.hit(tile) "item_gained": if event["player"] == local: @@ -465,7 +468,16 @@ func dig_at(tile: Vector2i) -> void: _next_dig_tick = session.tick() + GwSession.DIG_COOLDOWN_TICKS if MapView.SOLID.has(session.terrain_at(tile.x, tile.y)): map_view.hit(tile) - _own_hits[tile] = true + _own_hits[tile] = _own_hits.get(tile, 0) + 1 + + +## Counts one own hit on `tile` as reported by the core. +func _own_hit_reported(tile: Vector2i) -> void: + var left: int = _own_hits.get(tile, 0) - 1 + if left > 0: + _own_hits[tile] = left + else: + _own_hits.erase(tile) ## Writes the running world as the save. Returns false if nothing was diff --git a/game/tests/test_main.gd b/game/tests/test_main.gd index 45c51c9..f8222e2 100644 --- a/game/tests/test_main.gd +++ b/game/tests/test_main.gd @@ -406,3 +406,22 @@ func test_an_own_hit_that_breaks_the_rock_shows_its_burst_only_once() -> void: check_eq(main.session.terrain_at(rock.x, rock.y), GwSession.TERRAIN_GROUND, "three own hits break the rock") check_eq(main.map_view.burst_count(), 3, "one burst per own hit, none more when the rock breaks") _clean_up([directory]) + + +func test_own_hits_reported_late_show_no_second_burst() -> void: + var directory := TestFiles.temporary_dir("late") + var main := _start_main(directory) + main.play(false) + var rock: Vector2i = main.session.spawn_point() + Vector2i(7, -2) + main.dig_at(rock) + main._next_dig_tick = 0 + main.dig_at(rock) + # As for a guest: the report of the first hit comes after the second hit, + # and the second hit breaks the rock. + var reports: Array[Dictionary] = [ + {"type": "tile_hit", "player": main.session.local_player(), "x": rock.x, "y": rock.y}, + {"type": "tile_changed", "x": rock.x, "y": rock.y}, + ] + main._apply(reports) + check_eq(main.map_view.burst_count(), 2, "two own hits, two bursts") + _clean_up([directory]) -- 2.52.0 From 9f65823f2d1d16394f3f3465ad323fe41fabba53 Mon Sep 17 00:00:00 2001 From: Steven Wroblewski Date: Fri, 2 Oct 2026 12:27:26 +0200 Subject: [PATCH 07/17] Vitrine der Stilproben MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Das Startmenü führt zu einer Vitrine, die den gewählten Stil an einer kleinen Szene zeigt: Fels, toter und lebendiger Boden, Lebensmoos, eine Ranke, die Steine in eine erwachende Ruine trägt, eine wachsende Faserpflanze, ein hüpfendes Weidetier und zwei Spielfiguren. Knöpfe, die Tasten 1 bis 3 und F2 wechseln den Stil, Esc führt zurück ins Menü. Co-Authored-By: Claude Haiku 4.5 --- game/i18n/texts.csv | 12 ++ game/scripts/input_setup.gd | 5 + game/scripts/main.gd | 31 ++++ game/scripts/menu.gd | 8 +- game/scripts/showcase.gd | 268 ++++++++++++++++++++++++++++++++ game/scripts/showcase.gd.uid | 1 + game/tests/test_input_setup.gd | 2 +- game/tests/test_main.gd | 15 ++ game/tests/test_menu.gd | 10 ++ game/tests/test_showcase.gd | 61 ++++++++ game/tests/test_showcase.gd.uid | 1 + 11 files changed, 411 insertions(+), 3 deletions(-) create mode 100644 game/scripts/showcase.gd create mode 100644 game/scripts/showcase.gd.uid create mode 100644 game/tests/test_showcase.gd create mode 100644 game/tests/test_showcase.gd.uid diff --git a/game/i18n/texts.csv b/game/i18n/texts.csv index d6d2985..edc5fb7 100644 --- a/game/i18n/texts.csv +++ b/game/i18n/texts.csv @@ -38,3 +38,15 @@ SAVE_UNREADABLE,Der Spielstand lässt sich gerade nicht lesen,The save cannot be STYLE_WARM,Warm,Warm STYLE_PASTEL,Pastell,Pastel STYLE_GLOW,Glimmen,Glow +MENU_SHOWCASE,Stilproben ansehen,See the style samples +SHOWCASE_TITLE,Stilprobe: %s,Style sample: %s +SHOWCASE_BACK,Zurück,Back +SHOWCASE_ROCK,Weicher und harter Fels,Soft and hard rock +SHOWCASE_DEAD_SOIL,Toter Boden,Dead soil +SHOWCASE_LIVING_SOIL,Lebendiger Boden,Living soil +SHOWCASE_MOSS,Lebensmoos,Life moss +SHOWCASE_PLANT,Faserpflanze wächst,A fibre plant grows +SHOWCASE_GRAZER,Weidetier,Grazer +SHOWCASE_VINE,Ranke trägt Steine,A vine carries stones +SHOWCASE_RUIN,Ruine erwacht,A ruin wakes +SHOWCASE_FIGURES,Spielfiguren,Player figures diff --git a/game/scripts/input_setup.gd b/game/scripts/input_setup.gd index 5c57097..f1c84ca 100644 --- a/game/scripts/input_setup.gd +++ b/game/scripts/input_setup.gd @@ -13,6 +13,11 @@ static func install() -> void: _add_keys("save_game", [KEY_F5]) _add_keys("load_game", [KEY_F9]) _add_keys("toggle_pause", [KEY_ESCAPE]) + _add_keys("next_style", [KEY_F2]) + _add_keys("back", [KEY_ESCAPE]) + _add_keys("style_1", [KEY_1]) + _add_keys("style_2", [KEY_2]) + _add_keys("style_3", [KEY_3]) _add_mouse_button("dig", MOUSE_BUTTON_LEFT) diff --git a/game/scripts/main.gd b/game/scripts/main.gd index 67bd0bc..d62787d 100644 --- a/game/scripts/main.gd +++ b/game/scripts/main.gd @@ -43,6 +43,7 @@ var clock: TickClock ## The style the playing field is drawn in. var art: Art var menu: Menu +var showcase: Showcase var map_view: MapView var marker: TileMarker var avatar: Avatar @@ -103,8 +104,16 @@ func _ready() -> void: add_child(menu) menu.name_field.text = options.player_name if not options.player_name.is_empty() else store.player_name() menu.address_field.text = options.address if not options.address.is_empty() else store.last_address() + art.changed.connect(_redraw_art) menu.play_chosen.connect(play) menu.join_chosen.connect(join) + menu.showcase_chosen.connect(open_showcase) + showcase = Showcase.new() + showcase.art = art + showcase.closed.connect(close_showcase) + showcase.hide() + showcase.process_mode = Node.PROCESS_MODE_DISABLED + add_child(showcase) _show_menu("") match options.start: "solo": @@ -223,6 +232,28 @@ func _show_world() -> void: _own_hits.clear() +## Shows the style samples instead of the menu. +func open_showcase() -> void: + menu.hide() + showcase.time = 0.0 + showcase.process_mode = Node.PROCESS_MODE_INHERIT + showcase.show() + + +## Goes back from the style samples to the menu. +func close_showcase() -> void: + showcase.hide() + showcase.process_mode = Node.PROCESS_MODE_DISABLED + menu.show() + + +## Draws the playing field again after the style changed. +func _redraw_art() -> void: + map_view.redraw() + for node: Node2D in [marker, figures, avatar]: + node.queue_redraw() + + ## Says in the status line how the game is shared, and with how many. func _update_status() -> void: if link == null: diff --git a/game/scripts/menu.gd b/game/scripts/menu.gd index 2c8e515..8f4e32d 100644 --- a/game/scripts/menu.gd +++ b/game/scripts/menu.gd @@ -1,16 +1,18 @@ class_name Menu extends CanvasLayer ## The start screen: the player's name, then playing alone, opening the world -## for guests, or joining a host. +## for guests, or joining a host. Below, the way to the style samples. signal play_chosen(open: bool) signal join_chosen(address: String) +signal showcase_chosen var name_field := LineEdit.new() var address_field := LineEdit.new() var solo_button := Button.new() var host_button := Button.new() var join_button := Button.new() +var showcase_button := Button.new() var _status := Label.new() @@ -39,15 +41,17 @@ func _ready() -> void: row.add_child(button) address_field.placeholder_text = tr("MENU_ADDRESS") join_button.text = tr("MENU_JOIN") + showcase_button.text = tr("MENU_SHOWCASE") _status.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART _status.custom_minimum_size = Vector2(380, 0) - for control: Control in [title, name_field, row, HSeparator.new(), address_field, join_button, _status]: + for control: Control in [title, name_field, row, HSeparator.new(), address_field, join_button, HSeparator.new(), showcase_button, _status]: box.add_child(control) solo_button.pressed.connect(func() -> void: play_chosen.emit(false)) host_button.pressed.connect(func() -> void: play_chosen.emit(true)) join_button.pressed.connect(func() -> void: join_chosen.emit(address_field.text.strip_edges())) address_field.text_submitted.connect(func(_text: String) -> void: join_button.pressed.emit()) + showcase_button.pressed.connect(func() -> void: showcase_chosen.emit()) ## Shows `text` below the buttons. diff --git a/game/scripts/showcase.gd b/game/scripts/showcase.gd new file mode 100644 index 0000000..b641fd3 --- /dev/null +++ b/game/scripts/showcase.gd @@ -0,0 +1,268 @@ +class_name Showcase +extends CanvasLayer +## The showcase of the style samples: one small scene in the chosen style, +## with the terrain of the test map and things of later milestones. Dead +## soil turns living, moss glows, a vine carries stones into a waking ruin, a +## plant grows and an animal hops. Buttons switch the style. + +## The player wants to go back to the menu. +signal closed + +## The scene, one letter per tile: B edge of the map, S soft rock, H hard +## rock, . dead soil, L living soil. +const GRID: Array[String] = [ + "BBSSSHHH............", + "BSSSHHH.............", + "SS...H..............", + "..........LLLLLL....", + ".......LLLLLLLLLLLL.", + ".....LLLLLLLLLLLLLL.", + ".....LLLLLLLLLLLLLL.", + ".......LLLLLLLLLLL..", + ".........LLLLLL.....", + "....................", +] +## Pixels of the scene per pixel on screen. +const ZOOM := 3.0 +## Seconds from seed to grown plant, and the pause before it starts again. +const GROW_SECONDS := 6.0 +const GROWN_SECONDS := 2.0 +## Seconds between two hops, and how long a hop lasts. +const HOP_EVERY := 1.2 +const HOP_SECONDS := 0.5 +## How far stones travel along the vine per second, as a share of its length. +const STONE_SPEED := 0.1 +const STONES := 3 +## The path of the vine, in tiles: along the living soil into the ruin. +const VINE: Array[Vector2] = [ + Vector2(6.8, 6.9), Vector2(9.0, 7.3), Vector2(11.0, 6.9), + Vector2(13.0, 7.1), Vector2(15.0, 6.4), Vector2(17.3, 5.7), +] +## Where things stand, in tiles: the middle of their foot; for the figures +## the middle of the tile they stand on. +const MOSS: Array[Vector2] = [Vector2(8.0, 4.6), Vector2(9.8, 3.3), Vector2(14.2, 3.4), Vector2(16.8, 6.8)] +const PLANT := Vector2(10.9, 4.4) +const GRAZER_HOME := Vector2(12.8, 5.2) +## The ruin covers three by three tiles from here. +const RUIN := Vector2i(16, 3) +## The figures of two players: the own one and one of another colour. +const FIGURES: Array[Vector2] = [Vector2(3.0, 5.0), Vector2(1.6, 6.6)] + +var art: Art +## Seconds since the showcase opened. +var time := 0.0 +var style_buttons: Array[Button] = [] +var back_button := Button.new() +var _view := ShowcaseView.new() +var _title := Label.new() +var _captions: Array[Label] = [] +var _grid_codes := PackedByteArray() +var _solid := PackedByteArray() +var _living := PackedByteArray() + + +## The node that draws the scene. +class ShowcaseView extends Node2D: + var showcase: Showcase + + func _draw() -> void: + showcase.draw_scene(self) + + +func _ready() -> void: + _read_grid() + var background := ColorRect.new() + background.color = Color(0.08, 0.08, 0.09) + background.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + add_child(background) + _view.showcase = self + _view.scale = Vector2(ZOOM, ZOOM) + add_child(_view) + _title.add_theme_font_size_override("font_size", 28) + _title.position = Vector2(32, 20) + add_child(_title) + var row := HBoxContainer.new() + row.position = Vector2(32, 660) + row.add_theme_constant_override("separation", 12) + add_child(row) + for index in art.styles.size(): + var button := Button.new() + button.text = "%d %s" % [index + 1, tr(art.styles[index].title_key())] + button.pressed.connect(func() -> void: art.select(art.styles[index].key())) + style_buttons.append(button) + row.add_child(button) + back_button.text = tr("SHOWCASE_BACK") + back_button.pressed.connect(func() -> void: closed.emit()) + row.add_child(back_button) + _add_captions() + art.changed.connect(_on_style_changed) + _on_style_changed() + get_viewport().size_changed.connect(_layout) + _layout() + + +func _process(delta: float) -> void: + time += delta + _view.queue_redraw() + + +func _unhandled_input(event: InputEvent) -> void: + if not visible: + return + for index in mini(3, art.styles.size()): + if event.is_action_pressed("style_%d" % (index + 1)): + art.select(art.styles[index].key()) + if event.is_action_pressed("next_style"): + art.select_next() + elif event.is_action_pressed("back"): + closed.emit() + + +## How far the plant has grown at `seconds`, from 0 (a seed) to 1, over and +## over again. +static func growth_at(seconds: float) -> float: + return clampf(fposmod(seconds, GROW_SECONDS + GROWN_SECONDS) / GROW_SECONDS, 0.0, 1.0) + + +## How far the animal is through its hop at `seconds`: from 0 when it takes +## off to 1 when it lands, then 1 while it rests. +static func hop_at(seconds: float) -> float: + return clampf(fposmod(seconds, HOP_EVERY) / HOP_SECONDS, 0.0, 1.0) + + +## Where the animal is at `seconds`, in tiles: it hops around its home. +static func grazer_at(seconds: float) -> Vector2: + var hops := floorf(seconds / HOP_EVERY) + hop_at(seconds) + return GRAZER_HOME + Vector2(cos(hops * 0.9), sin(hops * 0.9) * 0.4) + + +## How far each stone is along the vine at `seconds`, from 0 to 1. +static func stones_at(seconds: float) -> Array[float]: + var shares: Array[float] = [] + for stone in STONES: + shares.append(fposmod(seconds * STONE_SPEED + float(stone) / STONES, 1.0)) + return shares + + +## How awake the ruin is at `seconds`: it wakes and dozes in slow breaths. +static func awake_at(seconds: float) -> float: + return 0.65 + 0.35 * sin(seconds * 1.2) + + +## The texts under the things in the scene, as keys. Tests read them. +func caption_keys() -> PackedStringArray: + var keys := PackedStringArray() + for label in _captions: + keys.append(label.get_meta("key")) + return keys + + +## Draws the whole scene onto `canvas`, while it draws. +func draw_scene(canvas: CanvasItem) -> void: + if art == null: + return + var size := GridMotion.TILE_SIZE + var width := GRID[0].length() + var height := GRID.size() + for y in height: + for x in width: + var tile := Vector2i(x, y) + var rect := Rect2(Vector2(tile) * size, Vector2(size, size)) + var code := _grid_codes[y * width + x] + var state := { + "solid": TileShape.mask(_solid, width, height, tile, false), + "living": TileShape.mask(_living, width, height, tile, false), + "variant": TileShape.variant(tile), + } + art.draw(canvas, _id_of(code), rect, state) + for at in MOSS: + art.draw(canvas, Art.MOSS, _tile_rect(at), {"variant": TileShape.variant(Vector2i(at)), "time": time}) + var path := PackedVector2Array() + for point in VINE: + path.append(point * size) + art.draw(canvas, Art.VINE, _bounds(path), {"points": path, "time": time}) + for share in stones_at(time): + var at := ArtStyle.along(path, share) + art.draw(canvas, Art.STONE, Rect2(at - Vector2(4, 5), Vector2(8, 8))) + art.draw(canvas, Art.FIBER_PLANT, _tile_rect(PLANT, Vector2(1.0, 1.8)), {"growth": growth_at(time), "time": time}) + var grazer := grazer_at(time) + var heading := grazer_at(time + 0.1) - grazer + art.draw(canvas, Art.GRAZER, _tile_rect(grazer), {"hop": hop_at(time), "facing": 1.0 if heading.x >= 0.0 else -1.0}) + var ruin := Rect2(Vector2(RUIN) * size, Vector2(3, 3) * size) + art.draw(canvas, Art.RUIN, ruin, {"awake": awake_at(time), "time": time}) + for player in FIGURES.size(): + var at := (FIGURES[player] + Vector2(0.5, 0.5)) * size + art.draw(canvas, Art.FIGURE, Avatar.figure_rect(at), {"player": player, "facing": 1.0, "step": time * 4.0 + player, "moving": true}) + + +func _read_grid() -> void: + var codes := {".": 0, "L": 1, "S": 2, "H": 3, "B": 4} + _grid_codes.clear() + for line in GRID: + for letter in line: + _grid_codes.append(codes[letter]) + _solid = TileShape.flags(_grid_codes, [2, 3, 4]) + _living = TileShape.flags(_grid_codes, [1]) + + +static func _id_of(code: int) -> StringName: + return [Art.DEAD_SOIL, Art.LIVING_SOIL, Art.SOFT_ROCK, Art.HARD_ROCK, Art.BEDROCK][code] + + +## A square of one tile, or of `size` tiles, whose bottom middle stands at +## `at` (in tiles) plus half a tile. +static func _tile_rect(at: Vector2, size := Vector2.ONE) -> Rect2: + var tile := GridMotion.TILE_SIZE + var foot := (at + Vector2(0.5, 1.0)) * tile + return Rect2(foot - Vector2(size.x * 0.5, size.y) * tile, size * tile) + + +static func _bounds(points: PackedVector2Array) -> Rect2: + var rect := Rect2(points[0], Vector2.ZERO) + for point in points: + rect = rect.expand(point) + return rect + + +func _add_captions() -> void: + var entries := [ + ["SHOWCASE_ROCK", Vector2(4.0, 3.1)], + ["SHOWCASE_DEAD_SOIL", Vector2(2.6, 8.8)], + ["SHOWCASE_LIVING_SOIL", Vector2(12.5, 9.15)], + ["SHOWCASE_MOSS", Vector2(8.4, 5.6)], + ["SHOWCASE_PLANT", Vector2(11.4, 2.3)], + ["SHOWCASE_GRAZER", Vector2(13.3, 6.3)], + ["SHOWCASE_VINE", Vector2(10.6, 7.7)], + ["SHOWCASE_RUIN", Vector2(17.5, 1.9)], + ["SHOWCASE_FIGURES", Vector2(2.6, 7.7)], + ] + for entry: Array in entries: + var label := Label.new() + label.text = tr(entry[0]) + label.set_meta("key", entry[0]) + label.set_meta("at", entry[1]) + label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + label.add_theme_font_size_override("font_size", 13) + label.add_theme_color_override("font_outline_color", Color(0.05, 0.05, 0.06)) + label.add_theme_constant_override("outline_size", 5) + _captions.append(label) + add_child(label) + + +## Puts the scene in the middle of the screen and the texts under their +## things. +func _layout() -> void: + var scene_size := Vector2(GRID[0].length(), GRID.size()) * GridMotion.TILE_SIZE * ZOOM + var screen := get_viewport().get_visible_rect().size + _view.position = ((screen - scene_size) / 2.0).floor() + for label in _captions: + var at: Vector2 = label.get_meta("at") + label.size = Vector2(160, 20) + label.position = _view.position + at * GridMotion.TILE_SIZE * ZOOM - Vector2(80, 0) + + +func _on_style_changed() -> void: + _title.text = tr("SHOWCASE_TITLE") % tr(art.style.title_key()) + for index in style_buttons.size(): + style_buttons[index].disabled = art.styles[index] == art.style + _view.queue_redraw() diff --git a/game/scripts/showcase.gd.uid b/game/scripts/showcase.gd.uid new file mode 100644 index 0000000..4f554d6 --- /dev/null +++ b/game/scripts/showcase.gd.uid @@ -0,0 +1 @@ +uid://bscs3cghniuuq diff --git a/game/tests/test_input_setup.gd b/game/tests/test_input_setup.gd index e6f5aae..19addff 100644 --- a/game/tests/test_input_setup.gd +++ b/game/tests/test_input_setup.gd @@ -3,7 +3,7 @@ extends TestCase func test_every_action_of_the_game_exists() -> void: InputSetup.install() - for action in ["move_up", "move_down", "move_left", "move_right", "dig", "save_game", "load_game", "toggle_pause"]: + for action in ["move_up", "move_down", "move_left", "move_right", "dig", "save_game", "load_game", "toggle_pause", "next_style", "back", "style_1", "style_2", "style_3"]: check(InputMap.has_action(action), "action %s" % action) diff --git a/game/tests/test_main.gd b/game/tests/test_main.gd index f8222e2..9475375 100644 --- a/game/tests/test_main.gd +++ b/game/tests/test_main.gd @@ -425,3 +425,18 @@ func test_own_hits_reported_late_show_no_second_burst() -> void: main._apply(reports) check_eq(main.map_view.burst_count(), 2, "two own hits, two bursts") _clean_up([directory]) + + +func test_the_style_samples_open_from_the_menu_and_lead_back_to_it() -> void: + var directory := TestFiles.temporary_dir("showcase") + var main := _start_main(directory) + main.menu.showcase_button.pressed.emit() + check(main.showcase.visible, "the samples show") + check(not main.menu.visible, "instead of the menu") + main.showcase.style_buttons[2].pressed.emit() + check_eq(main.art.style.key(), GlowStyle.KEY, "a button there chooses the style") + await tree.process_frame + main.showcase.back_button.pressed.emit() + check(main.menu.visible, "the menu is back") + check(not main.showcase.visible, "the samples are gone") + _clean_up([directory]) diff --git a/game/tests/test_menu.gd b/game/tests/test_menu.gd index 7238019..9818984 100644 --- a/game/tests/test_menu.gd +++ b/game/tests/test_menu.gd @@ -35,3 +35,13 @@ func test_the_name_and_the_status_line() -> void: check_eq(menu.status(), "Verbinde …", "the status is shown") check_eq(menu.solo_button.text, tr("MENU_SOLO"), "the labels come from the texts") _free(menu) + + +func test_the_way_to_the_style_samples() -> void: + var menu := _menu() + var chosen := [0] + menu.showcase_chosen.connect(func() -> void: chosen[0] += 1) + check_eq(menu.showcase_button.text, tr("MENU_SHOWCASE"), "the button says where it leads") + menu.showcase_button.pressed.emit() + check_eq(chosen[0], 1, "it asks for the samples") + _free(menu) diff --git a/game/tests/test_showcase.gd b/game/tests/test_showcase.gd new file mode 100644 index 0000000..21b669d --- /dev/null +++ b/game/tests/test_showcase.gd @@ -0,0 +1,61 @@ +extends TestCase +## The showcase of the style samples: how its things move, and that it shows +## every style. + + +func test_the_plant_grows_from_a_seed_and_starts_again() -> void: + check_eq(Showcase.growth_at(0.0), 0.0, "a seed at first") + check(is_equal_approx(Showcase.growth_at(Showcase.GROW_SECONDS / 2.0), 0.5), "half grown halfway") + check_eq(Showcase.growth_at(Showcase.GROW_SECONDS), 1.0, "grown") + check_eq(Showcase.growth_at(Showcase.GROW_SECONDS + Showcase.GROWN_SECONDS - 0.01), 1.0, "it stays grown for a while") + check_eq(Showcase.growth_at(Showcase.GROW_SECONDS + Showcase.GROWN_SECONDS), 0.0, "then a seed again") + + +func test_the_animal_hops_then_rests_near_its_home() -> void: + check_eq(Showcase.hop_at(0.0), 0.0, "taking off") + check(is_equal_approx(Showcase.hop_at(Showcase.HOP_SECONDS / 2.0), 0.5), "in the air") + check_eq(Showcase.hop_at(Showcase.HOP_SECONDS), 1.0, "landed") + check_eq(Showcase.hop_at(Showcase.HOP_EVERY - 0.01), 1.0, "resting until the next hop") + for tenth in 100: + var at := Showcase.grazer_at(tenth * 0.1) + check(at.distance_to(Showcase.GRAZER_HOME) <= 1.01, "near home at %.1f s: %s" % [tenth * 0.1, at]) + + +func test_stones_travel_along_the_vine_one_after_another() -> void: + var start := Showcase.stones_at(0.0) + check_eq(start.size(), Showcase.STONES, "every stone") + check_eq(start[0], 0.0, "the first one starts") + for stone in start: + check(stone >= 0.0 and stone < 1.0, "on the vine: %f" % stone) + var later := Showcase.stones_at(1.0) + check(is_equal_approx(later[0], Showcase.STONE_SPEED), "it moves on") + var round_trip := Showcase.stones_at(1.0 / Showcase.STONE_SPEED) + check(is_equal_approx(round_trip[0], 0.0) or is_equal_approx(round_trip[0], 1.0), "and comes round again") + + +func test_the_ruin_wakes_and_dozes() -> void: + for tenth in 100: + var awake := Showcase.awake_at(tenth * 0.1) + check(awake >= 0.29 and awake <= 1.0, "between dozing and awake: %f" % awake) + + +func test_the_showcase_shows_every_style_and_names_its_things() -> void: + I18n.install() + InputSetup.install() + var art := Art.new() + var showcase := Showcase.new() + showcase.art = art + tree.root.add_child(showcase) + for index in art.styles.size(): + showcase.style_buttons[index].pressed.emit() + check_eq(art.style, art.styles[index], "button %d chooses its style" % (index + 1)) + check(showcase.style_buttons[index].disabled, "the chosen one cannot be pressed again") + await tree.process_frame + for key in showcase.caption_keys(): + check(tr(key) != key, "a text for %s" % key) + var closed := [0] + showcase.closed.connect(func() -> void: closed[0] += 1) + showcase.back_button.pressed.emit() + check_eq(closed[0], 1, "back leads away") + tree.root.remove_child(showcase) + showcase.free() diff --git a/game/tests/test_showcase.gd.uid b/game/tests/test_showcase.gd.uid new file mode 100644 index 0000000..dc49606 --- /dev/null +++ b/game/tests/test_showcase.gd.uid @@ -0,0 +1 @@ +uid://bx2nja50aj8ju -- 2.52.0 From 714d604c58388c9b5d03bca3c5db735672d66d5d Mon Sep 17 00:00:00 2001 From: Steven Wroblewski Date: Fri, 2 Oct 2026 12:41:35 +0200 Subject: [PATCH 08/17] Vitrine weicht einer startenden Welt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ein Gast kann die Stilproben öffnen, während er sich verbindet. Kam die Welt dann an, lag sie unter der Vitrine, und „Zurück" legte das Menü über das laufende Spiel. Jetzt schließt die Hauptszene die Vitrine, sobald eine Welt erscheint. Co-Authored-By: Claude Haiku 4.5 --- game/scripts/main.gd | 12 +++++++++--- game/tests/test_main.gd | 12 ++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/game/scripts/main.gd b/game/scripts/main.gd index d62787d..f8d09bc 100644 --- a/game/scripts/main.gd +++ b/game/scripts/main.gd @@ -111,8 +111,7 @@ func _ready() -> void: showcase = Showcase.new() showcase.art = art showcase.closed.connect(close_showcase) - showcase.hide() - showcase.process_mode = Node.PROCESS_MODE_DISABLED + _hide_showcase() add_child(showcase) _show_menu("") match options.start: @@ -216,6 +215,7 @@ func _show_menu(status: String) -> void: ## Shows the world of the session and puts the figure on the spawn. func _show_world() -> void: menu.hide() + _hide_showcase() hud.show() for node: Node2D in [avatar, marker, figures]: node.show() @@ -242,9 +242,15 @@ func open_showcase() -> void: ## Goes back from the style samples to the menu. func close_showcase() -> void: + _hide_showcase() + menu.show() + + +## Hides the style samples and stops them. A world that starts while they are +## open, such as the one a joining guest receives, takes their place. +func _hide_showcase() -> void: showcase.hide() showcase.process_mode = Node.PROCESS_MODE_DISABLED - menu.show() ## Draws the playing field again after the style changed. diff --git a/game/tests/test_main.gd b/game/tests/test_main.gd index 9475375..642a12a 100644 --- a/game/tests/test_main.gd +++ b/game/tests/test_main.gd @@ -440,3 +440,15 @@ func test_the_style_samples_open_from_the_menu_and_lead_back_to_it() -> void: check(main.menu.visible, "the menu is back") check(not main.showcase.visible, "the samples are gone") _clean_up([directory]) + + +func test_a_world_that_starts_takes_the_place_of_the_style_samples() -> void: + var directory := TestFiles.temporary_dir("samples-world") + var main := _start_main(directory) + main.open_showcase() + check(main.showcase.visible, "the samples are open") + main.play(false) + check(not main.showcase.visible, "the world shows instead of the samples") + check_eq(main.showcase.process_mode, Node.PROCESS_MODE_DISABLED, "and the samples take no input") + check(not main.menu.visible, "the menu stays hidden") + _clean_up([directory]) -- 2.52.0 From ab81f66ef0b2444789bde49e6dc41692e088eb4f Mon Sep 17 00:00:00 2001 From: Steven Wroblewski Date: Fri, 2 Oct 2026 12:49:40 +0200 Subject: [PATCH 09/17] =?UTF-8?q?Stil=20w=C3=A4hlen,=20merken=20und=20foto?= =?UTF-8?q?grafieren?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit F2 wechselt im Spiel den Stil und meldet ihn, --style=NAME wählt ihn beim Start, und das Spiel merkt sich die Wahl in den Einstellungen. Das Bildschirmfoto-Werkzeug nimmt Bilder in einem bestimmten Stil und von der Vitrine auf und zeigt eine zweite Figur mit Namen. Co-Authored-By: Claude Haiku 4.5 --- game/i18n/texts.csv | 3 +- game/scripts/launch_options.gd | 9 ++++ game/scripts/main.gd | 9 +++- game/scripts/save_store.gd | 9 ++++ game/tests/test_launch_options.gd | 8 ++++ game/tests/test_main.gd | 27 +++++++++++ game/tests/test_save_store.gd | 9 ++++ game/tools/screenshot.gd | 79 +++++++++++++++++++++---------- 8 files changed, 126 insertions(+), 27 deletions(-) diff --git a/game/i18n/texts.csv b/game/i18n/texts.csv index edc5fb7..73b3e01 100644 --- a/game/i18n/texts.csv +++ b/game/i18n/texts.csv @@ -7,7 +7,7 @@ LOAD_FAILED,Der Spielstand ließ sich nicht laden,The save could not be loaded INVENTORY_FULL,Die Tasche ist voll,Your bag is full PAUSED,Pause,Paused SIMULATION_STOPPED,Die Simulation wurde angehalten,The simulation stopped -HELP,WASD: laufen · Linke Maustaste: Stein schlagen · F5: speichern · F9: laden · Esc: Pause,WASD: walk · Left mouse button: break rock · F5: save · F9: load · Esc: pause +HELP,WASD: laufen · Linke Maustaste: Stein schlagen · F2: Stil wechseln · F5: speichern · F9: laden · Esc: Pause,WASD: walk · Left mouse button: break rock · F2: change the style · F5: save · F9: load · Esc: pause DEFAULT_NAME,Spieler,Player MENU_NAME,Dein Name,Your name MENU_SOLO,Allein spielen,Play alone @@ -38,6 +38,7 @@ SAVE_UNREADABLE,Der Spielstand lässt sich gerade nicht lesen,The save cannot be STYLE_WARM,Warm,Warm STYLE_PASTEL,Pastell,Pastel STYLE_GLOW,Glimmen,Glow +STYLE_SWITCHED,Stil: %s,Style: %s MENU_SHOWCASE,Stilproben ansehen,See the style samples SHOWCASE_TITLE,Stilprobe: %s,Style sample: %s SHOWCASE_BACK,Zurück,Back diff --git a/game/scripts/launch_options.gd b/game/scripts/launch_options.gd index 8a469c6..241149e 100644 --- a/game/scripts/launch_options.gd +++ b/game/scripts/launch_options.gd @@ -9,6 +9,8 @@ extends RefCounted ## --host opens the world for guests at once ## --port=PORT the port an open world listens on ## --join=ADDRESS joins a host at once; ADDRESS may end in ":PORT" +## --style=NAME draws the playing field in the style NAME: warm, +## pastel or glow ## Longest profile name. const MAX_PROFILE_LENGTH := 32 @@ -21,6 +23,8 @@ var player_name := "" var start := "" var port := NetLink.DEFAULT_PORT var address := "" +## The style to draw in, or empty for the one in the settings. +var style := &"" ## Arguments that were not understood and are ignored. var ignored := PackedStringArray() @@ -50,6 +54,11 @@ static func parse(arguments: PackedStringArray) -> LaunchOptions: "--join": options.start = "join" options.address = value + "--style": + if Art.style_keys().has(StringName(value)): + options.style = StringName(value) + else: + options.ignored.append(argument) _: options.ignored.append(argument) return options diff --git a/game/scripts/main.gd b/game/scripts/main.gd index f8d09bc..76aedf0 100644 --- a/game/scripts/main.gd +++ b/game/scripts/main.gd @@ -83,6 +83,8 @@ func _ready() -> void: clock = TickClock.new(GwSession.TICKS_PER_SECOND) art = Art.new() art.load_overrides(Art.OVERRIDE_DIR) + if not art.select(options.style): + art.select(StringName(store.style())) map_view = MapView.new() map_view.art = art add_child(map_view) @@ -253,11 +255,13 @@ func _hide_showcase() -> void: showcase.process_mode = Node.PROCESS_MODE_DISABLED -## Draws the playing field again after the style changed. +## Draws the playing field again after the style changed, and remembers the +## style for the next start. func _redraw_art() -> void: map_view.redraw() for node: Node2D in [marker, figures, avatar]: node.queue_redraw() + store.set_style(String(art.style.key())) ## Says in the status line how the game is shared, and with how many. @@ -390,6 +394,9 @@ func _unhandled_input(event: InputEvent) -> void: _set_paused(not clock.paused) else: hud.flash("PAUSE_ONLY_ALONE") + elif event.is_action_pressed("next_style"): + art.select_next() + hud.flash("STYLE_SWITCHED", [tr(art.style.title_key())]) ## Goes back to the saved world. Only when playing alone: guests would no diff --git a/game/scripts/save_store.gd b/game/scripts/save_store.gd index 985fc09..9316d92 100644 --- a/game/scripts/save_store.gd +++ b/game/scripts/save_store.gd @@ -146,6 +146,15 @@ func set_last_address(address: String) -> void: _set_setting("network", "address", address) +## The key of the style the player chose last time, or an empty text. +func style() -> String: + return _setting("display", "style") + + +func set_style(key: String) -> void: + _set_setting("display", "style", key) + + func _setting(section: String, key: String) -> String: var settings := _read_settings(base_dir.path_join(SETTINGS_FILE)) if settings == null: diff --git a/game/tests/test_launch_options.gd b/game/tests/test_launch_options.gd index 2ac91ba..6ecb34f 100644 --- a/game/tests/test_launch_options.gd +++ b/game/tests/test_launch_options.gd @@ -28,3 +28,11 @@ func test_profile_names_cannot_leave_the_profile_folder() -> void: check_eq(options.storage_dir(), "user://", "a bad profile is ignored") check_eq(options.port, NetLink.DEFAULT_PORT, "a bad port is ignored") check_eq(options.ignored, PackedStringArray(["--profile=../oops", "--port=70000", "--fly"]), "everything ignored is listed") + + +func test_a_style_can_be_chosen() -> void: + check_eq(LaunchOptions.parse(PackedStringArray(["--style=glow"])).style, GlowStyle.KEY, "a known style") + check_eq(LaunchOptions.parse(PackedStringArray()).style, &"", "none without the argument") + var unknown := LaunchOptions.parse(PackedStringArray(["--style=watercolour"])) + check_eq(unknown.style, &"", "an unknown style is not taken") + check_eq(unknown.ignored, PackedStringArray(["--style=watercolour"]), "but listed as ignored") diff --git a/game/tests/test_main.gd b/game/tests/test_main.gd index 642a12a..628ed62 100644 --- a/game/tests/test_main.gd +++ b/game/tests/test_main.gd @@ -452,3 +452,30 @@ func test_a_world_that_starts_takes_the_place_of_the_style_samples() -> void: check_eq(main.showcase.process_mode, Node.PROCESS_MODE_DISABLED, "and the samples take no input") check(not main.menu.visible, "the menu stays hidden") _clean_up([directory]) + + +func test_f2_switches_the_style_and_the_choice_is_kept() -> void: + var directory := TestFiles.temporary_dir("style") + var main := _start_main(directory) + main.play(false) + var first: StringName = main.art.style.key() + _press(main, "next_style") + var second: StringName = main.art.style.key() + check(second != first, "another style") + check_eq(main.hud.message(), tr("STYLE_SWITCHED") % tr(main.art.style.title_key()), "the player is told") + _stop_main(main) + + var later := _start_main(directory) + check_eq(later.art.style.key(), second, "the next start draws in it again") + _clean_up([directory]) + + +func test_the_command_line_chooses_the_style() -> void: + var directory := TestFiles.temporary_dir("style-option") + var main: Node = load("res://main.tscn").instantiate() + main.storage_dir = directory + main.command_line = PackedStringArray(["--style=pastel"]) + tree.root.add_child(main) + _started.append(main) + check_eq(main.art.style.key(), PastelStyle.KEY, "the style of the command line") + _clean_up([directory]) diff --git a/game/tests/test_save_store.gd b/game/tests/test_save_store.gd index 7f88387..41ac43a 100644 --- a/game/tests/test_save_store.gd +++ b/game/tests/test_save_store.gd @@ -189,3 +189,12 @@ func test_settings_that_cannot_be_read_right_now_are_not_overwritten() -> void: check_eq(later.player_name(), "Ada", "the name on disk is the old one") check_eq(later.last_address(), "192.168.1.20", "the address is kept") TestFiles.remove_dir(directory) + + +func test_the_chosen_style_is_kept() -> void: + var directory := TestFiles.temporary_dir("style") + var store := SaveStore.new(directory) + check_eq(store.style(), "", "no style at first") + store.set_style("pastel") + check_eq(SaveStore.new(directory).style(), "pastel", "kept for the next start") + TestFiles.remove_dir(directory) diff --git a/game/tools/screenshot.gd b/game/tools/screenshot.gd index 85e012f..03256cc 100644 --- a/game/tools/screenshot.gd +++ b/game/tools/screenshot.gd @@ -1,14 +1,22 @@ extends SceneTree ## Starts the game in a temporary storage folder, walks the figure toward the -## rock beside the clearing, hits it once and saves a screenshot. Lets a -## change to the look be checked without playing by hand. The player's real -## key and save stay untouched. +## rock beside the clearing, hits it once and saves a screenshot. A second +## figure with a name stands beside it. Lets a change to the look be checked +## without playing by hand. The player's real key and save stay untouched. ## ## From the repository root (opens a window for a moment): ## godot --path game -s res://tools/screenshot.gd -- +## [--style=NAME] [--showcase] +## --style=NAME draws in that style; --showcase shows the style samples +## instead of the game. const STORAGE := "user://screenshot-run" -const FRAMES_AFTER_HIT := 30 +## Frames between the hit and the picture: the burst of chips is still in +## the air, and the crack is there. +const FRAMES_AFTER_HIT := 8 +## Seconds into the showcase at which the picture is taken: the plant is +## nearly grown and the animal in the air. +const SHOWCASE_SECONDS := 5.2 func _initialize() -> void: @@ -16,37 +24,58 @@ func _initialize() -> void: func _run() -> void: - var arguments := OS.get_cmdline_user_args() - var output := arguments[0] if arguments.size() > 0 else "user://screenshot.png" + var output := "user://screenshot.png" + var style := "" + var showcase := false + for argument in OS.get_cmdline_user_args(): + if argument.begins_with("--style="): + style = argument + elif argument == "--showcase": + showcase = true + else: + output = argument DirAccess.make_dir_recursive_absolute(STORAGE) var main: Node = load("res://main.tscn").instantiate() main.storage_dir = STORAGE - main.command_line = PackedStringArray() + main.command_line = PackedStringArray([style] if not style.is_empty() else []) root.add_child(main) - main.play(false) - await process_frame - - # Four tiles to the right, toward the rock outcrop beside the clearing. - for frame in 48: - main.avatar.walk(Vector2.RIGHT, 1.0 / 60.0) + if showcase: + main.open_showcase() await process_frame - var session: GwSession = main.session - var rock := session.spawn_point() + Vector2i(7, 0) - main.dig_at(rock) - for frame in FRAMES_AFTER_HIT: + main.showcase.time = SHOWCASE_SECONDS await process_frame + await process_frame + else: + await _play(main) var image := root.get_texture().get_image() var error := image.save_png(output) print("screenshot %s: %s" % ["saved" if error == OK else "failed", ProjectSettings.globalize_path(output)]) root.remove_child(main) main.free() - for file in DirAccess.get_files_at(STORAGE): - DirAccess.remove_absolute(STORAGE.path_join(file)) - for directory in DirAccess.get_directories_at(STORAGE): - var path := STORAGE.path_join(directory) - for file in DirAccess.get_files_at(path): - DirAccess.remove_absolute(path.path_join(file)) - DirAccess.remove_absolute(path) - DirAccess.remove_absolute(STORAGE) + _remove(STORAGE) quit(0 if error == OK else 1) + + +## Plays alone: walks four tiles to the right, toward the rock outcrop +## beside the clearing, and hits it once. +func _play(main: Node) -> void: + main.play(false) + await process_frame + for frame in 48: + main.avatar.walk(Vector2.RIGHT, 1.0 / 60.0) + await process_frame + var session: GwSession = main.session + main.figures.place(session.local_player(), main.avatar.position + Vector2(-40, 8)) + var rock := session.spawn_point() + Vector2i(7, 0) + main.dig_at(rock) + for frame in FRAMES_AFTER_HIT: + await process_frame + + +func _remove(path: String) -> void: + for file in DirAccess.get_files_at(path): + DirAccess.remove_absolute(path.path_join(file)) + for directory in DirAccess.get_directories_at(path): + _remove(path.path_join(directory)) + DirAccess.remove_absolute(path) -- 2.52.0 From e14ce3465bec53deb5cae538fa0f7c6845be914f Mon Sep 17 00:00:00 2001 From: Steven Wroblewski Date: Fri, 2 Oct 2026 13:02:06 +0200 Subject: [PATCH 10/17] =?UTF-8?q?Pixelgrafik=20f=C3=BCr=20Boden,=20Fels=20?= =?UTF-8?q?und=20Figuren?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PixelStyle zeichnet jede Grafik Pixel für Pixel in ein kleines Bild, 16 mal 16 Pixel pro Kachel, und hebt es auf. Fels ist außen rund, innen geschlossen und zeigt zum offenen Boden hin seine Vorderkante; Figuren stehen aufrecht und haben vier Laufbilder. Warm bekommt als erster Stil seine Palette und acht Spielerfarben. Die Texturfilterung steht auf Nearest, damit die Pixel scharf bleiben. Co-Authored-By: Claude Haiku 4.5 --- game/project.godot | 2 + game/scripts/art/pixel_style.gd | 471 ++++++++++++++++++++++++++++ game/scripts/art/pixel_style.gd.uid | 1 + game/scripts/art/warm_style.gd | 41 ++- game/tests/test_pixel_style.gd | 81 +++++ game/tests/test_pixel_style.gd.uid | 1 + game/tests/test_warm_style.gd | 10 + game/tests/test_warm_style.gd.uid | 1 + 8 files changed, 605 insertions(+), 3 deletions(-) create mode 100644 game/scripts/art/pixel_style.gd create mode 100644 game/scripts/art/pixel_style.gd.uid create mode 100644 game/tests/test_pixel_style.gd create mode 100644 game/tests/test_pixel_style.gd.uid create mode 100644 game/tests/test_warm_style.gd create mode 100644 game/tests/test_warm_style.gd.uid diff --git a/game/project.godot b/game/project.godot index 4a8f097..d451d83 100644 --- a/game/project.godot +++ b/game/project.godot @@ -19,3 +19,5 @@ window/size/viewport_height=720 renderer/rendering_method="gl_compatibility" renderer/rendering_method.mobile="gl_compatibility" +textures/canvas_textures/default_texture_filter=0 +2d/snap/snap_2d_transforms_to_pixel=true diff --git a/game/scripts/art/pixel_style.gd b/game/scripts/art/pixel_style.gd new file mode 100644 index 0000000..19757ce --- /dev/null +++ b/game/scripts/art/pixel_style.gd @@ -0,0 +1,471 @@ +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 +## 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 +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] + var bob := -1 if frame == 2 or frame == 4 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 diff --git a/game/scripts/art/pixel_style.gd.uid b/game/scripts/art/pixel_style.gd.uid new file mode 100644 index 0000000..d1c20f4 --- /dev/null +++ b/game/scripts/art/pixel_style.gd.uid @@ -0,0 +1 @@ +uid://jejn3txd0eor diff --git a/game/scripts/art/warm_style.gd b/game/scripts/art/warm_style.gd index 64a3203..5f097eb 100644 --- a/game/scripts/art/warm_style.gd +++ b/game/scripts/art/warm_style.gd @@ -1,11 +1,46 @@ 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. +extends PixelStyle +## Warm, full colours and dark outlines around figures and things, close to +## the classic farming games. const KEY := &"warm" +func _init() -> void: + palette = { + "outline": Color("3a2820"), "dust": Color("e9dcc6"), + "ground": Color("b98a5e"), "ground_dark": Color("a07650"), "ground_light": Color("cba077"), + "pebble": Color("8e8072"), "pebble_light": Color("b8ab9a"), "pebble_dark": Color("6b5f54"), + "dead_crack": Color("8a6646"), + "soft_top": Color("bcb0a0"), "soft_top_dark": Color("a69a8a"), "soft_top_light": Color("d6ccbd"), + "soft_face": Color("8f8170"), "soft_face_dark": Color("776a5b"), "soft_lip": Color("e4dccf"), + "hard_top": Color("7c88a2"), "hard_top_dark": Color("68738c"), "hard_top_light": Color("9aa6c0"), + "hard_face": Color("596380"), "hard_face_dark": Color("454d66"), "hard_lip": Color("b6c2da"), + "hard_glint": Color("dff4ff"), + "bed_top": Color("51453e"), "bed_top_dark": Color("433832"), "bed_top_light": Color("62554c"), + "bed_face": Color("382e29"), "bed_face_dark": Color("2b231f"), "bed_lip": Color("6e6157"), + "grass": Color("64a83f"), "grass_dark": Color("4e8f33"), "grass_light": Color("8acb52"), + "grass_edge": Color("3f7a2c"), + "flower_a": Color("f7e25c"), "flower_b": Color("fbf6ea"), "flower_c": Color("ef7fa6"), + "moss": Color("4e9a3a"), "moss_dark": Color("3b7b2e"), "moss_light": Color("7ec552"), "spore": Color("ffe57e"), + "vine": Color("4f9439"), "vine_dark": Color("38702a"), "leaf": Color("68b545"), "leaf_dark": Color("4c8d35"), + "stem": Color("5c9c3c"), "bud": Color("acd97a"), "fibre": Color("f8f3e6"), "fibre_shade": Color("dcd3c0"), + "seed": Color("7b5433"), + "stone": Color("a29a8f"), "stone_light": Color("c8c1b6"), "stone_dark": Color("7c756d"), + "wool": Color("f5f0e6"), "wool_shade": Color("dbd2c2"), "wool_light": Color("ffffff"), + "face": Color("4b3a31"), "hoof": Color("35281f"), + "ruin": Color("aaa395"), "ruin_dark": Color("8a8376"), "ruin_light": Color("c6bfb2"), + "ruin_moss": Color("5f9f43"), "door": Color("2b231f"), "glow": Color("ffd560"), "glow_light": Color("fff4c6"), + "skin": Color("f4cba5"), "skin_shade": Color("dba67f"), "hair": Color("6e4128"), "hair_light": Color("8f5a37"), + "eye": Color("2a1b15"), "pants": Color("4b5b8d"), "pants_dark": Color("3a4672"), "boots": Color("5b3a26"), + "marker_near": Color("fff6c2"), "marker_far": Color("ff705c"), + } + players = [ + Color("d9573f"), Color("3f7fd1"), Color("e9b23c"), Color("4ea84a"), + Color("9a62c9"), Color("39a9a0"), Color("e271a1"), Color("e8873b"), + ] + + func key() -> StringName: return KEY diff --git a/game/tests/test_pixel_style.gd b/game/tests/test_pixel_style.gd new file mode 100644 index 0000000..4a6871d --- /dev/null +++ b/game/tests/test_pixel_style.gd @@ -0,0 +1,81 @@ +extends TestCase +## The pixel art of the ground, the rock and the figures, which every pixel +## style draws. + + +## How many pixels of `image` are not transparent. +static func _opaque(image: Image) -> int: + var count := 0 + for y in image.get_height(): + for x in image.get_width(): + if image.get_pixel(x, y).a > 0.0: + count += 1 + return count + + +func test_the_field_is_drawn_in_sharp_pixels() -> void: + check_eq(ProjectSettings.get_setting("rendering/textures/canvas_textures/default_texture_filter"), 0, "each pixel a square, not blurred") + check(ProjectSettings.get_setting("rendering/2d/snap/snap_2d_transforms_to_pixel"), "on whole pixels of the screen") + + +func test_ground_fills_its_tile_and_lies_in_the_shadow_of_rock() -> void: + var style := WarmStyle.new() + var open := style._earth(3, 0).image + check_eq(open.get_size(), Vector2i(PixelStyle.TILE, PixelStyle.TILE), "a tile") + check_eq(_opaque(open), PixelStyle.TILE * PixelStyle.TILE, "no gaps") + var shaded := style._earth(3, TileShape.N).image + check(shaded.get_pixel(5, 0).get_luminance() < open.get_pixel(5, 0).get_luminance(), "darker below rock") + check_eq(shaded.get_pixel(5, 8), open.get_pixel(5, 8), "but only along the rock") + + +func test_a_lone_rock_is_round_and_shows_its_face() -> void: + var style := WarmStyle.new() + var lone := style._rock_image("soft", 0, 0).image + check_eq(lone.get_pixel(0, 0).a, 0.0, "a round corner, the ground shows there") + check_eq(lone.get_pixel(PixelStyle.TILE - 1, PixelStyle.TILE - 1).a, 0.0, "round at the bottom too") + check_eq(lone.get_pixel(8, 0), style.palette["outline"], "outlined at the top") + check_eq(lone.get_pixel(8, PixelStyle.TILE - 1), style.palette["outline"], "and where it meets the ground") + check_eq(lone.get_pixel(5, PixelStyle.TILE - 3), style.palette["soft_face"], "its face toward the open ground") + + +func test_rock_inside_a_mass_has_neither_edge_nor_face() -> void: + var style := WarmStyle.new() + var inside := style._rock_image("soft", 255, 0).image + check_eq(_opaque(inside), PixelStyle.TILE * PixelStyle.TILE, "no gaps") + for y in PixelStyle.TILE: + for x in PixelStyle.TILE: + var colour := inside.get_pixel(x, y) + check(colour != style.palette["outline"] and colour != style.palette["soft_face"], "inside at %d, %d" % [x, y]) + + +func test_only_the_corners_that_matter_tell_tiles_apart() -> void: + check_eq(PixelStyle._shape(TileShape.NE), 0, "a corner alone does not count") + check_eq(PixelStyle._shape(TileShape.N | TileShape.E | TileShape.NE), TileShape.N | TileShape.E | TileShape.NE, "between two sides it does") + check_eq(PixelStyle._shape(255), 255, "all of them inside a mass") + + +func test_each_picture_is_made_only_once() -> void: + var style := WarmStyle.new() + var made := [0] + var make := func() -> PixelStyle.Pixels: + made[0] += 1 + return PixelStyle.Pixels.new(1, 1) + var first := style._image("test/once", make) + var again := style._image("test/once", make) + check_eq(again, first, "the same picture") + check_eq(made[0], 1, "made once") + + +func test_a_figure_walks_in_four_steps_and_faces_both_ways() -> void: + var style := WarmStyle.new() + var frames: Array[PackedByteArray] = [] + for frame in 5: + var image := style._figure_image(0, frame, false).image + check_eq(image.get_size(), PixelStyle.FIGURE_SIZE, "a tile wide, one and a half tall") + check(not frames.has(image.get_data()), "frame %d differs from the others" % frame) + frames.append(image.get_data()) + var left := style._figure_image(0, 1, true).image + left.flip_x() + check_eq(left.get_data(), frames[1], "facing left mirrors facing right") + var other := style._figure_image(1, 0, false).image + check(other.get_data() != frames[0], "another player has another colour") diff --git a/game/tests/test_pixel_style.gd.uid b/game/tests/test_pixel_style.gd.uid new file mode 100644 index 0000000..2f7ba85 --- /dev/null +++ b/game/tests/test_pixel_style.gd.uid @@ -0,0 +1 @@ +uid://dr01kkrpn22sy diff --git a/game/tests/test_warm_style.gd b/game/tests/test_warm_style.gd new file mode 100644 index 0000000..3d11d47 --- /dev/null +++ b/game/tests/test_warm_style.gd @@ -0,0 +1,10 @@ +extends TestCase + + +func test_four_players_have_four_colours() -> void: + var style := WarmStyle.new() + var colours := {} + for player in 4: + colours[style.player_color(player)] = true + check_eq(colours.size(), 4, "four players, four colours") + check_eq(style.player_color(-1), style.player_color(0), "a player not in the world yet looks like the first") diff --git a/game/tests/test_warm_style.gd.uid b/game/tests/test_warm_style.gd.uid new file mode 100644 index 0000000..0ca6123 --- /dev/null +++ b/game/tests/test_warm_style.gd.uid @@ -0,0 +1 @@ +uid://d00aodstjeia5 -- 2.52.0 From d189a358cd4637ed16654fcb3a23e8b611307339 Mon Sep 17 00:00:00 2001 From: Steven Wroblewski Date: Fri, 2 Oct 2026 13:21:08 +0200 Subject: [PATCH 11/17] =?UTF-8?q?Kopf=20der=20Figur=20beh=C3=A4lt=20beim?= =?UTF-8?q?=20Laufen=20seinen=20Umriss?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In zwei der vier Laufbilder hob sich der Körper um ein Pixel, und der dunkle Umriss oben am Kopf fiel aus dem Bild. Laufende Figuren flackerten dadurch am Kopf. Jetzt sinkt der Körper, solange die Beine gespreizt sind, und über dem Kopf bleibt immer eine Zeile frei. Co-Authored-By: Claude Haiku 4.5 --- game/scripts/art/pixel_style.gd | 4 +++- game/tests/test_pixel_style.gd | 9 +++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/game/scripts/art/pixel_style.gd b/game/scripts/art/pixel_style.gd index 19757ce..4c011d1 100644 --- a/game/scripts/art/pixel_style.gd +++ b/game/scripts/art/pixel_style.gd @@ -440,7 +440,9 @@ func _shadow_image(width: int) -> Pixels: func _figure_image(colour: int, frame: int, left: bool) -> Pixels: var pixels := Pixels.new(FIGURE_SIZE.x, FIGURE_SIZE.y) var shirt := players[colour] - var bob := -1 if frame == 2 or frame == 4 else 0 + # 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: diff --git a/game/tests/test_pixel_style.gd b/game/tests/test_pixel_style.gd index 4a6871d..7e5396c 100644 --- a/game/tests/test_pixel_style.gd +++ b/game/tests/test_pixel_style.gd @@ -79,3 +79,12 @@ func test_a_figure_walks_in_four_steps_and_faces_both_ways() -> void: check_eq(left.get_data(), frames[1], "facing left mirrors facing right") var other := style._figure_image(1, 0, false).image check(other.get_data() != frames[0], "another player has another colour") + + +func test_the_head_keeps_its_outline_in_every_step() -> void: + var style := WarmStyle.new() + for frame in 5: + var image := style._figure_image(0, frame, false).image + for x in image.get_width(): + var top := image.get_pixel(x, 0) + check(top.a == 0.0 or top == style.palette["outline"], "frame %d, column %d: %s" % [frame, x, top]) -- 2.52.0 From deba758e875b0d8d4e6c35a6209694622aef2d8c Mon Sep 17 00:00:00 2001 From: Steven Wroblewski Date: Fri, 2 Oct 2026 13:29:32 +0200 Subject: [PATCH 12/17] =?UTF-8?q?Pixelgrafik=20f=C3=BCr=20die=20Dinge=20de?= =?UTF-8?q?r=20Vitrine?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Toter Boden hat Risse, lebendiger Boden Gras, Blumen und einen unregelmäßigen Rand. Moos funkelt, die Ranke trägt Blätter, die Faserpflanze wächst in sechs Stufen bis zu den Fasern und wiegt sich, das Weidetier zieht im Sprung die Beine an, und die Ruine leuchtet, wenn sie erwacht. Co-Authored-By: Claude Haiku 4.5 --- game/scripts/art/pixel_style.gd | 327 ++++++++++++++++++++++++++++ game/tests/test_pixel_things.gd | 59 +++++ game/tests/test_pixel_things.gd.uid | 1 + 3 files changed, 387 insertions(+) create mode 100644 game/tests/test_pixel_things.gd create mode 100644 game/tests/test_pixel_things.gd.uid diff --git a/game/scripts/art/pixel_style.gd b/game/scripts/art/pixel_style.gd index 4c011d1..a89d643 100644 --- a/game/scripts/art/pixel_style.gd +++ b/game/scripts/art/pixel_style.gd @@ -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 diff --git a/game/tests/test_pixel_things.gd b/game/tests/test_pixel_things.gd new file mode 100644 index 0000000..4574667 --- /dev/null +++ b/game/tests/test_pixel_things.gd @@ -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") diff --git a/game/tests/test_pixel_things.gd.uid b/game/tests/test_pixel_things.gd.uid new file mode 100644 index 0000000..88eb125 --- /dev/null +++ b/game/tests/test_pixel_things.gd.uid @@ -0,0 +1 @@ +uid://cbm4wa0gxi4p1 -- 2.52.0 From b4af22b8b900685ff2270cbad1b5becaeca7f332 Mon Sep 17 00:00:00 2001 From: Steven Wroblewski Date: Fri, 2 Oct 2026 13:47:39 +0200 Subject: [PATCH 13/17] Abwandlungen Pastell und Glimmen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pastell zeichnet mit weichen, hellen Farben und Umrissen im dunkleren Ton der Fläche. Glimmen zeigt eine gedämpfte, kühle tote Welt, in der alles Lebendige kräftig ist und leuchtet. Beide haben dieselben Farbnamen wie Warm und je acht Spielerfarben. Co-Authored-By: Claude Haiku 4.5 --- game/scripts/art/glow_style.gd | 42 ++++++++++++++++++++++++++--- game/scripts/art/pastel_style.gd | 42 ++++++++++++++++++++++++++--- game/tests/test_glow_style.gd | 18 +++++++++++++ game/tests/test_glow_style.gd.uid | 1 + game/tests/test_pastel_style.gd | 18 +++++++++++++ game/tests/test_pastel_style.gd.uid | 1 + 6 files changed, 116 insertions(+), 6 deletions(-) create mode 100644 game/tests/test_glow_style.gd create mode 100644 game/tests/test_glow_style.gd.uid create mode 100644 game/tests/test_pastel_style.gd create mode 100644 game/tests/test_pastel_style.gd.uid diff --git a/game/scripts/art/glow_style.gd b/game/scripts/art/glow_style.gd index 7f856c6..86b6363 100644 --- a/game/scripts/art/glow_style.gd +++ b/game/scripts/art/glow_style.gd @@ -1,11 +1,47 @@ 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. +extends PixelStyle +## A dim, cool world in grey and slate, in which everything living shines in +## strong colours and glows: the dead and the living apart at a glance. const KEY := &"glow" +func _init() -> void: + glow = 1.0 + palette = { + "outline": Color("15171f"), "dust": Color("b9b5af"), + "ground": Color("6f6a68"), "ground_dark": Color("5f5a59"), "ground_light": Color("7f7a77"), + "pebble": Color("57534f"), "pebble_light": Color("8a8580"), "pebble_dark": Color("45413e"), + "dead_crack": Color("4c4846"), + "soft_top": Color("8d8a87"), "soft_top_dark": Color("7c7976"), "soft_top_light": Color("a3a09c"), + "soft_face": Color("636160"), "soft_face_dark": Color("514f4f"), "soft_lip": Color("b5b2ae"), + "hard_top": Color("4f5a75"), "hard_top_dark": Color("434d66"), "hard_top_light": Color("62708f"), + "hard_face": Color("39425a"), "hard_face_dark": Color("2e354a"), "hard_lip": Color("7a89aa"), + "hard_glint": Color("8ff0ff"), + "bed_top": Color("2e2b30"), "bed_top_dark": Color("262328"), "bed_top_light": Color("3a363c"), + "bed_face": Color("1e1b20"), "bed_face_dark": Color("171519"), "bed_lip": Color("48434a"), + "grass": Color("3fbf5a"), "grass_dark": Color("2e9c48"), "grass_light": Color("6ee887"), + "grass_edge": Color("1f7a3a"), + "flower_a": Color("fff27a"), "flower_b": Color("e8fff0"), "flower_c": Color("ff8ad0"), + "moss": Color("35c46a"), "moss_dark": Color("25a052"), "moss_light": Color("7cf5a5"), "spore": Color("fff59a"), + "vine": Color("3dbb63"), "vine_dark": Color("2a8f4a"), "leaf": Color("52d47a"), "leaf_dark": Color("34a85c"), + "stem": Color("45c068"), "bud": Color("a0f5b8"), "fibre": Color("f0fff6"), "fibre_shade": Color("c8e8d6"), + "seed": Color("8a6a4a"), + "stone": Color("8e8b88"), "stone_light": Color("aeaaa6"), "stone_dark": Color("6c6967"), + "wool": Color("eef2f0"), "wool_shade": Color("cfd6d3"), "wool_light": Color("ffffff"), + "face": Color("3a3438"), "hoof": Color("2a2629"), + "ruin": Color("7d7a80"), "ruin_dark": Color("68656b"), "ruin_light": Color("94919a"), + "ruin_moss": Color("3fbf6a"), "door": Color("121014"), "glow": Color("ffe066"), "glow_light": Color("fffbd0"), + "skin": Color("f0c4a0"), "skin_shade": Color("d39e7a"), "hair": Color("3d2a24"), "hair_light": Color("5a3e34"), + "eye": Color("1a1416"), "pants": Color("3b4466"), "pants_dark": Color("2e3552"), "boots": Color("3a2a22"), + "marker_near": Color("c8fff0"), "marker_far": Color("ff6a5a"), + } + players = [ + Color("ff5f4f"), Color("4f9fff"), Color("ffc847"), Color("4fd06a"), + Color("b77aff"), Color("3fd8c8"), Color("ff7ac0"), Color("ff9a4a"), + ] + + func key() -> StringName: return KEY diff --git a/game/scripts/art/pastel_style.gd b/game/scripts/art/pastel_style.gd index bf90c9b..9eb52b4 100644 --- a/game/scripts/art/pastel_style.gd +++ b/game/scripts/art/pastel_style.gd @@ -1,11 +1,47 @@ 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. +extends PixelStyle +## Soft, light colours, and outlines in a darker shade of what they +## surround instead of one dark line. const KEY := &"pastel" +func _init() -> void: + tinted_outlines = true + palette = { + "outline": Color("7a6670"), "dust": Color("fff6ea"), + "ground": Color("e2c7a6"), "ground_dark": Color("d2b591"), "ground_light": Color("eedac0"), + "pebble": Color("c2b6aa"), "pebble_light": Color("ddd4ca"), "pebble_dark": Color("a3978c"), + "dead_crack": Color("c3a585"), + "soft_top": Color("dcd5cf"), "soft_top_dark": Color("cbc3bc"), "soft_top_light": Color("eeeae6"), + "soft_face": Color("bab1ab"), "soft_face_dark": Color("a89f99"), "soft_lip": Color("f6f3f1"), + "hard_top": Color("abb6da"), "hard_top_dark": Color("99a4c9"), "hard_top_light": Color("c5ceea"), + "hard_face": Color("8e98bf"), "hard_face_dark": Color("7c86ad"), "hard_lip": Color("dde4f6"), + "hard_glint": Color("ffffff"), + "bed_top": Color("8d8292"), "bed_top_dark": Color("7e7383"), "bed_top_light": Color("9e93a3"), + "bed_face": Color("6f6574"), "bed_face_dark": Color("615866"), "bed_lip": Color("b0a6b5"), + "grass": Color("9dd28b"), "grass_dark": Color("86c177"), "grass_light": Color("bae4a7"), + "grass_edge": Color("74ae67"), + "flower_a": Color("fff2a8"), "flower_b": Color("ffffff"), "flower_c": Color("ffb6cf"), + "moss": Color("88c57b"), "moss_dark": Color("72ae67"), "moss_light": Color("abde9a"), "spore": Color("fff0a8"), + "vine": Color("80c077"), "vine_dark": Color("67a460"), "leaf": Color("94d086"), "leaf_dark": Color("76b16a"), + "stem": Color("88c37b"), "bud": Color("cae9b2"), "fibre": Color("fffdf6"), "fibre_shade": Color("ede7db"), + "seed": Color("b38d6c"), + "stone": Color("c6bfb8"), "stone_light": Color("ded9d4"), "stone_dark": Color("aaa39c"), + "wool": Color("fffaf2"), "wool_shade": Color("ede5d9"), "wool_light": Color("ffffff"), + "face": Color("8c7c76"), "hoof": Color("71635d"), + "ruin": Color("cfc8d2"), "ruin_dark": Color("b7b0bd"), "ruin_light": Color("e2dde5"), + "ruin_moss": Color("94c888"), "door": Color("62596d"), "glow": Color("ffe7a2"), "glow_light": Color("fffbe9"), + "skin": Color("fcddc3"), "skin_shade": Color("efc3a4"), "hair": Color("b27c5c"), "hair_light": Color("cb987c"), + "eye": Color("5c4852"), "pants": Color("91a2d2"), "pants_dark": Color("7c8dbf"), "boots": Color("a28068"), + "marker_near": Color("ffffff"), "marker_far": Color("ff9c8c"), + } + players = [ + Color("f08c7c"), Color("7cb0ea"), Color("f2ce6c"), Color("88cb7c"), + Color("ba98e2"), Color("76d1c8"), Color("f2a2c6"), Color("f4af7a"), + ] + + func key() -> StringName: return KEY diff --git a/game/tests/test_glow_style.gd b/game/tests/test_glow_style.gd new file mode 100644 index 0000000..b0fe856 --- /dev/null +++ b/game/tests/test_glow_style.gd @@ -0,0 +1,18 @@ +extends TestCase + + +func test_four_players_have_four_colours() -> void: + var style := GlowStyle.new() + var colours := {} + for player in 4: + colours[style.player_color(player)] = true + check_eq(colours.size(), 4, "four players, four colours") + check_eq(style.player_color(-1), style.player_color(0), "a player not in the world yet looks like the first") + + +func test_it_has_every_colour_the_warm_style_has() -> void: + var names := GlowStyle.new().palette.keys() + var warm := WarmStyle.new().palette.keys() + names.sort() + warm.sort() + check_eq(names, warm, "the same colours by name") diff --git a/game/tests/test_glow_style.gd.uid b/game/tests/test_glow_style.gd.uid new file mode 100644 index 0000000..ac93ace --- /dev/null +++ b/game/tests/test_glow_style.gd.uid @@ -0,0 +1 @@ +uid://bllgwrsoumvpl diff --git a/game/tests/test_pastel_style.gd b/game/tests/test_pastel_style.gd new file mode 100644 index 0000000..92106eb --- /dev/null +++ b/game/tests/test_pastel_style.gd @@ -0,0 +1,18 @@ +extends TestCase + + +func test_four_players_have_four_colours() -> void: + var style := PastelStyle.new() + var colours := {} + for player in 4: + colours[style.player_color(player)] = true + check_eq(colours.size(), 4, "four players, four colours") + check_eq(style.player_color(-1), style.player_color(0), "a player not in the world yet looks like the first") + + +func test_it_has_every_colour_the_warm_style_has() -> void: + var names := PastelStyle.new().palette.keys() + var warm := WarmStyle.new().palette.keys() + names.sort() + warm.sort() + check_eq(names, warm, "the same colours by name") diff --git a/game/tests/test_pastel_style.gd.uid b/game/tests/test_pastel_style.gd.uid new file mode 100644 index 0000000..45fcb8a --- /dev/null +++ b/game/tests/test_pastel_style.gd.uid @@ -0,0 +1 @@ +uid://biq70tu64w7gu -- 2.52.0 From 96db4cd052994aa3dc12caee1ed13691005076c7 Mon Sep 17 00:00:00 2001 From: Steven Wroblewski Date: Fri, 2 Oct 2026 13:59:15 +0200 Subject: [PATCH 14/17] =?UTF-8?q?Regeln,=20Fragebogen=20und=20Bilder=20f?= =?UTF-8?q?=C3=BCr=20den=20Probelauf=20M1d?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CLAUDE.md beschreibt, wie die Grafik entsteht: Kennungen, Pixelgrafik aus Code, scharfe Pixel, Bilder, die Zeichnungen ersetzen, Blöcke und Kachelformen. Das Bildschirmfoto-Werkzeug und --style=NAME stehen bei ihren Befehlen. Eine stille Verbindung gilt nach etwa NetLink.TIMEOUT_MSEC als verloren. Ein Fragebogen mit Bildern der drei Stile begleitet den Probelauf. Co-Authored-By: Claude Haiku 4.5 --- CLAUDE.md | 16 ++++++++-- docs/playtests/m1d.md | 39 +++++++++++++++++++++++++ docs/playtests/m1d/glimmen-karte.png | Bin 0 -> 41236 bytes docs/playtests/m1d/glimmen-vitrine.png | Bin 0 -> 62998 bytes docs/playtests/m1d/pastell-karte.png | Bin 0 -> 42430 bytes docs/playtests/m1d/pastell-vitrine.png | Bin 0 -> 64116 bytes docs/playtests/m1d/warm-karte.png | Bin 0 -> 42146 bytes docs/playtests/m1d/warm-vitrine.png | Bin 0 -> 63382 bytes 8 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 docs/playtests/m1d.md create mode 100644 docs/playtests/m1d/glimmen-karte.png create mode 100644 docs/playtests/m1d/glimmen-vitrine.png create mode 100644 docs/playtests/m1d/pastell-karte.png create mode 100644 docs/playtests/m1d/pastell-vitrine.png create mode 100644 docs/playtests/m1d/warm-karte.png create mode 100644 docs/playtests/m1d/warm-vitrine.png diff --git a/CLAUDE.md b/CLAUDE.md index b2c9b78..d89c2a1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -51,12 +51,21 @@ Drei Wächter prüfen das. `core/clippy.toml` sperrt Pfade wie `std::collections - Alle Texte, die Spieler sehen, stehen in `game/i18n/texts.csv` (Deutsch und Englisch) und werden über `tr()` geholt. - Tests, die Dateien schreiben, nutzen `TestFiles.temporary_dir()` (Ordner unter `user://tests`), nie die echten Dateien des Spielers. - Godot erzeugt beim Import zu jedem Skript eine `.uid`-Datei. Sie gehört ins Archiv. Der Ordner `game/.godot/` gehört nicht hinein. -- Das Netzwerk liegt in `game/scripts/net_link.gd` (ENet, Port 24680) und `game/scripts/session_link.gd`. Nachrichten des Kerns gehen zuverlässig und in Reihenfolge über Kanal 0, Positionen der Figuren unzuverlässig, aber geordnet über Kanal 1; ENet drosselt sie nie. Eine Verbindung, deren Pakete unbeantwortet bleiben, gilt nach höchstens `NetLink.TIMEOUT_MSEC` als verloren. `NetLink.close_peer` schließt eine Verbindung mit `peer_disconnect_later`, damit wartende Nachrichten wie eine Ablehnung noch ankommen. +- Das Netzwerk liegt in `game/scripts/net_link.gd` (ENet, Port 24680) und `game/scripts/session_link.gd`. Nachrichten des Kerns gehen zuverlässig und in Reihenfolge über Kanal 0, Positionen der Figuren unzuverlässig, aber geordnet über Kanal 1; ENet drosselt sie nie. Eine Verbindung, deren Pakete unbeantwortet bleiben, gilt nach etwa `NetLink.TIMEOUT_MSEC` als verloren; ENet prüft die Grenze nur, wenn eine Wiederholung fällig ist. `NetLink.close_peer` schließt eine Verbindung mit `peer_disconnect_later`, damit wartende Nachrichten wie eine Ablehnung noch ankommen. - Nur der Host speichert. Laden mit F9 geht nur ohne geöffnete Welt („Allein spielen“). - Eine Pause beginnt nur ohne Gäste und lässt sich immer beenden. Nimmt der Host einen Gast an, endet sie; eine bloße Verbindung ohne Gruß beendet sie nicht. - Hält der Kern mit einem internen Fehler an, schließt ein Host seine Welt für Gäste, und ein Gast kehrt ins Menü zurück. Eine angehaltene Sitzung hinterlässt keine Aufzeichnung. - Tests mit Netzwerk lauschen nur auf `127.0.0.1` und lassen das System den Port wählen (Port 0), damit sie nie an einem belegten Port scheitern. +## Grafik + +- Alles auf dem Spielfeld zeichnet `Art` (`game/scripts/art/art.gd`), jedes Ding unter einer Kennung wie `terrain/soft_rock`. Andere Skripte zeichnen keine eigenen Formen oder Farben aufs Spielfeld; ausgenommen sind Texte wie die Namen über den Figuren. +- Die Grafik ist Pixelgrafik aus Code (Konzept 7.4): 16 × 16 Pixel pro Kachel, schräg von vorn gesehen. `PixelStyle` (`game/scripts/art/pixel_style.gd`) zeichnet jede Kennung Pixel für Pixel in ein kleines Bild und hebt es auf. Die Stile `WarmStyle`, `PastelStyle` und `GlowStyle` wählen nur Palette, Umrisse und Leuchten; Warm ist der Standard. F2 wechselt im Spiel, „Stilproben ansehen“ im Menü öffnet die Vitrine, und das Spiel merkt sich die Wahl. +- Eine neue Kennung braucht eine Zeichnung in `PixelStyle` und ihre Farben in jeder Palette; sonst zeichnet `ArtStyle` nur einen Platzhalter. `game/tests/test_art.gd` lässt jeden Stil jede Kennung zeichnen. +- Damit die Pixel scharf bleiben, stellt `project.godot` die Texturfilterung auf „Nearest“ und rastet 2D-Transformationen auf ganze Bildschirmpixel ein. Die Kamera vergrößert ganzzahlig (`CAMERA_ZOOM` in `main.gd`), und die Stile setzen ihre Bilder auf ganze Pixel. +- Ein Bild `game/art/.png` ersetzt die Zeichnung seiner Kennung, ohne dass sich Code ändert. So kann ein Artist Grafik austauschen. Ein solches Bild ist ein einzelnes festes Bild; Kachelränder und Bewegung zeichnet bisher nur der Code. +- Die Karte zeichnet in Blöcken von 16 × 16 Kacheln (`MapView`); nach einem Treffer werden nur die Blöcke um die Kachel neu gezeichnet. Jede Kachel kennt ihre Nachbarn (`TileShape`), damit Felsmassen außen rund und innen geschlossen sind und zum offenen Boden hin ihre Vorderkante zeigen. + ## Prüfen Vor jedem Commit müssen alle Befehle fehlerfrei laufen: @@ -76,10 +85,10 @@ Der Test `game/tests/test_two_instances.gd` startet einen zweiten Godot-Prozess Schlägt `core/tests/golden.rs` fehl, haben sich Regeln oder ein Format geändert. Sind Speicherstände, Aufzeichnungen oder Nachrichten betroffen, erhöhst du die Versionskonstante, die die Fehlermeldung nennt (`SNAPSHOT_VERSION`, `RECORDING_VERSION` oder `PROTOCOL_VERSION`). Erst danach passt du den festgeschriebenen Wert bewusst an. -Für Änderungen am Aussehen gibt es ein Werkzeug, das kurz ein Fenster öffnet und ein Bildschirmfoto speichert: +Für Änderungen am Aussehen gibt es ein Werkzeug, das kurz ein Fenster öffnet und ein Bildschirmfoto speichert. `--style=NAME` wählt einen Stil, `--showcase` zeigt die Vitrine statt des Spiels: ```bash -godot --path game -s res://tools/screenshot.gd -- +godot --path game -s res://tools/screenshot.gd -- [--style=NAME] [--showcase] ``` ## Spielen @@ -97,6 +106,7 @@ Argumente hinter `play.bat` gehen an das Spiel: | `--solo`, `--host` | Startet ohne Menü allein beziehungsweise mit offener Welt | | `--port=PORT` | Port einer offenen Welt | | `--join=ADRESSE` | Tritt ohne Menü einem Host bei; die Adresse darf auf `:PORT` enden | +| `--style=NAME` | Zeichnet im Stil `warm`, `pastel` oder `glow` statt im zuletzt gewählten | `play-zweites-fenster.bat` startet ein zweites Fenster mit dem Profil `zweites-fenster`. Die Datei reicht weitere Argumente ebenfalls an das Spiel weiter, etwa `--name=Zweiter`. diff --git a/docs/playtests/m1d.md b/docs/playtests/m1d.md new file mode 100644 index 0000000..6ef49cb --- /dev/null +++ b/docs/playtests/m1d.md @@ -0,0 +1,39 @@ +# Probelauf M1d: Stilproben + +M1d zeichnet das Spielfeld als Pixelgrafik aus Code, im Stil klassischer Farmspiele, aber eigenständig. Es gibt drei Abwandlungen: Warm, Pastell und Glimmen. Nach der ersten Probe war Warm dein Favorit; jetzt siehst du die Stile im Spiel und in Bewegung. Das beantwortet die Prüffrage von Meilenstein 1: „Welche der drei Stilproben soll es sein?“ Die Vitrine zeigt dazu Dinge aus späteren Meilensteinen, die es zum Spielen noch nicht gibt. + +## So siehst du die Proben + +1. Doppelklick auf `play.bat`. +2. Im Menü auf „Stilproben ansehen“. Die Vitrine zeigt + - toten und lebendigen Boden, + - Lebensmoos, + - eine Ranke, die Steine trägt, + - eine wachsende Faserpflanze, + - ein hüpfendes Weidetier, + - eine erwachende Ruine, + - zwei Spielfiguren. + + Mit 1, 2 und 3 oder den Knöpfen unten wechselst du den Stil, mit Esc oder „Zurück“ geht es ins Menü. +3. Im Spiel wechselt F2 den Stil. Lauf herum, schlag Stein und sieh dir Fels, Figur, Zielmarke und Splitter an. Das Spiel merkt sich den zuletzt gewählten Stil. +4. Zu zweit wie in M1c: Die Namen über den Figuren sind jetzt scharf, und dein eigener Schlag zeigt seine Splitter sofort, auch als Gast. + +So sehen die drei Proben aus: + +| Warm | Pastell | Glimmen | +|---|---|---| +| ![Warm, Vitrine](m1d/warm-vitrine.png) | ![Pastell, Vitrine](m1d/pastell-vitrine.png) | ![Glimmen, Vitrine](m1d/glimmen-vitrine.png) | +| ![Warm, Spiel](m1d/warm-karte.png) | ![Pastell, Spiel](m1d/pastell-karte.png) | ![Glimmen, Spiel](m1d/glimmen-karte.png) | + +## Fragen + +1. Bleibt es bei Warm, oder gefällt dir im Spiel ein anderer Stil besser? +2. Was gefällt dir an deinem Favoriten am besten, und was stört dich daran? +3. Gibt es an den anderen beiden etwas, das du übernehmen möchtest, etwa das Leuchten des Lebendigen aus Glimmen? +4. Erkennst du auf einen Blick, was Boden, weicher Fels und harter Fels ist? Und was lebendiger Boden ist? +5. Wirkt das Lebendige lebendig: das Moos, die Ranke, die wachsende Pflanze, das Tier, die erwachende Ruine? Oder wirkt es nüchtern? +6. Ist die Bewegung angenehm: Laufen, Wiegen, Wachsen und Hüpfen? Oder ist es zu viel oder zu wenig? +7. Findest du deine Figur sofort, auch neben anderen? Sind Figuren und Dinge groß genug? +8. Wirkt ein Schlag als Gast jetzt sofort, und sind die Namen über den Figuren scharf? + +Antworte formlos im Chat oder als Kommentar in Gitea. diff --git a/docs/playtests/m1d/glimmen-karte.png b/docs/playtests/m1d/glimmen-karte.png new file mode 100644 index 0000000000000000000000000000000000000000..a54c5fd926394b78d874360a9cea5a5386d8daf5 GIT binary patch literal 41236 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 literal 0 HcmV?d00001 diff --git a/docs/playtests/m1d/pastell-karte.png b/docs/playtests/m1d/pastell-karte.png new file mode 100644 index 0000000000000000000000000000000000000000..ca72b1a1e39e0b8e0ed74388a5be323d907edd95 GIT binary patch literal 42430 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 literal 0 HcmV?d00001 diff --git a/docs/playtests/m1d/warm-karte.png b/docs/playtests/m1d/warm-karte.png new file mode 100644 index 0000000000000000000000000000000000000000..3c1355fb403978cf1d2f9c295766c38f0d52564c GIT binary patch literal 42146 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

e5?R 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

  • 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%0DHfVCKM25jfe8~$2@bgrK^W2Tk*qQKg zR{IbivPr2gZU`FsXYF6l2w{PGyO%eK#K1(MRlt^kWF^NT0)BtQ&t1{ph05gi+1%3$ zz6M+78_-Nfvfx}Jsw5<|n7O5ljd&C+Fqw7yZa*8RJks2p!t-D&=jArZZqtDA@B>W5 z?gVU|wN&=h-#a@7+pMxfxg|-PtF;8XtbCofn>fugXg7}IfzP&SrpdIn`Ap(-v#OCe z8=D7p*OVc2K7LAJeW?VOBAz8XNzcV_>RX3B=bN3zIqZss%IbSIS4Lc4|Kay>Fnl^S zSQq0q-FVx&`(HmH2otw{#NR6`izq5TU^)Gjzr@Yb~@ym<~tdHFDQbKFaL0P^%cXDIa&M9QRr? z^cd$5sqQ4Kzp5<(7V{K4ipw6$3kc(JZ+@jFYX?}mhO+N z4;k<%x;qV3%LGMLypT2OrX0dmd@abixzHvCdNt|n~Jsv@~H#}@>@{jOtPDZPh z$Qsv)erod}Nc2Rvb?h?QxFTKMqTL=0{ri&>Y-hJsd3<9hw(hK{XktQ+u6nLSlM7Yr z&n8|TPu)AK=wwIxoI38kOPwK)VgJ;Dzhsc~?&KnfC2T>U!wvkD20D*sRtGX%2GebE z&m#E^$MI8+$RmnU3m+Brr5U*wWvmh3B2MqYT78&AmK(Ib9sO%N87#%8hX2%8UFrBo zyaeJ9>6MOeCv5(IG|3uQQaNR~g33v4V;u}^$F+UhhVM0AyAgc%qx@6}^-Ig~b2if% zBqbqoI=PsU;irPv9J?7`mq?Hb5w(zDV!9f@C_OJz8XcbNUe6+X znkI7b9pysnIZ#`c8j&_DHYB(Q2sr#CGwaG7WqC-VHNiTntys+{u*^rYbmBn1Hg08q zO)~gUTh|o*0>^b&efSwd`b`kxp&ODnSHR`A8}a}0R3{W5HQ72v%~V4{W241ta#o>C zME+L_S)wXI5mhd%uVV;&!ZqxAbu`E`SiX#=AEUV{Ct6ctFWa*hbw3F0xz@$DCTW=v z_xAO={R{%=dYF{jFDOcS67J1JEjjQK2_~3pND^({x=G?kvXxt0do@!9Z4lKZ0RfeK zmJ$hsJrXZW+G5N>*{=2inY5xVsRs*rRksveO=gDh{h4r_+BjECB*dG;=L|P8qs-Y3 znt?l}G^Qy{)pQ)+%dQ4~pL-N5b0pv$I93mPM#6w^Pk{gF$2l~Na2UtxM`5g*_T75L z508y<@$4JkYKHvdq`tfy73;eL4u$Su-$y-emSPmNd8ohRMSxU8#= z9XL005=;g^0xf)p8}D8`d;u)GD5~>
      k?D5Spe_72jica9;cv{5%i3cDn$K@qTd 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/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 i!v4?r^#5<1@Pa{1h2(MagK8aOp(I1I!^H Date: Fri, 2 Oct 2026 14:12:26 +0200 Subject: [PATCH 15/17] CLAUDE.md: was eine neue Grafik-Kennung braucht MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die Regel nannte nur Zeichnung und Farben. Eine neue Kennung braucht auch eine Konstante in Art, einen Eintrag in Art.IDS und einen Zweig mit Platzhalter in ArtStyle.draw. Eine fehlende Farbe führt zu einem Fehler, nicht zu einem Platzhalter. Co-Authored-By: Claude Haiku 4.5 --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index d89c2a1..1b2cf49 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -61,7 +61,7 @@ Drei Wächter prüfen das. `core/clippy.toml` sperrt Pfade wie `std::collections - Alles auf dem Spielfeld zeichnet `Art` (`game/scripts/art/art.gd`), jedes Ding unter einer Kennung wie `terrain/soft_rock`. Andere Skripte zeichnen keine eigenen Formen oder Farben aufs Spielfeld; ausgenommen sind Texte wie die Namen über den Figuren. - Die Grafik ist Pixelgrafik aus Code (Konzept 7.4): 16 × 16 Pixel pro Kachel, schräg von vorn gesehen. `PixelStyle` (`game/scripts/art/pixel_style.gd`) zeichnet jede Kennung Pixel für Pixel in ein kleines Bild und hebt es auf. Die Stile `WarmStyle`, `PastelStyle` und `GlowStyle` wählen nur Palette, Umrisse und Leuchten; Warm ist der Standard. F2 wechselt im Spiel, „Stilproben ansehen“ im Menü öffnet die Vitrine, und das Spiel merkt sich die Wahl. -- Eine neue Kennung braucht eine Zeichnung in `PixelStyle` und ihre Farben in jeder Palette; sonst zeichnet `ArtStyle` nur einen Platzhalter. `game/tests/test_art.gd` lässt jeden Stil jede Kennung zeichnen. +- Eine neue Kennung bekommt eine Konstante in `Art` und einen Eintrag in `Art.IDS`, einen Zweig mit Platzhalter in `ArtStyle.draw`, eine Zeichnung in `PixelStyle` und ihre Farben in jeder Palette. Fehlt nur die Zeichnung, zeichnet `ArtStyle` den Platzhalter; fehlt eine Farbe, bricht die Zeichnung mit einem Fehler ab. Nur Kennungen in `Art.IDS` lassen sich durch Bilder ersetzen, und `game/tests/test_art.gd` lässt jeden Stil jede dieser Kennungen zeichnen. - Damit die Pixel scharf bleiben, stellt `project.godot` die Texturfilterung auf „Nearest“ und rastet 2D-Transformationen auf ganze Bildschirmpixel ein. Die Kamera vergrößert ganzzahlig (`CAMERA_ZOOM` in `main.gd`), und die Stile setzen ihre Bilder auf ganze Pixel. - Ein Bild `game/art/.png` ersetzt die Zeichnung seiner Kennung, ohne dass sich Code ändert. So kann ein Artist Grafik austauschen. Ein solches Bild ist ein einzelnes festes Bild; Kachelränder und Bewegung zeichnet bisher nur der Code. - Die Karte zeichnet in Blöcken von 16 × 16 Kacheln (`MapView`); nach einem Treffer werden nur die Blöcke um die Kachel neu gezeichnet. Jede Kachel kennt ihre Nachbarn (`TileShape`), damit Felsmassen außen rund und innen geschlossen sind und zum offenen Boden hin ihre Vorderkante zeigen. -- 2.52.0 From 417cf30a9cd8b45b8eb5c9a229c92ac74a9f9632 Mon Sep 17 00:00:00 2001 From: Steven Wroblewski Date: Fri, 2 Oct 2026 14:36:43 +0200 Subject: [PATCH 16/17] =?UTF-8?q?Nachbesserungen=20aus=20der=20Gesamtpr?= =?UTF-8?q?=C3=BCfung=20von=20M1d?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Das Lebensmoos bekommt links und rechts Platz für seinen Umriss, der bei zwei von drei Formen abgeschnitten war; die Bilder der Vitrine sind neu aufgenommen. Die Pausenanzeige kehrt zurück, wenn eine Meldung während der Pause abläuft. Ein Test prüft die Splitter fremder Schläge, und der Test ohne Bilderordner hängt nicht mehr davon ab, was in res://art liegt. Der Fragebogen nennt die Felsproben und sagt, was sofort kommt und was erst mit der Antwort des Hosts. Co-Authored-By: Claude Haiku 4.5 --- docs/playtests/m1d.md | 7 ++++--- docs/playtests/m1d/glimmen-vitrine.png | Bin 62998 -> 62997 bytes docs/playtests/m1d/pastell-vitrine.png | Bin 64116 -> 64144 bytes docs/playtests/m1d/warm-vitrine.png | Bin 63382 -> 63422 bytes game/scripts/art/pixel_style.gd | 11 ++++++----- game/scripts/hud.gd | 7 +++++-- game/tests/test_art.gd | 2 +- game/tests/test_hud.gd | 13 +++++++++++++ game/tests/test_main.gd | 14 ++++++++++++++ game/tests/test_pixel_things.gd | 10 ++++++++++ 10 files changed, 53 insertions(+), 11 deletions(-) diff --git a/docs/playtests/m1d.md b/docs/playtests/m1d.md index 6ef49cb..eaea638 100644 --- a/docs/playtests/m1d.md +++ b/docs/playtests/m1d.md @@ -6,6 +6,7 @@ M1d zeichnet das Spielfeld als Pixelgrafik aus Code, im Stil klassischer Farmspi 1. Doppelklick auf `play.bat`. 2. Im Menü auf „Stilproben ansehen“. Die Vitrine zeigt + - weichen und harten Fels und Grundgestein, - toten und lebendigen Boden, - Lebensmoos, - eine Ranke, die Steine trägt, @@ -16,7 +17,7 @@ M1d zeichnet das Spielfeld als Pixelgrafik aus Code, im Stil klassischer Farmspi Mit 1, 2 und 3 oder den Knöpfen unten wechselst du den Stil, mit Esc oder „Zurück“ geht es ins Menü. 3. Im Spiel wechselt F2 den Stil. Lauf herum, schlag Stein und sieh dir Fels, Figur, Zielmarke und Splitter an. Das Spiel merkt sich den zuletzt gewählten Stil. -4. Zu zweit wie in M1c: Die Namen über den Figuren sind jetzt scharf, und dein eigener Schlag zeigt seine Splitter sofort, auch als Gast. +4. Zu zweit wie in M1c: Die Namen über den Figuren sind jetzt scharf, und dein eigener Schlag zeigt seine Splitter sofort, auch als Gast. Risse und das Brechen des Felses kommen weiter erst mit der Antwort des Hosts. Splitter fliegen bei jedem Schlag auf Fels, auch auf harten Fels und Grundgestein, die deine Steinspitzhacke noch nicht brechen kann. So sehen die drei Proben aus: @@ -30,10 +31,10 @@ So sehen die drei Proben aus: 1. Bleibt es bei Warm, oder gefällt dir im Spiel ein anderer Stil besser? 2. Was gefällt dir an deinem Favoriten am besten, und was stört dich daran? 3. Gibt es an den anderen beiden etwas, das du übernehmen möchtest, etwa das Leuchten des Lebendigen aus Glimmen? -4. Erkennst du auf einen Blick, was Boden, weicher Fels und harter Fels ist? Und was lebendiger Boden ist? +4. Erkennst du auf einen Blick, was Boden, weicher Fels, harter Fels und Grundgestein ist? Und was lebendiger Boden ist? 5. Wirkt das Lebendige lebendig: das Moos, die Ranke, die wachsende Pflanze, das Tier, die erwachende Ruine? Oder wirkt es nüchtern? 6. Ist die Bewegung angenehm: Laufen, Wiegen, Wachsen und Hüpfen? Oder ist es zu viel oder zu wenig? 7. Findest du deine Figur sofort, auch neben anderen? Sind Figuren und Dinge groß genug? -8. Wirkt ein Schlag als Gast jetzt sofort, und sind die Namen über den Figuren scharf? +8. Wirkt ein Schlag als Gast jetzt sofort, auch wenn Risse und Brechen erst mit der Antwort des Hosts kommen? Und sind die Namen über den Figuren scharf? Antworte formlos im Chat oder als Kommentar in Gitea. diff --git a/docs/playtests/m1d/glimmen-vitrine.png b/docs/playtests/m1d/glimmen-vitrine.png index 3049f212d3a563c13683ec3eb472d4750759f752..6293b3b7bf8bbb774275bbf221979c923b033790 100644 GIT binary patch delta 45265 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

      Zh_s83zIC`QTB$p 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 RD59@WSGj#7=lb2p{|DMzUnc+n delta 45301 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 d!Q4sq&`kE{{fXCSp4 delta 36107 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 diff --git a/game/scripts/art/pixel_style.gd b/game/scripts/art/pixel_style.gd index a89d643..9c7811f 100644 --- a/game/scripts/art/pixel_style.gd +++ b/game/scripts/art/pixel_style.gd @@ -573,14 +573,15 @@ 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)) + _put(canvas, _image("moss/%d/%d" % [variant, twinkle], func() -> Pixels: return _moss_image(variant, twinkle)), foot - Vector2(10, 13)) -## A cushion of moss whose spores twinkle one after another. +## A cushion of moss whose spores twinkle one after another, with room on +## both sides for its outline. func _moss_image(variant: int, twinkle: int) -> Pixels: - var pixels := Pixels.new(TILE, 13) + var pixels := Pixels.new(TILE + 2, 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)]: + for bump: Vector4 in [Vector4(6.5 + shift, 9.5, 4, 3.2), Vector4(13, 9.2, 4.3, 3.5), Vector4(9.8 - shift, 6.8, 3.8, 3.4)]: pixels.ellipse(bump.x, bump.y, bump.z, bump.w, _c("moss")) for y in pixels.size.y: for x in pixels.size.x: @@ -591,7 +592,7 @@ func _moss_image(variant: int, twinkle: int) -> Pixels: 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)] + var spores: Array[Vector2i] = [Vector2i(6 + shift, 8), Vector2i(10 - shift, 5), Vector2i(14, 8)] for index in spores.size(): var at := spores[index] var bright := index == twinkle diff --git a/game/scripts/hud.gd b/game/scripts/hud.gd index e46352e..e20479f 100644 --- a/game/scripts/hud.gd +++ b/game/scripts/hud.gd @@ -10,6 +10,7 @@ var _message := Label.new() var _status := Label.new() var _help := Label.new() var _message_left := 0.0 +var _paused := false func _ready() -> void: @@ -34,7 +35,7 @@ func _process(delta: float) -> void: return _message_left -= delta if _message_left <= 0.0: - _message.text = "" + _message.text = tr("PAUSED") if _paused else "" func show_stone(count: int) -> void: @@ -48,8 +49,10 @@ func flash(key: String, values: Array = []) -> void: _message_left = MESSAGE_SECONDS -## Shows or hides the pause notice. It stays until the game continues. +## Shows or hides the pause notice. It stays until the game continues and +## returns when a message shown in between ends. func show_paused(paused: bool) -> void: + _paused = paused _message.text = tr("PAUSED") if paused else "" _message_left = 0.0 diff --git a/game/tests/test_art.gd b/game/tests/test_art.gd index 76ed6c2..f02da1f 100644 --- a/game/tests/test_art.gd +++ b/game/tests/test_art.gd @@ -105,6 +105,6 @@ func test_a_picture_named_after_an_id_replaces_its_drawing() -> void: func test_without_the_folder_of_pictures_everything_is_drawn() -> void: var art := Art.new() - art.load_overrides(Art.OVERRIDE_DIR) + art.load_overrides("res://tests/no-pictures-here") for id in Art.IDS: check(not art.has_override(id), "no picture for %s" % id) diff --git a/game/tests/test_hud.gd b/game/tests/test_hud.gd index 6d1019d..f79ac73 100644 --- a/game/tests/test_hud.gd +++ b/game/tests/test_hud.gd @@ -30,3 +30,16 @@ func test_the_status_line_stays_until_it_is_replaced() -> void: check_eq(hud.message(), "", "the message is gone after a while") check_eq(hud.status_text(), tr("STATUS_HOST") % [24680, 2], "the status line is not") _free(hud) + + +func test_the_pause_notice_returns_after_a_message() -> void: + var hud := _hud() + hud.show_paused(true) + hud.flash("STYLE_SWITCHED", [tr("STYLE_PASTEL")]) + hud._process(Hud.MESSAGE_SECONDS + 0.1) + check_eq(hud.message(), tr("PAUSED"), "still paused, the notice is back") + hud.show_paused(false) + hud.flash("SAVED") + hud._process(Hud.MESSAGE_SECONDS + 0.1) + check_eq(hud.message(), "", "without a pause the message just goes") + _free(hud) diff --git a/game/tests/test_main.gd b/game/tests/test_main.gd index 628ed62..e9984db 100644 --- a/game/tests/test_main.gd +++ b/game/tests/test_main.gd @@ -479,3 +479,17 @@ func test_the_command_line_chooses_the_style() -> void: _started.append(main) check_eq(main.art.style.key(), PastelStyle.KEY, "the style of the command line") _clean_up([directory]) + + +func test_hits_of_other_players_show_their_bursts() -> void: + var directory := TestFiles.temporary_dir("other-hits") + var main := _start_main(directory) + main.play(false) + var rock: Vector2i = main.session.spawn_point() + Vector2i(7, -2) + var reports: Array[Dictionary] = [ + {"type": "tile_hit", "player": main.session.local_player() + 1, "x": rock.x, "y": rock.y}, + {"type": "tile_changed", "x": rock.x, "y": rock.y}, + ] + main._apply(reports) + check_eq(main.map_view.burst_count(), 2, "a hit and a breaking blow of another player") + _clean_up([directory]) diff --git a/game/tests/test_pixel_things.gd b/game/tests/test_pixel_things.gd index 4574667..fcdc5b9 100644 --- a/game/tests/test_pixel_things.gd +++ b/game/tests/test_pixel_things.gd @@ -57,3 +57,13 @@ func test_the_ruin_glows_in_its_doorway() -> void: 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") + + +func test_moss_keeps_its_outline_on_both_sides() -> void: + var style := WarmStyle.new() + for variant in 3: + var image := style._moss_image(variant, 0).image + for y in image.get_height(): + for x: int in [0, image.get_width() - 1]: + var pixel := image.get_pixel(x, y) + check(pixel.a == 0.0 or pixel == style.palette["outline"], "variant %d at %d, %d: %s" % [variant, x, y, pixel]) -- 2.52.0 From 949d5d2489eb55f1e65adafbd6befbe32b8d2fec Mon Sep 17 00:00:00 2001 From: Steven Wroblewski Date: Fri, 2 Oct 2026 14:50:00 +0200 Subject: [PATCH 17/17] Fragebogen M1d: Grundgestein bricht nie MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Der Satz zu den Splittern sagte, harter Fels und Grundgestein ließen sich mit der Steinspitzhacke noch nicht brechen. Harter Fels braucht eine Kupferspitzhacke, Grundgestein bricht nie. Co-Authored-By: Claude Opus 5.5 --- docs/playtests/m1d.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/playtests/m1d.md b/docs/playtests/m1d.md index eaea638..88f1457 100644 --- a/docs/playtests/m1d.md +++ b/docs/playtests/m1d.md @@ -17,7 +17,7 @@ M1d zeichnet das Spielfeld als Pixelgrafik aus Code, im Stil klassischer Farmspi Mit 1, 2 und 3 oder den Knöpfen unten wechselst du den Stil, mit Esc oder „Zurück“ geht es ins Menü. 3. Im Spiel wechselt F2 den Stil. Lauf herum, schlag Stein und sieh dir Fels, Figur, Zielmarke und Splitter an. Das Spiel merkt sich den zuletzt gewählten Stil. -4. Zu zweit wie in M1c: Die Namen über den Figuren sind jetzt scharf, und dein eigener Schlag zeigt seine Splitter sofort, auch als Gast. Risse und das Brechen des Felses kommen weiter erst mit der Antwort des Hosts. Splitter fliegen bei jedem Schlag auf Fels, auch auf harten Fels und Grundgestein, die deine Steinspitzhacke noch nicht brechen kann. +4. Zu zweit wie in M1c: Die Namen über den Figuren sind jetzt scharf, und dein eigener Schlag zeigt seine Splitter sofort, auch als Gast. Risse und das Brechen des Felses kommen weiter erst mit der Antwort des Hosts. Splitter fliegen bei jedem Schlag auf Fels, auch auf harten Fels, den erst eine Kupferspitzhacke bricht, und auf Grundgestein, das nie bricht. So sehen die drei Proben aus: -- 2.52.0