M1c: Gleichschritt im Netz #9
@@ -8,3 +8,30 @@ INVENTORY_FULL,Die Tasche ist voll,Your bag is full
|
|||||||
PAUSED,Pause,Paused
|
PAUSED,Pause,Paused
|
||||||
SIMULATION_STOPPED,Die Simulation wurde angehalten,The simulation stopped
|
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 · 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
|
class_name Hud
|
||||||
extends CanvasLayer
|
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
|
const MESSAGE_SECONDS := 2.5
|
||||||
|
|
||||||
var _stone := Label.new()
|
var _stone := Label.new()
|
||||||
var _message := Label.new()
|
var _message := Label.new()
|
||||||
|
var _status := Label.new()
|
||||||
var _help := Label.new()
|
var _help := Label.new()
|
||||||
var _message_left := 0.0
|
var _message_left := 0.0
|
||||||
|
|
||||||
@@ -15,9 +17,12 @@ func _ready() -> void:
|
|||||||
_stone.add_theme_font_size_override("font_size", 22)
|
_stone.add_theme_font_size_override("font_size", 22)
|
||||||
_message.position = Vector2(16, 48)
|
_message.position = Vector2(16, 48)
|
||||||
_message.add_theme_font_size_override("font_size", 18)
|
_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.text = tr("HELP")
|
||||||
_help.add_theme_font_size_override("font_size", 14)
|
_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_color_override("font_outline_color", Color.BLACK)
|
||||||
label.add_theme_constant_override("outline_size", 4)
|
label.add_theme_constant_override("outline_size", 4)
|
||||||
add_child(label)
|
add_child(label)
|
||||||
@@ -36,9 +41,10 @@ func show_stone(count: int) -> void:
|
|||||||
_stone.text = tr("STONE_COUNT") % count
|
_stone.text = tr("STONE_COUNT") % count
|
||||||
|
|
||||||
|
|
||||||
## Shows the text for `key` for a few seconds.
|
## Shows the text for `key` for a few seconds, with `values` filled into
|
||||||
func flash(key: String) -> void:
|
## its placeholders.
|
||||||
_message.text = tr(key)
|
func flash(key: String, values: Array = []) -> void:
|
||||||
|
_message.text = tr(key) % values if not values.is_empty() else tr(key)
|
||||||
_message_left = MESSAGE_SECONDS
|
_message_left = MESSAGE_SECONDS
|
||||||
|
|
||||||
|
|
||||||
@@ -48,6 +54,16 @@ func show_paused(paused: bool) -> void:
|
|||||||
_message_left = 0.0
|
_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.
|
## The text currently shown as a message. Tests read it.
|
||||||
func message() -> String:
|
func message() -> String:
|
||||||
return _message.text
|
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