M1d: Stilproben #12

Merged
steven merged 17 commits from m1d-stilproben into main 2026-10-02 13:01:53 +00:00
11 changed files with 411 additions and 3 deletions
Showing only changes of commit 9f65823f2d - Show all commits
+12
View File
@@ -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
1 keys de en
38 STYLE_WARM Warm Warm
39 STYLE_PASTEL Pastell Pastel
40 STYLE_GLOW Glimmen Glow
41 MENU_SHOWCASE Stilproben ansehen See the style samples
42 SHOWCASE_TITLE Stilprobe: %s Style sample: %s
43 SHOWCASE_BACK Zurück Back
44 SHOWCASE_ROCK Weicher und harter Fels Soft and hard rock
45 SHOWCASE_DEAD_SOIL Toter Boden Dead soil
46 SHOWCASE_LIVING_SOIL Lebendiger Boden Living soil
47 SHOWCASE_MOSS Lebensmoos Life moss
48 SHOWCASE_PLANT Faserpflanze wächst A fibre plant grows
49 SHOWCASE_GRAZER Weidetier Grazer
50 SHOWCASE_VINE Ranke trägt Steine A vine carries stones
51 SHOWCASE_RUIN Ruine erwacht A ruin wakes
52 SHOWCASE_FIGURES Spielfiguren Player figures
+5
View File
@@ -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)
+31
View File
@@ -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:
+6 -2
View File
@@ -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.
+268
View File
@@ -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()
+1
View File
@@ -0,0 +1 @@
uid://bscs3cghniuuq
+1 -1
View File
@@ -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)
+15
View File
@@ -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])
+10
View File
@@ -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)
+61
View File
@@ -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()
+1
View File
@@ -0,0 +1 @@
uid://bx2nja50aj8ju