update
This commit is contained in:
34
README.md
34
README.md
@@ -172,6 +172,22 @@ Ruhe.
|
||||
|
||||
Wer keine Würfel mehr hat, scheidet aus und schaut zu. Der Letzte gewinnt.
|
||||
|
||||
## Kampagne
|
||||
|
||||
Im Menü unter **Kampagne** liegt eine Seekarte mit sechs Kapitänen, von
|
||||
*Roter Jack* (leicht, 900) bis *Admiral Krähe* (schwer, 1360). Ein Klick auf
|
||||
einen Hafen startet die Partie: der Kapitän spielt auf seiner Stufe, seine
|
||||
Mannschaft eine Stufe darunter.
|
||||
|
||||
* Du hast eine **Wertung** (Elo, Start 1000) und daraus einen **Rang**:
|
||||
Schiffsjunge, Matrose, Maat, Steuermann, Kapitän, Admiral.
|
||||
* **Geschlagen** ist ein Kapitän, wenn er vor dir ausscheidet - oder du
|
||||
gewinnst. Er verschwindet dann für zwei Partien auf See und **kommt
|
||||
stärker zurück**: mehr Wertung, eine Stufe höher, ab „schwer" mehr
|
||||
Mannschaft.
|
||||
* Verlierst du, bleibt er, und deine Wertung sinkt.
|
||||
* Der Stand liegt in `user://kampagne.json`. „Neu anfangen" löscht ihn.
|
||||
|
||||
## Münzen
|
||||
|
||||
Münzen ersetzen die frühere Booster-Phase. Die kostete jede Runde eine eigene
|
||||
@@ -248,12 +264,13 @@ Runden.
|
||||
die Münzen zu seiner Zone.
|
||||
* Klänge (Würfel, Münze, Becher, Gewinn) werden beim Start berechnet. Es gibt
|
||||
keine Tondateien, so wie es auch keine Bilddateien gibt.
|
||||
* Kein Bild, kein Mesh aus einer Datei. **Stoffe** sind Regeln (`stoffe.gd`):
|
||||
Farbverlauf, Rauschart, Reliefstärke, Rauheit. Daraus entstehen zur
|
||||
Laufzeit drei Texturen je Stoff - Farbe, Relief, Rauheit. Das Relief macht
|
||||
aus gemustertem Plastik Filz, Holz und Messing. **Formen** sind Bauregeln
|
||||
(`formen.gd`): Becher = Kegelstumpf + Rand + zwei Bänder, Laterne = Sockel +
|
||||
Glas + Kappe. Kanten sind schmale Ringe. Der Plan steht in `assets.md`.
|
||||
* Kein Bild, kein Mesh aus einer Datei. Der Look ist **Low-Poly wie Knete**:
|
||||
flache, pastellige Farben, runde Kanten, weiche Schatten, heller Himmel.
|
||||
**Stoffe** (`stoffe.gd`) sind nur Farbe und Rauheit - was Holz von Filz
|
||||
trennt, ist die Form. **Formen** (`formen.gd`) entstehen aus Profilen, die
|
||||
um die Achse gedreht werden; Fasen sind Viertelkreise im Profil. Becher =
|
||||
Kegelstumpf mit runden Kanten + Messingrand + Band. Der Plan steht in
|
||||
`assets.md`.
|
||||
* Auf dem Handy nur Querformat - ein Tisch von oben braucht Breite.
|
||||
* Die Regeln stehen im Menü unter **Regeln**, inklusive aller sechs Münzen.
|
||||
|
||||
@@ -286,8 +303,9 @@ vermutlich haarscharf, rufen sie „Genau".
|
||||
| `scripts/ai.gd` | Bot-Entscheidungen (Binomialabschätzung für Gebot/Anfechtung) |
|
||||
| `scripts/hud.gd` | Spiel-UI |
|
||||
| `scripts/klang.gd` | Autoload: berechnete Klänge, keine Dateien |
|
||||
| `scripts/stoffe.gd` | Materialien aus Regeln, Texturen zur Laufzeit - keine Dateien |
|
||||
| `scripts/formen.gd` | Becher, Fass, Laterne aus Primitiven nach Bauregeln |
|
||||
| `scripts/kampagne.gd` | Autoload: Seekarte, Kapitäne, Wertung, Speicherstand |
|
||||
| `scripts/stoffe.gd` | Flache Farben der Knete-Palette, ein Material je Stoff |
|
||||
| `scripts/formen.gd` | Gedrehte Profile mit Fasen: Scheiben, Becher, Laterne |
|
||||
| `scripts/icons.gd` | Gezeichnete Münz- und Würfelbilder fürs HUD |
|
||||
| `scripts/hud_panels.gd` | Vorrat, Topf, Erklärung, Verlauf, Protokoll |
|
||||
| `tests/` | Headless-Tests, `make test` (~30 s); `make sim` misst die Bots - siehe `tests/README.md` |
|
||||
|
||||
24
assets.md
24
assets.md
@@ -6,15 +6,21 @@ gebaut ist.
|
||||
|
||||
## Stand
|
||||
|
||||
- Stoffe: Filz, Holz, dunkles Holz, Messing, Knochen. Je Regel drei
|
||||
Laufzeit-Texturen aus `NoiseTexture2D`: Farbe (Rauschen durch
|
||||
Farbverlauf), Relief (`as_normal_map`), Rauheit. Kein Shadercode.
|
||||
- Formen: Becher, Fass, Laterne aus Primitiven. Kanten als Torus-Ringe.
|
||||
- Gelernt: Rauschen nur als Farbe bleibt flach - das Relief macht den
|
||||
Unterschied. Grobes, glänzendes Relief auf Filz sieht nach nassem Moos aus:
|
||||
fein, matt, kaum Höhe. Maserung entsteht durch gestreckte UV (1:9).
|
||||
- Verworfen: Dielen auf dem Boden (aus Kamerahöhe nur Rauschen), Lampe über
|
||||
der Mitte (hängt der Kamera vor der Linse), Fass am Boden (ein Fleck).
|
||||
Der Look ist **Low-Poly wie Knete** (Vorbild: pastelliges Haus-Diorama):
|
||||
flache Farben, runde Kanten, weiche Schatten, heller Himmel. Der frühere
|
||||
Plan einer dunklen Taverne mit Maserung und Relief ist verworfen - er wirkte
|
||||
aus Kamerahöhe schäbig.
|
||||
|
||||
- `stoffe.gd`: eine Farbe je Stoff, matt, kaum Glanz. Keine Texturen.
|
||||
- `formen.gd`: Profile (Radius, Höhe) um die Achse gedreht, Fasen als
|
||||
Viertelkreise. Scheibe, runder Zylinder, Ring, Becher, Laterne.
|
||||
- Tisch: Holzscheibe mit Filzeinlage auf einem Mittelfuss, auf einer runden
|
||||
Sockelplatte wie ein Diorama. Zonen: Filzscheiben mit Holzring.
|
||||
- Licht: eine Sonne mit weichen Schatten, viel Umgebungslicht, kein Nebel,
|
||||
kein Glow. Belichtung 0,9, sonst brennt die Platte aus.
|
||||
- Gelernt: Der Look lebt von den Fasen und dem Licht, nicht vom Material.
|
||||
Ohne Fasen sieht flache Farbe nach Prototyp aus. Und aus Kamerahöhe muss
|
||||
am Rand Himmel bleiben, sonst ist alles Platte.
|
||||
|
||||
## Grundidee
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ config/features=PackedStringArray("4.7", "Mobile")
|
||||
BoosterDB="*res://scripts/boosters.gd"
|
||||
Net="*res://scripts/net.gd"
|
||||
Klang="*res://scripts/klang.gd"
|
||||
Kampagne="*res://scripts/kampagne.gd"
|
||||
|
||||
[display]
|
||||
|
||||
|
||||
@@ -84,6 +84,7 @@ func _phase_reveal(result: Dictionary) -> void:
|
||||
g.seats[l].count = 0
|
||||
g.host_values[l] = []
|
||||
g.table.set_dice_count(l, 0)
|
||||
g.ausgeschieden.append(l)
|
||||
g._log("%s scheidet aus." % g.seats[l].name)
|
||||
else:
|
||||
g._zieh_muenze(l)
|
||||
|
||||
@@ -1,10 +1,76 @@
|
||||
extends RefCounted
|
||||
## Formen aus Regeln: jedes Ding hat eine Grundform aus Godot-Primitiven, und
|
||||
## Regeln setzen Teile darauf - Rand, Bänder, Reifen, Kappe. Kanten sind
|
||||
## schmale Ringe statt Fasen. Kein Mesh kommt aus einer Datei.
|
||||
## Formen aus Regeln. Der Kern des Low-Poly-Looks sind runde Kanten: jede
|
||||
## Form wird als Profil (Radius, Höhe) beschrieben und um die Achse gedreht.
|
||||
## Fasen sind Viertelkreise im Profil. Kein Mesh kommt aus einer Datei.
|
||||
## Kein class_name: den Klassencache baut nur der Editor, headless fehlt er.
|
||||
|
||||
const Stoffe = preload("res://scripts/stoffe.gd")
|
||||
const FASE_SEG := 4 ## Punkte je Viertelkreis - vier reichen bei glatten Normalen
|
||||
|
||||
|
||||
## Ein Profil um die Y-Achse drehen. `profil` läuft von unten (Achse) nach
|
||||
## oben (Achse), Punkte sind (radius, höhe). Glatte Normalen, damit die
|
||||
## Fasen weich wirken und nicht wie Absätze.
|
||||
static func drehen(profil: PackedVector2Array, segmente := 40) -> ArrayMesh:
|
||||
var st := SurfaceTool.new()
|
||||
st.begin(Mesh.PRIMITIVE_TRIANGLES)
|
||||
st.set_smooth_group(0)
|
||||
for i in range(profil.size() - 1):
|
||||
var a := profil[i]
|
||||
var b := profil[i + 1]
|
||||
for k in segmente:
|
||||
var t0 := TAU * float(k) / segmente
|
||||
var t1 := TAU * float(k + 1) / segmente
|
||||
var p00 := Vector3(cos(t0) * a.x, a.y, sin(t0) * a.x)
|
||||
var p10 := Vector3(cos(t1) * a.x, a.y, sin(t1) * a.x)
|
||||
var p01 := Vector3(cos(t0) * b.x, b.y, sin(t0) * b.x)
|
||||
var p11 := Vector3(cos(t1) * b.x, b.y, sin(t1) * b.x)
|
||||
if a.x > 0.0001:
|
||||
st.add_vertex(p00)
|
||||
st.add_vertex(p10)
|
||||
st.add_vertex(p01)
|
||||
if b.x > 0.0001:
|
||||
st.add_vertex(p10)
|
||||
st.add_vertex(p11)
|
||||
st.add_vertex(p01)
|
||||
st.index()
|
||||
st.generate_normals()
|
||||
return st.commit()
|
||||
|
||||
|
||||
## Viertelkreis von `von` nach `bis` um die Ecke `mitte` (Profilkoordinaten).
|
||||
static func _bogen(mitte: Vector2, r: float, a0: float, a1: float) -> PackedVector2Array:
|
||||
var out := PackedVector2Array()
|
||||
for k in range(1, FASE_SEG + 1):
|
||||
var t := lerpf(a0, a1, float(k) / FASE_SEG)
|
||||
out.append(mitte + Vector2(cos(t), sin(t)) * r)
|
||||
return out
|
||||
|
||||
|
||||
## Zylinder oder Kegelstumpf mit runden Kanten oben und unten.
|
||||
static func rund_zylinder(r_unten: float, r_oben: float, hoehe: float, fase: float) -> ArrayMesh:
|
||||
fase = minf(fase, minf(hoehe * 0.5, minf(r_unten, r_oben) * 0.9))
|
||||
var p := PackedVector2Array()
|
||||
p.append(Vector2(0.0, 0.0))
|
||||
p.append(Vector2(r_unten - fase, 0.0))
|
||||
p.append_array(_bogen(Vector2(r_unten - fase, fase), fase, -PI * 0.5, 0.0))
|
||||
p.append_array(_bogen(Vector2(r_oben - fase, hoehe - fase), fase, 0.0, PI * 0.5))
|
||||
p.append(Vector2(0.0, hoehe))
|
||||
return drehen(p)
|
||||
|
||||
|
||||
static func scheibe(radius: float, hoehe: float, fase: float) -> ArrayMesh:
|
||||
return rund_zylinder(radius, radius, hoehe, fase)
|
||||
|
||||
|
||||
## Ring mit rundem Querschnitt - für Ränder und Bänder.
|
||||
static func ring(radius: float, dicke: float) -> TorusMesh:
|
||||
var t := TorusMesh.new()
|
||||
t.inner_radius = radius - dicke
|
||||
t.outer_radius = radius + dicke
|
||||
t.rings = 40
|
||||
t.ring_segments = 10
|
||||
return t
|
||||
|
||||
|
||||
static func _teil(mesh: Mesh, stoff: Material, pos: Vector3, eltern: Node3D) -> MeshInstance3D:
|
||||
@@ -16,65 +82,31 @@ static func _teil(mesh: Mesh, stoff: Material, pos: Vector3, eltern: Node3D) ->
|
||||
return mi
|
||||
|
||||
|
||||
static func _zylinder(oben: float, unten: float, hoehe: float, segmente := 28) -> CylinderMesh:
|
||||
var c := CylinderMesh.new()
|
||||
c.top_radius = oben
|
||||
c.bottom_radius = unten
|
||||
c.height = hoehe
|
||||
c.radial_segments = segmente
|
||||
return c
|
||||
|
||||
|
||||
static func _ring(radius: float, dicke: float) -> TorusMesh:
|
||||
var t := TorusMesh.new()
|
||||
t.inner_radius = radius - dicke
|
||||
t.outer_radius = radius + dicke
|
||||
t.rings = 32
|
||||
t.ring_segments = 8
|
||||
return t
|
||||
|
||||
|
||||
## Würfelbecher: Kegelstumpf aus dunklem Holz, oben ein Messingrand, zwei
|
||||
## Messingbänder um den Bauch. Der Deckel oben ist geschlossen - die Kamera
|
||||
## schaut von oben, ein offener Becher zeigte nur ein schwarzes Loch.
|
||||
## Würfelbecher: Kegelstumpf mit runden Kanten, oben ein Messingrand, ein
|
||||
## Band um den Bauch. Geschlossen - die Kamera schaut von oben.
|
||||
static func becher(hoehe := 0.44, oben := 0.33, unten := 0.42) -> Node3D:
|
||||
var n := Node3D.new()
|
||||
_teil(_zylinder(oben, unten, hoehe), Stoffe.holz_dunkel(), Vector3(0, hoehe * 0.5, 0), n)
|
||||
_teil(_ring(oben, 0.012), Stoffe.messing(), Vector3(0, hoehe - 0.004, 0), n)
|
||||
for k in [0.22, 0.62]:
|
||||
var r: float = lerpf(unten, oben, k)
|
||||
_teil(_ring(r, 0.008), Stoffe.messing(), Vector3(0, hoehe * k, 0), n)
|
||||
_teil(rund_zylinder(unten, oben, hoehe, 0.05), Stoffe.holz_dunkel(), Vector3.ZERO, n)
|
||||
_teil(ring(oben - 0.01, 0.018), Stoffe.messing(), Vector3(0, hoehe - 0.02, 0), n)
|
||||
_teil(ring(lerpf(unten, oben, 0.35) + 0.004, 0.012), Stoffe.messing(), Vector3(0, hoehe * 0.35, 0), n)
|
||||
return n
|
||||
|
||||
|
||||
## Fass: bauchiger Zylinder, drei Reifen, Boden und Deckel etwas kleiner.
|
||||
static func fass(hoehe := 0.6, radius := 0.24) -> Node3D:
|
||||
var n := Node3D.new()
|
||||
_teil(_zylinder(radius * 0.86, radius * 0.86, hoehe), Stoffe.holz(), Vector3(0, hoehe * 0.5, 0), n)
|
||||
_teil(_zylinder(radius, radius, hoehe * 0.5), Stoffe.holz(), Vector3(0, hoehe * 0.5, 0), n)
|
||||
for k in [0.12, 0.5, 0.88]:
|
||||
var r: float = radius if absf(k - 0.5) < 0.2 else radius * 0.88
|
||||
_teil(_ring(r, 0.012), Stoffe.messing(), Vector3(0, hoehe * k, 0), n)
|
||||
return n
|
||||
|
||||
|
||||
## Laterne: Sockel, Glaskörper (leuchtend), Kappe mit Ring.
|
||||
## Laterne: Sockel, leuchtendes Glas, Kappe, Bügel.
|
||||
static func laterne(hoehe := 0.5) -> Node3D:
|
||||
var n := Node3D.new()
|
||||
_teil(_zylinder(0.09, 0.11, 0.05), Stoffe.messing(), Vector3(0, 0.025, 0), n)
|
||||
var glas := StandardMaterial3D.new()
|
||||
glas.albedo_color = Color(1.0, 0.85, 0.5, 0.75)
|
||||
glas.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
_teil(rund_zylinder(0.11, 0.09, 0.05, 0.015), Stoffe.messing(), Vector3.ZERO, n)
|
||||
var glas := Stoffe.eigene("glas")
|
||||
glas.emission_enabled = true
|
||||
glas.emission = Color(1.0, 0.75, 0.35)
|
||||
glas.emission_energy_multiplier = 2.5
|
||||
_teil(_zylinder(0.07, 0.08, hoehe * 0.6), glas, Vector3(0, 0.05 + hoehe * 0.3, 0), n)
|
||||
_teil(_zylinder(0.03, 0.10, 0.08), Stoffe.messing(), Vector3(0, 0.05 + hoehe * 0.6 + 0.04, 0), n)
|
||||
_teil(_ring(0.035, 0.006), Stoffe.messing(), Vector3(0, 0.05 + hoehe * 0.6 + 0.10, 0), n)
|
||||
glas.emission = Color(1.0, 0.80, 0.40)
|
||||
glas.emission_energy_multiplier = 1.6
|
||||
_teil(rund_zylinder(0.08, 0.07, hoehe * 0.6, 0.02), glas, Vector3(0, 0.05, 0), n)
|
||||
_teil(rund_zylinder(0.10, 0.03, 0.08, 0.015), Stoffe.messing(), Vector3(0, 0.05 + hoehe * 0.6, 0), n)
|
||||
_teil(ring(0.035, 0.007), Stoffe.messing(), Vector3(0, 0.05 + hoehe * 0.6 + 0.10, 0), n)
|
||||
var licht := OmniLight3D.new()
|
||||
licht.light_color = Color(1.0, 0.8, 0.45)
|
||||
licht.light_energy = 1.2
|
||||
licht.omni_range = 1.8
|
||||
licht.light_color = Color(1.0, 0.85, 0.55)
|
||||
licht.light_energy = 0.8
|
||||
licht.omni_range = 1.6
|
||||
licht.position = Vector3(0, 0.05 + hoehe * 0.3, 0)
|
||||
n.add_child(licht)
|
||||
return n
|
||||
|
||||
@@ -25,6 +25,8 @@ var dice_start := START_DICE
|
||||
## Nur für `make sim`; fünfzig Partien in Sekunden statt Minuten.
|
||||
var simulation := false
|
||||
var runde := 0 ## laufende Rundennummer, für Anzeige und Messung
|
||||
var ausgeschieden: Array = [] ## Plätze in der Reihenfolge ihres Ausscheidens
|
||||
var cfg_start: Dictionary = {} ## Startaufstellung, fürs Nochmal
|
||||
|
||||
var rng := RandomNumberGenerator.new()
|
||||
var seats: Array = [] ## [{peer, name, ai, count, alive}]
|
||||
@@ -67,6 +69,7 @@ func _ready() -> void:
|
||||
|
||||
|
||||
func setup(cfg: Dictionary) -> void:
|
||||
cfg_start = cfg.duplicate(true)
|
||||
DiceAI.stufe = int(cfg.get("stufe", 1))
|
||||
seats.clear()
|
||||
if cfg.has("seats"):
|
||||
@@ -83,8 +86,11 @@ func setup(cfg: Dictionary) -> void:
|
||||
for p in peers:
|
||||
seats.append({"peer": int(p), "name": String(cfg.names[p]), "ai": false,
|
||||
"count": dice, "alive": true, "verwaist": false})
|
||||
var bots: Array = cfg.get("bots", [])
|
||||
for i in int(cfg.ai):
|
||||
seats.append({"peer": 0, "name": "Bot %d" % (i + 1), "ai": true,
|
||||
var b: Dictionary = bots[i] if i < bots.size() else {}
|
||||
seats.append({"peer": 0, "name": String(b.get("name", "Bot %d" % (i + 1))), "ai": true,
|
||||
"stufe": int(b.get("stufe", DiceAI.stufe)),
|
||||
"count": dice, "alive": true, "verwaist": false})
|
||||
|
||||
# Der Spielserver spielt nicht mit und bekommt deshalb keinen Platz.
|
||||
@@ -108,6 +114,7 @@ func setup(cfg: Dictionary) -> void:
|
||||
hud.challenge_pressed.connect(_on_challenge_pressed)
|
||||
hud.exact_pressed.connect(_on_exact_pressed)
|
||||
hud.rematch_pressed.connect(_on_rematch)
|
||||
hud.karte_pressed.connect(_on_karte)
|
||||
hud.booster_chosen.connect(_on_booster_chosen)
|
||||
hud.quit_pressed.connect(_on_quit)
|
||||
hud.pause_changed.connect(_on_pause_changed)
|
||||
@@ -209,6 +216,7 @@ func _host_loop() -> void:
|
||||
winner = s.name
|
||||
banner = "%s gewinnt" % winner
|
||||
_log(banner)
|
||||
Kampagne.ergebnis(seats, ausgeschieden)
|
||||
_push_state()
|
||||
|
||||
|
||||
@@ -433,6 +441,7 @@ func _phase_bid() -> Dictionary:
|
||||
await _sleep(1.1)
|
||||
# Erst planen, dann passend dazu eine Münze werfen. Die kann die
|
||||
# Lage ändern - also danach noch einmal entscheiden.
|
||||
DiceAI.stufe = int(seats[turn].get("stufe", DiceAI.stufe))
|
||||
var a := DiceAI.choose_action(host_values[turn], _total_dice(), bid,
|
||||
must_bid.get(turn, false), rng, _bot_wissen(turn))
|
||||
if await _bot_muenze(turn, a):
|
||||
@@ -788,7 +797,19 @@ func _on_rematch() -> void:
|
||||
for s in seats:
|
||||
if s.ai:
|
||||
bots += 1
|
||||
Net.rematch = {"name": Net.player_name, "ai": bots, "stufe": DiceAI.stufe}
|
||||
var kap := ""
|
||||
if Kampagne.kampagnenpartie and cfg_start.get("bots", []).size() > 0:
|
||||
kap = String(cfg_start.bots[0].name)
|
||||
Net.rematch = {"name": Net.player_name, "ai": bots, "stufe": int(cfg_start.get("stufe", 1)),
|
||||
"bots": cfg_start.get("bots", []), "kampagne": kap}
|
||||
_on_quit()
|
||||
|
||||
|
||||
## Zurück auf die Seekarte: das frisch geladene Menü zeigt sie sofort.
|
||||
func _on_karte() -> void:
|
||||
if _quitting:
|
||||
return
|
||||
Kampagne.zur_karte = true
|
||||
_on_quit()
|
||||
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ signal bid_pressed(count: int, face: int)
|
||||
signal challenge_pressed
|
||||
signal exact_pressed
|
||||
signal rematch_pressed
|
||||
signal karte_pressed
|
||||
signal booster_chosen(id: String, target: int)
|
||||
signal quit_pressed
|
||||
signal pause_changed(paused: bool)
|
||||
@@ -54,6 +55,7 @@ var pause_slot: Control
|
||||
var over_slot: Control
|
||||
var over_title: Label
|
||||
var over_again: Button
|
||||
var over_karte: Button
|
||||
var pause_hint: Label
|
||||
var boost_panel: PanelContainer
|
||||
var boost_cards: HBoxContainer
|
||||
@@ -178,9 +180,12 @@ func _build() -> void:
|
||||
|
||||
# --- Letzte Wette in der Tischmitte, als so viele Würfel wie angesagt
|
||||
var bid_slot := _slot(0, 0, 1, 1, 0, 0, 0, 0)
|
||||
var bid_panel := _panel(Color(0.05, 0.06, 0.09, 0.55))
|
||||
bid_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
bid_slot.add_child(bid_panel)
|
||||
bid_box = VBoxContainer.new()
|
||||
bid_box.add_theme_constant_override("separation", 8)
|
||||
bid_slot.add_child(bid_box)
|
||||
bid_panel.add_child(bid_box)
|
||||
bid_name = _label("", 21, ACCENT)
|
||||
bid_name.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
# Auf die freie Mitte begrenzen, sonst läuft der Text über die Nachbarplätze.
|
||||
@@ -205,11 +210,14 @@ func _build() -> void:
|
||||
root.add_child(banner_label)
|
||||
|
||||
# --- Wer ist dran: direkt über der Ansageleiste, wo der Blick ohnehin liegt
|
||||
# Im eigenen Feld: auf der hellen Platte wäre gelbe Schrift unlesbar.
|
||||
var tp := _panel(Color(0.05, 0.06, 0.09, 0.85))
|
||||
tp.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_slot(0, 1, 1, 1, 16, -152, -16, -114).add_child(tp)
|
||||
turn_label = _label("", 19, ACCENT)
|
||||
turn_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
turn_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_place(turn_label, 0, 1, 1, 1, 16, -146, -16, -114)
|
||||
root.add_child(turn_label)
|
||||
tp.add_child(turn_label)
|
||||
|
||||
# --- Ansage: kompakte Leiste unten in der Mitte
|
||||
action_panel = _panel()
|
||||
@@ -312,10 +320,12 @@ func _build() -> void:
|
||||
# den Tisch; hier steht nur, wer was geworfen hat und wie es fiel.
|
||||
wurf_slot = _slot(0, 0, 0, 0, 16, 100, 336, 200)
|
||||
wurf_slot.visible = false
|
||||
var wp := _panel()
|
||||
wurf_slot.add_child(wp)
|
||||
wurf_label = _label("", 16, ACCENT)
|
||||
wurf_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
wurf_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
wurf_slot.add_child(wurf_label)
|
||||
wp.add_child(wurf_label)
|
||||
|
||||
# --- Protokoll unten rechts: was der Topf tut, wer geht, wer zurückkommt.
|
||||
var lp := _panel()
|
||||
@@ -427,6 +437,12 @@ func _build() -> void:
|
||||
over_again.custom_minimum_size = Vector2(0, 42)
|
||||
over_again.pressed.connect(func() -> void: rematch_pressed.emit())
|
||||
ov.add_child(over_again)
|
||||
over_karte = Button.new()
|
||||
over_karte.text = "Zur Seekarte"
|
||||
over_karte.custom_minimum_size = Vector2(0, 42)
|
||||
over_karte.pressed.connect(func() -> void: karte_pressed.emit())
|
||||
over_karte.visible = false
|
||||
ov.add_child(over_karte)
|
||||
var menue := Button.new()
|
||||
menue.text = "Zum Menü"
|
||||
menue.custom_minimum_size = Vector2(0, 42)
|
||||
@@ -621,6 +637,10 @@ func update_state(st: Dictionary, my_seat: int) -> void:
|
||||
banner_label.text = "" if st.phase == "over" else st.banner
|
||||
if st.phase == "over" and not over_slot.visible:
|
||||
over_title.text = String(st.banner)
|
||||
# Kampagne: Wertung und Rang statt nur des Siegers.
|
||||
if Kampagne.kampagnenpartie and Kampagne.letztes != "":
|
||||
over_title.text = String(st.banner) + "\n" + Kampagne.letztes
|
||||
over_karte.visible = Kampagne.kampagnenpartie
|
||||
# Nochmal: solo immer, im Netz nur für den Raumbesitzer - der Server
|
||||
# setzt den Raum dann für alle neu auf.
|
||||
over_again.visible = Net.room_code == Net.SOLO or Net.is_owner
|
||||
|
||||
173
scripts/kampagne.gd
Normal file
173
scripts/kampagne.gd
Normal file
@@ -0,0 +1,173 @@
|
||||
extends Node
|
||||
## Kampagne: eine Seekarte mit Kapitänen. Wen du schlägst, der verschwindet
|
||||
## auf See und kommt nach zwei Partien stärker zurück - höhere Wertung, bessere
|
||||
## Bots, mehr Mannschaft. Du hast eine Wertung (Elo) und daraus einen Rang.
|
||||
## Der Stand liegt in user://kampagne.json.
|
||||
|
||||
const DATEI := "user://kampagne.json"
|
||||
const K := 32.0 ## Elo-Schrittweite
|
||||
const AUF_SEE := 2 ## Partien, bis ein Geschlagener zurück ist
|
||||
const RAENGE := [
|
||||
[0, "Schiffsjunge"], [950, "Matrose"], [1050, "Maat"],
|
||||
[1150, "Steuermann"], [1280, "Kapitän"], [1420, "Admiral"],
|
||||
]
|
||||
## Kapitäne mit Wertung, Bot-Stufe, Mannschaft (weitere Bots eine Stufe
|
||||
## darunter) und Platz auf der Karte (0..1 in Breite und Höhe).
|
||||
const START_KAPITAENE := [
|
||||
{"name": "Roter Jack", "elo": 900, "stufe": 0, "mannschaft": 0, "pos": Vector2(0.14, 0.32)},
|
||||
{"name": "Salz-Marie", "elo": 960, "stufe": 0, "mannschaft": 1, "pos": Vector2(0.33, 0.68)},
|
||||
{"name": "Einauge Bill", "elo": 1030, "stufe": 1, "mannschaft": 1, "pos": Vector2(0.52, 0.24)},
|
||||
{"name": "Käpt'n Grau", "elo": 1110, "stufe": 1, "mannschaft": 2, "pos": Vector2(0.70, 0.62)},
|
||||
{"name": "Die Witwe", "elo": 1210, "stufe": 2, "mannschaft": 2, "pos": Vector2(0.87, 0.30)},
|
||||
{"name": "Admiral Krähe", "elo": 1360, "stufe": 2, "mannschaft": 3, "pos": Vector2(0.50, 0.86)},
|
||||
]
|
||||
|
||||
var datei := DATEI ## Tests schreiben woandershin
|
||||
var elo := 1000.0
|
||||
var partien := 0
|
||||
var siege := 0
|
||||
var kapitaene: Array = [] ## {name, elo, stufe, mannschaft, pos, weg, geschlagen}
|
||||
var aktiv := "" ## Kapitän der laufenden Partie, "" = keine Kampagnenpartie
|
||||
var letztes := "" ## Text zum letzten Ergebnis, fürs Endfeld
|
||||
var zur_karte := false ## nach dem Ende zurück auf die Karte statt ins Hauptmenü
|
||||
var kampagnenpartie := false ## die laufende (oder gerade beendete) Partie gehört zur Kampagne
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
laden()
|
||||
|
||||
|
||||
static func rang(wert: float) -> String:
|
||||
var r := ""
|
||||
for stufe in RAENGE:
|
||||
if wert >= float(stufe[0]):
|
||||
r = String(stufe[1])
|
||||
return r
|
||||
|
||||
|
||||
func mein_rang() -> String:
|
||||
return rang(elo)
|
||||
|
||||
|
||||
func zuruecksetzen() -> void:
|
||||
elo = 1000.0
|
||||
partien = 0
|
||||
siege = 0
|
||||
aktiv = ""
|
||||
letztes = ""
|
||||
kapitaene = []
|
||||
for k in START_KAPITAENE:
|
||||
var e: Dictionary = k.duplicate()
|
||||
e.weg = 0
|
||||
e.geschlagen = 0
|
||||
kapitaene.append(e)
|
||||
|
||||
|
||||
func kapitaen(name: String) -> Dictionary:
|
||||
for k in kapitaene:
|
||||
if k.name == name:
|
||||
return k
|
||||
return {}
|
||||
|
||||
|
||||
## Partie gegen einen Kapitän: er selbst auf seiner Stufe, seine Mannschaft
|
||||
## eine Stufe darunter. Der Spieler sitzt auf Platz 0.
|
||||
func starten(name: String) -> void:
|
||||
var k := kapitaen(name)
|
||||
if k.is_empty() or int(k.weg) > 0:
|
||||
return
|
||||
aktiv = name
|
||||
kampagnenpartie = true
|
||||
var bots: Array = [{"name": name, "stufe": int(k.stufe)}]
|
||||
for i in int(k.mannschaft):
|
||||
bots.append({"name": "Maat %d" % (i + 1), "stufe": maxi(0, int(k.stufe) - 1)})
|
||||
Net.start_singleplayer(Net.player_name, bots.size(), int(k.stufe), 5, bots)
|
||||
|
||||
|
||||
## Wird vom Spiel am Ende gerufen. `ausgeschieden` ist die Reihenfolge der
|
||||
## Plätze, die rausgeflogen sind. Geschlagen ist der Kapitän, wenn er vor dem
|
||||
## Spieler ging - oder der Spieler gewonnen hat.
|
||||
func ergebnis(seats: Array, ausgeschieden: Array) -> void:
|
||||
if aktiv == "":
|
||||
return
|
||||
var k := kapitaen(aktiv)
|
||||
aktiv = ""
|
||||
if k.is_empty():
|
||||
return
|
||||
var spieler := 0
|
||||
var kap := -1
|
||||
for i in seats.size():
|
||||
if String(seats[i].name) == String(k.name):
|
||||
kap = i
|
||||
var sieg: bool
|
||||
if seats[spieler].alive:
|
||||
sieg = true
|
||||
elif kap >= 0 and ausgeschieden.has(kap) and ausgeschieden.has(spieler):
|
||||
sieg = ausgeschieden.find(kap) < ausgeschieden.find(spieler)
|
||||
else:
|
||||
sieg = false
|
||||
|
||||
var erwartet := 1.0 / (1.0 + pow(10.0, (float(k.elo) - elo) / 400.0))
|
||||
var punkte := 1.0 if sieg else 0.0
|
||||
var vorher := elo
|
||||
var delta := K * (punkte - erwartet)
|
||||
elo = maxf(0.0, elo + delta)
|
||||
k.elo = maxf(0.0, float(k.elo) - delta)
|
||||
partien += 1
|
||||
|
||||
# Die anderen auf See rücken einen Schritt näher an die Rückkehr.
|
||||
for o in kapitaene:
|
||||
if o != k and int(o.weg) > 0:
|
||||
o.weg = int(o.weg) - 1
|
||||
if int(o.weg) == 0:
|
||||
_zurueck(o)
|
||||
if sieg:
|
||||
siege += 1
|
||||
k.weg = AUF_SEE
|
||||
k.geschlagen = int(k.geschlagen) + 1
|
||||
letztes = "Sieg gegen %s! %d → %d (%s)" % [k.name, int(round(vorher)), int(round(elo)), mein_rang()]
|
||||
else:
|
||||
letztes = "%s behält die Oberhand. %d → %d (%s)" % [k.name, int(round(vorher)), int(round(elo)), mein_rang()]
|
||||
speichern()
|
||||
|
||||
|
||||
## Zurück von See: besser als vorher. Stufe bis 2, dann wächst die Mannschaft.
|
||||
func _zurueck(k: Dictionary) -> void:
|
||||
k.elo = float(k.elo) + 60.0
|
||||
if int(k.stufe) < 2:
|
||||
k.stufe = int(k.stufe) + 1
|
||||
else:
|
||||
k.mannschaft = mini(4, int(k.mannschaft) + 1)
|
||||
|
||||
|
||||
func speichern() -> void:
|
||||
var daten := {"elo": elo, "partien": partien, "siege": siege, "kapitaene": []}
|
||||
for k in kapitaene:
|
||||
daten.kapitaene.append({"name": k.name, "elo": k.elo, "stufe": k.stufe,
|
||||
"mannschaft": k.mannschaft, "weg": k.weg, "geschlagen": k.geschlagen})
|
||||
var f := FileAccess.open(datei, FileAccess.WRITE)
|
||||
if f != null:
|
||||
f.store_string(JSON.stringify(daten))
|
||||
f.close()
|
||||
|
||||
|
||||
func laden() -> void:
|
||||
zuruecksetzen()
|
||||
if not FileAccess.file_exists(datei):
|
||||
return
|
||||
var text := FileAccess.get_file_as_string(datei)
|
||||
var daten = JSON.parse_string(text)
|
||||
if not (daten is Dictionary):
|
||||
return
|
||||
elo = float(daten.get("elo", 1000.0))
|
||||
partien = int(daten.get("partien", 0))
|
||||
siege = int(daten.get("siege", 0))
|
||||
for d in daten.get("kapitaene", []):
|
||||
var k := kapitaen(String(d.get("name", "")))
|
||||
if k.is_empty():
|
||||
continue
|
||||
k.elo = float(d.get("elo", k.elo))
|
||||
k.stufe = int(d.get("stufe", k.stufe))
|
||||
k.mannschaft = int(d.get("mannschaft", k.mannschaft))
|
||||
k.weg = int(d.get("weg", 0))
|
||||
k.geschlagen = int(d.get("geschlagen", 0))
|
||||
@@ -35,7 +35,13 @@ func _ready() -> void:
|
||||
if not Net.rematch.is_empty():
|
||||
var r: Dictionary = Net.rematch
|
||||
Net.rematch = {}
|
||||
Net.start_singleplayer(String(r.name), int(r.ai), int(r.stufe))
|
||||
var kap := String(r.get("kampagne", ""))
|
||||
if kap != "":
|
||||
# Nochmal gegen denselben Kapitän: nur, wenn er noch da ist.
|
||||
Kampagne.starten(kap)
|
||||
else:
|
||||
Kampagne.kampagnenpartie = false
|
||||
Net.start_singleplayer(String(r.name), int(r.ai), int(r.stufe), 5, r.get("bots", []))
|
||||
|
||||
|
||||
## Das Spiel startet im Vollbild; F11 schaltet um.
|
||||
|
||||
@@ -14,6 +14,10 @@ var zurueck_btn: Button ## "Zurück in Raum XY" nach einem Abbruch
|
||||
var solo_stufe: OptionButton
|
||||
var lobby_stufe: OptionButton
|
||||
|
||||
# Kampagne
|
||||
var kamp_kopf: Label
|
||||
var karte: Control
|
||||
|
||||
# Mehrspieler
|
||||
var name_edit: LineEdit
|
||||
var code_edit: LineEdit
|
||||
@@ -41,11 +45,16 @@ func _ready() -> void:
|
||||
screens["join"] = _screen(root, _build_join())
|
||||
screens["lobby"] = _screen(root, _build_lobby())
|
||||
screens["regeln"] = _screen(root, _build_regeln())
|
||||
screens["kampagne"] = _screen(root, _build_kampagne())
|
||||
|
||||
Net.lobby_changed.connect(_refresh)
|
||||
Net.connection_failed.connect(_on_failed)
|
||||
Net.server_closed.connect(_on_closed)
|
||||
_show("main")
|
||||
if Kampagne.zur_karte:
|
||||
Kampagne.zur_karte = false
|
||||
_show("kampagne")
|
||||
else:
|
||||
_show("main")
|
||||
|
||||
|
||||
# ------------------------------------------------------------ Aufbau
|
||||
@@ -63,6 +72,8 @@ func _show(which: String) -> void:
|
||||
for key: String in screens:
|
||||
screens[key].visible = key == which
|
||||
_sagen("")
|
||||
if which == "kampagne":
|
||||
_karte_fuellen()
|
||||
if which == "main" and zurueck_btn != null:
|
||||
zurueck_btn.visible = not Net.letzter_raum.is_empty()
|
||||
if zurueck_btn.visible:
|
||||
@@ -93,6 +104,82 @@ func _text(t: String, size: int, col := Color.WHITE) -> Label:
|
||||
return l
|
||||
|
||||
|
||||
## Gezeichnete Karte im Knete-Look: helle See, sandige Inseln unter jedem
|
||||
## Hafen, eine gestrichelte Route von leicht nach schwer.
|
||||
class Seekarte extends Control:
|
||||
func _draw() -> void:
|
||||
var g := size
|
||||
draw_rect(Rect2(Vector2.ZERO, g), Color(0.56, 0.74, 0.88))
|
||||
var sortiert: Array = Kampagne.kapitaene.duplicate()
|
||||
sortiert.sort_custom(func(a, b): return float(a.elo) < float(b.elo))
|
||||
var letzte := Vector2(-1, -1)
|
||||
for k in sortiert:
|
||||
var p: Vector2 = Vector2(k.pos) * g
|
||||
if letzte.x >= 0.0:
|
||||
var n := 14
|
||||
for i in n:
|
||||
if i % 2 == 0:
|
||||
draw_line(letzte.lerp(p, float(i) / n), letzte.lerp(p, float(i + 1) / n),
|
||||
Color(1, 1, 1, 0.45), 2.0)
|
||||
letzte = p
|
||||
for k in Kampagne.kapitaene:
|
||||
var p: Vector2 = Vector2(k.pos) * g
|
||||
var da: bool = int(k.weg) == 0
|
||||
draw_circle(p + Vector2(0, 6), 62.0, Color(0.0, 0.0, 0.0, 0.10))
|
||||
draw_circle(p, 62.0, Color(0.92, 0.84, 0.68) if da else Color(0.75, 0.78, 0.82))
|
||||
draw_circle(p + Vector2(-10, -30), 16.0, Color(0.35, 0.72, 0.44) if da else Color(0.6, 0.65, 0.66))
|
||||
draw_circle(p + Vector2(14, -34), 11.0, Color(0.35, 0.72, 0.44) if da else Color(0.6, 0.65, 0.66))
|
||||
|
||||
|
||||
## Die Seekarte: Kapitäne als Häfen. Wer auf See ist, steht ausgegraut da
|
||||
## mit der Zahl der Partien bis zur Rückkehr.
|
||||
func _build_kampagne() -> Control:
|
||||
var box := _box(940)
|
||||
box.add_child(_text("Seekarte", 26, Color(1.0, 0.82, 0.32)))
|
||||
kamp_kopf = _text("", 15, Color(0.85, 0.87, 0.92))
|
||||
box.add_child(kamp_kopf)
|
||||
karte = Seekarte.new()
|
||||
karte.custom_minimum_size = Vector2(920, 440)
|
||||
box.add_child(karte)
|
||||
var row := HBoxContainer.new()
|
||||
row.add_theme_constant_override("separation", 8)
|
||||
box.add_child(row)
|
||||
_button("Neu anfangen", row, func() -> void:
|
||||
Kampagne.zuruecksetzen()
|
||||
Kampagne.speichern()
|
||||
_karte_fuellen())
|
||||
_button("Zurück", row, _show.bind("main"))
|
||||
return box
|
||||
|
||||
|
||||
func _karte_fuellen() -> void:
|
||||
kamp_kopf.text = "Du: %s, Wertung %d · %d Partien, %d Siege" % [
|
||||
Kampagne.mein_rang(), int(round(Kampagne.elo)), Kampagne.partien, Kampagne.siege]
|
||||
for c in karte.get_children():
|
||||
if c is Button:
|
||||
karte.remove_child(c)
|
||||
c.queue_free()
|
||||
karte.queue_redraw()
|
||||
var groesse := karte.custom_minimum_size
|
||||
for k in Kampagne.kapitaene:
|
||||
var b := Button.new()
|
||||
var weg := int(k.weg)
|
||||
if weg > 0:
|
||||
b.text = "%s\nauf See · zurück in %d" % [k.name, weg]
|
||||
b.disabled = true
|
||||
else:
|
||||
b.text = "%s\n%s · %d" % [k.name, ["leicht", "normal", "schwer"][int(k.stufe)], int(round(k.elo))]
|
||||
b.pressed.connect(_on_kapitaen.bind(String(k.name)))
|
||||
b.custom_minimum_size = Vector2(170, 52)
|
||||
var p: Vector2 = k.pos
|
||||
b.position = Vector2(p.x * groesse.x - 85, p.y * groesse.y + 4)
|
||||
karte.add_child(b)
|
||||
|
||||
|
||||
func _on_kapitaen(name: String) -> void:
|
||||
Kampagne.starten(name)
|
||||
|
||||
|
||||
func _on_wieder_rein() -> void:
|
||||
_show("join")
|
||||
_sagen("Verbinde mit Raum %s …" % Net.letzter_raum.get("code", ""))
|
||||
@@ -184,6 +271,7 @@ func _build_main() -> Control:
|
||||
# Nach einem Verbindungsabbruch: derselbe Name, derselbe Code, ein Klick.
|
||||
zurueck_btn = _button("", box, _on_wieder_rein)
|
||||
zurueck_btn.visible = false
|
||||
_button("Kampagne", box, _show.bind("kampagne"))
|
||||
_button("Einzelspieler", box, _show.bind("single"))
|
||||
_button("Mehrspieler", box, _show.bind("multi"))
|
||||
_button("Regeln", box, _show.bind("regeln"))
|
||||
@@ -330,6 +418,7 @@ func _build_lobby() -> Control:
|
||||
# ------------------------------------------------------------ Aktionen
|
||||
|
||||
func _on_solo_start() -> void:
|
||||
Kampagne.kampagnenpartie = false
|
||||
Net.start_singleplayer("Spieler", solo_bots.value, solo_stufe.selected)
|
||||
|
||||
|
||||
|
||||
@@ -258,7 +258,9 @@ func _push_lobby(code: String, data: Dictionary, besitzer: int) -> void:
|
||||
# ── Start ────────────────────────────────────────────────────────────────────
|
||||
|
||||
## Einzelspieler: kein Netzwerk, nur der lokale Spieler und Bots.
|
||||
func start_singleplayer(pname: String, ai_count: int, stufe := 1, dice := 5) -> void:
|
||||
## `bots`: optionale Liste {name, stufe} - die Kampagne setzt so Kapitän und
|
||||
## Mannschaft mit eigenen Stufen an den Tisch.
|
||||
func start_singleplayer(pname: String, ai_count: int, stufe := 1, dice := 5, bots: Array = []) -> void:
|
||||
player_name = pname
|
||||
multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new()
|
||||
room_code = SOLO
|
||||
@@ -268,7 +270,7 @@ func start_singleplayer(pname: String, ai_count: int, stufe := 1, dice := 5) ->
|
||||
in_game = true
|
||||
lobby_changed.emit()
|
||||
game_started.emit(SOLO, {"peers": [1], "names": lobby.duplicate(), "ai": ai_count,
|
||||
"stufe": stufe, "dice": dice})
|
||||
"stufe": stufe, "dice": dice, "bots": bots})
|
||||
|
||||
|
||||
## Nach einem Abbruch mit demselben Namen zurück in die laufende Partie.
|
||||
|
||||
@@ -1,113 +1,38 @@
|
||||
extends RefCounted
|
||||
## Materialien aus Regeln, ohne eine einzige Datei. Je Stoff ein Eintrag:
|
||||
## Farbverlauf, Rauschart, Reliefstärke, Rauheit, Maßstab. Daraus baut
|
||||
## `mach()` ein StandardMaterial3D mit drei zur Laufzeit erzeugten Texturen:
|
||||
## Farbe (Rauschen durch einen Farbverlauf), Relief (dasselbe Rauschen als
|
||||
## Normalenkarte) und Rauheit. Das Relief ist der Unterschied zwischen
|
||||
## "gemustertes Plastik" und Filz, Holz, Messing.
|
||||
##
|
||||
## Materialien für den Low-Poly-Look: eine flache Farbe, matt, kein Muster,
|
||||
## kein Relief. Was "Holz" von "Filz" unterscheidet, ist die Farbe und die
|
||||
## Form - nicht eine Textur. Die Palette ist pastellig und satt, wie Knete.
|
||||
## Kein class_name: den Klassencache baut nur der Editor, headless fehlt er.
|
||||
|
||||
const STOFFE := {
|
||||
"filz": {
|
||||
# Feiner Flaum, kaum Relief, ganz matt. Grobes, glänzendes Relief sah
|
||||
# nach nassem Moos aus.
|
||||
"farben": [Color(0.06, 0.18, 0.11), Color(0.08, 0.23, 0.14), Color(0.10, 0.26, 0.16)],
|
||||
"art": FastNoiseLite.TYPE_SIMPLEX_SMOOTH, "freq": 0.35, "oktaven": 3,
|
||||
"relief": 0.12, "rauheit": [0.96, 1.0], "metall": 0.0, "uv": Vector3(14, 14, 1),
|
||||
"glanz": 0.05,
|
||||
},
|
||||
"holz": {
|
||||
"farben": [Color(0.20, 0.12, 0.06), Color(0.36, 0.23, 0.11), Color(0.46, 0.31, 0.16)],
|
||||
"art": FastNoiseLite.TYPE_SIMPLEX, "freq": 0.03, "oktaven": 5, "warp": 25.0,
|
||||
"relief": 0.6, "rauheit": [0.45, 0.75], "metall": 0.0,
|
||||
# stark gestreckt: aus dem Rauschen wird Maserung
|
||||
"uv": Vector3(1.0, 9.0, 1.0),
|
||||
},
|
||||
"holz_dunkel": {
|
||||
"farben": [Color(0.14, 0.06, 0.04), Color(0.30, 0.12, 0.07), Color(0.40, 0.18, 0.10)],
|
||||
"art": FastNoiseLite.TYPE_SIMPLEX, "freq": 0.03, "oktaven": 5, "warp": 25.0,
|
||||
"relief": 0.5, "rauheit": [0.4, 0.7], "metall": 0.0, "uv": Vector3(1.0, 7.0, 1.0),
|
||||
},
|
||||
"messing": {
|
||||
"farben": [Color(0.55, 0.40, 0.14), Color(0.80, 0.62, 0.26), Color(0.95, 0.80, 0.42)],
|
||||
"art": FastNoiseLite.TYPE_CELLULAR, "freq": 0.12, "oktaven": 2,
|
||||
"relief": 0.25, "rauheit": [0.2, 0.5], "metall": 0.9, "uv": Vector3(3, 3, 1),
|
||||
},
|
||||
"knochen": {
|
||||
# Farbe kommt von den Vertexfarben (Körper hell, Augen dunkel); hier
|
||||
# nur feine Dellen und eine warme, leicht fettige Oberfläche.
|
||||
"farben": [], "art": FastNoiseLite.TYPE_SIMPLEX_SMOOTH, "freq": 0.25, "oktaven": 3,
|
||||
"relief": 0.2, "rauheit": [0.45, 0.65], "metall": 0.0, "uv": Vector3(4, 4, 1),
|
||||
},
|
||||
const FARBEN := {
|
||||
"filz": Color(0.33, 0.70, 0.42),
|
||||
"filz_dunkel": Color(0.26, 0.60, 0.36),
|
||||
"holz": Color(0.86, 0.68, 0.46),
|
||||
"holz_dunkel": Color(0.76, 0.42, 0.30),
|
||||
"holz_rand": Color(0.60, 0.40, 0.26),
|
||||
"messing": Color(0.96, 0.80, 0.36),
|
||||
"knochen": Color(0.99, 0.96, 0.90),
|
||||
"boden": Color(0.90, 0.84, 0.72),
|
||||
"glas": Color(1.0, 0.92, 0.55),
|
||||
}
|
||||
|
||||
static var _fertig: Dictionary = {} ## name -> StandardMaterial3D, einmal gebaut
|
||||
static var _fertig: Dictionary = {}
|
||||
|
||||
|
||||
static func _rauschen(r: Dictionary, seed: int) -> FastNoiseLite:
|
||||
var n := FastNoiseLite.new()
|
||||
n.seed = seed
|
||||
n.noise_type = r.art
|
||||
n.frequency = r.freq
|
||||
n.fractal_octaves = r.oktaven
|
||||
if r.has("warp"):
|
||||
n.domain_warp_enabled = true
|
||||
n.domain_warp_amplitude = r.warp
|
||||
return n
|
||||
|
||||
|
||||
static func _textur(n: FastNoiseLite, groesse: int) -> NoiseTexture2D:
|
||||
var t := NoiseTexture2D.new()
|
||||
t.noise = n
|
||||
t.width = groesse
|
||||
t.height = groesse
|
||||
t.seamless = true
|
||||
t.generate_mipmaps = true
|
||||
return t
|
||||
|
||||
|
||||
## Baut das Material nach Regel `name`. Gleiche Regel, gleiches Material:
|
||||
## alle Becher teilen eine Instanz, so bleibt es ein Renderobjekt je Stoff.
|
||||
## Ein Stoff, eine Instanz: gleiche Farbe teilt das Material.
|
||||
static func mach(name: String) -> StandardMaterial3D:
|
||||
if _fertig.has(name):
|
||||
return _fertig[name]
|
||||
var r: Dictionary = STOFFE[name]
|
||||
var m := StandardMaterial3D.new()
|
||||
var basis := _rauschen(r, name.hash() % 1000)
|
||||
|
||||
if not r.farben.is_empty():
|
||||
var verlauf := Gradient.new()
|
||||
var farben: Array = r.farben
|
||||
verlauf.offsets = PackedFloat32Array([0.0, 0.5, 1.0])
|
||||
verlauf.colors = PackedColorArray([farben[0], farben[1], farben[2]])
|
||||
var farbe := _textur(basis, 256)
|
||||
farbe.color_ramp = verlauf
|
||||
m.albedo_texture = farbe
|
||||
else:
|
||||
m.vertex_color_use_as_albedo = true
|
||||
|
||||
var relief := _textur(basis, 256)
|
||||
relief.as_normal_map = true
|
||||
relief.bump_strength = float(r.relief) * 8.0
|
||||
m.normal_enabled = true
|
||||
m.normal_texture = relief
|
||||
m.normal_scale = 1.0
|
||||
|
||||
# Rauheit schwankt mit dem Rauschen zwischen den zwei Werten.
|
||||
var rau := _textur(_rauschen(r, name.hash() % 1000 + 7), 128)
|
||||
m.roughness_texture = rau
|
||||
m.roughness_texture_channel = BaseMaterial3D.TEXTURE_CHANNEL_RED
|
||||
m.roughness = float(r.rauheit[1])
|
||||
m.metallic = float(r.metall)
|
||||
m.metallic_specular = float(r.get("glanz", 0.5 if r.metall > 0.5 else 0.3))
|
||||
m.uv1_scale = r.uv
|
||||
m.texture_filter = BaseMaterial3D.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS_ANISOTROPIC
|
||||
m.albedo_color = FARBEN[name]
|
||||
m.roughness = 0.85
|
||||
m.metallic = 0.0
|
||||
# Fast kein Glanzpunkt: Knete glänzt nicht, sie leuchtet vom Licht her.
|
||||
m.metallic_specular = 0.15
|
||||
_fertig[name] = m
|
||||
return m
|
||||
|
||||
|
||||
## Eigene Instanz, wenn ein Objekt Farbe oder Leuchten ändern muss.
|
||||
static func eigene(name: String) -> StandardMaterial3D:
|
||||
return mach(name).duplicate()
|
||||
|
||||
@@ -116,6 +41,10 @@ static func filz() -> StandardMaterial3D:
|
||||
return mach("filz")
|
||||
|
||||
|
||||
static func filz_dunkel() -> StandardMaterial3D:
|
||||
return mach("filz_dunkel")
|
||||
|
||||
|
||||
static func holz() -> StandardMaterial3D:
|
||||
return mach("holz")
|
||||
|
||||
@@ -124,22 +53,36 @@ static func holz_dunkel() -> StandardMaterial3D:
|
||||
return mach("holz_dunkel")
|
||||
|
||||
|
||||
static func boden() -> StandardMaterial3D:
|
||||
return mach("boden")
|
||||
|
||||
|
||||
static func messing(emission := 0.0) -> StandardMaterial3D:
|
||||
if emission <= 0.0:
|
||||
return mach("messing")
|
||||
var m := eigene("messing")
|
||||
if emission > 0.0:
|
||||
m.emission_enabled = true
|
||||
m.emission = Color(1.0, 0.75, 0.30)
|
||||
m.emission_energy_multiplier = emission
|
||||
m.emission_enabled = true
|
||||
m.emission = Color(1.0, 0.80, 0.35)
|
||||
m.emission_energy_multiplier = emission
|
||||
return m
|
||||
|
||||
|
||||
## Würfel: Farbe kommt aus den Vertexfarben (Körper hell, Augen dunkel).
|
||||
static func knochen() -> StandardMaterial3D:
|
||||
return mach("knochen")
|
||||
if _fertig.has("knochen"):
|
||||
return _fertig["knochen"]
|
||||
var m := StandardMaterial3D.new()
|
||||
m.vertex_color_use_as_albedo = true
|
||||
m.roughness = 0.8
|
||||
m.metallic_specular = 0.15
|
||||
_fertig["knochen"] = m
|
||||
return m
|
||||
|
||||
|
||||
## Zonenrand: Holz, das leuchten kann, wenn der Platz dran ist.
|
||||
## Zonenrand: dunkleres Holz als die Becher, damit sich beides trennt; kann
|
||||
## leuchten, wenn der Platz dran ist.
|
||||
static func rand() -> StandardMaterial3D:
|
||||
var m := eigene("holz")
|
||||
var m := eigene("holz_rand")
|
||||
m.emission = Color(1.0, 0.78, 0.30)
|
||||
m.emission_enabled = false
|
||||
return m
|
||||
|
||||
164
scripts/table.gd
164
scripts/table.gd
@@ -12,7 +12,7 @@ const Stoffe = preload("res://scripts/stoffe.gd") ## Materialien aus Regeln, o
|
||||
const Formen = preload("res://scripts/formen.gd") ## Becher, Fass, Laterne aus Primitiven
|
||||
const RIM_RUHE := Color(0.22, 0.15, 0.09) ## Zonenrand, wenn der Platz wartet
|
||||
const RIM_ZUG := Color(0.85, 0.66, 0.22) ## Zonenrand des Platzes, der dran ist
|
||||
const TILT_DEG := 15.0 ## Neigung der Kamera gegen die Senkrechte
|
||||
const TILT_DEG := 45.0 ## Neigung der Kamera gegen die Senkrechte
|
||||
const HUD_TOP := 0.13 ## Bildanteil oben für die Gebotsanzeige
|
||||
const HUD_BOTTOM := 0.17 ## Bildanteil unten für die Ansage-Leiste
|
||||
|
||||
@@ -33,7 +33,7 @@ var _bid_anchor: Array[Vector3] = []
|
||||
var plates: Array[Sprite3D] = []
|
||||
var rims: Array[MeshInstance3D] = [] ## Rand der Zone, leuchtet beim Zug
|
||||
var topf_muenzen: Dictionary = {} ## Wurfnummer -> Münze in der Tischmitte
|
||||
static var _muenz_mesh: CylinderMesh
|
||||
static var _muenz_mesh: ArrayMesh
|
||||
static var _muenz_mat: StandardMaterial3D
|
||||
var dice: Array = [] ## seat -> Array[Die]
|
||||
|
||||
@@ -62,96 +62,80 @@ func build(count: int, local_index: int) -> void:
|
||||
## Formregeln bleibt weg: vom Boden aus ist es aus Kamerahöhe nur ein Fleck.
|
||||
func _deko() -> void:
|
||||
var laterne := Formen.laterne()
|
||||
laterne.position = Vector3(-(table_r + 0.35), TABLE_H, table_r * 0.55)
|
||||
laterne.position = Vector3(-(table_r + 0.45), 0.0, table_r * 0.55) # steht auf der Platte
|
||||
add_child(laterne)
|
||||
|
||||
|
||||
func _build_room() -> void:
|
||||
# Low-Poly-Look: heller Himmel, eine Sonne mit weichen Schatten, viel
|
||||
# Umgebungslicht. Kein Nebel, kein Glow - die Formen sollen klar stehen.
|
||||
var env := WorldEnvironment.new()
|
||||
var e := Environment.new()
|
||||
e.background_mode = Environment.BG_COLOR
|
||||
e.background_color = Color(0.04, 0.05, 0.07)
|
||||
e.background_color = Color(0.56, 0.74, 0.88)
|
||||
e.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
|
||||
e.ambient_light_color = Color(0.35, 0.38, 0.45)
|
||||
e.ambient_light_energy = 1.1
|
||||
e.ambient_light_color = Color(0.70, 0.78, 0.92)
|
||||
e.ambient_light_energy = 0.75
|
||||
e.tonemap_mode = Environment.TONE_MAPPER_FILMIC
|
||||
e.glow_enabled = true
|
||||
e.glow_intensity = 0.45
|
||||
e.glow_bloom = 0.05
|
||||
e.glow_hdr_threshold = 0.9
|
||||
e.fog_enabled = true
|
||||
e.fog_light_color = Color(0.05, 0.06, 0.09)
|
||||
e.fog_density = 0.02
|
||||
e.tonemap_exposure = 0.9
|
||||
env.environment = e
|
||||
add_child(env)
|
||||
|
||||
var lamp := SpotLight3D.new()
|
||||
lamp.position = Vector3(0, TABLE_H + table_r * 1.45, 0)
|
||||
lamp.rotation_degrees = Vector3(-90, 0, 0)
|
||||
lamp.light_energy = 2.5
|
||||
lamp.light_color = Color(1.0, 0.94, 0.82)
|
||||
lamp.spot_range = table_r * 4.2
|
||||
lamp.spot_angle = 60.0
|
||||
lamp.spot_attenuation = 0.6
|
||||
lamp.shadow_enabled = true
|
||||
add_child(lamp)
|
||||
var sonne := DirectionalLight3D.new()
|
||||
sonne.light_color = Color(1.0, 0.96, 0.90)
|
||||
sonne.light_energy = 1.1
|
||||
sonne.shadow_enabled = true
|
||||
sonne.light_angular_distance = 3.0 # weiche Schattenkanten
|
||||
sonne.directional_shadow_mode = DirectionalLight3D.SHADOW_ORTHOGONAL
|
||||
sonne.directional_shadow_max_distance = 30.0
|
||||
sonne.rotation_degrees = Vector3(-52.0, 35.0, 0.0)
|
||||
add_child(sonne)
|
||||
|
||||
# Kein sichtbarer Lampenschirm: die Kamera schaut fast senkrecht von oben,
|
||||
# alles über der Tischmitte hinge ihr vor der Linse.
|
||||
# Sockelplatte wie ein Diorama: eine runde Scheibe mit weicher Kante,
|
||||
# darunter nur Himmel.
|
||||
var platte := MeshInstance3D.new()
|
||||
# Klein genug, dass an den Rändern Himmel bleibt - sonst ist alles Platte.
|
||||
platte.mesh = Formen.scheibe(table_r * 1.55, 0.18, 0.08)
|
||||
platte.position = Vector3(0, -0.18, 0)
|
||||
platte.material_override = Stoffe.boden()
|
||||
add_child(platte)
|
||||
var boden := StaticBody3D.new()
|
||||
var bc := CollisionShape3D.new()
|
||||
var bs := BoxShape3D.new()
|
||||
bs.size = Vector3(40, 0.2, 40)
|
||||
bc.shape = bs
|
||||
bc.position = Vector3(0, -0.1, 0)
|
||||
boden.add_child(bc)
|
||||
add_child(boden)
|
||||
|
||||
var fill := DirectionalLight3D.new()
|
||||
fill.rotation_degrees = Vector3(-55, 35, 0)
|
||||
fill.light_energy = 0.25
|
||||
add_child(fill)
|
||||
|
||||
# Boden
|
||||
var floor_body := StaticBody3D.new()
|
||||
var floor_col := CollisionShape3D.new()
|
||||
var fs := BoxShape3D.new()
|
||||
fs.size = Vector3(24, 0.4, 24)
|
||||
floor_col.shape = fs
|
||||
floor_col.position = Vector3(0, -0.2, 0)
|
||||
floor_body.add_child(floor_col)
|
||||
var floor_mi := MeshInstance3D.new()
|
||||
var fpm := PlaneMesh.new()
|
||||
fpm.size = Vector2(24, 24)
|
||||
floor_mi.mesh = fpm
|
||||
# Bewusst glatt und dunkel: eine Maserung auf der riesigen Fläche wird aus
|
||||
# Kamerahöhe zu Rauschen, und der Nebel soll den Boden ohnehin schlucken.
|
||||
floor_mi.material_override = _mat(Color(0.08, 0.07, 0.09), 0.9)
|
||||
floor_body.add_child(floor_mi)
|
||||
add_child(floor_body)
|
||||
|
||||
# Tischplatte
|
||||
var table := StaticBody3D.new()
|
||||
var tcol := CollisionShape3D.new()
|
||||
# Tischplatte: Holz mit runder Kante, darauf die Filzeinlage.
|
||||
var tisch := MeshInstance3D.new()
|
||||
tisch.mesh = Formen.scheibe(table_r, 0.10, 0.045)
|
||||
tisch.position = Vector3(0, TABLE_H - 0.10, 0)
|
||||
tisch.material_override = Stoffe.holz()
|
||||
add_child(tisch)
|
||||
var einlage := MeshInstance3D.new()
|
||||
einlage.mesh = Formen.scheibe(table_r - 0.16, 0.025, 0.012)
|
||||
einlage.position = Vector3(0, TABLE_H - 0.012, 0)
|
||||
einlage.material_override = Stoffe.filz()
|
||||
add_child(einlage)
|
||||
var tcol := StaticBody3D.new()
|
||||
var tc := CollisionShape3D.new()
|
||||
var tcs := CylinderShape3D.new()
|
||||
tcs.radius = table_r
|
||||
tcs.height = 0.12
|
||||
tcol.shape = tcs
|
||||
tcol.position = Vector3(0, TABLE_H - 0.06, 0)
|
||||
table.add_child(tcol)
|
||||
var tmi := MeshInstance3D.new()
|
||||
var tcm := CylinderMesh.new()
|
||||
tcm.top_radius = table_r
|
||||
tcm.bottom_radius = table_r
|
||||
tcm.height = 0.12
|
||||
tmi.mesh = tcm
|
||||
tmi.position = Vector3(0, TABLE_H - 0.06, 0)
|
||||
tmi.material_override = Stoffe.filz()
|
||||
table.add_child(tmi)
|
||||
add_child(table)
|
||||
tc.shape = tcs
|
||||
# Oberkante auf Höhe des Zonenfilzes, damit Würfel darauf liegen, nicht darin.
|
||||
tc.position = Vector3(0, TABLE_H - 0.048, 0)
|
||||
tcol.add_child(tc)
|
||||
add_child(tcol)
|
||||
|
||||
# Tischbein
|
||||
var leg := MeshInstance3D.new()
|
||||
var lcm := CylinderMesh.new()
|
||||
lcm.top_radius = table_r * 0.09
|
||||
lcm.bottom_radius = table_r * 0.22
|
||||
lcm.height = TABLE_H - 0.12
|
||||
leg.mesh = lcm
|
||||
leg.position = Vector3(0, (TABLE_H - 0.12) * 0.5, 0)
|
||||
leg.material_override = Stoffe.holz()
|
||||
add_child(leg)
|
||||
# Ein dicker Mittelfuss statt vier Beinen - aus Kamerahöhe sieht man ohnehin
|
||||
# nur den Ansatz, und rund passt zur Knete.
|
||||
var fuss := MeshInstance3D.new()
|
||||
fuss.mesh = Formen.rund_zylinder(0.55, 0.35, TABLE_H - 0.10, 0.06)
|
||||
fuss.material_override = Stoffe.holz()
|
||||
add_child(fuss)
|
||||
|
||||
|
||||
func _build_seat(i: int) -> void:
|
||||
@@ -180,24 +164,14 @@ func _build_seat(i: int) -> void:
|
||||
|
||||
# Filzfläche
|
||||
var felt := MeshInstance3D.new()
|
||||
var fm := CylinderMesh.new()
|
||||
fm.top_radius = ZONE_RAD
|
||||
fm.bottom_radius = ZONE_RAD
|
||||
fm.height = 0.012
|
||||
fm.radial_segments = 32
|
||||
felt.mesh = fm
|
||||
felt.position = Vector3(0, 0.006, 0)
|
||||
felt.material_override = Stoffe.filz()
|
||||
felt.mesh = Formen.scheibe(ZONE_RAD + 0.02, 0.012, 0.006)
|
||||
felt.position = Vector3(0, 0.0, 0)
|
||||
felt.material_override = Stoffe.filz_dunkel()
|
||||
zone.add_child(felt)
|
||||
|
||||
# Sichtbarer Rand
|
||||
var rim := MeshInstance3D.new()
|
||||
var tm := TorusMesh.new()
|
||||
tm.inner_radius = ZONE_RAD
|
||||
tm.outer_radius = ZONE_RAD + 0.07
|
||||
tm.rings = 32
|
||||
tm.ring_segments = 8
|
||||
rim.mesh = tm
|
||||
rim.mesh = Formen.ring(ZONE_RAD + 0.045, 0.035)
|
||||
rim.position = Vector3(0, 0.02, 0)
|
||||
rim.material_override = Stoffe.rand()
|
||||
zone.add_child(rim)
|
||||
@@ -287,6 +261,10 @@ func _frame_camera() -> void:
|
||||
var band := 1.0 - HUD_TOP - HUD_BOTTOM
|
||||
var half_v := tan(fov_v * 0.5)
|
||||
var dist := (table_r * cos(tilt)) / (band * half_v)
|
||||
# Geneigt wächst die nahe Tischhälfte durch die Perspektive; je schräger,
|
||||
# desto mehr Abstand braucht es, damit die eigene Zone nicht unter der
|
||||
# Ansageleiste verschwindet.
|
||||
dist *= 1.0 + 0.38 * sin(tilt)
|
||||
# Waagerecht ist normalerweise reichlich Platz, in schmalen Fenstern nicht.
|
||||
dist = maxf(dist, table_r / (0.98 * tan(fov_v * 0.5) * aspect))
|
||||
# Das freie Band liegt nicht mittig, der Tisch rückt entsprechend nach oben.
|
||||
@@ -527,7 +505,7 @@ func set_topf(liste: Array, ziel := -1) -> void:
|
||||
var ang := r.randf() * TAU
|
||||
var rad := r.randf_range(0.30, 0.55)
|
||||
var wappen: bool = m.get("stark", false)
|
||||
var lage := Vector3(cos(ang) * rad, TABLE_H + 0.007, sin(ang) * rad)
|
||||
var lage := Vector3(cos(ang) * rad, TABLE_H + 0.013, sin(ang) * rad) # auf der Einlage
|
||||
var mi := MeshInstance3D.new()
|
||||
mi.mesh = _muenz_form()
|
||||
mi.material_override = _muenz_stoff(wappen)
|
||||
@@ -565,15 +543,11 @@ static var _praeg_mesh: TorusMesh
|
||||
static var _praeg_mat: StandardMaterial3D
|
||||
|
||||
|
||||
static func _muenz_form() -> CylinderMesh:
|
||||
static func _muenz_form() -> ArrayMesh:
|
||||
if _muenz_mesh == null:
|
||||
_muenz_mesh = CylinderMesh.new()
|
||||
# Aus Kamerahöhe muss eine Münze fast so gross sein wie ein Würfel,
|
||||
# sonst ist sie ein Punkt.
|
||||
_muenz_mesh.top_radius = 0.11
|
||||
_muenz_mesh.bottom_radius = 0.11
|
||||
_muenz_mesh.height = 0.014
|
||||
_muenz_mesh.radial_segments = 24
|
||||
_muenz_mesh = Formen.scheibe(0.11, 0.016, 0.006)
|
||||
return _muenz_mesh
|
||||
|
||||
|
||||
|
||||
78
tests/kampagne.gd
Normal file
78
tests/kampagne.gd
Normal file
@@ -0,0 +1,78 @@
|
||||
extends "res://tests/basis.gd"
|
||||
## Kampagne: Wertung, Rang, Verschwinden und Rückkehr der Kapitäne,
|
||||
## Speichern und Laden, und eine echte Partie gegen einen Kapitän im
|
||||
## Simulationsmodus.
|
||||
|
||||
var game: Node
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
Kampagne.datei = "user://kampagne_test.json"
|
||||
Kampagne.zuruecksetzen()
|
||||
add_child(preload("res://scenes/main.tscn").instantiate())
|
||||
await get_tree().process_frame
|
||||
_lauf()
|
||||
|
||||
|
||||
func _seats(alive: Array, namen: Array) -> Array:
|
||||
var out: Array = []
|
||||
for i in namen.size():
|
||||
out.append({"name": namen[i], "alive": alive[i]})
|
||||
return out
|
||||
|
||||
|
||||
func _lauf() -> void:
|
||||
# --- Wertung und Rang
|
||||
_pruef("Start: 1000, Matrose", int(Kampagne.elo) == 1000 and Kampagne.mein_rang() == "Matrose")
|
||||
_pruef("Rang steigt mit der Wertung", Kampagne.rang(1300) == "Kapitän" and Kampagne.rang(800) == "Schiffsjunge")
|
||||
|
||||
# --- Sieg: Kapitän verschwindet, Wertung steigt
|
||||
Kampagne.aktiv = "Roter Jack"
|
||||
Kampagne.ergebnis(_seats([true, false], ["Spieler", "Roter Jack"]), [1])
|
||||
var jack: Dictionary = Kampagne.kapitaen("Roter Jack")
|
||||
_pruef("Sieg: Wertung steigt", Kampagne.elo > 1000.0)
|
||||
_pruef("Sieg: Kapitän geht auf See", int(jack.weg) == Kampagne.AUF_SEE and int(jack.geschlagen) == 1)
|
||||
_pruef("Ergebnistext steht", Kampagne.letztes.begins_with("Sieg gegen Roter Jack"))
|
||||
Kampagne.starten("Roter Jack")
|
||||
_pruef("Kein Start gegen einen auf See", Kampagne.aktiv == "" and _find(get_tree().root, "Game") == null)
|
||||
var vorher := Kampagne.elo
|
||||
|
||||
# --- Niederlage: Spieler fliegt vor dem Kapitän
|
||||
Kampagne.aktiv = "Salz-Marie"
|
||||
Kampagne.ergebnis(_seats([false, false, true], ["Spieler", "Salz-Marie", "Maat 1"]), [0, 1])
|
||||
_pruef("Niederlage: Wertung sinkt", Kampagne.elo < vorher)
|
||||
_pruef("Niederlage: Kapitänin bleibt", int(Kampagne.kapitaen("Salz-Marie").weg) == 0)
|
||||
_pruef("Jack rückt näher", int(Kampagne.kapitaen("Roter Jack").weg) == Kampagne.AUF_SEE - 1)
|
||||
|
||||
# --- Sieg, weil der Kapitän vor dem Spieler ging, obwohl ein Maat gewann
|
||||
Kampagne.aktiv = "Salz-Marie"
|
||||
Kampagne.ergebnis(_seats([false, false, true], ["Spieler", "Salz-Marie", "Maat 1"]), [1, 0])
|
||||
_pruef("Kapitän vor mir raus zählt als Sieg", int(Kampagne.kapitaen("Salz-Marie").weg) == Kampagne.AUF_SEE)
|
||||
jack = Kampagne.kapitaen("Roter Jack")
|
||||
_pruef("Jack ist zurück, stärker", int(jack.weg) == 0 and int(jack.stufe) == 1 and float(jack.elo) > 900.0)
|
||||
|
||||
# --- Speichern und Laden
|
||||
var elo_alt := Kampagne.elo
|
||||
Kampagne.speichern()
|
||||
Kampagne.elo = 0.0
|
||||
Kampagne.laden()
|
||||
_pruef("Laden stellt Wertung und Kapitäne wieder her",
|
||||
absf(Kampagne.elo - elo_alt) < 0.01 and int(Kampagne.kapitaen("Salz-Marie").weg) == Kampagne.AUF_SEE
|
||||
and int(Kampagne.kapitaen("Roter Jack").stufe) == 1)
|
||||
|
||||
# --- Echte Partie gegen Jack im Simulationsmodus
|
||||
var partien_vorher := Kampagne.partien
|
||||
Kampagne.starten("Roter Jack")
|
||||
await _warte(func(): game = _find(get_tree().root, "Game"); return game != null, 20.0)
|
||||
_pruef("Partie gegen den Kapitän steht", game != null and game.seats.size() == 2
|
||||
and String(game.seats[1].name) == "Roter Jack" and int(game.seats[1].stufe) == 1)
|
||||
game.tempo = 0.0
|
||||
game.simulation = true
|
||||
game.seats[game.local_seat].ai = true
|
||||
await _warte(func(): return game.phase == "over", 120.0)
|
||||
_pruef("Partie endet", game.phase == "over")
|
||||
_pruef("Ergebnis verbucht", Kampagne.partien == partien_vorher + 1 and Kampagne.aktiv == "" and Kampagne.letztes != "")
|
||||
_pruef("Endfeld zeigt Wertung", game.hud.over_slot.visible and Kampagne.letztes in game.hud.over_title.text
|
||||
and game.hud.over_karte.visible)
|
||||
DirAccess.remove_absolute(ProjectSettings.globalize_path(Kampagne.datei))
|
||||
_fertig()
|
||||
6
tests/kampagne.tscn
Normal file
6
tests/kampagne.tscn
Normal file
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://tests/kampagne.gd" id="1"]
|
||||
|
||||
[node name="Test" type="Node"]
|
||||
script = ExtResource("1")
|
||||
@@ -22,6 +22,7 @@ starte() { # name, timeout, argumente... -> Hintergrund
|
||||
|
||||
starte logik 90
|
||||
starte genau 90
|
||||
starte kampagne 90
|
||||
starte nochmal 60
|
||||
starte bots 90 2
|
||||
rm -f "$TMP/code.txt" "$TMP/code2.txt"
|
||||
@@ -52,7 +53,7 @@ done
|
||||
wait 2>/dev/null
|
||||
|
||||
ergebnis=0
|
||||
for name in logik genau nochmal bots mp_start:host mp_start:join mp_start:host2 mp_start:join2 mp_start:srv; do
|
||||
for name in logik genau kampagne nochmal bots mp_start:host mp_start:join mp_start:host2 mp_start:join2 mp_start:srv; do
|
||||
log="$TMP/$name.log"
|
||||
echo "=== $name"
|
||||
grep -E "^(OK |FEHL|SERVER:| |TEST:)" "$log"
|
||||
|
||||
@@ -19,6 +19,9 @@ func _ready() -> void:
|
||||
main.menu._show("main")
|
||||
await get_tree().create_timer(0.2).timeout
|
||||
await _bild("menue_start")
|
||||
main.menu._show("kampagne")
|
||||
await get_tree().create_timer(0.2).timeout
|
||||
await _bild("menue_seekarte")
|
||||
Net.start_singleplayer("Tester", 3, 1)
|
||||
_lauf()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user