Initial commit

This commit is contained in:
TheThomaas 2026-09-29 15:56:43 +02:00
commit 47dbea8ea4
24 changed files with 526 additions and 0 deletions

0
README.md Normal file
View file

15
data/l10n.gd Normal file
View file

@ -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

1
data/l10n.gd.uid Normal file
View file

@ -0,0 +1 @@
uid://byeysk5pnt16v

8
data/learned_move.gd Normal file
View file

@ -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

1
data/learned_move.gd.uid Normal file
View file

@ -0,0 +1 @@
uid://dxpd7ihc8eew5

22
data/move_data.gd Normal file
View file

@ -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)

1
data/move_data.gd.uid Normal file
View file

@ -0,0 +1 @@
uid://chc1mf57j5m4p

28
data/pokemon_data.gd Normal file
View file

@ -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

1
data/pokemon_data.gd.uid Normal file
View file

@ -0,0 +1 @@
uid://cwmxfian0pdjk

19
editor/api_text.gd Normal file
View file

@ -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

1
editor/api_text.gd.uid Normal file
View file

@ -0,0 +1 @@
uid://cchtb0yvmqssy

100
editor/importer_dock.gd Normal file
View file

@ -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")

View file

@ -0,0 +1 @@
uid://dm018deoynvvc

56
editor/move_importer.gd Normal file
View file

@ -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)

View file

@ -0,0 +1 @@
uid://bh51lh1reyxog

63
editor/pokeapi_client.gd Normal file
View file

@ -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)

View file

@ -0,0 +1 @@
uid://cl808n66wkjwr

125
editor/pokemon_importer.gd Normal file
View file

@ -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

View file

@ -0,0 +1 @@
uid://bhq06l4h5f024

7
plugin.cfg Normal file
View file

@ -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"

15
plugin.gd Normal file
View file

@ -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()

1
plugin.gd.uid Normal file
View file

@ -0,0 +1 @@
uid://b5wcxsx45dxd4

57
settings.gd Normal file
View file

@ -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)}

1
settings.gd.uid Normal file
View file

@ -0,0 +1 @@
uid://dovcv62sbjna