M1c: Gleichschritt im Netz #9
@@ -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