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.
|
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
|
||||||
|
|
||||||
Münzen ersetzen die frühere Booster-Phase. Die kostete jede Runde eine eigene
|
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.
|
die Münzen zu seiner Zone.
|
||||||
* Klänge (Würfel, Münze, Becher, Gewinn) werden beim Start berechnet. Es gibt
|
* Klänge (Würfel, Münze, Becher, Gewinn) werden beim Start berechnet. Es gibt
|
||||||
keine Tondateien, so wie es auch keine Bilddateien gibt.
|
keine Tondateien, so wie es auch keine Bilddateien gibt.
|
||||||
* Kein Bild, kein Mesh aus einer Datei. **Stoffe** sind Regeln (`stoffe.gd`):
|
* Kein Bild, kein Mesh aus einer Datei. Der Look ist **Low-Poly wie Knete**:
|
||||||
Farbverlauf, Rauschart, Reliefstärke, Rauheit. Daraus entstehen zur
|
flache, pastellige Farben, runde Kanten, weiche Schatten, heller Himmel.
|
||||||
Laufzeit drei Texturen je Stoff - Farbe, Relief, Rauheit. Das Relief macht
|
**Stoffe** (`stoffe.gd`) sind nur Farbe und Rauheit - was Holz von Filz
|
||||||
aus gemustertem Plastik Filz, Holz und Messing. **Formen** sind Bauregeln
|
trennt, ist die Form. **Formen** (`formen.gd`) entstehen aus Profilen, die
|
||||||
(`formen.gd`): Becher = Kegelstumpf + Rand + zwei Bänder, Laterne = Sockel +
|
um die Achse gedreht werden; Fasen sind Viertelkreise im Profil. Becher =
|
||||||
Glas + Kappe. Kanten sind schmale Ringe. Der Plan steht in `assets.md`.
|
Kegelstumpf mit runden Kanten + Messingrand + Band. Der Plan steht in
|
||||||
|
`assets.md`.
|
||||||
* Auf dem Handy nur Querformat - ein Tisch von oben braucht Breite.
|
* Auf dem Handy nur Querformat - ein Tisch von oben braucht Breite.
|
||||||
* Die Regeln stehen im Menü unter **Regeln**, inklusive aller sechs Münzen.
|
* 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/ai.gd` | Bot-Entscheidungen (Binomialabschätzung für Gebot/Anfechtung) |
|
||||||
| `scripts/hud.gd` | Spiel-UI |
|
| `scripts/hud.gd` | Spiel-UI |
|
||||||
| `scripts/klang.gd` | Autoload: berechnete Klänge, keine Dateien |
|
| `scripts/klang.gd` | Autoload: berechnete Klänge, keine Dateien |
|
||||||
| `scripts/stoffe.gd` | Materialien aus Regeln, Texturen zur Laufzeit - keine Dateien |
|
| `scripts/kampagne.gd` | Autoload: Seekarte, Kapitäne, Wertung, Speicherstand |
|
||||||
| `scripts/formen.gd` | Becher, Fass, Laterne aus Primitiven nach Bauregeln |
|
| `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/icons.gd` | Gezeichnete Münz- und Würfelbilder fürs HUD |
|
||||||
| `scripts/hud_panels.gd` | Vorrat, Topf, Erklärung, Verlauf, Protokoll |
|
| `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` |
|
| `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
|
## Stand
|
||||||
|
|
||||||
- Stoffe: Filz, Holz, dunkles Holz, Messing, Knochen. Je Regel drei
|
Der Look ist **Low-Poly wie Knete** (Vorbild: pastelliges Haus-Diorama):
|
||||||
Laufzeit-Texturen aus `NoiseTexture2D`: Farbe (Rauschen durch
|
flache Farben, runde Kanten, weiche Schatten, heller Himmel. Der frühere
|
||||||
Farbverlauf), Relief (`as_normal_map`), Rauheit. Kein Shadercode.
|
Plan einer dunklen Taverne mit Maserung und Relief ist verworfen - er wirkte
|
||||||
- Formen: Becher, Fass, Laterne aus Primitiven. Kanten als Torus-Ringe.
|
aus Kamerahöhe schäbig.
|
||||||
- Gelernt: Rauschen nur als Farbe bleibt flach - das Relief macht den
|
|
||||||
Unterschied. Grobes, glänzendes Relief auf Filz sieht nach nassem Moos aus:
|
- `stoffe.gd`: eine Farbe je Stoff, matt, kaum Glanz. Keine Texturen.
|
||||||
fein, matt, kaum Höhe. Maserung entsteht durch gestreckte UV (1:9).
|
- `formen.gd`: Profile (Radius, Höhe) um die Achse gedreht, Fasen als
|
||||||
- Verworfen: Dielen auf dem Boden (aus Kamerahöhe nur Rauschen), Lampe über
|
Viertelkreise. Scheibe, runder Zylinder, Ring, Becher, Laterne.
|
||||||
der Mitte (hängt der Kamera vor der Linse), Fass am Boden (ein Fleck).
|
- 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
|
## Grundidee
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ config/features=PackedStringArray("4.7", "Mobile")
|
|||||||
BoosterDB="*res://scripts/boosters.gd"
|
BoosterDB="*res://scripts/boosters.gd"
|
||||||
Net="*res://scripts/net.gd"
|
Net="*res://scripts/net.gd"
|
||||||
Klang="*res://scripts/klang.gd"
|
Klang="*res://scripts/klang.gd"
|
||||||
|
Kampagne="*res://scripts/kampagne.gd"
|
||||||
|
|
||||||
[display]
|
[display]
|
||||||
|
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ func _phase_reveal(result: Dictionary) -> void:
|
|||||||
g.seats[l].count = 0
|
g.seats[l].count = 0
|
||||||
g.host_values[l] = []
|
g.host_values[l] = []
|
||||||
g.table.set_dice_count(l, 0)
|
g.table.set_dice_count(l, 0)
|
||||||
|
g.ausgeschieden.append(l)
|
||||||
g._log("%s scheidet aus." % g.seats[l].name)
|
g._log("%s scheidet aus." % g.seats[l].name)
|
||||||
else:
|
else:
|
||||||
g._zieh_muenze(l)
|
g._zieh_muenze(l)
|
||||||
|
|||||||
@@ -1,10 +1,76 @@
|
|||||||
extends RefCounted
|
extends RefCounted
|
||||||
## Formen aus Regeln: jedes Ding hat eine Grundform aus Godot-Primitiven, und
|
## Formen aus Regeln. Der Kern des Low-Poly-Looks sind runde Kanten: jede
|
||||||
## Regeln setzen Teile darauf - Rand, Bänder, Reifen, Kappe. Kanten sind
|
## Form wird als Profil (Radius, Höhe) beschrieben und um die Achse gedreht.
|
||||||
## schmale Ringe statt Fasen. Kein Mesh kommt aus einer Datei.
|
## Fasen sind Viertelkreise im Profil. Kein Mesh kommt aus einer Datei.
|
||||||
## Kein class_name: den Klassencache baut nur der Editor, headless fehlt er.
|
## Kein class_name: den Klassencache baut nur der Editor, headless fehlt er.
|
||||||
|
|
||||||
const Stoffe = preload("res://scripts/stoffe.gd")
|
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:
|
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
|
return mi
|
||||||
|
|
||||||
|
|
||||||
static func _zylinder(oben: float, unten: float, hoehe: float, segmente := 28) -> CylinderMesh:
|
## Würfelbecher: Kegelstumpf mit runden Kanten, oben ein Messingrand, ein
|
||||||
var c := CylinderMesh.new()
|
## Band um den Bauch. Geschlossen - die Kamera schaut von oben.
|
||||||
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.
|
|
||||||
static func becher(hoehe := 0.44, oben := 0.33, unten := 0.42) -> Node3D:
|
static func becher(hoehe := 0.44, oben := 0.33, unten := 0.42) -> Node3D:
|
||||||
var n := Node3D.new()
|
var n := Node3D.new()
|
||||||
_teil(_zylinder(oben, unten, hoehe), Stoffe.holz_dunkel(), Vector3(0, hoehe * 0.5, 0), n)
|
_teil(rund_zylinder(unten, oben, hoehe, 0.05), Stoffe.holz_dunkel(), Vector3.ZERO, n)
|
||||||
_teil(_ring(oben, 0.012), Stoffe.messing(), Vector3(0, hoehe - 0.004, 0), n)
|
_teil(ring(oben - 0.01, 0.018), Stoffe.messing(), Vector3(0, hoehe - 0.02, 0), n)
|
||||||
for k in [0.22, 0.62]:
|
_teil(ring(lerpf(unten, oben, 0.35) + 0.004, 0.012), Stoffe.messing(), Vector3(0, hoehe * 0.35, 0), n)
|
||||||
var r: float = lerpf(unten, oben, k)
|
|
||||||
_teil(_ring(r, 0.008), Stoffe.messing(), Vector3(0, hoehe * k, 0), n)
|
|
||||||
return n
|
return n
|
||||||
|
|
||||||
|
|
||||||
## Fass: bauchiger Zylinder, drei Reifen, Boden und Deckel etwas kleiner.
|
## Laterne: Sockel, leuchtendes Glas, Kappe, Bügel.
|
||||||
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.
|
|
||||||
static func laterne(hoehe := 0.5) -> Node3D:
|
static func laterne(hoehe := 0.5) -> Node3D:
|
||||||
var n := Node3D.new()
|
var n := Node3D.new()
|
||||||
_teil(_zylinder(0.09, 0.11, 0.05), Stoffe.messing(), Vector3(0, 0.025, 0), n)
|
_teil(rund_zylinder(0.11, 0.09, 0.05, 0.015), Stoffe.messing(), Vector3.ZERO, n)
|
||||||
var glas := StandardMaterial3D.new()
|
var glas := Stoffe.eigene("glas")
|
||||||
glas.albedo_color = Color(1.0, 0.85, 0.5, 0.75)
|
|
||||||
glas.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
|
||||||
glas.emission_enabled = true
|
glas.emission_enabled = true
|
||||||
glas.emission = Color(1.0, 0.75, 0.35)
|
glas.emission = Color(1.0, 0.80, 0.40)
|
||||||
glas.emission_energy_multiplier = 2.5
|
glas.emission_energy_multiplier = 1.6
|
||||||
_teil(_zylinder(0.07, 0.08, hoehe * 0.6), glas, Vector3(0, 0.05 + hoehe * 0.3, 0), n)
|
_teil(rund_zylinder(0.08, 0.07, hoehe * 0.6, 0.02), glas, Vector3(0, 0.05, 0), n)
|
||||||
_teil(_zylinder(0.03, 0.10, 0.08), Stoffe.messing(), Vector3(0, 0.05 + hoehe * 0.6 + 0.04, 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.006), Stoffe.messing(), Vector3(0, 0.05 + hoehe * 0.6 + 0.10, 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()
|
var licht := OmniLight3D.new()
|
||||||
licht.light_color = Color(1.0, 0.8, 0.45)
|
licht.light_color = Color(1.0, 0.85, 0.55)
|
||||||
licht.light_energy = 1.2
|
licht.light_energy = 0.8
|
||||||
licht.omni_range = 1.8
|
licht.omni_range = 1.6
|
||||||
licht.position = Vector3(0, 0.05 + hoehe * 0.3, 0)
|
licht.position = Vector3(0, 0.05 + hoehe * 0.3, 0)
|
||||||
n.add_child(licht)
|
n.add_child(licht)
|
||||||
return n
|
return n
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ var dice_start := START_DICE
|
|||||||
## Nur für `make sim`; fünfzig Partien in Sekunden statt Minuten.
|
## Nur für `make sim`; fünfzig Partien in Sekunden statt Minuten.
|
||||||
var simulation := false
|
var simulation := false
|
||||||
var runde := 0 ## laufende Rundennummer, für Anzeige und Messung
|
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 rng := RandomNumberGenerator.new()
|
||||||
var seats: Array = [] ## [{peer, name, ai, count, alive}]
|
var seats: Array = [] ## [{peer, name, ai, count, alive}]
|
||||||
@@ -67,6 +69,7 @@ func _ready() -> void:
|
|||||||
|
|
||||||
|
|
||||||
func setup(cfg: Dictionary) -> void:
|
func setup(cfg: Dictionary) -> void:
|
||||||
|
cfg_start = cfg.duplicate(true)
|
||||||
DiceAI.stufe = int(cfg.get("stufe", 1))
|
DiceAI.stufe = int(cfg.get("stufe", 1))
|
||||||
seats.clear()
|
seats.clear()
|
||||||
if cfg.has("seats"):
|
if cfg.has("seats"):
|
||||||
@@ -83,8 +86,11 @@ func setup(cfg: Dictionary) -> void:
|
|||||||
for p in peers:
|
for p in peers:
|
||||||
seats.append({"peer": int(p), "name": String(cfg.names[p]), "ai": false,
|
seats.append({"peer": int(p), "name": String(cfg.names[p]), "ai": false,
|
||||||
"count": dice, "alive": true, "verwaist": false})
|
"count": dice, "alive": true, "verwaist": false})
|
||||||
|
var bots: Array = cfg.get("bots", [])
|
||||||
for i in int(cfg.ai):
|
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})
|
"count": dice, "alive": true, "verwaist": false})
|
||||||
|
|
||||||
# Der Spielserver spielt nicht mit und bekommt deshalb keinen Platz.
|
# 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.challenge_pressed.connect(_on_challenge_pressed)
|
||||||
hud.exact_pressed.connect(_on_exact_pressed)
|
hud.exact_pressed.connect(_on_exact_pressed)
|
||||||
hud.rematch_pressed.connect(_on_rematch)
|
hud.rematch_pressed.connect(_on_rematch)
|
||||||
|
hud.karte_pressed.connect(_on_karte)
|
||||||
hud.booster_chosen.connect(_on_booster_chosen)
|
hud.booster_chosen.connect(_on_booster_chosen)
|
||||||
hud.quit_pressed.connect(_on_quit)
|
hud.quit_pressed.connect(_on_quit)
|
||||||
hud.pause_changed.connect(_on_pause_changed)
|
hud.pause_changed.connect(_on_pause_changed)
|
||||||
@@ -209,6 +216,7 @@ func _host_loop() -> void:
|
|||||||
winner = s.name
|
winner = s.name
|
||||||
banner = "%s gewinnt" % winner
|
banner = "%s gewinnt" % winner
|
||||||
_log(banner)
|
_log(banner)
|
||||||
|
Kampagne.ergebnis(seats, ausgeschieden)
|
||||||
_push_state()
|
_push_state()
|
||||||
|
|
||||||
|
|
||||||
@@ -433,6 +441,7 @@ func _phase_bid() -> Dictionary:
|
|||||||
await _sleep(1.1)
|
await _sleep(1.1)
|
||||||
# Erst planen, dann passend dazu eine Münze werfen. Die kann die
|
# Erst planen, dann passend dazu eine Münze werfen. Die kann die
|
||||||
# Lage ändern - also danach noch einmal entscheiden.
|
# 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,
|
var a := DiceAI.choose_action(host_values[turn], _total_dice(), bid,
|
||||||
must_bid.get(turn, false), rng, _bot_wissen(turn))
|
must_bid.get(turn, false), rng, _bot_wissen(turn))
|
||||||
if await _bot_muenze(turn, a):
|
if await _bot_muenze(turn, a):
|
||||||
@@ -788,7 +797,19 @@ func _on_rematch() -> void:
|
|||||||
for s in seats:
|
for s in seats:
|
||||||
if s.ai:
|
if s.ai:
|
||||||
bots += 1
|
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()
|
_on_quit()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ signal bid_pressed(count: int, face: int)
|
|||||||
signal challenge_pressed
|
signal challenge_pressed
|
||||||
signal exact_pressed
|
signal exact_pressed
|
||||||
signal rematch_pressed
|
signal rematch_pressed
|
||||||
|
signal karte_pressed
|
||||||
signal booster_chosen(id: String, target: int)
|
signal booster_chosen(id: String, target: int)
|
||||||
signal quit_pressed
|
signal quit_pressed
|
||||||
signal pause_changed(paused: bool)
|
signal pause_changed(paused: bool)
|
||||||
@@ -54,6 +55,7 @@ var pause_slot: Control
|
|||||||
var over_slot: Control
|
var over_slot: Control
|
||||||
var over_title: Label
|
var over_title: Label
|
||||||
var over_again: Button
|
var over_again: Button
|
||||||
|
var over_karte: Button
|
||||||
var pause_hint: Label
|
var pause_hint: Label
|
||||||
var boost_panel: PanelContainer
|
var boost_panel: PanelContainer
|
||||||
var boost_cards: HBoxContainer
|
var boost_cards: HBoxContainer
|
||||||
@@ -178,9 +180,12 @@ func _build() -> void:
|
|||||||
|
|
||||||
# --- Letzte Wette in der Tischmitte, als so viele Würfel wie angesagt
|
# --- 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_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 = VBoxContainer.new()
|
||||||
bid_box.add_theme_constant_override("separation", 8)
|
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 = _label("", 21, ACCENT)
|
||||||
bid_name.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
bid_name.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
# Auf die freie Mitte begrenzen, sonst läuft der Text über die Nachbarplätze.
|
# 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)
|
root.add_child(banner_label)
|
||||||
|
|
||||||
# --- Wer ist dran: direkt über der Ansageleiste, wo der Blick ohnehin liegt
|
# --- 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 = _label("", 19, ACCENT)
|
||||||
turn_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
turn_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
turn_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
turn_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
_place(turn_label, 0, 1, 1, 1, 16, -146, -16, -114)
|
tp.add_child(turn_label)
|
||||||
root.add_child(turn_label)
|
|
||||||
|
|
||||||
# --- Ansage: kompakte Leiste unten in der Mitte
|
# --- Ansage: kompakte Leiste unten in der Mitte
|
||||||
action_panel = _panel()
|
action_panel = _panel()
|
||||||
@@ -312,10 +320,12 @@ func _build() -> void:
|
|||||||
# den Tisch; hier steht nur, wer was geworfen hat und wie es fiel.
|
# 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 = _slot(0, 0, 0, 0, 16, 100, 336, 200)
|
||||||
wurf_slot.visible = false
|
wurf_slot.visible = false
|
||||||
|
var wp := _panel()
|
||||||
|
wurf_slot.add_child(wp)
|
||||||
wurf_label = _label("", 16, ACCENT)
|
wurf_label = _label("", 16, ACCENT)
|
||||||
wurf_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
wurf_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
wurf_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
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.
|
# --- Protokoll unten rechts: was der Topf tut, wer geht, wer zurückkommt.
|
||||||
var lp := _panel()
|
var lp := _panel()
|
||||||
@@ -427,6 +437,12 @@ func _build() -> void:
|
|||||||
over_again.custom_minimum_size = Vector2(0, 42)
|
over_again.custom_minimum_size = Vector2(0, 42)
|
||||||
over_again.pressed.connect(func() -> void: rematch_pressed.emit())
|
over_again.pressed.connect(func() -> void: rematch_pressed.emit())
|
||||||
ov.add_child(over_again)
|
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()
|
var menue := Button.new()
|
||||||
menue.text = "Zum Menü"
|
menue.text = "Zum Menü"
|
||||||
menue.custom_minimum_size = Vector2(0, 42)
|
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
|
banner_label.text = "" if st.phase == "over" else st.banner
|
||||||
if st.phase == "over" and not over_slot.visible:
|
if st.phase == "over" and not over_slot.visible:
|
||||||
over_title.text = String(st.banner)
|
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
|
# Nochmal: solo immer, im Netz nur für den Raumbesitzer - der Server
|
||||||
# setzt den Raum dann für alle neu auf.
|
# setzt den Raum dann für alle neu auf.
|
||||||
over_again.visible = Net.room_code == Net.SOLO or Net.is_owner
|
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():
|
if not Net.rematch.is_empty():
|
||||||
var r: Dictionary = Net.rematch
|
var r: Dictionary = Net.rematch
|
||||||
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.
|
## 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 solo_stufe: OptionButton
|
||||||
var lobby_stufe: OptionButton
|
var lobby_stufe: OptionButton
|
||||||
|
|
||||||
|
# Kampagne
|
||||||
|
var kamp_kopf: Label
|
||||||
|
var karte: Control
|
||||||
|
|
||||||
# Mehrspieler
|
# Mehrspieler
|
||||||
var name_edit: LineEdit
|
var name_edit: LineEdit
|
||||||
var code_edit: LineEdit
|
var code_edit: LineEdit
|
||||||
@@ -41,11 +45,16 @@ func _ready() -> void:
|
|||||||
screens["join"] = _screen(root, _build_join())
|
screens["join"] = _screen(root, _build_join())
|
||||||
screens["lobby"] = _screen(root, _build_lobby())
|
screens["lobby"] = _screen(root, _build_lobby())
|
||||||
screens["regeln"] = _screen(root, _build_regeln())
|
screens["regeln"] = _screen(root, _build_regeln())
|
||||||
|
screens["kampagne"] = _screen(root, _build_kampagne())
|
||||||
|
|
||||||
Net.lobby_changed.connect(_refresh)
|
Net.lobby_changed.connect(_refresh)
|
||||||
Net.connection_failed.connect(_on_failed)
|
Net.connection_failed.connect(_on_failed)
|
||||||
Net.server_closed.connect(_on_closed)
|
Net.server_closed.connect(_on_closed)
|
||||||
_show("main")
|
if Kampagne.zur_karte:
|
||||||
|
Kampagne.zur_karte = false
|
||||||
|
_show("kampagne")
|
||||||
|
else:
|
||||||
|
_show("main")
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------ Aufbau
|
# ------------------------------------------------------------ Aufbau
|
||||||
@@ -63,6 +72,8 @@ func _show(which: String) -> void:
|
|||||||
for key: String in screens:
|
for key: String in screens:
|
||||||
screens[key].visible = key == which
|
screens[key].visible = key == which
|
||||||
_sagen("")
|
_sagen("")
|
||||||
|
if which == "kampagne":
|
||||||
|
_karte_fuellen()
|
||||||
if which == "main" and zurueck_btn != null:
|
if which == "main" and zurueck_btn != null:
|
||||||
zurueck_btn.visible = not Net.letzter_raum.is_empty()
|
zurueck_btn.visible = not Net.letzter_raum.is_empty()
|
||||||
if zurueck_btn.visible:
|
if zurueck_btn.visible:
|
||||||
@@ -93,6 +104,82 @@ func _text(t: String, size: int, col := Color.WHITE) -> Label:
|
|||||||
return l
|
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:
|
func _on_wieder_rein() -> void:
|
||||||
_show("join")
|
_show("join")
|
||||||
_sagen("Verbinde mit Raum %s …" % Net.letzter_raum.get("code", ""))
|
_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.
|
# Nach einem Verbindungsabbruch: derselbe Name, derselbe Code, ein Klick.
|
||||||
zurueck_btn = _button("", box, _on_wieder_rein)
|
zurueck_btn = _button("", box, _on_wieder_rein)
|
||||||
zurueck_btn.visible = false
|
zurueck_btn.visible = false
|
||||||
|
_button("Kampagne", box, _show.bind("kampagne"))
|
||||||
_button("Einzelspieler", box, _show.bind("single"))
|
_button("Einzelspieler", box, _show.bind("single"))
|
||||||
_button("Mehrspieler", box, _show.bind("multi"))
|
_button("Mehrspieler", box, _show.bind("multi"))
|
||||||
_button("Regeln", box, _show.bind("regeln"))
|
_button("Regeln", box, _show.bind("regeln"))
|
||||||
@@ -330,6 +418,7 @@ func _build_lobby() -> Control:
|
|||||||
# ------------------------------------------------------------ Aktionen
|
# ------------------------------------------------------------ Aktionen
|
||||||
|
|
||||||
func _on_solo_start() -> void:
|
func _on_solo_start() -> void:
|
||||||
|
Kampagne.kampagnenpartie = false
|
||||||
Net.start_singleplayer("Spieler", solo_bots.value, solo_stufe.selected)
|
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 ────────────────────────────────────────────────────────────────────
|
# ── Start ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
## Einzelspieler: kein Netzwerk, nur der lokale Spieler und Bots.
|
## 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
|
player_name = pname
|
||||||
multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new()
|
multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new()
|
||||||
room_code = SOLO
|
room_code = SOLO
|
||||||
@@ -268,7 +270,7 @@ func start_singleplayer(pname: String, ai_count: int, stufe := 1, dice := 5) ->
|
|||||||
in_game = true
|
in_game = true
|
||||||
lobby_changed.emit()
|
lobby_changed.emit()
|
||||||
game_started.emit(SOLO, {"peers": [1], "names": lobby.duplicate(), "ai": ai_count,
|
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.
|
## Nach einem Abbruch mit demselben Namen zurück in die laufende Partie.
|
||||||
|
|||||||
@@ -1,113 +1,38 @@
|
|||||||
extends RefCounted
|
extends RefCounted
|
||||||
## Materialien aus Regeln, ohne eine einzige Datei. Je Stoff ein Eintrag:
|
## Materialien für den Low-Poly-Look: eine flache Farbe, matt, kein Muster,
|
||||||
## Farbverlauf, Rauschart, Reliefstärke, Rauheit, Maßstab. Daraus baut
|
## kein Relief. Was "Holz" von "Filz" unterscheidet, ist die Farbe und die
|
||||||
## `mach()` ein StandardMaterial3D mit drei zur Laufzeit erzeugten Texturen:
|
## Form - nicht eine Textur. Die Palette ist pastellig und satt, wie Knete.
|
||||||
## Farbe (Rauschen durch einen Farbverlauf), Relief (dasselbe Rauschen als
|
|
||||||
## Normalenkarte) und Rauheit. Das Relief ist der Unterschied zwischen
|
|
||||||
## "gemustertes Plastik" und Filz, Holz, Messing.
|
|
||||||
##
|
|
||||||
## Kein class_name: den Klassencache baut nur der Editor, headless fehlt er.
|
## Kein class_name: den Klassencache baut nur der Editor, headless fehlt er.
|
||||||
|
|
||||||
const STOFFE := {
|
const FARBEN := {
|
||||||
"filz": {
|
"filz": Color(0.33, 0.70, 0.42),
|
||||||
# Feiner Flaum, kaum Relief, ganz matt. Grobes, glänzendes Relief sah
|
"filz_dunkel": Color(0.26, 0.60, 0.36),
|
||||||
# nach nassem Moos aus.
|
"holz": Color(0.86, 0.68, 0.46),
|
||||||
"farben": [Color(0.06, 0.18, 0.11), Color(0.08, 0.23, 0.14), Color(0.10, 0.26, 0.16)],
|
"holz_dunkel": Color(0.76, 0.42, 0.30),
|
||||||
"art": FastNoiseLite.TYPE_SIMPLEX_SMOOTH, "freq": 0.35, "oktaven": 3,
|
"holz_rand": Color(0.60, 0.40, 0.26),
|
||||||
"relief": 0.12, "rauheit": [0.96, 1.0], "metall": 0.0, "uv": Vector3(14, 14, 1),
|
"messing": Color(0.96, 0.80, 0.36),
|
||||||
"glanz": 0.05,
|
"knochen": Color(0.99, 0.96, 0.90),
|
||||||
},
|
"boden": Color(0.90, 0.84, 0.72),
|
||||||
"holz": {
|
"glas": Color(1.0, 0.92, 0.55),
|
||||||
"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),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static var _fertig: Dictionary = {} ## name -> StandardMaterial3D, einmal gebaut
|
static var _fertig: Dictionary = {}
|
||||||
|
|
||||||
|
|
||||||
static func _rauschen(r: Dictionary, seed: int) -> FastNoiseLite:
|
## Ein Stoff, eine Instanz: gleiche Farbe teilt das Material.
|
||||||
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.
|
|
||||||
static func mach(name: String) -> StandardMaterial3D:
|
static func mach(name: String) -> StandardMaterial3D:
|
||||||
if _fertig.has(name):
|
if _fertig.has(name):
|
||||||
return _fertig[name]
|
return _fertig[name]
|
||||||
var r: Dictionary = STOFFE[name]
|
|
||||||
var m := StandardMaterial3D.new()
|
var m := StandardMaterial3D.new()
|
||||||
var basis := _rauschen(r, name.hash() % 1000)
|
m.albedo_color = FARBEN[name]
|
||||||
|
m.roughness = 0.85
|
||||||
if not r.farben.is_empty():
|
m.metallic = 0.0
|
||||||
var verlauf := Gradient.new()
|
# Fast kein Glanzpunkt: Knete glänzt nicht, sie leuchtet vom Licht her.
|
||||||
var farben: Array = r.farben
|
m.metallic_specular = 0.15
|
||||||
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
|
|
||||||
_fertig[name] = m
|
_fertig[name] = m
|
||||||
return m
|
return m
|
||||||
|
|
||||||
|
|
||||||
## Eigene Instanz, wenn ein Objekt Farbe oder Leuchten ändern muss.
|
|
||||||
static func eigene(name: String) -> StandardMaterial3D:
|
static func eigene(name: String) -> StandardMaterial3D:
|
||||||
return mach(name).duplicate()
|
return mach(name).duplicate()
|
||||||
|
|
||||||
@@ -116,6 +41,10 @@ static func filz() -> StandardMaterial3D:
|
|||||||
return mach("filz")
|
return mach("filz")
|
||||||
|
|
||||||
|
|
||||||
|
static func filz_dunkel() -> StandardMaterial3D:
|
||||||
|
return mach("filz_dunkel")
|
||||||
|
|
||||||
|
|
||||||
static func holz() -> StandardMaterial3D:
|
static func holz() -> StandardMaterial3D:
|
||||||
return mach("holz")
|
return mach("holz")
|
||||||
|
|
||||||
@@ -124,22 +53,36 @@ static func holz_dunkel() -> StandardMaterial3D:
|
|||||||
return mach("holz_dunkel")
|
return mach("holz_dunkel")
|
||||||
|
|
||||||
|
|
||||||
|
static func boden() -> StandardMaterial3D:
|
||||||
|
return mach("boden")
|
||||||
|
|
||||||
|
|
||||||
static func messing(emission := 0.0) -> StandardMaterial3D:
|
static func messing(emission := 0.0) -> StandardMaterial3D:
|
||||||
|
if emission <= 0.0:
|
||||||
|
return mach("messing")
|
||||||
var m := eigene("messing")
|
var m := eigene("messing")
|
||||||
if emission > 0.0:
|
m.emission_enabled = true
|
||||||
m.emission_enabled = true
|
m.emission = Color(1.0, 0.80, 0.35)
|
||||||
m.emission = Color(1.0, 0.75, 0.30)
|
m.emission_energy_multiplier = emission
|
||||||
m.emission_energy_multiplier = emission
|
|
||||||
return m
|
return m
|
||||||
|
|
||||||
|
|
||||||
|
## Würfel: Farbe kommt aus den Vertexfarben (Körper hell, Augen dunkel).
|
||||||
static func knochen() -> StandardMaterial3D:
|
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:
|
static func rand() -> StandardMaterial3D:
|
||||||
var m := eigene("holz")
|
var m := eigene("holz_rand")
|
||||||
m.emission = Color(1.0, 0.78, 0.30)
|
m.emission = Color(1.0, 0.78, 0.30)
|
||||||
m.emission_enabled = false
|
m.emission_enabled = false
|
||||||
return m
|
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 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_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 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_TOP := 0.13 ## Bildanteil oben für die Gebotsanzeige
|
||||||
const HUD_BOTTOM := 0.17 ## Bildanteil unten für die Ansage-Leiste
|
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 plates: Array[Sprite3D] = []
|
||||||
var rims: Array[MeshInstance3D] = [] ## Rand der Zone, leuchtet beim Zug
|
var rims: Array[MeshInstance3D] = [] ## Rand der Zone, leuchtet beim Zug
|
||||||
var topf_muenzen: Dictionary = {} ## Wurfnummer -> Münze in der Tischmitte
|
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
|
static var _muenz_mat: StandardMaterial3D
|
||||||
var dice: Array = [] ## seat -> Array[Die]
|
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.
|
## Formregeln bleibt weg: vom Boden aus ist es aus Kamerahöhe nur ein Fleck.
|
||||||
func _deko() -> void:
|
func _deko() -> void:
|
||||||
var laterne := Formen.laterne()
|
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)
|
add_child(laterne)
|
||||||
|
|
||||||
|
|
||||||
func _build_room() -> void:
|
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 env := WorldEnvironment.new()
|
||||||
var e := Environment.new()
|
var e := Environment.new()
|
||||||
e.background_mode = Environment.BG_COLOR
|
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_source = Environment.AMBIENT_SOURCE_COLOR
|
||||||
e.ambient_light_color = Color(0.35, 0.38, 0.45)
|
e.ambient_light_color = Color(0.70, 0.78, 0.92)
|
||||||
e.ambient_light_energy = 1.1
|
e.ambient_light_energy = 0.75
|
||||||
e.tonemap_mode = Environment.TONE_MAPPER_FILMIC
|
e.tonemap_mode = Environment.TONE_MAPPER_FILMIC
|
||||||
e.glow_enabled = true
|
e.tonemap_exposure = 0.9
|
||||||
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
|
|
||||||
env.environment = e
|
env.environment = e
|
||||||
add_child(env)
|
add_child(env)
|
||||||
|
|
||||||
var lamp := SpotLight3D.new()
|
var sonne := DirectionalLight3D.new()
|
||||||
lamp.position = Vector3(0, TABLE_H + table_r * 1.45, 0)
|
sonne.light_color = Color(1.0, 0.96, 0.90)
|
||||||
lamp.rotation_degrees = Vector3(-90, 0, 0)
|
sonne.light_energy = 1.1
|
||||||
lamp.light_energy = 2.5
|
sonne.shadow_enabled = true
|
||||||
lamp.light_color = Color(1.0, 0.94, 0.82)
|
sonne.light_angular_distance = 3.0 # weiche Schattenkanten
|
||||||
lamp.spot_range = table_r * 4.2
|
sonne.directional_shadow_mode = DirectionalLight3D.SHADOW_ORTHOGONAL
|
||||||
lamp.spot_angle = 60.0
|
sonne.directional_shadow_max_distance = 30.0
|
||||||
lamp.spot_attenuation = 0.6
|
sonne.rotation_degrees = Vector3(-52.0, 35.0, 0.0)
|
||||||
lamp.shadow_enabled = true
|
add_child(sonne)
|
||||||
add_child(lamp)
|
|
||||||
|
|
||||||
# Kein sichtbarer Lampenschirm: die Kamera schaut fast senkrecht von oben,
|
# Sockelplatte wie ein Diorama: eine runde Scheibe mit weicher Kante,
|
||||||
# alles über der Tischmitte hinge ihr vor der Linse.
|
# 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()
|
# Tischplatte: Holz mit runder Kante, darauf die Filzeinlage.
|
||||||
fill.rotation_degrees = Vector3(-55, 35, 0)
|
var tisch := MeshInstance3D.new()
|
||||||
fill.light_energy = 0.25
|
tisch.mesh = Formen.scheibe(table_r, 0.10, 0.045)
|
||||||
add_child(fill)
|
tisch.position = Vector3(0, TABLE_H - 0.10, 0)
|
||||||
|
tisch.material_override = Stoffe.holz()
|
||||||
# Boden
|
add_child(tisch)
|
||||||
var floor_body := StaticBody3D.new()
|
var einlage := MeshInstance3D.new()
|
||||||
var floor_col := CollisionShape3D.new()
|
einlage.mesh = Formen.scheibe(table_r - 0.16, 0.025, 0.012)
|
||||||
var fs := BoxShape3D.new()
|
einlage.position = Vector3(0, TABLE_H - 0.012, 0)
|
||||||
fs.size = Vector3(24, 0.4, 24)
|
einlage.material_override = Stoffe.filz()
|
||||||
floor_col.shape = fs
|
add_child(einlage)
|
||||||
floor_col.position = Vector3(0, -0.2, 0)
|
var tcol := StaticBody3D.new()
|
||||||
floor_body.add_child(floor_col)
|
var tc := CollisionShape3D.new()
|
||||||
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()
|
|
||||||
var tcs := CylinderShape3D.new()
|
var tcs := CylinderShape3D.new()
|
||||||
tcs.radius = table_r
|
tcs.radius = table_r
|
||||||
tcs.height = 0.12
|
tcs.height = 0.12
|
||||||
tcol.shape = tcs
|
tc.shape = tcs
|
||||||
tcol.position = Vector3(0, TABLE_H - 0.06, 0)
|
# Oberkante auf Höhe des Zonenfilzes, damit Würfel darauf liegen, nicht darin.
|
||||||
table.add_child(tcol)
|
tc.position = Vector3(0, TABLE_H - 0.048, 0)
|
||||||
var tmi := MeshInstance3D.new()
|
tcol.add_child(tc)
|
||||||
var tcm := CylinderMesh.new()
|
add_child(tcol)
|
||||||
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)
|
|
||||||
|
|
||||||
# Tischbein
|
# Ein dicker Mittelfuss statt vier Beinen - aus Kamerahöhe sieht man ohnehin
|
||||||
var leg := MeshInstance3D.new()
|
# nur den Ansatz, und rund passt zur Knete.
|
||||||
var lcm := CylinderMesh.new()
|
var fuss := MeshInstance3D.new()
|
||||||
lcm.top_radius = table_r * 0.09
|
fuss.mesh = Formen.rund_zylinder(0.55, 0.35, TABLE_H - 0.10, 0.06)
|
||||||
lcm.bottom_radius = table_r * 0.22
|
fuss.material_override = Stoffe.holz()
|
||||||
lcm.height = TABLE_H - 0.12
|
add_child(fuss)
|
||||||
leg.mesh = lcm
|
|
||||||
leg.position = Vector3(0, (TABLE_H - 0.12) * 0.5, 0)
|
|
||||||
leg.material_override = Stoffe.holz()
|
|
||||||
add_child(leg)
|
|
||||||
|
|
||||||
|
|
||||||
func _build_seat(i: int) -> void:
|
func _build_seat(i: int) -> void:
|
||||||
@@ -180,24 +164,14 @@ func _build_seat(i: int) -> void:
|
|||||||
|
|
||||||
# Filzfläche
|
# Filzfläche
|
||||||
var felt := MeshInstance3D.new()
|
var felt := MeshInstance3D.new()
|
||||||
var fm := CylinderMesh.new()
|
felt.mesh = Formen.scheibe(ZONE_RAD + 0.02, 0.012, 0.006)
|
||||||
fm.top_radius = ZONE_RAD
|
felt.position = Vector3(0, 0.0, 0)
|
||||||
fm.bottom_radius = ZONE_RAD
|
felt.material_override = Stoffe.filz_dunkel()
|
||||||
fm.height = 0.012
|
|
||||||
fm.radial_segments = 32
|
|
||||||
felt.mesh = fm
|
|
||||||
felt.position = Vector3(0, 0.006, 0)
|
|
||||||
felt.material_override = Stoffe.filz()
|
|
||||||
zone.add_child(felt)
|
zone.add_child(felt)
|
||||||
|
|
||||||
# Sichtbarer Rand
|
# Sichtbarer Rand
|
||||||
var rim := MeshInstance3D.new()
|
var rim := MeshInstance3D.new()
|
||||||
var tm := TorusMesh.new()
|
rim.mesh = Formen.ring(ZONE_RAD + 0.045, 0.035)
|
||||||
tm.inner_radius = ZONE_RAD
|
|
||||||
tm.outer_radius = ZONE_RAD + 0.07
|
|
||||||
tm.rings = 32
|
|
||||||
tm.ring_segments = 8
|
|
||||||
rim.mesh = tm
|
|
||||||
rim.position = Vector3(0, 0.02, 0)
|
rim.position = Vector3(0, 0.02, 0)
|
||||||
rim.material_override = Stoffe.rand()
|
rim.material_override = Stoffe.rand()
|
||||||
zone.add_child(rim)
|
zone.add_child(rim)
|
||||||
@@ -287,6 +261,10 @@ func _frame_camera() -> void:
|
|||||||
var band := 1.0 - HUD_TOP - HUD_BOTTOM
|
var band := 1.0 - HUD_TOP - HUD_BOTTOM
|
||||||
var half_v := tan(fov_v * 0.5)
|
var half_v := tan(fov_v * 0.5)
|
||||||
var dist := (table_r * cos(tilt)) / (band * half_v)
|
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.
|
# Waagerecht ist normalerweise reichlich Platz, in schmalen Fenstern nicht.
|
||||||
dist = maxf(dist, table_r / (0.98 * tan(fov_v * 0.5) * aspect))
|
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.
|
# 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 ang := r.randf() * TAU
|
||||||
var rad := r.randf_range(0.30, 0.55)
|
var rad := r.randf_range(0.30, 0.55)
|
||||||
var wappen: bool = m.get("stark", false)
|
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()
|
var mi := MeshInstance3D.new()
|
||||||
mi.mesh = _muenz_form()
|
mi.mesh = _muenz_form()
|
||||||
mi.material_override = _muenz_stoff(wappen)
|
mi.material_override = _muenz_stoff(wappen)
|
||||||
@@ -565,15 +543,11 @@ static var _praeg_mesh: TorusMesh
|
|||||||
static var _praeg_mat: StandardMaterial3D
|
static var _praeg_mat: StandardMaterial3D
|
||||||
|
|
||||||
|
|
||||||
static func _muenz_form() -> CylinderMesh:
|
static func _muenz_form() -> ArrayMesh:
|
||||||
if _muenz_mesh == null:
|
if _muenz_mesh == null:
|
||||||
_muenz_mesh = CylinderMesh.new()
|
|
||||||
# Aus Kamerahöhe muss eine Münze fast so gross sein wie ein Würfel,
|
# Aus Kamerahöhe muss eine Münze fast so gross sein wie ein Würfel,
|
||||||
# sonst ist sie ein Punkt.
|
# sonst ist sie ein Punkt.
|
||||||
_muenz_mesh.top_radius = 0.11
|
_muenz_mesh = Formen.scheibe(0.11, 0.016, 0.006)
|
||||||
_muenz_mesh.bottom_radius = 0.11
|
|
||||||
_muenz_mesh.height = 0.014
|
|
||||||
_muenz_mesh.radial_segments = 24
|
|
||||||
return _muenz_mesh
|
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 logik 90
|
||||||
starte genau 90
|
starte genau 90
|
||||||
|
starte kampagne 90
|
||||||
starte nochmal 60
|
starte nochmal 60
|
||||||
starte bots 90 2
|
starte bots 90 2
|
||||||
rm -f "$TMP/code.txt" "$TMP/code2.txt"
|
rm -f "$TMP/code.txt" "$TMP/code2.txt"
|
||||||
@@ -52,7 +53,7 @@ done
|
|||||||
wait 2>/dev/null
|
wait 2>/dev/null
|
||||||
|
|
||||||
ergebnis=0
|
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"
|
log="$TMP/$name.log"
|
||||||
echo "=== $name"
|
echo "=== $name"
|
||||||
grep -E "^(OK |FEHL|SERVER:| |TEST:)" "$log"
|
grep -E "^(OK |FEHL|SERVER:| |TEST:)" "$log"
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ func _ready() -> void:
|
|||||||
main.menu._show("main")
|
main.menu._show("main")
|
||||||
await get_tree().create_timer(0.2).timeout
|
await get_tree().create_timer(0.2).timeout
|
||||||
await _bild("menue_start")
|
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)
|
Net.start_singleplayer("Tester", 3, 1)
|
||||||
_lauf()
|
_lauf()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user