Compare commits
6 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
beaffbf990 | ||
|
|
ffa986afe4 | ||
|
|
4d39596516 | ||
|
|
252f9a96dd | ||
|
|
bdc1fdf591 | ||
|
|
0ce9053b3b |
24
game/core/debug_overlay.gd
Normal file
24
game/core/debug_overlay.gd
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
extends CanvasLayer
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if event is InputEventKey and event.pressed and not event.echo:
|
||||
match event.keycode:
|
||||
KEY_F1:
|
||||
FlagManager.debug_print_all()
|
||||
KEY_F2:
|
||||
QuestManager.debug_print_all()
|
||||
KEY_F3:
|
||||
# Test rapide : donne 3 badges
|
||||
FlagManager.set_flag(&"badges_gagnes", 3)
|
||||
print("3 badges ajoutés !")
|
||||
KEY_F4:
|
||||
# Test rapide : force la rencontre du Prof
|
||||
FlagManager.set_flag(&"prof_rencontre", true)
|
||||
KEY_F5:
|
||||
_give_test_quests()
|
||||
|
||||
func _give_test_quests() -> void:
|
||||
QuestManager.start_quest(&"main_01_prof", true) # true = force (ignore prérequis)
|
||||
QuestManager.start_quest(&"main_02_starter", true)
|
||||
QuestManager.start_quest(&"side_chenipan", true)
|
||||
print("[DEBUG] 3 quêtes de test démarrées.")
|
||||
1
game/core/debug_overlay.gd.uid
Normal file
1
game/core/debug_overlay.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cniwr4iowd8we
|
||||
201
game/core/flag_manager.gd
Normal file
201
game/core/flag_manager.gd
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
extends Node
|
||||
## Gestionnaire global de flags de progression.
|
||||
## Un flag = un fait du monde identifié par un StringName.
|
||||
##
|
||||
## Types supportés : bool, int, String, float, Variant (fallback).
|
||||
##
|
||||
## Écriture : set_flag(), set_flag_once(), increment_flag()
|
||||
## Lecture : get_flag(), has_flag(), is_flag_true()
|
||||
## Conditions : evaluate_condition() pour tester des conditions composées.
|
||||
|
||||
# Stockage : flag_name (StringName) -> valeur (Variant)
|
||||
var _flags: Dictionary = {}
|
||||
|
||||
# Flags marqués "one-shot" (ne peuvent être modifiés qu'une fois)
|
||||
var _one_shot_flags: Dictionary = {} # flag_name -> true
|
||||
|
||||
# --- Écriture ---
|
||||
|
||||
## Définit un flag à une valeur. Écrase toujours.
|
||||
func set_flag(flag_name: StringName, value: Variant) -> void:
|
||||
var old_value: Variant = _flags.get(flag_name, null)
|
||||
if old_value == value:
|
||||
return
|
||||
_flags[flag_name] = value
|
||||
GameEvents.flag_changed.emit(flag_name, old_value, value)
|
||||
_emit_legacy_signal(flag_name, value)
|
||||
|
||||
## Définit un flag uniquement s'il n'a jamais été défini.
|
||||
## Retourne true si le flag a effectivement été posé.
|
||||
func set_flag_once(flag_name: StringName, value: Variant = true) -> bool:
|
||||
if flag_name in _flags:
|
||||
return false
|
||||
set_flag(flag_name, value)
|
||||
_one_shot_flags[flag_name] = true
|
||||
return true
|
||||
|
||||
## Incrémente un flag numérique. Crée le flag à 0 si inexistant.
|
||||
func increment_flag(flag_name: StringName, amount: int = 1) -> void:
|
||||
var current: int = int(_flags.get(flag_name, 0))
|
||||
set_flag(flag_name, current + amount)
|
||||
|
||||
## Décrémente un flag numérique (utile pour consommation de ressources).
|
||||
func decrement_flag(flag_name: StringName, amount: int = 1) -> void:
|
||||
increment_flag(flag_name, -amount)
|
||||
|
||||
## Supprime un flag (à utiliser avec prudence en dev).
|
||||
func clear_flag(flag_name: StringName) -> void:
|
||||
if flag_name in _flags:
|
||||
var old_value: Variant = _flags[flag_name]
|
||||
_flags.erase(flag_name)
|
||||
GameEvents.flag_changed.emit(flag_name, old_value, null)
|
||||
|
||||
# --- Lecture ---
|
||||
|
||||
func get_flag(flag_name: StringName, default: Variant = null) -> Variant:
|
||||
return _flags.get(flag_name, default)
|
||||
|
||||
func has_flag(flag_name: StringName) -> bool:
|
||||
return flag_name in _flags
|
||||
|
||||
## Retourne true si le flag existe ET vaut `true` (booléen).
|
||||
## Pour un int, retourne true si non-zéro.
|
||||
func is_flag_true(flag_name: StringName) -> bool:
|
||||
if not flag_name in _flags:
|
||||
return false
|
||||
var v: Variant = _flags[flag_name]
|
||||
if v is bool:
|
||||
return v
|
||||
if v is int or v is float:
|
||||
return v != 0
|
||||
return v != null
|
||||
|
||||
func get_int(flag_name: StringName, default: int = 0) -> int:
|
||||
return int(_flags.get(flag_name, default))
|
||||
|
||||
func get_bool(flag_name: StringName, default: bool = false) -> bool:
|
||||
return bool(_flags.get(flag_name, default))
|
||||
|
||||
func get_string(flag_name: StringName, default: String = "") -> String:
|
||||
return String(_flags.get(flag_name, default))
|
||||
|
||||
# --- Conditions composées ---
|
||||
|
||||
## Évalue un dictionnaire de conditions :
|
||||
## {
|
||||
## &"prof_rencontre": true, # flag == true
|
||||
## &"badges_gagnes": 3, # flag >= 3 (int)
|
||||
## &"starter_choisi": "pikachu", # flag == "pikachu"
|
||||
## }
|
||||
## Retourne true si TOUTES les conditions sont satisfaites.
|
||||
func evaluate_condition(conditions: Dictionary) -> bool:
|
||||
for flag_name in conditions:
|
||||
var expected: Variant = conditions[flag_name]
|
||||
var actual: Variant = _flags.get(flag_name, null)
|
||||
|
||||
# Booléen attendu : on teste la vérité (marche pour bool et int)
|
||||
if expected is bool:
|
||||
if is_flag_true(flag_name) != expected:
|
||||
return false
|
||||
# Int attendu : on teste >= (utile pour les compteurs)
|
||||
elif expected is int:
|
||||
if int(actual if actual != null else 0) < expected:
|
||||
return false
|
||||
# String / autre : égalité stricte
|
||||
else:
|
||||
if actual != expected:
|
||||
return false
|
||||
return true
|
||||
|
||||
## Version "au moins une condition vraie" (OR).
|
||||
func evaluate_any_condition(conditions: Dictionary) -> bool:
|
||||
for flag_name in conditions:
|
||||
var expected: Variant = conditions[flag_name]
|
||||
var actual: Variant = _flags.get(flag_name, null)
|
||||
if expected is bool:
|
||||
if is_flag_true(flag_name) == expected:
|
||||
return true
|
||||
elif expected is int:
|
||||
if int(actual if actual != null else 0) >= expected:
|
||||
return true
|
||||
else:
|
||||
if actual == expected:
|
||||
return true
|
||||
return false
|
||||
|
||||
# --- Signaux spécifiques (optionnel mais pratique) ---
|
||||
|
||||
## Émet des signaux dédiés pour certains flags pour simplifier les connexions.
|
||||
## Exemple : GameEvents.badge_gagne.emit() si le flag "badges_gagnes" change.
|
||||
func _emit_legacy_signal(flag_name: StringName, value: Variant) -> void:
|
||||
# Ajoute ici tes mappings custom si tu veux des signaux dédiés.
|
||||
# Ex :
|
||||
# if flag_name == &"badges_gagnes":
|
||||
# GameEvents.badge_count_changed.emit(int(value))
|
||||
pass
|
||||
|
||||
# --- Connexion automatique aux événements du jeu ---
|
||||
|
||||
func _ready() -> void:
|
||||
_connect_gameplay_events()
|
||||
|
||||
func _connect_gameplay_events() -> void:
|
||||
GameEvents.pokemon_captured.connect(_on_pokemon_captured)
|
||||
GameEvents.pokemon_defeated.connect(_on_pokemon_defeated)
|
||||
GameEvents.item_collected.connect(_on_item_collected)
|
||||
GameEvents.npc_talked.connect(_on_npc_talked)
|
||||
GameEvents.location_reached.connect(_on_location_reached)
|
||||
|
||||
func _on_pokemon_captured(species_id: StringName) -> void:
|
||||
increment_flag(&"nb_pokemon_captures", 1)
|
||||
set_flag_once(&"premier_pokemon_capture", true)
|
||||
# Flag spécifique par espèce : ex. "capture_chenipan" = true
|
||||
set_flag(StringName("capture_%s" % species_id), true)
|
||||
increment_flag(StringName("nb_captures_%s" % species_id), 1)
|
||||
|
||||
func _on_pokemon_defeated(species_id: StringName) -> void:
|
||||
increment_flag(&"nb_pokemon_vaincus", 1)
|
||||
increment_flag(StringName("nb_vaincus_%s" % species_id), 1)
|
||||
|
||||
func _on_item_collected(item_id: StringName, amount: int) -> void:
|
||||
increment_flag(StringName("item_%s" % item_id), amount)
|
||||
|
||||
func _on_npc_talked(npc_id: StringName) -> void:
|
||||
set_flag(StringName("talked_%s" % npc_id), true)
|
||||
increment_flag(StringName("nb_dialogues_%s" % npc_id), 1)
|
||||
|
||||
func _on_location_reached(location_id: StringName) -> void:
|
||||
set_flag(StringName("visited_%s" % location_id), true)
|
||||
|
||||
# --- Sauvegarde / Chargement ---
|
||||
|
||||
func get_save_data() -> Dictionary:
|
||||
return {
|
||||
"flags": _flags.duplicate(true),
|
||||
"one_shot": _one_shot_flags.duplicate(),
|
||||
}
|
||||
|
||||
func load_save_data(data: Dictionary) -> void:
|
||||
_flags = data.get("flags", {}).duplicate(true)
|
||||
_one_shot_flags = data.get("one_shot", {}).duplicate()
|
||||
|
||||
# --- Debug ---
|
||||
|
||||
func debug_print_all() -> void:
|
||||
print("\n=== FLAGS ===")
|
||||
if _flags.is_empty():
|
||||
print("(aucun flag)")
|
||||
else:
|
||||
var keys: Array = _flags.keys()
|
||||
keys.sort()
|
||||
for flag_name in keys:
|
||||
var v: Variant = _flags[flag_name]
|
||||
var type_str := type_string(typeof(v))
|
||||
var one_shot := " [one-shot]" if flag_name in _one_shot_flags else ""
|
||||
print(" %s = %s (%s)%s" % [flag_name, str(v), type_str, one_shot])
|
||||
print("=============\n")
|
||||
|
||||
func debug_clear_all() -> void:
|
||||
_flags.clear()
|
||||
_one_shot_flags.clear()
|
||||
print("[FlagManager] Tous les flags ont été effacés.")
|
||||
1
game/core/flag_manager.gd.uid
Normal file
1
game/core/flag_manager.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://ceb0ky3nlq1do
|
||||
|
|
@ -49,6 +49,11 @@ func _set_selected(index : int) -> void:
|
|||
|
||||
func save() -> void:
|
||||
var config := ConfigFile.new()
|
||||
|
||||
config.set_value("game", "version", 1)
|
||||
config.set_value("game", "timestamp", Time.get_datetime_string_from_system())
|
||||
config.set_value("flags", "flags", FlagManager.get_save_data())
|
||||
config.set_value("quests", "quests", QuestManager.get_save_data())
|
||||
config.set_value("progress", "story_progress", story_progress)
|
||||
config.set_value("team", "data", player_team.to_dict())
|
||||
config.set_value("storage", "data", player_storage.to_dict())
|
||||
|
|
@ -62,3 +67,7 @@ func load_data() -> void:
|
|||
player_team = PokemonTeam.from_dict(team_data) if team_data is Dictionary else PokemonTeam.new()
|
||||
var storage_data : Variant = config.get_value("storage", "data", {})
|
||||
player_storage = PokemonStorage.from_dict(storage_data) if storage_data is Dictionary else PokemonStorage.new()
|
||||
var flags_data : Variant = config.get_value("flags", "flags", {})
|
||||
FlagManager.load_save_data(flags_data)
|
||||
var quests_data : Variant = config.get_value("quests", "quests", {})
|
||||
QuestManager.load_save_data(quests_data)
|
||||
|
|
|
|||
17
game/core/game_events.gd
Normal file
17
game/core/game_events.gd
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
extends Node
|
||||
|
||||
# --- Quêtes (existants) ---
|
||||
signal quest_started(quest_id: StringName)
|
||||
signal quest_completed(quest_id: StringName)
|
||||
signal quest_available(quest_id: StringName)
|
||||
signal quest_objective_updated(quest_id: StringName, objective_index: int, current: int, required: int)
|
||||
|
||||
# --- Gameplay (existants) ---
|
||||
signal pokemon_captured(species_id: StringName)
|
||||
signal pokemon_defeated(species_id: StringName)
|
||||
signal item_collected(item_id: StringName, amount: int)
|
||||
signal npc_talked(npc_id: StringName)
|
||||
signal location_reached(location_id: StringName)
|
||||
|
||||
# --- Flags (nouveau) ---
|
||||
signal flag_changed(flag_name: StringName, old_value: Variant, new_value: Variant)
|
||||
1
game/core/game_events.gd.uid
Normal file
1
game/core/game_events.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://ct3v0qsc3bvcn
|
||||
263
game/core/quest_manager.gd
Normal file
263
game/core/quest_manager.gd
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
extends Node
|
||||
|
||||
var all_quests: Dictionary = {} # quest_id -> QuestData
|
||||
var active_quests: Dictionary = {} # quest_id -> QuestData
|
||||
var completed_quests: Dictionary = {} # quest_id -> QuestData
|
||||
var failed_quests: Dictionary = {}
|
||||
|
||||
const QUESTS_PATH := "res://game/quests/data/"
|
||||
|
||||
func _ready() -> void:
|
||||
_load_all_quests()
|
||||
_connect_events()
|
||||
_refresh_quest_states()
|
||||
|
||||
GameEvents.flag_changed.connect(_on_flag_changed)
|
||||
|
||||
func _on_flag_changed(_flag_name: StringName, _old_value: Variant, _new_value: Variant) -> void:
|
||||
_refresh_quest_states()
|
||||
|
||||
# --- Chargement ---
|
||||
|
||||
func _load_all_quests() -> void:
|
||||
var dir := DirAccess.open(QUESTS_PATH)
|
||||
if dir == null:
|
||||
push_error("Dossier de quêtes introuvable : " + QUESTS_PATH)
|
||||
return
|
||||
for file in dir.get_files():
|
||||
if file.ends_with(".tres"):
|
||||
var quest: QuestData = load(QUESTS_PATH + file)
|
||||
if quest and quest.quest_id != &"":
|
||||
all_quests[quest.quest_id] = quest
|
||||
|
||||
# --- Calcul d'état (cœur du système de chaînes) ---
|
||||
|
||||
## Recalcule l'état de toutes les quêtes non-commencées :
|
||||
## une quête passe de LOCKED à AVAILABLE si ses prérequis sont satisfaits.
|
||||
func _refresh_quest_states() -> void:
|
||||
for quest_id in all_quests:
|
||||
var quest: QuestData = all_quests[quest_id]
|
||||
if quest.state in [QuestData.QuestState.ACTIVE, QuestData.QuestState.COMPLETED, QuestData.QuestState.FAILED]:
|
||||
continue
|
||||
if _are_prerequisites_met(quest):
|
||||
if quest.state == QuestData.QuestState.LOCKED:
|
||||
quest.state = QuestData.QuestState.AVAILABLE
|
||||
_emit_quest_available(quest_id)
|
||||
else:
|
||||
quest.state = QuestData.QuestState.LOCKED
|
||||
|
||||
func _are_prerequisites_met(quest: QuestData) -> bool:
|
||||
# 1. Prérequis de quêtes
|
||||
for prereq in quest.prerequisites:
|
||||
if not prereq in completed_quests:
|
||||
return false
|
||||
|
||||
# 2. Flags bloquants (si l'un est vrai → la quête ne peut PAS se débloquer)
|
||||
if not quest.blocking_flags.is_empty():
|
||||
if FlagManager.evaluate_condition(quest.blocking_flags):
|
||||
return false
|
||||
|
||||
# 3. Flags requis (tous doivent être vrais)
|
||||
if not quest.required_flags.is_empty():
|
||||
if not FlagManager.evaluate_condition(quest.required_flags):
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
func _emit_quest_available(quest_id: StringName) -> void:
|
||||
# Optionnel : signal pour notifier l'UI ou un PNJ
|
||||
GameEvents.quest_available.emit(quest_id)
|
||||
|
||||
# --- Connexion aux événements ---
|
||||
|
||||
func _connect_events() -> void:
|
||||
GameEvents.pokemon_captured.connect(_on_pokemon_captured)
|
||||
GameEvents.pokemon_defeated.connect(_on_pokemon_defeated)
|
||||
GameEvents.item_collected.connect(_on_item_collected)
|
||||
GameEvents.npc_talked.connect(_on_npc_talked)
|
||||
GameEvents.location_reached.connect(_on_location_reached)
|
||||
|
||||
# --- Démarrage de quête ---
|
||||
|
||||
func start_quest(quest_id: StringName, force: bool = false) -> bool:
|
||||
if quest_id in active_quests or quest_id in completed_quests:
|
||||
return false
|
||||
if not quest_id in all_quests:
|
||||
push_warning("Quête inconnue : " + str(quest_id))
|
||||
return false
|
||||
|
||||
var quest: QuestData = all_quests[quest_id]
|
||||
|
||||
# Vérifie les prérequis (sauf si forcé, pour debug)
|
||||
if not force and not _are_prerequisites_met(quest):
|
||||
return false
|
||||
|
||||
# Reset les compteurs
|
||||
for obj in quest.objectives:
|
||||
obj.current_count = 0
|
||||
|
||||
quest.state = QuestData.QuestState.ACTIVE
|
||||
active_quests[quest_id] = quest
|
||||
GameEvents.quest_started.emit(quest_id)
|
||||
return true
|
||||
|
||||
## Démarre automatiquement toutes les quêtes disponibles qui n'attendent
|
||||
## pas de déblocage manuel. À appeler après un chargement de zone, par ex.
|
||||
func auto_start_available_quests() -> void:
|
||||
for quest_id in all_quests:
|
||||
var quest: QuestData = all_quests[quest_id]
|
||||
if quest.state == QuestData.QuestState.AVAILABLE and not quest.requires_manual_unlock:
|
||||
start_quest(quest_id)
|
||||
|
||||
# --- Progression ---
|
||||
|
||||
func _progress_objective(event_type: QuestObjective.Type, target: StringName) -> void:
|
||||
for quest_id in active_quests.keys():
|
||||
var quest: QuestData = active_quests[quest_id]
|
||||
var changed := false
|
||||
|
||||
for i in quest.objectives.size():
|
||||
var obj: QuestObjective = quest.objectives[i]
|
||||
if obj.is_complete():
|
||||
continue
|
||||
if obj.matches(event_type, target):
|
||||
obj.current_count = min(obj.current_count + 1, obj.required_count)
|
||||
changed = true
|
||||
GameEvents.quest_objective_updated.emit(
|
||||
quest_id, i, obj.current_count, obj.required_count
|
||||
)
|
||||
|
||||
if changed and quest.are_all_objectives_complete():
|
||||
_complete_quest(quest_id)
|
||||
|
||||
# --- Handlers d'événements ---
|
||||
|
||||
func _on_pokemon_captured(species_id: StringName) -> void:
|
||||
_progress_objective(QuestObjective.Type.CAPTURE, species_id)
|
||||
|
||||
func _on_pokemon_defeated(species_id: StringName) -> void:
|
||||
_progress_objective(QuestObjective.Type.DEFEAT, species_id)
|
||||
|
||||
func _on_item_collected(item_id: StringName, amount: int) -> void:
|
||||
# On avance de `amount` unités
|
||||
for quest_id in active_quests.keys():
|
||||
var quest: QuestData = active_quests[quest_id]
|
||||
for i in quest.objectives.size():
|
||||
var obj: QuestObjective = quest.objectives[i]
|
||||
if obj.type == QuestObjective.Type.COLLECT \
|
||||
and obj.target_id == item_id \
|
||||
and not obj.is_complete():
|
||||
obj.current_count = min(obj.current_count + amount, obj.required_count)
|
||||
GameEvents.quest_objective_updated.emit(
|
||||
quest_id, i, obj.current_count, obj.required_count
|
||||
)
|
||||
if quest.are_all_objectives_complete():
|
||||
_complete_quest(quest_id)
|
||||
|
||||
func _on_npc_talked(npc_id: StringName) -> void:
|
||||
_progress_objective(QuestObjective.Type.TALK_TO, npc_id)
|
||||
|
||||
func _on_location_reached(location_id: StringName) -> void:
|
||||
_progress_objective(QuestObjective.Type.REACH_LOCATION, location_id)
|
||||
|
||||
# --- Fin de quête ---
|
||||
|
||||
func _complete_quest(quest_id: StringName) -> void:
|
||||
var quest: QuestData = active_quests[quest_id]
|
||||
quest.state = QuestData.QuestState.COMPLETED
|
||||
active_quests.erase(quest_id)
|
||||
completed_quests[quest_id] = quest
|
||||
|
||||
_give_rewards(quest)
|
||||
GameEvents.quest_completed.emit(quest_id)
|
||||
|
||||
# 🔑 C'est ici que la magie opère : les quêtes suivantes se débloquent !
|
||||
_refresh_quest_states()
|
||||
|
||||
# Optionnel : auto-démarrer les quêtes suivantes qui ne demandent
|
||||
# pas de déblocage manuel (ex: chaîne principale purement scriptée)
|
||||
auto_start_available_quests()
|
||||
|
||||
func _give_rewards(quest: QuestData) -> void:
|
||||
for reward in quest.rewards:
|
||||
match reward.type:
|
||||
QuestReward.Type.ITEM:
|
||||
# À adapter à ton système d'inventaire
|
||||
GameEvents.item_collected.emit(reward.target_id, reward.amount)
|
||||
QuestReward.Type.MONEY:
|
||||
# Inventory.add_money(reward.amount)
|
||||
pass
|
||||
QuestReward.Type.POKEMON:
|
||||
# Team.add_pokemon(reward.target_id)
|
||||
pass
|
||||
QuestReward.Type.UNLOCK_QUEST:
|
||||
start_quest(reward.target_id)
|
||||
|
||||
# --- Sauvegarde / Chargement ---
|
||||
|
||||
func get_save_data() -> Dictionary:
|
||||
var data := {
|
||||
"active": {}, # quest_id -> Array[int] (progressions)
|
||||
"completed": [], # Array[StringName]
|
||||
"failed": []
|
||||
}
|
||||
for id in active_quests:
|
||||
var quest: QuestData = active_quests[id]
|
||||
var counts: Array[int] = []
|
||||
for obj in quest.objectives:
|
||||
counts.append(obj.current_count)
|
||||
data["active"][id] = counts
|
||||
data["completed"] = completed_quests.keys()
|
||||
data["failed"] = failed_quests.keys()
|
||||
return data
|
||||
|
||||
func load_save_data(data: Dictionary) -> void:
|
||||
active_quests.clear()
|
||||
completed_quests.clear()
|
||||
failed_quests.clear()
|
||||
|
||||
# Reset tous les états à LOCKED
|
||||
for q in all_quests.values():
|
||||
q.state = QuestData.QuestState.LOCKED
|
||||
for obj in q.objectives:
|
||||
obj.current_count = 0
|
||||
|
||||
# Restaure les complétées
|
||||
for id in data.get("completed", []):
|
||||
if id in all_quests:
|
||||
all_quests[id].state = QuestData.QuestState.COMPLETED
|
||||
completed_quests[id] = all_quests[id]
|
||||
|
||||
# Restaure les actives
|
||||
for id in data.get("active", {}):
|
||||
if id in all_quests:
|
||||
var quest: QuestData = all_quests[id]
|
||||
var counts: Array = data["active"][id]
|
||||
for i in quest.objectives.size():
|
||||
quest.objectives[i].current_count = counts[i] if i < counts.size() else 0
|
||||
quest.state = QuestData.QuestState.ACTIVE
|
||||
active_quests[id] = quest
|
||||
|
||||
# Recalcule les LOCKED / AVAILABLE
|
||||
_refresh_quest_states()
|
||||
|
||||
func debug_print_all() -> void:
|
||||
print("\n=== ÉTAT DES QUÊTES ===")
|
||||
for quest_id in all_quests:
|
||||
var q: QuestData = all_quests[quest_id]
|
||||
var state_str := q.get_state_name()
|
||||
var cat_str := q.get_category_name()
|
||||
|
||||
print("\n[%s][%s] %s — %s" % [cat_str, state_str, q.quest_id, q.title])
|
||||
for i in q.objectives.size():
|
||||
var obj := q.objectives[i]
|
||||
var done := "✓" if obj.is_complete() else " "
|
||||
var optional := " (optionnel)" if obj.is_optional else ""
|
||||
print(" [%s] %s : %d/%d%s" % [
|
||||
done, obj.description, obj.current_count, obj.required_count, optional
|
||||
])
|
||||
print("\n=======================\n")
|
||||
|
||||
func _input(event):
|
||||
if event.is_action_pressed("debug_quests"):
|
||||
QuestManager.debug_print_all()
|
||||
1
game/core/quest_manager.gd.uid
Normal file
1
game/core/quest_manager.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://owgkeygdlsmv
|
||||
28
game/quest_journal/objective_row.gd
Normal file
28
game/quest_journal/objective_row.gd
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
extends HBoxContainer
|
||||
|
||||
@onready var check_icon: Label = $CheckIcon
|
||||
@onready var objective_label: Label = $ObjectiveLabel
|
||||
|
||||
func setup(obj: QuestObjective, is_quest_completed: bool) -> void:
|
||||
# Texte : description + compteur si nécessaire
|
||||
if obj.required_count > 1:
|
||||
objective_label.text = "%s (%d/%d)" % [
|
||||
obj.description, obj.current_count, obj.required_count
|
||||
]
|
||||
else:
|
||||
objective_label.text = obj.description
|
||||
|
||||
# Petit marqueur "optionnel"
|
||||
if obj.is_optional:
|
||||
objective_label.text += " [optionnel]"
|
||||
|
||||
# Icône et couleur selon l'état
|
||||
var complete := obj.is_complete() or is_quest_completed
|
||||
if complete:
|
||||
check_icon.text = "☑"
|
||||
check_icon.add_theme_color_override("font_color", Color(0.4, 0.9, 0.4))
|
||||
objective_label.add_theme_color_override("font_color", Color(0.6, 0.6, 0.6))
|
||||
else:
|
||||
check_icon.text = "☐"
|
||||
check_icon.add_theme_color_override("font_color", Color(0.8, 0.8, 0.8))
|
||||
objective_label.add_theme_color_override("font_color", Color(1, 1, 1))
|
||||
1
game/quest_journal/objective_row.gd.uid
Normal file
1
game/quest_journal/objective_row.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://b1uu6845ugtwd
|
||||
15
game/quest_journal/objective_row.tscn
Normal file
15
game/quest_journal/objective_row.tscn
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
[gd_scene format=3 uid="uid://bmi4nup81860h"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://b1uu6845ugtwd" path="res://game/quest_journal/objective_row.gd" id="1_0bes5"]
|
||||
|
||||
[node name="ObjectiveRow" type="HBoxContainer" unique_id=208897443]
|
||||
script = ExtResource("1_0bes5")
|
||||
|
||||
[node name="CheckIcon" type="Label" parent="." unique_id=264135563]
|
||||
custom_minimum_size = Vector2(24, 24)
|
||||
custom_maximum_size = Vector2(24, 24)
|
||||
layout_mode = 2
|
||||
|
||||
[node name="ObjectiveLabel" type="Label" parent="." unique_id=1304877640]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
30
game/quest_journal/quest_entry.gd
Normal file
30
game/quest_journal/quest_entry.gd
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
extends Button
|
||||
class_name QuestEntry
|
||||
|
||||
signal quest_selected(quest_id: StringName)
|
||||
|
||||
@onready var title_label: Label = $HBoxContainer/VBoxContainer/TitleLabel
|
||||
@onready var progress_label: Label = $HBoxContainer/VBoxContainer/ProgressLabel
|
||||
@onready var status_icon: Label = $HBoxContainer/StatusIcon
|
||||
const QuestData = preload("uid://bldyeahkrms8n")
|
||||
|
||||
var _quest_id: StringName = &""
|
||||
|
||||
func setup(quest: QuestData, is_completed: bool) -> void:
|
||||
_quest_id = quest.quest_id
|
||||
title_label.text = quest.title
|
||||
|
||||
if is_completed:
|
||||
progress_label.text = "Terminée"
|
||||
status_icon.text = "✓"
|
||||
status_icon.add_theme_color_override("font_color", Color(0.4, 0.9, 0.4))
|
||||
else:
|
||||
var total := quest.get_required_objectives().size()
|
||||
var done := 0
|
||||
for obj in quest.get_required_objectives():
|
||||
if obj.is_complete(): done += 1
|
||||
progress_label.text = "%d/%d objectifs" % [done, total]
|
||||
status_icon.text = "▶" if done < total else "✓"
|
||||
status_icon.add_theme_color_override("font_color", Color(0.9, 0.8, 0.4))
|
||||
|
||||
pressed.connect(func(): quest_selected.emit(_quest_id))
|
||||
1
game/quest_journal/quest_entry.gd.uid
Normal file
1
game/quest_journal/quest_entry.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://hejqtr201qcw
|
||||
29
game/quest_journal/quest_entry.tscn
Normal file
29
game/quest_journal/quest_entry.tscn
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
[gd_scene format=3 uid="uid://cssst3sesinb"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://hejqtr201qcw" path="res://game/quest_journal/quest_entry.gd" id="1_xva0s"]
|
||||
|
||||
[sub_resource type="LabelSettings" id="LabelSettings_3agn1"]
|
||||
font_size = 12
|
||||
|
||||
[node name="QuestEntry" type="Button" unique_id=1674293762]
|
||||
flat = true
|
||||
script = ExtResource("1_xva0s")
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="." unique_id=122221174]
|
||||
layout_mode = 0
|
||||
offset_right = 40.0
|
||||
offset_bottom = 40.0
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="HBoxContainer" unique_id=39281024]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="TitleLabel" type="Label" parent="HBoxContainer/VBoxContainer" unique_id=798269508]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="ProgressLabel" type="Label" parent="HBoxContainer/VBoxContainer" unique_id=1640888083]
|
||||
layout_mode = 2
|
||||
label_settings = SubResource("LabelSettings_3agn1")
|
||||
|
||||
[node name="StatusIcon" type="Label" parent="HBoxContainer" unique_id=1154347679]
|
||||
layout_mode = 2
|
||||
154
game/quest_journal/quest_journal.gd
Normal file
154
game/quest_journal/quest_journal.gd
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
extends Control
|
||||
class_name QuestJournal
|
||||
|
||||
enum Tab { MAIN, SIDE, COMPLETED }
|
||||
|
||||
const QUEST_ENTRY_SCENE := preload("res://game/quest_journal/quest_entry.tscn")
|
||||
const OBJECTIVE_ROW_SCENE := preload("res://game/quest_journal/objective_row.tscn")
|
||||
const REWARD_ROW_SCENE := preload("res://game/quest_journal/reward_row.tscn")
|
||||
|
||||
@onready var tab_bar: TabBar = $MarginContainer/HBoxContainer/LeftPanel/TabBar
|
||||
@onready var quest_list: VBoxContainer = $MarginContainer/HBoxContainer/LeftPanel/ScrollContainer/QuestList
|
||||
@onready var footer_label: Label = $MarginContainer/HBoxContainer/LeftPanel/FooterLabel
|
||||
|
||||
@onready var detail_title: Label = $MarginContainer/HBoxContainer/RightPanel/DetailScroll/DetailContent/DetailTitle
|
||||
@onready var detail_category: Label = $MarginContainer/HBoxContainer/RightPanel/DetailScroll/DetailContent/DetailCategory
|
||||
@onready var detail_description: RichTextLabel = $MarginContainer/HBoxContainer/RightPanel/DetailScroll/DetailContent/DetailDescription
|
||||
@onready var objectives_list: VBoxContainer = $MarginContainer/HBoxContainer/RightPanel/DetailScroll/DetailContent/ObjectivesList
|
||||
@onready var rewards_list: VBoxContainer = $MarginContainer/HBoxContainer/RightPanel/DetailScroll/DetailContent/RewardsList
|
||||
|
||||
var _current_tab: Tab = Tab.MAIN
|
||||
var _selected_quest_id: StringName = &""
|
||||
|
||||
func _ready() -> void:
|
||||
tab_bar.tab_changed.connect(_on_tab_changed)
|
||||
GameEvents.quest_started.connect(_on_quest_changed)
|
||||
GameEvents.quest_completed.connect(_on_quest_changed)
|
||||
GameEvents.quest_objective_updated.connect(_on_objective_updated)
|
||||
refresh()
|
||||
|
||||
func _on_tab_changed(index: int) -> void:
|
||||
_current_tab = index as Tab
|
||||
refresh()
|
||||
|
||||
func _on_quest_changed(_id: StringName) -> void:
|
||||
refresh()
|
||||
|
||||
func _on_objective_updated(_qid: StringName, _idx: int, _cur: int, _req: int) -> void:
|
||||
refresh() # simple et suffisant
|
||||
|
||||
# --- Rafraîchissement global ---
|
||||
|
||||
func refresh() -> void:
|
||||
_clear_list()
|
||||
var quests := _get_quests_for_tab(_current_tab)
|
||||
|
||||
if quests.is_empty():
|
||||
_show_empty_state()
|
||||
_clear_detail()
|
||||
return
|
||||
|
||||
# Tri : principales par chapter_index, secondaires par titre
|
||||
if _current_tab == Tab.MAIN:
|
||||
quests.sort_custom(func(a, b): return a.chapter_index < b.chapter_index)
|
||||
else:
|
||||
quests.sort_custom(func(a, b): return a.title < b.title)
|
||||
|
||||
for quest in quests:
|
||||
var entry := QUEST_ENTRY_SCENE.instantiate()
|
||||
entry.setup(quest, _current_tab == Tab.COMPLETED)
|
||||
entry.quest_selected.connect(_on_quest_selected)
|
||||
quest_list.add_child(entry)
|
||||
|
||||
# Auto-sélection : garde la sélection actuelle si elle est toujours valide
|
||||
var still_valid := quests.any(func(q): return q.quest_id == _selected_quest_id)
|
||||
if still_valid:
|
||||
_on_quest_selected(_selected_quest_id)
|
||||
elif quests.size() > 0:
|
||||
_on_quest_selected(quests[0].quest_id)
|
||||
else:
|
||||
_clear_detail()
|
||||
|
||||
_update_footer()
|
||||
|
||||
func _get_quests_for_tab(tab: Tab) -> Array[QuestData]:
|
||||
var result: Array[QuestData] = []
|
||||
match tab:
|
||||
Tab.MAIN:
|
||||
for q in QuestManager.active_quests.values():
|
||||
if q.category == QuestData.Category.MAIN:
|
||||
result.append(q)
|
||||
Tab.SIDE:
|
||||
for q in QuestManager.active_quests.values():
|
||||
if q.category == QuestData.Category.SIDE:
|
||||
result.append(q)
|
||||
Tab.COMPLETED:
|
||||
for q in QuestManager.completed_quests.values():
|
||||
result.append(q)
|
||||
return result
|
||||
|
||||
func _clear_list() -> void:
|
||||
for child in quest_list.get_children():
|
||||
child.queue_free()
|
||||
|
||||
func _show_empty_state() -> void:
|
||||
var label := Label.new()
|
||||
label.text = "Aucune quête dans cette catégorie."
|
||||
label.add_theme_color_override("font_color", Color(0.6, 0.6, 0.6))
|
||||
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
quest_list.add_child(label)
|
||||
|
||||
func _update_footer() -> void:
|
||||
var main_count := 0
|
||||
var side_count := 0
|
||||
for q in QuestManager.active_quests.values():
|
||||
if q.category == QuestData.Category.MAIN: main_count += 1
|
||||
elif q.category == QuestData.Category.SIDE: side_count += 1
|
||||
footer_label.text = "Principales : %d | Secondaires : %d | Terminées : %d" % [
|
||||
main_count, side_count, QuestManager.completed_quests.size()
|
||||
]
|
||||
|
||||
# --- Détail d'une quête ---
|
||||
|
||||
func _on_quest_selected(quest_id: StringName) -> void:
|
||||
_selected_quest_id = quest_id
|
||||
var quest: QuestData = QuestManager.all_quests.get(quest_id)
|
||||
if quest == null:
|
||||
_clear_detail()
|
||||
return
|
||||
|
||||
detail_title.text = quest.title
|
||||
detail_category.text = _category_label(quest.category)
|
||||
detail_description.text = quest.description
|
||||
|
||||
# Objectifs
|
||||
for child in objectives_list.get_children():
|
||||
child.queue_free()
|
||||
for obj in quest.objectives:
|
||||
var row := OBJECTIVE_ROW_SCENE.instantiate()
|
||||
row.setup(obj, _current_tab == Tab.COMPLETED)
|
||||
objectives_list.add_child(row)
|
||||
|
||||
# Récompenses
|
||||
for child in rewards_list.get_children():
|
||||
child.queue_free()
|
||||
for reward in quest.rewards:
|
||||
var row := REWARD_ROW_SCENE.instantiate()
|
||||
row.setup(reward)
|
||||
rewards_list.add_child(row)
|
||||
|
||||
func _clear_detail() -> void:
|
||||
detail_title.text = ""
|
||||
detail_category.text = ""
|
||||
detail_description.text = ""
|
||||
for child in objectives_list.get_children():
|
||||
child.queue_free()
|
||||
for child in rewards_list.get_children():
|
||||
child.queue_free()
|
||||
|
||||
func _category_label(cat: QuestData.Category) -> String:
|
||||
match cat:
|
||||
QuestData.Category.MAIN: return "QUÊTE PRINCIPALE"
|
||||
QuestData.Category.SIDE: return "QUÊTE SECONDAIRE"
|
||||
QuestData.Category.HIDDEN: return "QUÊTE SECRÈTE"
|
||||
return ""
|
||||
1
game/quest_journal/quest_journal.gd.uid
Normal file
1
game/quest_journal/quest_journal.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://b3vt467ykvg8u
|
||||
105
game/quest_journal/quest_journal.tscn
Normal file
105
game/quest_journal/quest_journal.tscn
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
[gd_scene format=3 uid="uid://cluiqkghowxdw"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://b3vt467ykvg8u" path="res://game/quest_journal/quest_journal.gd" id="1_ri51t"]
|
||||
|
||||
[sub_resource type="LabelSettings" id="LabelSettings_3agn1"]
|
||||
font_size = 12
|
||||
font_color = Color(0.8051242, 0.8051241, 7.70092e-07, 1)
|
||||
|
||||
[node name="QuestJournal" type="Control" unique_id=625487910]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
script = ExtResource("1_ri51t")
|
||||
|
||||
[node name="Background" type="ColorRect" parent="." unique_id=530768479]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
color = Color(0, 0, 0, 0.42745098)
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="." unique_id=1311181226]
|
||||
process_mode = 3
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
theme_override_constants/margin_left = 40
|
||||
theme_override_constants/margin_top = 40
|
||||
theme_override_constants/margin_right = 40
|
||||
theme_override_constants/margin_bottom = 40
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer" unique_id=1393741966]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="LeftPanel" type="VBoxContainer" parent="MarginContainer/HBoxContainer" unique_id=422872187]
|
||||
custom_minimum_size = Vector2(380, 0)
|
||||
custom_maximum_size = Vector2(380, -1)
|
||||
layout_mode = 2
|
||||
|
||||
[node name="TitleLabel" type="Label" parent="MarginContainer/HBoxContainer/LeftPanel" unique_id=510808795]
|
||||
layout_mode = 2
|
||||
text = "JOURNAL DE QUÊTES"
|
||||
|
||||
[node name="TabBar" type="TabBar" parent="MarginContainer/HBoxContainer/LeftPanel" unique_id=1903671008]
|
||||
layout_mode = 2
|
||||
current_tab = 0
|
||||
tab_count = 3
|
||||
tab_0/title = "Principales"
|
||||
tab_1/title = "Secondaires"
|
||||
tab_2/title = "Terminées"
|
||||
|
||||
[node name="ScrollContainer" type="ScrollContainer" parent="MarginContainer/HBoxContainer/LeftPanel" unique_id=678478455]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="QuestList" type="VBoxContainer" parent="MarginContainer/HBoxContainer/LeftPanel/ScrollContainer" unique_id=1539103998]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="FooterLabel" type="Label" parent="MarginContainer/HBoxContainer/LeftPanel" unique_id=1538203911]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="RightPanel" type="PanelContainer" parent="MarginContainer/HBoxContainer" unique_id=614836913]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="DetailScroll" type="ScrollContainer" parent="MarginContainer/HBoxContainer/RightPanel" unique_id=195451550]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="DetailContent" type="VBoxContainer" parent="MarginContainer/HBoxContainer/RightPanel/DetailScroll" unique_id=2096180388]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="DetailTitle" type="Label" parent="MarginContainer/HBoxContainer/RightPanel/DetailScroll/DetailContent" unique_id=2026354509]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="DetailCategory" type="Label" parent="MarginContainer/HBoxContainer/RightPanel/DetailScroll/DetailContent" unique_id=1664568446]
|
||||
layout_mode = 2
|
||||
label_settings = SubResource("LabelSettings_3agn1")
|
||||
|
||||
[node name="HSeparator" type="HSeparator" parent="MarginContainer/HBoxContainer/RightPanel/DetailScroll/DetailContent" unique_id=21965256]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="DetailDescription" type="RichTextLabel" parent="MarginContainer/HBoxContainer/RightPanel/DetailScroll/DetailContent" unique_id=1262773543]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="ObjectivesTitle" type="Label" parent="MarginContainer/HBoxContainer/RightPanel/DetailScroll/DetailContent" unique_id=510015609]
|
||||
layout_mode = 2
|
||||
text = "OBJECTIFS"
|
||||
|
||||
[node name="ObjectivesList" type="VBoxContainer" parent="MarginContainer/HBoxContainer/RightPanel/DetailScroll/DetailContent" unique_id=954789404]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="RewardsTitle" type="Label" parent="MarginContainer/HBoxContainer/RightPanel/DetailScroll/DetailContent" unique_id=767078748]
|
||||
layout_mode = 2
|
||||
text = "RÉCOMPENSES"
|
||||
|
||||
[node name="RewardsList" type="VBoxContainer" parent="MarginContainer/HBoxContainer/RightPanel/DetailScroll/DetailContent" unique_id=851905982]
|
||||
layout_mode = 2
|
||||
20
game/quest_journal/reward_row.gd
Normal file
20
game/quest_journal/reward_row.gd
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
extends HBoxContainer
|
||||
|
||||
@onready var icon_label: Label = $IconLabel
|
||||
@onready var reward_label: Label = $RewardLabel
|
||||
|
||||
func setup(reward: QuestReward) -> void:
|
||||
match reward.type:
|
||||
QuestReward.Type.ITEM:
|
||||
icon_label.text = "🎁"
|
||||
reward_label.text = "%s × %d" % [reward.target_id, reward.amount]
|
||||
QuestReward.Type.MONEY:
|
||||
icon_label.text = "💰"
|
||||
reward_label.text = "%d pièces" % reward.amount
|
||||
QuestReward.Type.POKEMON:
|
||||
icon_label.text = "🐾"
|
||||
reward_label.text = "Pokémon : %s" % reward.target_id
|
||||
QuestReward.Type.UNLOCK_QUEST:
|
||||
icon_label.text = "🔓"
|
||||
reward_label.text = "Nouvelle quête : %s" % reward.target_id
|
||||
icon_label.add_theme_color_override("font_color", Color(0.9, 0.8, 0.4))
|
||||
1
game/quest_journal/reward_row.gd.uid
Normal file
1
game/quest_journal/reward_row.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://b0m0dg8eccu4u
|
||||
15
game/quest_journal/reward_row.tscn
Normal file
15
game/quest_journal/reward_row.tscn
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
[gd_scene format=3 uid="uid://bhyrhg21amey2"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://b0m0dg8eccu4u" path="res://game/quest_journal/reward_row.gd" id="1_qcyvc"]
|
||||
|
||||
[node name="RewardRow" type="HBoxContainer" unique_id=1679893101]
|
||||
script = ExtResource("1_qcyvc")
|
||||
|
||||
[node name="IconLabel" type="Label" parent="." unique_id=2121958636]
|
||||
custom_minimum_size = Vector2(24, 24)
|
||||
custom_maximum_size = Vector2(24, 24)
|
||||
layout_mode = 2
|
||||
|
||||
[node name="RewardLabel" type="Label" parent="." unique_id=513948004]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
25
game/quests/data/main_01_prof.tres
Normal file
25
game/quests/data/main_01_prof.tres
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
[gd_resource type="Resource" script_class="QuestData" format=3 uid="uid://cx1rj8j6ww65u"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dj84i3mki0cws" path="res://game/quests/quest_objective.gd" id="1_yy5gg"]
|
||||
[ext_resource type="Script" uid="uid://bpjxqwo7c5f64" path="res://game/quests/quest_reward.gd" id="2_w6uu2"]
|
||||
[ext_resource type="Script" uid="uid://bldyeahkrms8n" path="res://game/quests/quest_data.gd" id="3_8rvwr"]
|
||||
|
||||
[sub_resource type="Resource" id="Resource_fl2hl"]
|
||||
script = ExtResource("1_yy5gg")
|
||||
target_id = &"prof_chen"
|
||||
metadata/_custom_type_script = "uid://dj84i3mki0cws"
|
||||
|
||||
[sub_resource type="Resource" id="Resource_5xet1"]
|
||||
script = ExtResource("2_w6uu2")
|
||||
target_id = &"pokedex"
|
||||
metadata/_custom_type_script = "uid://bpjxqwo7c5f64"
|
||||
|
||||
[resource]
|
||||
script = ExtResource("3_8rvwr")
|
||||
quest_id = &"main_01_prof"
|
||||
title = "Prof"
|
||||
category = 0
|
||||
objectives = Array[ExtResource("1_yy5gg")]([SubResource("Resource_fl2hl")])
|
||||
rewards = Array[ExtResource("2_w6uu2")]([SubResource("Resource_5xet1")])
|
||||
requires_manual_unlock = true
|
||||
metadata/_custom_type_script = "uid://bldyeahkrms8n"
|
||||
14
game/quests/data/main_01_start.tres
Normal file
14
game/quests/data/main_01_start.tres
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
[gd_resource type="Resource" script_class="QuestData" format=3 uid="uid://d007l7h0dy3vt"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dj84i3mki0cws" path="res://game/quests/quest_objective.gd" id="1_dsryk"]
|
||||
[ext_resource type="Script" uid="uid://bpjxqwo7c5f64" path="res://game/quests/quest_reward.gd" id="2_0a632"]
|
||||
[ext_resource type="Script" uid="uid://bldyeahkrms8n" path="res://game/quests/quest_data.gd" id="3_0ncdg"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("3_0ncdg")
|
||||
quest_id = &"main_01_start"
|
||||
title = "Reçois ton premier Pokémon"
|
||||
category = 0
|
||||
requires_manual_unlock = true
|
||||
chapter_index = 1
|
||||
metadata/_custom_type_script = "uid://bldyeahkrms8n"
|
||||
14
game/quests/data/main_02_route1.tres
Normal file
14
game/quests/data/main_02_route1.tres
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
[gd_resource type="Resource" script_class="QuestData" format=3 uid="uid://b2o84nycfwg3g"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dj84i3mki0cws" path="res://game/quests/quest_objective.gd" id="1_pjbw5"]
|
||||
[ext_resource type="Script" uid="uid://bpjxqwo7c5f64" path="res://game/quests/quest_reward.gd" id="2_eesy8"]
|
||||
[ext_resource type="Script" uid="uid://bldyeahkrms8n" path="res://game/quests/quest_data.gd" id="3_wi6e2"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("3_wi6e2")
|
||||
quest_id = &"main_02_route1"
|
||||
title = "Traverse la Route 1"
|
||||
category = 0
|
||||
prerequisites = Array[StringName]([&"main_01_start"])
|
||||
chapter_index = 2
|
||||
metadata/_custom_type_script = "uid://bldyeahkrms8n"
|
||||
27
game/quests/data/main_02_starter.tres
Normal file
27
game/quests/data/main_02_starter.tres
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
[gd_resource type="Resource" script_class="QuestData" format=3 uid="uid://bbdba5sej5opj"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dj84i3mki0cws" path="res://game/quests/quest_objective.gd" id="1_reajj"]
|
||||
[ext_resource type="Script" uid="uid://bpjxqwo7c5f64" path="res://game/quests/quest_reward.gd" id="2_0q14y"]
|
||||
[ext_resource type="Script" uid="uid://bldyeahkrms8n" path="res://game/quests/quest_data.gd" id="3_5inpn"]
|
||||
|
||||
[sub_resource type="Resource" id="Resource_1x48t"]
|
||||
script = ExtResource("1_reajj")
|
||||
type = 2
|
||||
target_id = &"pikachu"
|
||||
metadata/_custom_type_script = "uid://dj84i3mki0cws"
|
||||
|
||||
[sub_resource type="Resource" id="Resource_pjk6h"]
|
||||
script = ExtResource("2_0q14y")
|
||||
target_id = &"pokeball"
|
||||
amount = 5
|
||||
metadata/_custom_type_script = "uid://bpjxqwo7c5f64"
|
||||
|
||||
[resource]
|
||||
script = ExtResource("3_5inpn")
|
||||
quest_id = &"main_02_starter"
|
||||
title = "Starter"
|
||||
category = 0
|
||||
objectives = Array[ExtResource("1_reajj")]([SubResource("Resource_1x48t")])
|
||||
rewards = Array[ExtResource("2_0q14y")]([SubResource("Resource_pjk6h")])
|
||||
prerequisites = Array[StringName]([&"main_01_prof"])
|
||||
metadata/_custom_type_script = "uid://bldyeahkrms8n"
|
||||
14
game/quests/data/main_03_arène1.tres
Normal file
14
game/quests/data/main_03_arène1.tres
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
[gd_resource type="Resource" script_class="QuestData" format=3 uid="uid://djsmge1a745q2"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dj84i3mki0cws" path="res://game/quests/quest_objective.gd" id="1_y246s"]
|
||||
[ext_resource type="Script" uid="uid://bpjxqwo7c5f64" path="res://game/quests/quest_reward.gd" id="2_ywsws"]
|
||||
[ext_resource type="Script" uid="uid://bldyeahkrms8n" path="res://game/quests/quest_data.gd" id="3_bp48w"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("3_bp48w")
|
||||
quest_id = &"main_03_arène1"
|
||||
title = "Bats l'Arène de Jadielle"
|
||||
category = 0
|
||||
prerequisites = Array[StringName]([&"main_02_route1"])
|
||||
chapter_index = 3
|
||||
metadata/_custom_type_script = "uid://bldyeahkrms8n"
|
||||
14
game/quests/data/main_04_team_rocket.tres
Normal file
14
game/quests/data/main_04_team_rocket.tres
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
[gd_resource type="Resource" script_class="QuestData" format=3 uid="uid://cywdaoplv561d"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dj84i3mki0cws" path="res://game/quests/quest_objective.gd" id="1_gq0xw"]
|
||||
[ext_resource type="Script" uid="uid://bpjxqwo7c5f64" path="res://game/quests/quest_reward.gd" id="2_nwtdp"]
|
||||
[ext_resource type="Script" uid="uid://bldyeahkrms8n" path="res://game/quests/quest_data.gd" id="3_dcxfq"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("3_dcxfq")
|
||||
quest_id = &"main_04_team_rocket"
|
||||
title = "Chasse la Team Rocket"
|
||||
category = 0
|
||||
prerequisites = Array[StringName]([&"main_03_arène1"])
|
||||
chapter_index = 4
|
||||
metadata/_custom_type_script = "uid://bldyeahkrms8n"
|
||||
18
game/quests/data/side_badges.tres
Normal file
18
game/quests/data/side_badges.tres
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
[gd_resource type="Resource" script_class="QuestData" format=3 uid="uid://d7q633m7osyu"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dj84i3mki0cws" path="res://game/quests/quest_objective.gd" id="1_wkil8"]
|
||||
[ext_resource type="Script" uid="uid://bpjxqwo7c5f64" path="res://game/quests/quest_reward.gd" id="2_eemhw"]
|
||||
[ext_resource type="Script" uid="uid://bldyeahkrms8n" path="res://game/quests/quest_data.gd" id="3_r5m7j"]
|
||||
|
||||
[sub_resource type="Resource" id="Resource_l3iqy"]
|
||||
script = ExtResource("1_wkil8")
|
||||
target_id = &"champion"
|
||||
metadata/_custom_type_script = "uid://dj84i3mki0cws"
|
||||
|
||||
[resource]
|
||||
script = ExtResource("3_r5m7j")
|
||||
quest_id = &"side_badges"
|
||||
title = "Side Badges"
|
||||
objectives = Array[ExtResource("1_wkil8")]([SubResource("Resource_l3iqy")])
|
||||
requires_manual_unlock = true
|
||||
metadata/_custom_type_script = "uid://bldyeahkrms8n"
|
||||
33
game/quests/data/side_chenipan.tres
Normal file
33
game/quests/data/side_chenipan.tres
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
[gd_resource type="Resource" script_class="QuestData" format=3 uid="uid://c0iff4kfhxidm"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dj84i3mki0cws" path="res://game/quests/quest_objective.gd" id="1_mcbuv"]
|
||||
[ext_resource type="Script" uid="uid://bpjxqwo7c5f64" path="res://game/quests/quest_reward.gd" id="2_6way2"]
|
||||
[ext_resource type="Script" uid="uid://bldyeahkrms8n" path="res://game/quests/quest_data.gd" id="3_fslg5"]
|
||||
|
||||
[sub_resource type="Resource" id="Resource_lmqnn"]
|
||||
script = ExtResource("1_mcbuv")
|
||||
type = 2
|
||||
target_id = &"caterpie"
|
||||
description = "Capturer un Chenipan"
|
||||
metadata/_custom_type_script = "uid://dj84i3mki0cws"
|
||||
|
||||
[sub_resource type="Resource" id="Resource_1xmq8"]
|
||||
script = ExtResource("1_mcbuv")
|
||||
target_id = &"prof_chen"
|
||||
description = "Rapporter le Chenipan au Professeur"
|
||||
metadata/_custom_type_script = "uid://dj84i3mki0cws"
|
||||
|
||||
[sub_resource type="Resource" id="Resource_xnmnt"]
|
||||
script = ExtResource("2_6way2")
|
||||
target_id = &"pokeball"
|
||||
metadata/_custom_type_script = "uid://bpjxqwo7c5f64"
|
||||
|
||||
[resource]
|
||||
script = ExtResource("3_fslg5")
|
||||
quest_id = &"side_chenipan"
|
||||
title = "Le Chenipan du Professeur"
|
||||
description = "Le Professeur Chen veut étudier un Chenipan."
|
||||
objectives = Array[ExtResource("1_mcbuv")]([SubResource("Resource_lmqnn"), SubResource("Resource_1xmq8")])
|
||||
rewards = Array[ExtResource("2_6way2")]([SubResource("Resource_xnmnt")])
|
||||
requires_manual_unlock = true
|
||||
metadata/_custom_type_script = "uid://bldyeahkrms8n"
|
||||
13
game/quests/data/side_pêcheur.tres
Normal file
13
game/quests/data/side_pêcheur.tres
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
[gd_resource type="Resource" script_class="QuestData" format=3 uid="uid://5xw8pcuefrfl"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dj84i3mki0cws" path="res://game/quests/quest_objective.gd" id="1_rk5ju"]
|
||||
[ext_resource type="Script" uid="uid://bpjxqwo7c5f64" path="res://game/quests/quest_reward.gd" id="2_r8x02"]
|
||||
[ext_resource type="Script" uid="uid://bldyeahkrms8n" path="res://game/quests/quest_data.gd" id="3_5wjtt"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("3_5wjtt")
|
||||
quest_id = &"side_pêcheur"
|
||||
title = "Le Pêcheur Poissirène"
|
||||
prerequisites = Array[StringName]([&"main_02_route1"])
|
||||
requires_manual_unlock = true
|
||||
metadata/_custom_type_script = "uid://bldyeahkrms8n"
|
||||
41
game/quests/quest_data.gd
Normal file
41
game/quests/quest_data.gd
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
extends Resource
|
||||
class_name QuestData
|
||||
|
||||
enum QuestState { LOCKED, AVAILABLE, ACTIVE, COMPLETED, FAILED }
|
||||
enum Category { MAIN, SIDE, HIDDEN }
|
||||
|
||||
@export var quest_id: StringName = &""
|
||||
@export var title: String = ""
|
||||
@export_multiline var description: String = ""
|
||||
@export var category: Category = Category.SIDE
|
||||
@export var objectives: Array[QuestObjective] = []
|
||||
@export var rewards: Array[QuestReward] = []
|
||||
@export var prerequisites: Array[StringName] = []
|
||||
@export var requires_manual_unlock: bool = false
|
||||
@export var chapter_index: int = 0
|
||||
|
||||
## NOUVEAU : conditions sur les flags pour que la quête soit disponible.
|
||||
## Exemple :
|
||||
## { &"prof_rencontre": true, &"badges_gagnes": 3 }
|
||||
@export var required_flags: Dictionary = {}
|
||||
|
||||
## NOUVEAU : si l'un de ces flags est vrai, la quête ne se débloque PAS.
|
||||
## Utile pour les choix exclusifs (ex: avoir choisi la Team Rocket).
|
||||
@export var blocking_flags: Dictionary = {}
|
||||
|
||||
var state: QuestState = QuestState.LOCKED
|
||||
|
||||
func get_required_objectives() -> Array[QuestObjective]:
|
||||
return objectives.filter(func(o): return not o.is_optional)
|
||||
|
||||
func are_all_objectives_complete() -> bool:
|
||||
for obj in get_required_objectives():
|
||||
if not obj.is_complete():
|
||||
return false
|
||||
return true
|
||||
|
||||
func get_state_name() -> String:
|
||||
return QuestState.keys()[state]
|
||||
|
||||
func get_category_name() -> String:
|
||||
return Category.keys()[category]
|
||||
1
game/quests/quest_data.gd.uid
Normal file
1
game/quests/quest_data.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bldyeahkrms8n
|
||||
27
game/quests/quest_objective.gd
Normal file
27
game/quests/quest_objective.gd
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
extends Resource
|
||||
class_name QuestObjective
|
||||
|
||||
enum Type {
|
||||
TALK_TO, # Parler à un PNJ
|
||||
COLLECT, # Récolter/Ramasser un objet
|
||||
CAPTURE, # Capturer un Pokémon
|
||||
DEFEAT, # Battre un Pokémon
|
||||
REACH_LOCATION, # Atteindre un lieu
|
||||
CUSTOM # Pour extensions futures
|
||||
}
|
||||
|
||||
@export var type: Type = Type.TALK_TO
|
||||
@export var target_id: StringName = &"" # ex: &"chenipan", &"prof_chen"
|
||||
@export var required_count: int = 1
|
||||
@export var is_optional: bool = false
|
||||
@export var description: String = "" # Texte affiché dans le journal
|
||||
|
||||
# Progression (non exportée : gérée en runtime par le QuestManager)
|
||||
var current_count: int = 0
|
||||
|
||||
func is_complete() -> bool:
|
||||
return current_count >= required_count
|
||||
|
||||
## Retourne true si l'événement correspond à cet objectif
|
||||
func matches(event_type: Type, event_target: StringName) -> bool:
|
||||
return type == event_type and target_id == event_target
|
||||
1
game/quests/quest_objective.gd.uid
Normal file
1
game/quests/quest_objective.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dj84i3mki0cws
|
||||
13
game/quests/quest_reward.gd
Normal file
13
game/quests/quest_reward.gd
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
extends Resource
|
||||
class_name QuestReward
|
||||
|
||||
enum Type {
|
||||
ITEM, # Donner un objet
|
||||
MONEY, # Donner de l'argent
|
||||
POKEMON, # Donner un Pokémon
|
||||
UNLOCK_QUEST # Débloquer une nouvelle quête
|
||||
}
|
||||
|
||||
@export var type: Type = Type.ITEM
|
||||
@export var target_id: StringName = &"" # ex: &"pokeball", &"pokedex"
|
||||
@export var amount: int = 1
|
||||
1
game/quests/quest_reward.gd.uid
Normal file
1
game/quests/quest_reward.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bpjxqwo7c5f64
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
[ext_resource type="Script" uid="uid://7hglx6nfu063" path="res://addons/controller_icons/objects/ControllerIconTexture.gd" id="1_4xhdt"]
|
||||
[ext_resource type="Script" uid="uid://b3ddqgiawrh5c" path="res://game/ui/hud/team_hud.gd" id="2_lkjh2"]
|
||||
[ext_resource type="Script" uid="uid://cniwr4iowd8we" path="res://game/core/debug_overlay.gd" id="3_euh0j"]
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_uyiaf"]
|
||||
|
||||
|
|
@ -92,3 +93,6 @@ anchor_bottom = 1.0
|
|||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
script = ExtResource("2_lkjh2")
|
||||
|
||||
[node name="DebugOverlay" type="CanvasLayer" parent="." unique_id=1710349785]
|
||||
script = ExtResource("3_euh0j")
|
||||
|
|
|
|||
|
|
@ -23,6 +23,10 @@ func _ready() -> void:
|
|||
animation_player.play("RESET")
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if event.is_action_pressed("open_quest_journal"):
|
||||
toggle_journal()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
if is_transitioning or sub_screen != null: return #une page ouverte gere ses propres entrees
|
||||
if event.is_action_pressed("pause"):
|
||||
if get_tree().paused:
|
||||
|
|
@ -123,3 +127,23 @@ func _on_restart_pressed() -> void:
|
|||
func _on_quit_pressed() -> void:
|
||||
UiAudio.play(click_sound)
|
||||
get_tree().quit()
|
||||
|
||||
|
||||
|
||||
const QUEST_JOURNAL_SCENE := preload("res://game/quest_journal/quest_journal.tscn")
|
||||
|
||||
var _journal_instance: Control = null
|
||||
|
||||
func toggle_journal() -> void:
|
||||
if _journal_instance and is_instance_valid(_journal_instance):
|
||||
_journal_instance.queue_free()
|
||||
_journal_instance = null
|
||||
get_tree().paused = false
|
||||
else:
|
||||
_journal_instance = QUEST_JOURNAL_SCENE.instantiate()
|
||||
add_child(_journal_instance)
|
||||
get_tree().paused = true
|
||||
|
||||
|
||||
func _on_quests_pressed() -> void:
|
||||
toggle_journal()
|
||||
|
|
|
|||
|
|
@ -205,7 +205,6 @@ libraries/ = SubResource("AnimationLibrary_2nc7s")
|
|||
|
||||
[connection signal="pressed" from="PanelContainer/VBoxContainer/Resume" to="." method="_on_resume_pressed"]
|
||||
[connection signal="pressed" from="PanelContainer/VBoxContainer/Team" to="." method="_on_team_pressed"]
|
||||
[connection signal="pressed" from="PanelContainer/VBoxContainer/Team" to="." method="_on_team_pressed"]
|
||||
[connection signal="pressed" from="PanelContainer/VBoxContainer/Storage" to="." method="_on_storage_pressed"]
|
||||
[connection signal="pressed" from="PanelContainer/VBoxContainer/Save" to="." method="_on_save_pressed"]
|
||||
[connection signal="pressed" from="PanelContainer/VBoxContainer/Load" to="." method="_on_load_pressed"]
|
||||
|
|
|
|||
|
|
@ -24,9 +24,12 @@ buses/default_bus_layout="res://game/core/default_bus_layout.tres"
|
|||
[autoload]
|
||||
|
||||
ControllerIcons="*uid://b7ho1rjcpht1d"
|
||||
GameData="*uid://d4beiel88cwc7"
|
||||
UiAudio="*uid://7e7fe2xk3aue"
|
||||
GameData="*uid://d4beiel88cwc7"
|
||||
GameEvents="*uid://ct3v0qsc3bvcn"
|
||||
QuestManager="*uid://owgkeygdlsmv"
|
||||
Pokedex="*uid://dijnkrewbwgmx"
|
||||
FlagManager="*uid://ceb0ky3nlq1do"
|
||||
|
||||
[display]
|
||||
|
||||
|
|
@ -163,6 +166,16 @@ player_aim_switch_target={
|
|||
"events": [Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":0,"pressure":0.0,"pressed":true,"script":null)
|
||||
]
|
||||
}
|
||||
debug_quests={
|
||||
"deadzone": 0.2,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":67,"key_label":0,"unicode":99,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
open_quest_journal={
|
||||
"deadzone": 0.2,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":74,"key_label":0,"unicode":106,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
|
||||
[physics]
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue