Startmenü, fremde Figuren und Statuszeile
Das Startmenü fragt nach dem Namen und bietet allein spielen, die Welt öffnen oder beitreten. Die Figuren der anderen Spieler gleiten zu ihrer neuesten Position und tragen ihren Namen. Die Anzeige füllt Werte in Meldungen ein und hat eine Statuszeile. Die Texte für Menü und Netzwerk gibt es auf Deutsch und Englisch. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,3 +8,30 @@ 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
|
||||
|
||||
|
@@ -0,0 +1,63 @@
|
||||
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.
|
||||
|
||||
## 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)
|
||||
const NAME_SIZE := 8
|
||||
const NAME_WIDTH := 80.0
|
||||
|
||||
var session: GwSession
|
||||
var _shown: Dictionary[int, Vector2] = {}
|
||||
var _latest: Dictionary[int, Vector2] = {}
|
||||
|
||||
|
||||
## 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:
|
||||
_shown.erase(player)
|
||||
_latest.erase(player)
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func clear() -> void:
|
||||
_shown.clear()
|
||||
_latest.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)
|
||||
|
||||
|
||||
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)
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
var font := ThemeDB.fallback_font
|
||||
for player in _shown:
|
||||
var at := _shown[player]
|
||||
draw_circle(at, Avatar.RADIUS + 1.0, OUTLINE)
|
||||
draw_circle(at, Avatar.RADIUS, COLOR)
|
||||
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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://0krucvyguufx
|
||||
+21
-5
@@ -1,11 +1,13 @@
|
||||
class_name Hud
|
||||
extends CanvasLayer
|
||||
## Text over the game: the stone count, short messages and a help line.
|
||||
## Text over the game: the stone count, short messages, a status line for
|
||||
## playing together and a help line.
|
||||
|
||||
const MESSAGE_SECONDS := 2.5
|
||||
|
||||
var _stone := Label.new()
|
||||
var _message := Label.new()
|
||||
var _status := Label.new()
|
||||
var _help := Label.new()
|
||||
var _message_left := 0.0
|
||||
|
||||
@@ -15,9 +17,12 @@ func _ready() -> void:
|
||||
_stone.add_theme_font_size_override("font_size", 22)
|
||||
_message.position = Vector2(16, 48)
|
||||
_message.add_theme_font_size_override("font_size", 18)
|
||||
_status.add_theme_font_size_override("font_size", 14)
|
||||
_status.set_anchors_and_offsets_preset(Control.PRESET_TOP_RIGHT, Control.PRESET_MODE_MINSIZE, 16)
|
||||
_status.grow_horizontal = Control.GROW_DIRECTION_BEGIN
|
||||
_help.text = tr("HELP")
|
||||
_help.add_theme_font_size_override("font_size", 14)
|
||||
for label: Label in [_stone, _message, _help]:
|
||||
for label: Label in [_stone, _message, _status, _help]:
|
||||
label.add_theme_color_override("font_outline_color", Color.BLACK)
|
||||
label.add_theme_constant_override("outline_size", 4)
|
||||
add_child(label)
|
||||
@@ -36,9 +41,10 @@ func show_stone(count: int) -> void:
|
||||
_stone.text = tr("STONE_COUNT") % count
|
||||
|
||||
|
||||
## Shows the text for `key` for a few seconds.
|
||||
func flash(key: String) -> void:
|
||||
_message.text = tr(key)
|
||||
## Shows the text for `key` for a few seconds, with `values` filled into
|
||||
## its placeholders.
|
||||
func flash(key: String, values: Array = []) -> void:
|
||||
_message.text = tr(key) % values if not values.is_empty() else tr(key)
|
||||
_message_left = MESSAGE_SECONDS
|
||||
|
||||
|
||||
@@ -48,6 +54,16 @@ func show_paused(paused: bool) -> void:
|
||||
_message_left = 0.0
|
||||
|
||||
|
||||
## Shows `text` in the top right corner until it is replaced.
|
||||
func show_status(text: String) -> void:
|
||||
_status.text = text
|
||||
|
||||
|
||||
## The text currently shown in the status line. Tests read it.
|
||||
func status_text() -> String:
|
||||
return _status.text
|
||||
|
||||
|
||||
## The text currently shown as a message. Tests read it.
|
||||
func message() -> String:
|
||||
return _message.text
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
class_name Menu
|
||||
extends CanvasLayer
|
||||
## The start screen: the player's name, then playing alone, opening the world
|
||||
## for guests, or joining a host.
|
||||
|
||||
signal play_chosen(open: bool)
|
||||
signal join_chosen(address: String)
|
||||
|
||||
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 _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")
|
||||
_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]:
|
||||
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())
|
||||
|
||||
|
||||
## 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()
|
||||
@@ -0,0 +1 @@
|
||||
uid://dppcwwh67rtu6
|
||||
@@ -0,0 +1,34 @@
|
||||
extends TestCase
|
||||
|
||||
|
||||
func test_a_figure_appears_where_it_is_first_placed() -> void:
|
||||
var figures := Figures.new()
|
||||
check_eq(figures.shown_at(2), Vector2.INF, "nobody is shown at first")
|
||||
figures.place(2, Vector2(10.0, 20.0))
|
||||
check_eq(figures.shown_at(2), Vector2(10.0, 20.0), "at once at its first position")
|
||||
figures.free()
|
||||
|
||||
|
||||
func test_a_figure_glides_toward_its_latest_position() -> void:
|
||||
var figures := Figures.new()
|
||||
figures.place(1, Vector2.ZERO)
|
||||
figures.place(1, Vector2(100.0, 0.0))
|
||||
figures._process(0.02)
|
||||
var halfway := figures.shown_at(1).x
|
||||
check(halfway > 0.0 and halfway < 100.0, "part of the way after a frame: %s" % halfway)
|
||||
for frame in 60:
|
||||
figures._process(0.02)
|
||||
check(figures.shown_at(1).distance_to(Vector2(100.0, 0.0)) < 0.01, "there after a second")
|
||||
figures.free()
|
||||
|
||||
|
||||
func test_figures_can_be_forgotten() -> void:
|
||||
var figures := Figures.new()
|
||||
figures.place(1, Vector2.ZERO)
|
||||
figures.place(3, Vector2.ONE)
|
||||
figures.forget(1)
|
||||
check_eq(figures.shown_at(1), Vector2.INF, "one is gone")
|
||||
check_eq(figures.shown_at(3), Vector2.ONE, "the other stays")
|
||||
figures.clear()
|
||||
check_eq(figures.shown_at(3), Vector2.INF, "all are gone")
|
||||
figures.free()
|
||||
@@ -0,0 +1 @@
|
||||
uid://x5pqgp2p1hku
|
||||
@@ -0,0 +1,32 @@
|
||||
extends TestCase
|
||||
|
||||
|
||||
func _hud() -> Hud:
|
||||
I18n.install()
|
||||
var hud := Hud.new()
|
||||
tree.root.add_child(hud)
|
||||
return hud
|
||||
|
||||
|
||||
func _free(hud: Hud) -> void:
|
||||
tree.root.remove_child(hud)
|
||||
hud.free()
|
||||
|
||||
|
||||
func test_a_message_can_carry_values() -> void:
|
||||
var hud := _hud()
|
||||
hud.flash("PLAYER_JOINED", ["Grace"])
|
||||
check_eq(hud.message(), tr("PLAYER_JOINED") % "Grace", "the name is filled in")
|
||||
hud.flash("SAVED")
|
||||
check_eq(hud.message(), tr("SAVED"), "a message without values")
|
||||
_free(hud)
|
||||
|
||||
|
||||
func test_the_status_line_stays_until_it_is_replaced() -> void:
|
||||
var hud := _hud()
|
||||
hud.show_status(tr("STATUS_HOST") % [24680, 2])
|
||||
hud.flash("SAVED")
|
||||
hud._process(10.0)
|
||||
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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://8e4vlt0gnipa
|
||||
@@ -0,0 +1,37 @@
|
||||
extends TestCase
|
||||
|
||||
|
||||
func _menu() -> Menu:
|
||||
I18n.install()
|
||||
var menu := Menu.new()
|
||||
tree.root.add_child(menu)
|
||||
return menu
|
||||
|
||||
|
||||
func _free(menu: Menu) -> void:
|
||||
tree.root.remove_child(menu)
|
||||
menu.free()
|
||||
|
||||
|
||||
func test_the_buttons_say_what_the_player_chose() -> void:
|
||||
var menu := _menu()
|
||||
var chosen: Array = []
|
||||
menu.play_chosen.connect(func(open: bool) -> void: chosen.append(["play", open]))
|
||||
menu.join_chosen.connect(func(address: String) -> void: chosen.append(["join", address]))
|
||||
menu.address_field.text = " 192.168.178.20:25000 "
|
||||
menu.solo_button.pressed.emit()
|
||||
menu.host_button.pressed.emit()
|
||||
menu.join_button.pressed.emit()
|
||||
menu.address_field.text_submitted.emit(menu.address_field.text)
|
||||
check_eq(chosen, [["play", false], ["play", true], ["join", "192.168.178.20:25000"], ["join", "192.168.178.20:25000"]], "in order")
|
||||
_free(menu)
|
||||
|
||||
|
||||
func test_the_name_and_the_status_line() -> void:
|
||||
var menu := _menu()
|
||||
menu.name_field.text = " Ada "
|
||||
check_eq(menu.player_name(), "Ada", "without the spaces around it")
|
||||
menu.show_status("Verbinde …")
|
||||
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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dh6uogkq28wa4
|
||||
Reference in New Issue
Block a user