update!
This commit is contained in:
@@ -62,7 +62,7 @@ Alle als Children unter `World/Systems` und `Dungeon/Systems` instanziert:
|
||||
- Escape: Pause / Cancel UI
|
||||
|
||||
### Dialog (Ollama)
|
||||
- HTTP `localhost:11434/api/generate`, Modell `llama3.2`
|
||||
- HTTP `localhost:11434/api/generate`, Modell `mistral-nemo`
|
||||
- Pro NPC: System-Prompt aus `lore` + `personality`
|
||||
- Fallback: `npc.fallback_text` wenn Ollama nicht erreichbar — Spiel bleibt voll spielbar
|
||||
|
||||
@@ -92,7 +92,7 @@ Alle als Children unter `World/Systems` und `Dungeon/Systems` instanziert:
|
||||
- `godot-4 --headless --quit-after 600 res://scenes/dungeon/dungeon.tscn` → Kein Error
|
||||
|
||||
## Bekannte Lücken / TODOs
|
||||
- Ollama muss installiert werden (Modell `llama3.2` erwartet) — Stub-Fallback funktioniert sonst
|
||||
- Ollama muss installiert werden (Modell `mistral-nemo` erwartet) — Stub-Fallback funktioniert sonst
|
||||
- Save/Load Slots im Hauptmenü (Autoload existiert, UI fehlt)
|
||||
- Multiplayer Late-Join: Building-/Wave-State wird beim spätem Beitritt nicht synchronisiert
|
||||
- Damage Numbers / Hit FX / Particle Effects fehlen (rein Polish)
|
||||
|
||||
31
autoloads/game_lore.gd
Normal file
31
autoloads/game_lore.gd
Normal file
@@ -0,0 +1,31 @@
|
||||
extends Node
|
||||
|
||||
const WORLD_LORE: String = """Das Land heißt Aerwen. Vor siebzig Wintern öffnete sich der erste Schlund über Aerwen. Niemand weiß, woher sie kommen. Die Alten nennen es 'das Stille Beben' — der Himmel setzte für einen Atemzug aus, dann waren die ersten Risse da.
|
||||
|
||||
Die Schlünde sind Tore zu einer Anderseite — einem Land, das parallel zu unserem zu liegen scheint, aber gehässiger ist. Was dort lebt, atmet durch die Tore in unsere Welt: Knochenwächter, Schattenwölfe, manchmal Schlimmeres. Die meisten Dörfer und Städte sind in den letzten siebzig Jahren gefallen. Aerwen ist heute eine Karte aus leeren Höfen, verfallenen Stadtmauern und einigen wenigen sturen Siedlungen.
|
||||
|
||||
Schimmerthal ist eine dieser Siedlungen. Es liegt in einer flachen Senke, umgeben von brüchigen Wegen und verlassenen Gehöften. Die Bewohner nennen sich selbst 'die Sturen' — die, die nicht weiterziehen wollten oder konnten. Im Zentrum steht die Krumme-Wagen-Taverne, benannt nach einem verkrümmten Handwagen vor der Tür, der dort steht, seit niemand mehr lebt, der sich daran erinnern kann.
|
||||
|
||||
Unter der Taverne liegt ein Anker — ein alter Stein, von dem niemand mehr weiß, wer ihn gesetzt hat. Solange der Anker steht, schwächt er die Schlünde im Umkreis und lässt die Reisenden nicht vollständig sterben.
|
||||
|
||||
Die Schlünde (was Reisende 'Portale' nennen) atmen. Alle paar Stunden öffnet sich irgendwo in der Nähe ein neuer. Wer hindurchschreitet, kommt in einer Höhle der Anderseite heraus. Tötet man die Wächter im Schlund, kollabiert der Riss. Aber er wird ersetzt — irgendwo weiter draußen reißt ein neuer, stärkerer Schlund auf. Die Anderseite gibt nicht auf.
|
||||
|
||||
Etwa einmal pro Atemzug (so nennen die Bewohner die Wellen) reißt der Himmel rot auf — ein 'Tor des Herrn'. Dahinter sitzt eine große Bestie, ein Herr der Anderseite, der einen Schwarm anführt. Wird der Herr besiegt, beruhigt sich die Anderseite kurz. Danach kehrt sie wieder, lauter als vorher — der nächste Atemzug bringt stärkere Wächter. Verstreicht aber die Stunde, ohne dass der Herr fällt, dann erwacht er und führt seinen Schwarm durch den Riss, direkt auf die Taverne zu. Das nennen die Bewohner 'die Invasion'. Fällt die Taverne, fällt der Anker. Niemand weiß, was dann mit dem Land geschieht. Die wenigen, die einmal eine Invasion überlebt haben, sprechen nicht darüber.
|
||||
|
||||
Reisende werden die genannt, die nicht ganz sterben können. Wer einmal den Anker unter der Taverne berührt hat, den lässt der Tod los: ihre Wunden schließen sich, sie tauchen am Anker wieder auf, wenn sie fallen — solange der Anker steht. Es ist kein Segen. Manche im Dorf flüstern, ein Reisender trage ein Stück Anderseite in sich. Nur Reisende können tief in die Schlünde gehen, weil die Anderseite gewöhnliche Lebende zerreißt; einen Reisenden setzt sie nur unangenehm zurück. Reisende kommen selten und gehen oft. Niemand weiß, warum der Anker manche markiert und andere nicht.
|
||||
|
||||
Die wichtigsten Bewohner: Brena, Wirtin der Krumme-Wagen-Taverne, geboren in Schimmerthal, übernahm das Haus von ihrer Großmutter Mara, die als Kind den ersten Schlund aufreißen sah. Halvor, der Schmied, verlor sein rechtes Auge an einen Knochenwächter im fünften Sommer nach den Schlünden. Eyrie, die Murmlerin, ist alt genug, um sich an 'die Zeit davor' zu erinnern, und liest die Risse — sie weiß oft tagelang vorher, wann ein roter Schlund aufbricht. Rolf, der Bauer, bestellt die kleinen Felder östlich des Dorfes und hat zwei Söhne an die Schlünde verloren.
|
||||
|
||||
Geschichten im Dorf: Manche flüstern, die Anderseite seien wir selbst, vor langer Zeit — das gilt als Ammenmärchen. Andere sagen, der Anker sei kein Stein, sondern ein Versprechen, das jemand vor sehr langer Zeit gegeben hat. Eyrie sagt selten etwas dazu; wenn man sie fragt, lächelt sie nur: 'Frag den Anker, wenn du willst.' Es gibt Reisende, die behaupten, in der Anderseite manchmal Häuser zu sehen, die genau so aussehen wie die im Dorf. Halvor schnaubt, wenn er das hört."""
|
||||
|
||||
const DIALECT_HINTS: String = """- Sage 'Schlund' oder 'Riss' oder 'Tor' statt 'Portal'.
|
||||
- Sage 'Atemzug' statt 'Welle'.
|
||||
- Sage 'Reisender' (m) / 'Reisende' (f) statt 'Spieler'.
|
||||
- Sage 'Wächter' oder 'Herr' statt 'Boss'.
|
||||
- Sage 'Anderseite' für das, wohin die Schlünde führen.
|
||||
- Sage 'Anker' für den Stein unter der Taverne.
|
||||
- Im Zweifel ehrlich: 'Das weiß niemand.' statt erfinden."""
|
||||
|
||||
|
||||
func build_npc_context(_profile) -> String:
|
||||
return "WELT:\n%s\n\nSPRACHE:\n%s" % [WORLD_LORE, DIALECT_HINTS]
|
||||
1
autoloads/game_lore.gd.uid
Normal file
1
autoloads/game_lore.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://lyslauvf34ot
|
||||
@@ -29,6 +29,9 @@ func reset_run() -> void:
|
||||
func change_scene(path: String) -> void:
|
||||
current_scene = path
|
||||
EventBus.scene_change_requested.emit(path)
|
||||
call_deferred("_do_change_scene", path)
|
||||
|
||||
func _do_change_scene(path: String) -> void:
|
||||
get_tree().change_scene_to_file(path)
|
||||
|
||||
func set_paused(value: bool) -> void:
|
||||
|
||||
@@ -22,6 +22,7 @@ Net="*res://autoloads/net.gd"
|
||||
Stats="*res://autoloads/stats.gd"
|
||||
GameState="*res://autoloads/game_state.gd"
|
||||
SaveLoad="*res://autoloads/save_load.gd"
|
||||
GameLore="*res://autoloads/game_lore.gd"
|
||||
|
||||
[dotnet]
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ extends Node
|
||||
const ROOM_HEIGHT: float = 4.0
|
||||
const WALL_THICKNESS: float = 0.4
|
||||
const CORRIDOR_WIDTH: float = 4.0
|
||||
const DOOR_WIDTH: float = 4.5
|
||||
|
||||
var rng: RandomNumberGenerator
|
||||
var rooms: Array = []
|
||||
@@ -17,7 +18,7 @@ func generate(parent: Node3D, seed: int, scale_difficulty: float = 1.0) -> Dicti
|
||||
for i in range(room_count):
|
||||
var w: float = rng.randf_range(8.0, 14.0)
|
||||
var d: float = rng.randf_range(8.0, 14.0)
|
||||
rooms.append({"pos": pos, "size": Vector3(w, ROOM_HEIGHT, d)})
|
||||
rooms.append({"pos": pos, "size": Vector3(w, ROOM_HEIGHT, d), "openings": []})
|
||||
if i == room_count - 1:
|
||||
break
|
||||
var corridor_len: float = rng.randf_range(4.0, 8.0)
|
||||
@@ -26,23 +27,61 @@ func generate(parent: Node3D, seed: int, scale_difficulty: float = 1.0) -> Dicti
|
||||
if rng.randf() < 0.5:
|
||||
var rotate_left: bool = rng.randf() < 0.5
|
||||
dir = dir.rotated(Vector3.UP, PI * 0.5 * (1 if rotate_left else -1))
|
||||
_compute_openings()
|
||||
_build_geometry(parent)
|
||||
return {"rooms": rooms, "spawn": rooms[0].pos + Vector3(0, 1, 0), "boss": rooms[-1].pos}
|
||||
|
||||
func _compute_openings() -> void:
|
||||
for i in range(rooms.size() - 1):
|
||||
var rel: Vector3 = rooms[i + 1].pos - rooms[i].pos
|
||||
if abs(rel.x) > abs(rel.z):
|
||||
if rel.x > 0:
|
||||
rooms[i].openings.append("east")
|
||||
rooms[i + 1].openings.append("west")
|
||||
else:
|
||||
rooms[i].openings.append("west")
|
||||
rooms[i + 1].openings.append("east")
|
||||
else:
|
||||
if rel.z > 0:
|
||||
rooms[i].openings.append("south")
|
||||
rooms[i + 1].openings.append("north")
|
||||
else:
|
||||
rooms[i].openings.append("north")
|
||||
rooms[i + 1].openings.append("south")
|
||||
|
||||
func _build_geometry(parent: Node3D) -> void:
|
||||
for r in rooms:
|
||||
_build_room(parent, r.pos, r.size)
|
||||
_build_room(parent, r.pos, r.size, r.openings)
|
||||
for i in range(rooms.size() - 1):
|
||||
_build_corridor(parent, rooms[i].pos, rooms[i + 1].pos)
|
||||
|
||||
func _build_room(parent: Node3D, center: Vector3, size: Vector3) -> void:
|
||||
func _build_room(parent: Node3D, center: Vector3, size: Vector3, openings: Array) -> void:
|
||||
_add_floor(parent, center, Vector2(size.x, size.z))
|
||||
var hw: float = size.x * 0.5
|
||||
var hd: float = size.z * 0.5
|
||||
_add_wall(parent, center + Vector3(0, ROOM_HEIGHT * 0.5, -hd), Vector3(size.x, ROOM_HEIGHT, WALL_THICKNESS))
|
||||
_add_wall(parent, center + Vector3(0, ROOM_HEIGHT * 0.5, hd), Vector3(size.x, ROOM_HEIGHT, WALL_THICKNESS))
|
||||
_add_wall(parent, center + Vector3(-hw, ROOM_HEIGHT * 0.5, 0), Vector3(WALL_THICKNESS, ROOM_HEIGHT, size.z))
|
||||
_add_wall(parent, center + Vector3(hw, ROOM_HEIGHT * 0.5, 0), Vector3(WALL_THICKNESS, ROOM_HEIGHT, size.z))
|
||||
_add_wall_with_opening(parent, center + Vector3(0, ROOM_HEIGHT * 0.5, -hd), Vector3(size.x, ROOM_HEIGHT, WALL_THICKNESS), "north", openings, true)
|
||||
_add_wall_with_opening(parent, center + Vector3(0, ROOM_HEIGHT * 0.5, hd), Vector3(size.x, ROOM_HEIGHT, WALL_THICKNESS), "south", openings, true)
|
||||
_add_wall_with_opening(parent, center + Vector3(-hw, ROOM_HEIGHT * 0.5, 0), Vector3(WALL_THICKNESS, ROOM_HEIGHT, size.z), "west", openings, false)
|
||||
_add_wall_with_opening(parent, center + Vector3(hw, ROOM_HEIGHT * 0.5, 0), Vector3(WALL_THICKNESS, ROOM_HEIGHT, size.z), "east", openings, false)
|
||||
|
||||
func _add_wall_with_opening(parent: Node3D, center: Vector3, size: Vector3, side: String, openings: Array, axis_x: bool) -> void:
|
||||
if not openings.has(side):
|
||||
_add_wall(parent, center, size)
|
||||
return
|
||||
if axis_x:
|
||||
var seg_len: float = (size.x - DOOR_WIDTH) * 0.5
|
||||
if seg_len <= 0.1:
|
||||
return
|
||||
var seg_offset: float = DOOR_WIDTH * 0.5 + seg_len * 0.5
|
||||
_add_wall(parent, center + Vector3(-seg_offset, 0, 0), Vector3(seg_len, size.y, size.z))
|
||||
_add_wall(parent, center + Vector3(seg_offset, 0, 0), Vector3(seg_len, size.y, size.z))
|
||||
else:
|
||||
var seg_len: float = (size.z - DOOR_WIDTH) * 0.5
|
||||
if seg_len <= 0.1:
|
||||
return
|
||||
var seg_offset: float = DOOR_WIDTH * 0.5 + seg_len * 0.5
|
||||
_add_wall(parent, center + Vector3(0, 0, -seg_offset), Vector3(size.x, size.y, seg_len))
|
||||
_add_wall(parent, center + Vector3(0, 0, seg_offset), Vector3(size.x, size.y, seg_len))
|
||||
|
||||
func _build_corridor(parent: Node3D, from: Vector3, to: Vector3) -> void:
|
||||
var mid: Vector3 = (from + to) * 0.5
|
||||
|
||||
@@ -2,6 +2,7 @@ extends Node3D
|
||||
|
||||
const PLAYER_SCENE: PackedScene = preload("res://scenes/entities/player/player.tscn")
|
||||
const ENEMY_SCENE: PackedScene = preload("res://scenes/entities/enemy/enemy.tscn")
|
||||
const PORTAL_SCENE: PackedScene = preload("res://scenes/entities/portal/portal.tscn")
|
||||
const GENERATOR: GDScript = preload("res://scenes/dungeon/dungeon_generator.gd")
|
||||
|
||||
@onready var players_root: Node3D = $EntityRoot/Players
|
||||
@@ -45,9 +46,9 @@ func _populate_dungeon() -> void:
|
||||
var room: Dictionary = data.rooms[i]
|
||||
var n: int = 2 + (1 if GameState.dungeon_red else 0)
|
||||
for j in range(n):
|
||||
var off := Vector3(randf_range(-room.size.x * 0.3, room.size.x * 0.3), 0.5, randf_range(-room.size.z * 0.3, room.size.z * 0.3))
|
||||
var off := Vector3(randf_range(-room.size.x * 0.3, room.size.x * 0.3), 1.0, randf_range(-room.size.z * 0.3, room.size.z * 0.3))
|
||||
spawn_system.spawn_enemy_at(room.pos + off, GameState.dungeon_red, difficulty * 0.5)
|
||||
spawn_system.spawn_boss_at(data.boss + Vector3(0, 0.5, 0), difficulty)
|
||||
spawn_system.spawn_boss_at(data.boss + Vector3(0, 2.0, 0), difficulty)
|
||||
|
||||
func _spawn_player(peer_id: int) -> void:
|
||||
if players_root.get_node_or_null(str(peer_id)) != null:
|
||||
@@ -70,13 +71,21 @@ func _on_peer_disconnected(id: int) -> void:
|
||||
if node:
|
||||
node.queue_free()
|
||||
|
||||
func _on_boss_defeated(_b: Node) -> void:
|
||||
if Net.is_host():
|
||||
var t := get_tree().create_timer(2.0)
|
||||
t.timeout.connect(func():
|
||||
GameState.dungeon_seed = 0
|
||||
_return.rpc())
|
||||
func _on_boss_defeated(b: Node) -> void:
|
||||
if not Net.is_host():
|
||||
return
|
||||
var portal: StaticBody3D = PORTAL_SCENE.instantiate()
|
||||
portal.is_return = true
|
||||
portal.name = "ReturnPortal"
|
||||
var portals_root: Node3D = $EntityRoot/Portals
|
||||
portals_root.add_child(portal, true)
|
||||
var spawn_pos: Vector3 = (b as Node3D).global_position if b is Node3D else data.boss
|
||||
portal.global_position = spawn_pos + Vector3(0, 1, 0)
|
||||
EventBus.portal_spawned.emit(portal)
|
||||
var t := get_tree().create_timer(3.0)
|
||||
t.timeout.connect(_auto_return.bind(portal))
|
||||
|
||||
@rpc("authority", "reliable", "call_local")
|
||||
func _return() -> void:
|
||||
func _auto_return(portal: Node) -> void:
|
||||
if is_instance_valid(portal):
|
||||
portal.queue_free()
|
||||
GameState.change_scene(GameState.SCENE_WORLD)
|
||||
|
||||
@@ -11,6 +11,7 @@ extends StaticBody3D
|
||||
var spawn_timer: float = 0.0
|
||||
var spawned_count: int = 0
|
||||
var dead: bool = false
|
||||
var attacked: bool = false
|
||||
|
||||
func _enter_tree() -> void:
|
||||
set_multiplayer_authority(1)
|
||||
@@ -51,6 +52,8 @@ func _physics_process(delta: float) -> void:
|
||||
return
|
||||
if not multiplayer.is_server() and multiplayer.multiplayer_peer != null:
|
||||
return
|
||||
if not attacked:
|
||||
return
|
||||
spawn_timer = max(0.0, spawn_timer - delta)
|
||||
if spawn_timer <= 0.0 and spawned_count < int(Stats.get_stat(self, "spawn_count", 5)):
|
||||
var spawn_sys: Node = get_node_or_null("/root/World/Systems/SpawnSystem")
|
||||
@@ -62,6 +65,8 @@ func _physics_process(delta: float) -> void:
|
||||
func _on_health_changed(entity: Node, current: float, max: float) -> void:
|
||||
if entity != self:
|
||||
return
|
||||
if not attacked and current < max:
|
||||
attacked = true
|
||||
var ratio: float = clamp(current / max if max > 0 else 0.0, 0.0, 1.0)
|
||||
healthbar.scale.x = max(0.01, ratio * 2.0)
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ func _ready() -> void:
|
||||
func _process(_delta: float) -> void:
|
||||
if nearby_player and nearby_player.is_multiplayer_authority():
|
||||
prompt.visible = true
|
||||
if Input.is_action_just_pressed("interact"):
|
||||
if Input.is_action_just_pressed("interact") and not nearby_player.ui_capturing:
|
||||
EventBus.dialog_opened.emit(nearby_player, self)
|
||||
else:
|
||||
prompt.visible = false
|
||||
|
||||
@@ -35,6 +35,7 @@ func _ready() -> void:
|
||||
if stats_resource == null:
|
||||
stats_resource = PlayerStats.new()
|
||||
Stats.register(self, stats_resource)
|
||||
Stats.restore_player(peer_id, self)
|
||||
Stats.set_stat(self, "role", role)
|
||||
name_label.text = Net.player_names.get(peer_id, "P%d" % peer_id)
|
||||
EventBus.entity_died.connect(_on_entity_died_clear_target)
|
||||
@@ -63,6 +64,7 @@ func _request_target(path_str: String) -> void:
|
||||
current_target = t
|
||||
|
||||
func _exit_tree() -> void:
|
||||
Stats.cache_player(peer_id, self)
|
||||
Stats.deregister(self)
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
@@ -85,6 +87,8 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||
return
|
||||
if build_mode:
|
||||
return
|
||||
if ui_capturing:
|
||||
return
|
||||
if event.is_action_pressed("class_tank"):
|
||||
_request_role(GameState.ROLE_TANK)
|
||||
elif event.is_action_pressed("class_damage"):
|
||||
|
||||
@@ -2,6 +2,7 @@ extends StaticBody3D
|
||||
|
||||
@export var stats_resource: PortalStats
|
||||
@export var is_red: bool = false
|
||||
@export var is_return: bool = false
|
||||
|
||||
@onready var mesh: MeshInstance3D = $Mesh
|
||||
@onready var name_label: Label3D = $NameLabel
|
||||
@@ -22,7 +23,15 @@ func _ready() -> void:
|
||||
stats_resource.is_red = is_red
|
||||
Stats.register(self, stats_resource)
|
||||
enter_area.body_entered.connect(_on_body_entered)
|
||||
if is_red:
|
||||
if is_return:
|
||||
var mat: StandardMaterial3D = StandardMaterial3D.new()
|
||||
mat.albedo_color = Color(1.0, 0.85, 0.3)
|
||||
mat.emission_enabled = true
|
||||
mat.emission = Color(1.0, 0.8, 0.2)
|
||||
mat.emission_energy_multiplier = 1.2
|
||||
mesh.material_override = mat
|
||||
name_label.text = "Zurück"
|
||||
elif is_red:
|
||||
var mat: StandardMaterial3D = StandardMaterial3D.new()
|
||||
mat.albedo_color = Color(0.95, 0.2, 0.2)
|
||||
mat.emission_enabled = true
|
||||
@@ -45,7 +54,10 @@ func _on_body_entered(body: Node) -> void:
|
||||
return
|
||||
triggered = true
|
||||
EventBus.portal_entered.emit(self, body)
|
||||
_request_enter.rpc_id(1, is_red, global_position)
|
||||
if is_return:
|
||||
_request_return.rpc_id(1)
|
||||
else:
|
||||
_request_enter.rpc_id(1, is_red, global_position)
|
||||
|
||||
@rpc("any_peer", "reliable", "call_local")
|
||||
func _request_enter(red: bool, return_pos: Vector3) -> void:
|
||||
@@ -60,3 +72,13 @@ func _do_enter(seed: int, red: bool, return_pos: Vector3) -> void:
|
||||
GameState.dungeon_red = red
|
||||
GameState.portal_return_position = return_pos
|
||||
GameState.change_scene(GameState.SCENE_DUNGEON)
|
||||
|
||||
@rpc("any_peer", "reliable", "call_local")
|
||||
func _request_return() -> void:
|
||||
if not multiplayer.is_server() and multiplayer.multiplayer_peer != null:
|
||||
return
|
||||
_do_return.rpc()
|
||||
|
||||
@rpc("authority", "reliable", "call_local")
|
||||
func _do_return() -> void:
|
||||
GameState.change_scene(GameState.SCENE_WORLD)
|
||||
|
||||
@@ -84,7 +84,19 @@ func _process(_delta: float) -> void:
|
||||
_update_build_preview()
|
||||
_update_minimap()
|
||||
|
||||
func _is_typing() -> bool:
|
||||
var f := get_viewport().gui_get_focus_owner()
|
||||
return f is LineEdit or f is TextEdit
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if _is_typing():
|
||||
if event.is_action_pressed("pause"):
|
||||
var f := get_viewport().gui_get_focus_owner()
|
||||
if f is LineEdit:
|
||||
(f as LineEdit).release_focus()
|
||||
_capture_ui(_any_panel_visible())
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if event.is_action_pressed("inventory"):
|
||||
_toggle_panel(inventory_panel)
|
||||
_refresh_inventory()
|
||||
|
||||
@@ -180,9 +180,10 @@ layout_mode = 2
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 140)
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
bbcode_enabled = true
|
||||
scroll_following = true
|
||||
fit_content = true
|
||||
scroll_active = true
|
||||
|
||||
[node name="ChatInput" type="LineEdit" parent="ChatPanel/VBox"]
|
||||
unique_name_in_owner = true
|
||||
@@ -309,9 +310,10 @@ theme_override_font_sizes/font_size = 22
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 280)
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
bbcode_enabled = true
|
||||
scroll_following = true
|
||||
fit_content = true
|
||||
scroll_active = true
|
||||
|
||||
[node name="DialogInput" type="LineEdit" parent="DialogPanel/VBox"]
|
||||
unique_name_in_owner = true
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
extends Node
|
||||
|
||||
const OLLAMA_URL: String = "http://127.0.0.1:11434/api/generate"
|
||||
const DEFAULT_MODEL: String = "qwen2.5:0.5b"
|
||||
const DEFAULT_MODEL: String = "mistral-nemo"
|
||||
const REQUEST_TIMEOUT: float = 25.0
|
||||
|
||||
var _http: HTTPRequest
|
||||
@@ -19,8 +19,9 @@ func ask(npc: Node, player: Node, question: String) -> void:
|
||||
request_ask.rpc_id(1, npc.get_path(), player.get_path(), question)
|
||||
return
|
||||
var profile: NpcProfile = npc.profile
|
||||
var system_prompt: String = "Du bist %s, eine NPC in einem mittelalterlichen Dorf. Lore: %s. Persönlichkeit: %s. Antworte knapp (max 2 Sätze) auf Deutsch und bleibe immer in deiner Rolle. Erfinde keine Fakten über die Welt." % [profile.display_name, profile.lore, profile.personality]
|
||||
var prompt: String = "Spieler: %s\n%s:" % [question, profile.display_name]
|
||||
var world_context: String = GameLore.build_npc_context(profile)
|
||||
var system_prompt: String = "%s\n\nDU BIST:\nName: %s\nHintergrund: %s\nPersönlichkeit: %s\n\nREGELN:\n- Antworte kurz: maximal 2 Sätze.\n- Antworte auf Deutsch.\n- Bleibe in deiner Rolle, schreibe NIE 'Als KI...' oder Ähnliches.\n- Nutze die WELT-Begriffe (Schlund, Atemzug, Reisender, Anker, Anderseite).\n- Wenn dich etwas gefragt wird, das du nicht wissen kannst, sag das ehrlich.\n- Erfinde keine Fakten, die nicht in der WELT stehen." % [world_context, profile.display_name, profile.lore, profile.personality]
|
||||
var prompt: String = "Reisender: %s\n%s:" % [question, profile.display_name]
|
||||
_send_request(npc, player, system_prompt, prompt)
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
|
||||
@@ -11,10 +11,18 @@ func spawn_default_npcs() -> void:
|
||||
if root == null:
|
||||
return
|
||||
var profiles: Array = [
|
||||
_make_profile(&"barkeep", "Brena", "Barkeep at the Crooked Wheel tavern.", "Warm, gossipy, has lived here her whole life."),
|
||||
_make_profile(&"smith", "Halvor", "Village blacksmith. Lost an eye to a portal monster.", "Gruff, terse, cares deeply for the village."),
|
||||
_make_profile(&"sage", "Eyrie", "Village sage. Reads the portals, claims to remember the time before.", "Cryptic, slow-spoken, weary."),
|
||||
_make_profile(&"farmer", "Rolf", "Tends the small fields east of the village.", "Anxious, practical, wants the wars to end."),
|
||||
_make_profile(&"barkeep", "Brena",
|
||||
"Wirtin der Krumme-Wagen-Taverne in Schimmerthal. Übernahm das Haus von ihrer Großmutter Mara, die als Kind den ersten Schlund über Aerwen aufreißen sah. Kennt jede Geschichte des Dorfes und erzählt sie gern weiter.",
|
||||
"Warm, geschwätzig, hat ihr ganzes Leben in Schimmerthal verbracht. Spricht gerne von früher und vom Anker unter der Taverne, an dem die Reisenden hängen."),
|
||||
_make_profile(&"smith", "Halvor",
|
||||
"Schmied von Schimmerthal. Verlor sein rechtes Auge an einen Knochenwächter im fünften Sommer nach den Schlünden. Schmiedet Waffen für die Reisenden und sagt, jedes gute Stück Stahl sei ein Stück Schuld, das er zurückzahlt.",
|
||||
"Grob, knapp, hängt am Dorf. Wenig Geduld für Mätzchen, aber stiller Respekt vor jedem Reisenden, der den Anker hält. Spricht ungern über das Auge, aber lügt nicht darüber."),
|
||||
_make_profile(&"sage", "Eyrie",
|
||||
"Die Murmlerin von Schimmerthal. Alt genug, um sich an die Zeit vor den Schlünden zu erinnern. Liest die Risse — weiß oft tagelang vorher, wann ein roter Schlund aufbricht. Lebt in einer Hütte am Rand des Dorfes.",
|
||||
"Kryptisch, langsam, müde. Antwortet oft mit einer Gegenfrage. Wenn etwas zu groß ist für eine Antwort, sagt sie: 'Frag den Anker, wenn du willst.' Lächelt selten, aber wenn, dann nicht freundlich."),
|
||||
_make_profile(&"farmer", "Rolf",
|
||||
"Bauer in Schimmerthal. Bestellt die kleinen Felder östlich des Dorfes. Hat zwei Söhne an die Schlünde verloren, spricht aber nicht darüber. Glaubt nicht, dass die Wirren je enden.",
|
||||
"Ängstlich, praktisch, müde. Wünscht sich, dass die Kriege enden, hofft aber nicht mehr darauf. Vermeidet Reisende eher, antwortet höflich, aber kurz."),
|
||||
]
|
||||
var radius: float = 6.0
|
||||
for i in range(profiles.size()):
|
||||
|
||||
Reference in New Issue
Block a user