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)