commit 47dbea8ea41887b098cf084da069163cb1026748 Author: TheThomaas Date: Tue Sep 29 15:56:43 2026 +0200 Initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/data/l10n.gd b/data/l10n.gd new file mode 100644 index 0000000..2a6af36 --- /dev/null +++ b/data/l10n.gd @@ -0,0 +1,15 @@ +# data/l10n.gd +@tool +class_name L10n +extends RefCounted + +const FALLBACK := "en" + +static func current_lang() -> String: + return TranslationServer.get_locale().substr(0, 2) + +static func pick(texts : Dictionary, default : String = "") -> String: + var lang := current_lang() + if texts.has(lang): return String(texts[lang]) + if texts.has(FALLBACK): return String(texts[FALLBACK]) + return default diff --git a/data/l10n.gd.uid b/data/l10n.gd.uid new file mode 100644 index 0000000..18f2545 --- /dev/null +++ b/data/l10n.gd.uid @@ -0,0 +1 @@ +uid://byeysk5pnt16v diff --git a/data/learned_move.gd b/data/learned_move.gd new file mode 100644 index 0000000..2935a10 --- /dev/null +++ b/data/learned_move.gd @@ -0,0 +1,8 @@ +# data/learned_move.gd +@tool +class_name LearnedMove +extends Resource + +@export var move : MoveData +@export var level : int +@export var method : String diff --git a/data/learned_move.gd.uid b/data/learned_move.gd.uid new file mode 100644 index 0000000..38a4d60 --- /dev/null +++ b/data/learned_move.gd.uid @@ -0,0 +1 @@ +uid://dxpd7ihc8eew5 diff --git a/data/move_data.gd b/data/move_data.gd new file mode 100644 index 0000000..70fa985 --- /dev/null +++ b/data/move_data.gd @@ -0,0 +1,22 @@ +# data/move_data.gd +@tool +class_name MoveData +extends Resource + +@export var id : int +@export var api_name : String +@export var names : Dictionary = {} # {"fr": "Éclair", "en": "Thunder Shock"} +@export var descriptions : Dictionary = {} +@export var type : String +@export var category : String # "physical", "special", "status" +@export var power : int +@export var accuracy : int +@export var pp : int +@export var priority : int +@export var target : String + +func get_display_name() -> String: + return L10n.pick(names, api_name.capitalize()) + +func get_description() -> String: + return L10n.pick(descriptions) diff --git a/data/move_data.gd.uid b/data/move_data.gd.uid new file mode 100644 index 0000000..7817a51 --- /dev/null +++ b/data/move_data.gd.uid @@ -0,0 +1 @@ +uid://chc1mf57j5m4p diff --git a/data/pokemon_data.gd b/data/pokemon_data.gd new file mode 100644 index 0000000..59c0890 --- /dev/null +++ b/data/pokemon_data.gd @@ -0,0 +1,28 @@ +# data/pokemon_data.gd +@tool +class_name PokemonData +extends Resource + +@export var id : int +@export var api_name : String +@export var names : Dictionary = {} +@export var descriptions : Dictionary = {} +@export var types : PackedStringArray +@export var abilities : PackedStringArray +@export var base_stats : Dictionary = {} +@export var height : float +@export var weight : float +@export var menu_sprite : Texture2D +@export var shiny_menu_sprite : Texture2D +@export var model_scene : PackedScene +@export var shiny_albedo : Texture2D +@export var learnset : Array[LearnedMove] = [] + +func get_display_name() -> String: + return L10n.pick(names, api_name.capitalize()) + +func get_description() -> String: + return L10n.pick(descriptions) + +func get_menu_sprite(shiny : bool) -> Texture2D: + return shiny_menu_sprite if shiny and shiny_menu_sprite else menu_sprite diff --git a/data/pokemon_data.gd.uid b/data/pokemon_data.gd.uid new file mode 100644 index 0000000..05b0ffd --- /dev/null +++ b/data/pokemon_data.gd.uid @@ -0,0 +1 @@ +uid://cwmxfian0pdjk diff --git a/editor/api_text.gd b/editor/api_text.gd new file mode 100644 index 0000000..7e7d0d1 --- /dev/null +++ b/editor/api_text.gd @@ -0,0 +1,19 @@ +# editor/api_text.gd +@tool +class_name PokeApiText +extends RefCounted + +static func names(entries : Array, languages : PackedStringArray) -> Dictionary: + var out := {} + for n in entries: + var lang : String = n["language"]["name"] + if lang in languages: out[lang] = n["name"] + return out + +static func flavors(entries : Array, languages : PackedStringArray) -> Dictionary: + var out := {} + for e in entries: # les plus récentes sont à la fin et écrasent les anciennes + var lang : String = e["language"]["name"] + if lang in languages: + out[lang] = String(e["flavor_text"]).replace("\n", " ").replace("\f", " ") + return out diff --git a/editor/api_text.gd.uid b/editor/api_text.gd.uid new file mode 100644 index 0000000..98b626d --- /dev/null +++ b/editor/api_text.gd.uid @@ -0,0 +1 @@ +uid://cchtb0yvmqssy diff --git a/editor/importer_dock.gd b/editor/importer_dock.gd new file mode 100644 index 0000000..3ea20c0 --- /dev/null +++ b/editor/importer_dock.gd @@ -0,0 +1,100 @@ +# editor/importer_dock.gd (interface uniquement) +@tool +extends VBoxContainer + +var editor_filesystem : EditorFileSystem + +var _client : PokeApiClient +var _input : LineEdit +var _overwrite : CheckBox +var _buttons : Array[Button] = [] +var _progress : ProgressBar +var _log : RichTextLabel +var _busy : bool = false + +func _ready() -> void: + custom_minimum_size = Vector2(260, 0) + + _client = PokeApiClient.new() + _client.progress.connect(_on_progress) + add_child(_client) + + _add_button("Importer toutes les capacités", _on_import_moves) + add_child(HSeparator.new()) + + var label := Label.new() + label.text = "Pokémon (ids, noms, plages) :" + add_child(label) + + _input = LineEdit.new() + _input.placeholder_text = "25, eevee, 1-9, mr-mime" + add_child(_input) + + _overwrite = CheckBox.new() + _overwrite.text = "Écraser les fichiers existants" + add_child(_overwrite) + + _add_button("Importer les pokémon", _on_import_pokemon) + + _progress = ProgressBar.new() + add_child(_progress) + + _log = RichTextLabel.new() + _log.custom_minimum_size = Vector2(0, 200) + _log.size_flags_vertical = Control.SIZE_EXPAND_FILL + _log.scroll_following = true + _log.selection_enabled = true + add_child(_log) + +func _add_button(text : String, callback : Callable) -> void: + var b := Button.new() + b.text = text + b.pressed.connect(callback) + add_child(b) + _buttons.append(b) + +func _on_import_moves() -> void: + if _busy: return + _set_busy(true) + var importer := PokeApiMoveImporter.new(_client, _overwrite.button_pressed) + importer.message.connect(_print) + await importer.run() + editor_filesystem.scan() + _set_busy(false) + +func _on_import_pokemon() -> void: + if _busy: return + var tokens := _parse_input(_input.text) + if tokens.is_empty(): + _print("Rien à importer.") + return + _set_busy(true) + var importer := PokeApiPokemonImporter.new(_client, editor_filesystem, _overwrite.button_pressed) + importer.message.connect(_print) + await importer.run(tokens) + _set_busy(false) + +func _parse_input(text : String) -> Array: + var out : Array = [] + for tok in text.split(",", false): + var t : String = tok.strip_edges().to_lower() + if t.is_empty(): continue + var parts := t.split("-") + if parts.size() == 2 and parts[0].is_valid_int() and parts[1].is_valid_int(): + for i in range(int(parts[0]), int(parts[1]) + 1): out.append(i) + elif t.is_valid_int(): + out.append(int(t)) + else: + out.append(t) + return out + +func _on_progress(done : int, total : int) -> void: + _progress.max_value = total + _progress.value = done + +func _set_busy(value : bool) -> void: + _busy = value + for b in _buttons: b.disabled = value + +func _print(msg : String) -> void: + _log.append_text(msg + "\n") diff --git a/editor/importer_dock.gd.uid b/editor/importer_dock.gd.uid new file mode 100644 index 0000000..8c6e982 --- /dev/null +++ b/editor/importer_dock.gd.uid @@ -0,0 +1 @@ +uid://dm018deoynvvc diff --git a/editor/move_importer.gd b/editor/move_importer.gd new file mode 100644 index 0000000..824d2cb --- /dev/null +++ b/editor/move_importer.gd @@ -0,0 +1,56 @@ +# editor/move_importer.gd +@tool +class_name PokeApiMoveImporter +extends RefCounted + +signal message(text : String) + +var _client : PokeApiClient +var _overwrite : bool +var _languages : PackedStringArray +var _dir : String + +func _init(client : PokeApiClient, overwrite : bool) -> void: + _client = client + _overwrite = overwrite + _languages = PokeApiSettings.languages() + _dir = PokeApiSettings.moves_dir() + +func run() -> void: + DirAccess.make_dir_recursive_absolute(_dir) + var list = await _client.get_json(PokeApiClient.API + "move?limit=2000") + if list == null: + message.emit("[color=red]Impossible de récupérer la liste des capacités.[/color]") + return + + var entries : Array = [] + for r in list["results"]: + var id : int = int(String(r["url"]).trim_suffix("/").get_file()) + if id < 10000: entries.append(r) # ignore les capacités "shadow" (Colosseum/XD) + + message.emit("%d capacités à traiter..." % entries.size()) + await _client.run_parallel(entries, _import_move) + message.emit("Capacités terminées.") + +func _import_move(entry : Dictionary) -> void: + var path : String = _dir + entry["name"] + ".tres" + if !_overwrite and FileAccess.file_exists(path): return + + var j = await _client.get_json(entry["url"]) + if j == null: + message.emit("[color=red]Échec : %s[/color]" % entry["name"]) + return + + var m := MoveData.new() + m.id = j["id"] + m.api_name = j["name"] + m.names = PokeApiText.names(j["names"], _languages) + m.descriptions = PokeApiText.flavors(j["flavor_text_entries"], _languages) + m.type = j["type"]["name"] + m.category = j["damage_class"]["name"] + m.power = int(j["power"]) if j["power"] != null else 0 + m.accuracy = int(j["accuracy"]) if j["accuracy"] != null else 0 + m.pp = int(j["pp"]) if j["pp"] != null else 0 + m.priority = j["priority"] + m.target = j["target"]["name"] + ResourceSaver.save(m, path) diff --git a/editor/move_importer.gd.uid b/editor/move_importer.gd.uid new file mode 100644 index 0000000..8b641cf --- /dev/null +++ b/editor/move_importer.gd.uid @@ -0,0 +1 @@ +uid://bh51lh1reyxog diff --git a/editor/pokeapi_client.gd b/editor/pokeapi_client.gd new file mode 100644 index 0000000..1d6058c --- /dev/null +++ b/editor/pokeapi_client.gd @@ -0,0 +1,63 @@ +# editor/pokeapi_client.gd +@tool +class_name PokeApiClient +extends Node + +signal progress(done : int, total : int) + +const API := "https://pokeapi.co/api/v2/" + +var _in_flight : int = 0 +var _done : int = 0 +var _total : int = 0 + +func get_bytes(url : String) -> PackedByteArray: + for attempt in 3: + var http := HTTPRequest.new() + add_child(http) + if http.request(url) != OK: + http.queue_free() + await get_tree().create_timer(1.0).timeout + continue + var res : Array = await http.request_completed + http.queue_free() + if res[0] == HTTPRequest.RESULT_SUCCESS and res[1] == 200: + return res[3] + if res[1] == 404: break + await get_tree().create_timer(1.0).timeout + return PackedByteArray() + +func get_json(url : String) -> Variant: + var bytes := await get_bytes(url) + if bytes.is_empty(): return null + return JSON.parse_string(bytes.get_string_from_utf8()) + +## Télécharge une URL vers un fichier. Retourne le chemin, ou "" en cas d'échec. +func download_file(url : Variant, path : String) -> String: + if url == null: return "" + var bytes := await get_bytes(String(url)) + if bytes.is_empty(): return "" + var f := FileAccess.open(path, FileAccess.WRITE) + if f == null: return "" + f.store_buffer(bytes) + f.close() + return path + +## Lance worker(item) pour chaque élément, avec un nombre limité de tâches simultanées. +func run_parallel(items : Array, worker : Callable) -> void: + _done = 0 + _total = items.size() + progress.emit(0, _total) + for item in items: + while _in_flight >= PokeApiSettings.max_concurrent(): + await get_tree().process_frame + _in_flight += 1 + _run_task(worker, item) + while _in_flight > 0: + await get_tree().process_frame + +func _run_task(worker : Callable, item : Variant) -> void: + await worker.call(item) + _in_flight -= 1 + _done += 1 + progress.emit(_done, _total) diff --git a/editor/pokeapi_client.gd.uid b/editor/pokeapi_client.gd.uid new file mode 100644 index 0000000..3c2f3e2 --- /dev/null +++ b/editor/pokeapi_client.gd.uid @@ -0,0 +1 @@ +uid://cl808n66wkjwr diff --git a/editor/pokemon_importer.gd b/editor/pokemon_importer.gd new file mode 100644 index 0000000..6f3cb6f --- /dev/null +++ b/editor/pokemon_importer.gd @@ -0,0 +1,125 @@ +# editor/pokemon_importer.gd +@tool +class_name PokeApiPokemonImporter +extends RefCounted + +signal message(text : String) + +var _client : PokeApiClient +var _fs : EditorFileSystem +var _overwrite : bool +var _languages : PackedStringArray +var _pending : Array = [] # pokémon récupérés, en attente de sauvegarde + +func _init(client : PokeApiClient, fs : EditorFileSystem, overwrite : bool) -> void: + _client = client + _fs = fs + _overwrite = overwrite + _languages = PokeApiSettings.languages() + +func run(tokens : Array) -> void: + DirAccess.make_dir_recursive_absolute(PokeApiSettings.pokemon_dir()) + DirAccess.make_dir_recursive_absolute(PokeApiSettings.sprites_dir()) + if !DirAccess.dir_exists_absolute(PokeApiSettings.moves_dir()): + message.emit("[color=orange]Aucune capacité importée : les learnsets seront vides.[/color]") + + _pending.clear() + await _client.run_parallel(tokens, _fetch_pokemon) # phase 1 : téléchargement + await _save_all() # phase 2 : sprites + .tres + _pending.clear() + message.emit("Pokémon terminés.") + +func _fetch_pokemon(token : Variant) -> void: + var j = await _client.get_json(PokeApiClient.API + "pokemon/" + str(token)) + if j == null: + message.emit("[color=red]Introuvable : %s[/color]" % str(token)) + return + + var poke_name : String = j["name"] + var stem := PokeApiSettings.pokemon_stem(int(j["id"]), poke_name) + var path : String = PokeApiSettings.pokemon_dir() + stem + ".tres" + if !_overwrite and FileAccess.file_exists(path): + message.emit("Déjà présent : %s" % poke_name) + return + + var species = await _client.get_json(j["species"]["url"]) + + var d := PokemonData.new() + d.id = j["id"] + d.api_name = poke_name + d.height = j["height"] / 10.0 + d.weight = j["weight"] / 10.0 + for t in j["types"]: + d.types.append(t["type"]["name"]) + for a in j["abilities"]: + d.abilities.append(a["ability"]["name"]) + for s in j["stats"]: + d.base_stats[s["stat"]["name"]] = s["base_stat"] + + d.names = {"en": poke_name.capitalize()} # repli si l'espèce est indisponible + if species != null: + d.names = PokeApiText.names(species["names"], _languages) + d.descriptions = PokeApiText.flavors(species["flavor_text_entries"], _languages) + + var sprites : Dictionary = j["sprites"] + var art : Dictionary = sprites.get("other", {}).get("official-artwork", {}) + var sprite_url = art.get("front_default", sprites.get("front_default")) + var shiny_url = art.get("front_shiny", sprites.get("front_shiny")) + var dir := PokeApiSettings.sprites_dir() + + _pending.append({ + "data": d, "path": path, "moves": j["moves"], + "png": await _client.download_file(sprite_url, dir + stem + ".png"), + "shiny_png": await _client.download_file(shiny_url, dir + stem + "_shiny.png"), + }) + +func _save_all() -> void: + var pngs := PackedStringArray() + for p in _pending: + for key in ["png", "shiny_png"]: + if p[key] != "": pngs.append(p[key]) + if !pngs.is_empty(): + _fs.scan() + while _fs.is_scanning(): + await _client.get_tree().process_frame + _fs.reimport_files(pngs) + + for p in _pending: + var d : PokemonData = p["data"] + d.menu_sprite = _load_texture(p["png"]) + d.shiny_menu_sprite = _load_texture(p["shiny_png"]) + _link_models(d) + d.learnset = _build_learnset(p["moves"]) + ResourceSaver.save(d, p["path"]) + message.emit("OK : %s (%d capacités)" % [d.get_display_name(), d.learnset.size()]) + + _fs.scan() + +func _load_texture(path : String) -> Texture2D: + if path.is_empty(): return null + return ResourceLoader.load(path, "Texture2D", ResourceLoader.CACHE_MODE_REPLACE) as Texture2D + +func _link_models(d : PokemonData) -> void: + var dir := PokeApiSettings.models_dir() + if dir.is_empty(): return + var model_path := "%s/%s.glb" % [dir, d.api_name] + if ResourceLoader.exists(model_path): + d.model_scene = load(model_path) as PackedScene + var tex_path := "%s/%s_shiny.png" % [dir, d.api_name] + if ResourceLoader.exists(tex_path): + d.shiny_albedo = load(tex_path) as Texture2D + +func _build_learnset(raw_moves : Array) -> Array[LearnedMove]: + var result : Array[LearnedMove] = [] + for m in raw_moves: + var move_path : String = PokeApiSettings.moves_dir() + String(m["move"]["name"]) + ".tres" + if !FileAccess.file_exists(move_path): continue + var details : Array = m["version_group_details"] + var last : Dictionary = details[details.size() - 1] # jeu le plus récent + var lm := LearnedMove.new() + lm.move = load(move_path) as MoveData + lm.level = last["level_learned_at"] + lm.method = last["move_learn_method"]["name"] + result.append(lm) + result.sort_custom(func(a : LearnedMove, b : LearnedMove) -> bool: return a.level < b.level) + return result diff --git a/editor/pokemon_importer.gd.uid b/editor/pokemon_importer.gd.uid new file mode 100644 index 0000000..9252561 --- /dev/null +++ b/editor/pokemon_importer.gd.uid @@ -0,0 +1 @@ +uid://bhq06l4h5f024 diff --git a/plugin.cfg b/plugin.cfg new file mode 100644 index 0000000..a16fb36 --- /dev/null +++ b/plugin.cfg @@ -0,0 +1,7 @@ +; plugin.cfg +[plugin] +name="PokeAPI Importer" +description="Importe pokémon et capacités depuis PokeAPI en ressources .tres" +author="" +version="1.0" +script="plugin.gd" diff --git a/plugin.gd b/plugin.gd new file mode 100644 index 0000000..d55adce --- /dev/null +++ b/plugin.gd @@ -0,0 +1,15 @@ +@tool +extends EditorPlugin + +var _dock : Control + +func _enter_tree() -> void: + PokeApiSettings.register() + _dock = preload("editor/importer_dock.gd").new() + _dock.name = "PokeAPI" + _dock.editor_filesystem = EditorInterface.get_resource_filesystem() + add_control_to_dock(DOCK_SLOT_RIGHT_UL, _dock) + +func _exit_tree() -> void: + remove_control_from_docks(_dock) + _dock.queue_free() diff --git a/plugin.gd.uid b/plugin.gd.uid new file mode 100644 index 0000000..10de2a3 --- /dev/null +++ b/plugin.gd.uid @@ -0,0 +1 @@ +uid://b5wcxsx45dxd4 diff --git a/settings.gd b/settings.gd new file mode 100644 index 0000000..8ddb310 --- /dev/null +++ b/settings.gd @@ -0,0 +1,57 @@ +# settings.gd +@tool +class_name PokeApiSettings +extends RefCounted + +const KEY_DATA_DIR := "pokeapi_importer/data_dir" +const KEY_LANGUAGES := "pokeapi_importer/languages" +const KEY_MODELS_DIR := "pokeapi_importer/models_dir" +const KEY_MAX_CONCURRENT := "pokeapi_importer/max_concurrent_requests" + +const DEFAULT_DATA_DIR := "res://data/pokeapi" +const DEFAULT_MODELS_DIR := "" +const DEFAULT_MAX_CONCURRENT := 8 + +static func default_languages() -> PackedStringArray: + return PackedStringArray(["fr", "en"]) + +static func register() -> void: + _add(KEY_DATA_DIR, DEFAULT_DATA_DIR, TYPE_STRING, PROPERTY_HINT_DIR) + _add(KEY_LANGUAGES, default_languages(), TYPE_PACKED_STRING_ARRAY) + _add(KEY_MODELS_DIR, DEFAULT_MODELS_DIR, TYPE_STRING, PROPERTY_HINT_DIR) + _add(KEY_MAX_CONCURRENT, DEFAULT_MAX_CONCURRENT, TYPE_INT, PROPERTY_HINT_RANGE, "1,16") + +static func _add(key : String, default : Variant, type : int, + hint : int = PROPERTY_HINT_NONE, hint_string : String = "") -> void: + if !ProjectSettings.has_setting(key): + ProjectSettings.set_setting(key, default) + ProjectSettings.set_initial_value(key, default) + ProjectSettings.add_property_info({ + "name": key, "type": type, "hint": hint, "hint_string": hint_string}) + +static func data_dir() -> String: + return String(ProjectSettings.get_setting(KEY_DATA_DIR, DEFAULT_DATA_DIR)).trim_suffix("/") + +static func languages() -> PackedStringArray: + return PackedStringArray(ProjectSettings.get_setting(KEY_LANGUAGES, default_languages())) + +static func models_dir() -> String: + return String(ProjectSettings.get_setting(KEY_MODELS_DIR, DEFAULT_MODELS_DIR)).trim_suffix("/") + +static func max_concurrent() -> int: + return int(ProjectSettings.get_setting(KEY_MAX_CONCURRENT, DEFAULT_MAX_CONCURRENT)) + +# chemins dérivés (avec "/" final pour les dossiers) +static func moves_dir() -> String: return data_dir() + "/moves/" +static func pokemon_dir() -> String: return data_dir() + "/pokemon/" +static func sprites_dir() -> String: return data_dir() + "/sprites/" + +# nommage des fichiers pokémon : "0025_pikachu" +static func pokemon_stem(id : int, api_name : String) -> String: + return "%04d_%s" % [id, api_name] + +## "0025_pikachu" -> {"id": 25, "name": "pikachu"} ; {} si le format ne correspond pas +static func parse_pokemon_stem(stem : String) -> Dictionary: + var sep := stem.find("_") + if sep <= 0 or !stem.substr(0, sep).is_valid_int(): return {} + return {"id": int(stem.substr(0, sep)), "name": stem.substr(sep + 1)} diff --git a/settings.gd.uid b/settings.gd.uid new file mode 100644 index 0000000..f03980d --- /dev/null +++ b/settings.gd.uid @@ -0,0 +1 @@ +uid://dovcv62sbjna