feat(gamedev): ronda 1 — pixelize + luma→alpha + export-godot (grupo gamedev)

Tres funciones CPU-only del lote gamedev 2D + 2 helpers puros + grupo de capacidad:

- comfyui_pixelize_image_py_ml (impure): Fase 2 pixelart — downscale nearest +
  cuantizacion a N colores / paleta fija (game-boy/pico-8/nes) + re-upscale nearest.
- comfyui_matting_luma_to_alpha_py_ml (impure): frame VFX sobre negro -> RGBA por
  luminancia ponderada (translucidos con additive blend).
- comfyui_export_asset_to_godot_py_pipelines (impure): puente ComfyUI -> Godot 4 —
  copia a res://assets/<dir> por kind + .import por tipo + filtro Nearest si pixelart
  + reimport headless best-effort. Compone los 2 helpers puros.
- godot_map_asset_dir_py_core, godot_clean_asset_name_py_core (pure): nucleos
  reutilizables del pipeline.
- docs/capabilities/gamedev-2d.md + INDEX: grupo nuevo gamedev.

Tests 33/33 verdes (offline PIL/numpy). Golden real verificado: asset de
~/ComfyUI/output -> /tmp/godot_test_proj con .import correcto y reimport headless
real de Godot 4.7. Sin GPU, sin red, sin tocar proyectos del usuario.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-26 19:43:47 +02:00
parent 9508fff282
commit e57da2f6d5
17 changed files with 1529 additions and 0 deletions
@@ -0,0 +1,54 @@
---
name: godot_clean_asset_name
kind: function
lang: py
domain: core
version: "1.0.0"
purity: pure
signature: "def godot_clean_asset_name(filename: str, *, override: str | None = None) -> str"
description: "Normaliza el nombre de archivo de un asset salido de ComfyUI (patron <prefijo>_NNNNN_.<ext>) a un nombre limpio y seguro para res://: snake_case, minusculas, sin el sufijo numerico _NNNNN_, sin espacios ni caracteres raros, conservando la extension. Pura: solo manipula el string, no toca disco. Pensada para el puente ComfyUI -> Godot."
tags: [godot, gamedev, core, assets, naming]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
params:
- name: filename
desc: "nombre o ruta del asset de origen (se toma solo el basename)."
- name: override
desc: "nombre base deseado sin extension; si se pasa se usa en lugar del nombre de origen (igual se normaliza a snake_case y se le anade la extension del origen). keyword-only."
output: "nombre de archivo limpio 'snake_case.ext' (ext en minuscula; sin punto si el origen no tenia extension)."
tested: true
tests: [test_strips_comfyui_suffix, test_normalizes_spaces_and_case, test_takes_basename_from_path, test_override_name, test_empty_fallback]
test_file_path: "python/functions/core/godot_clean_asset_name_test.py"
file_path: "python/functions/core/godot_clean_asset_name.py"
---
## Ejemplo
```python
import sys, os
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
from core.godot_clean_asset_name import godot_clean_asset_name
godot_clean_asset_name("svd_motion_hi_00001_.webp") # 'svd_motion_hi.webp'
godot_clean_asset_name("/x/ComfyUI/output/bench_00042_.png") # 'bench.png'
godot_clean_asset_name("x.png", override="explosion loop") # 'explosion_loop.png'
```
## Cuando usarla
Al exportar un asset de ComfyUI a Godot, para renombrar el `<prefijo>_NNNNN_.<ext>`
a un nombre semántico y seguro (lo usa `comfyui_export_asset_to_godot`). También
para limpiar cualquier nombre de archivo antes de meterlo en `res://`.
## Gotchas
- Solo quita el sufijo numérico `_NNNNN_` del **nombre de origen**; con `override`
se respetan los dígitos finales (p.ej. `hero_v2` no pierde el `2`).
- Reduce cualquier carácter no `[a-z0-9_]` a `_` y colapsa repetidos; un nombre que
quede vacío cae a `"asset"`.
- Conserva la extensión en minúscula; si el origen no tiene extensión, devuelve solo
el nombre sin punto.
@@ -0,0 +1,49 @@
"""godot_clean_asset_name — normaliza el nombre de archivo de un asset para Godot.
Funcion pura: toma el nombre de un asset salido de ComfyUI (patron
`<prefijo>_NNNNN_.<ext>`, p.ej. `svd_motion_hi_00001_.webp`) y devuelve un nombre
limpio, semantico y seguro para `res://`: snake_case, minusculas, sin el sufijo
numerico `_NNNNN_`, sin espacios ni caracteres raros, conservando la extension.
No toca disco. Pensada para el puente ComfyUI -> Godot (renombrar al exportar).
"""
import os
import re
_NNNNN_SUFFIX = re.compile(r"_\d{3,}_?$") # sufijo numerico de ComfyUI: _00001_ / _00001
_NON_SAFE = re.compile(r"[^a-z0-9_]+") # cualquier cosa que no sea snake_case
_MULTI_US = re.compile(r"_{2,}")
def godot_clean_asset_name(filename: str, *, override: str | None = None) -> str:
"""Limpia el nombre de un asset a snake_case seguro conservando la extension.
Args:
filename: nombre o ruta del asset de origen (se toma solo el basename).
override: nombre base deseado (sin extension); si se pasa, se usa en lugar
del nombre de origen (igual se normaliza a snake_case y se le anade la
extension del origen). keyword-only.
Returns:
Nombre de archivo limpio "snake_case.ext" (ext en minuscula, sin punto si
el origen no tenia extension).
"""
base = os.path.basename(filename or "")
stem, ext = os.path.splitext(base)
ext = ext.lower()
raw = override if override is not None else stem
name = raw.strip().lower().replace(" ", "_").replace("-", "_")
if override is None:
name = _NNNNN_SUFFIX.sub("", name) # quita _00001_ solo del nombre de origen
name = _NON_SAFE.sub("_", name)
name = _MULTI_US.sub("_", name).strip("_")
if not name:
name = "asset"
return f"{name}{ext}" if ext else name
if __name__ == "__main__":
import sys
print(godot_clean_asset_name(sys.argv[1] if len(sys.argv) > 1 else "svd_motion_hi_00001_.webp"))
@@ -0,0 +1,31 @@
"""Tests de godot_clean_asset_name (pura, offline)."""
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from core.godot_clean_asset_name import godot_clean_asset_name # noqa: E402
def test_strips_comfyui_suffix():
assert godot_clean_asset_name("svd_motion_hi_00001_.webp") == "svd_motion_hi.webp"
assert godot_clean_asset_name("3d_robot_mesh_00001_.glb") == "3d_robot_mesh.glb"
def test_normalizes_spaces_and_case():
assert godot_clean_asset_name("My Hero Sprite.PNG") == "my_hero_sprite.png"
assert godot_clean_asset_name("fire-flare.png") == "fire_flare.png"
def test_takes_basename_from_path():
assert godot_clean_asset_name("/home/x/ComfyUI/output/bench_00042_.png") == "bench.png"
def test_override_name():
assert godot_clean_asset_name("svd_00001_.webp", override="explosion loop") == "explosion_loop.webp"
# override conserva digitos finales (no es sufijo ComfyUI a quitar)
assert godot_clean_asset_name("x.png", override="hero_v2") == "hero_v2.png"
def test_empty_fallback():
assert godot_clean_asset_name("_00001_.png") == "asset.png"
@@ -0,0 +1,51 @@
---
name: godot_map_asset_dir
kind: function
lang: py
domain: core
version: "1.0.0"
purity: pure
signature: "def godot_map_asset_dir(kind: str) -> str"
description: "Mapea el tipo de asset (sprite, pixelart, tileset, vfx, sfx, music, model) a su subcarpeta canonica bajo res://assets/ de un proyecto Godot 4, segun la convencion del puente ComfyUI -> Godot. Pura: solo resuelve una ruta relativa POSIX, no toca disco. kind desconocido -> ValueError."
tags: [godot, gamedev, core, assets, mapping]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
params:
- name: kind
desc: "tipo de asset: 'sprite', 'pixelart', 'tileset', 'vfx', 'sfx', 'music' o 'model' (case-insensitive, se ignoran espacios)."
output: "subruta relativa POSIX bajo assets/ (p.ej. 'sprites', 'tilesets', 'audio/music', 'models')."
tested: true
tests: [test_all_kinds, test_case_insensitive, test_unknown_kind_raises]
test_file_path: "python/functions/core/godot_map_asset_dir_test.py"
file_path: "python/functions/core/godot_map_asset_dir.py"
---
## Ejemplo
```python
import sys, os
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
from core.godot_map_asset_dir import godot_map_asset_dir
godot_map_asset_dir("pixelart") # 'sprites'
godot_map_asset_dir("music") # 'audio/music'
godot_map_asset_dir("model") # 'models'
```
## Cuando usarla
Al construir la ruta destino de un asset dentro de un proyecto Godot (lo usa el
pipeline `comfyui_export_asset_to_godot`). Centraliza la convención tipo -> carpeta
para no esparcir el mapeo por varias funciones.
## Gotchas
- `pixelart` cae en `sprites/` (el pixelart no es una carpeta, es un atributo: lo
que cambia es el filtro Nearest, no la ubicación).
- `kind` desconocido lanza `ValueError` (no devuelve un default silencioso).
- Devuelve siempre subrutas POSIX (`/`), no rutas absolutas: el llamador antepone
`<proyecto>/assets/`.
@@ -0,0 +1,46 @@
"""godot_map_asset_dir — mapea el tipo de asset a su subcarpeta res://assets/.
Funcion pura: dado el `kind` de un asset (sprite, pixelart, tileset, vfx, sfx,
music, model) devuelve el subdirectorio canonico bajo `res://assets/` donde debe
vivir en un proyecto Godot 4, segun la convencion del puente ComfyUI -> Godot
(docs/comfyui-godot-integration.md). No toca disco: solo resuelve una ruta
relativa. `kind` desconocido -> ValueError.
"""
# kind -> subruta relativa bajo assets/ (convencion del puente ComfyUI->Godot).
_KIND_TO_DIR = {
"sprite": "sprites",
"pixelart": "sprites",
"tileset": "tilesets",
"vfx": "vfx",
"sfx": "audio/sfx",
"music": "audio/music",
"model": "models",
}
def godot_map_asset_dir(kind: str) -> str:
"""Resuelve la subcarpeta de assets para un tipo de asset Godot.
Args:
kind: tipo de asset: "sprite", "pixelart", "tileset", "vfx", "sfx",
"music" o "model".
Returns:
Subruta relativa (POSIX) bajo `assets/`, p.ej. "sprites" o "audio/music".
Raises:
ValueError: si `kind` no es uno de los tipos soportados.
"""
key = (kind or "").strip().lower()
if key not in _KIND_TO_DIR:
raise ValueError(
f"kind desconocido: {kind!r}. Soportados: {sorted(_KIND_TO_DIR)}"
)
return _KIND_TO_DIR[key]
if __name__ == "__main__":
import sys
print(godot_map_asset_dir(sys.argv[1] if len(sys.argv) > 1 else "pixelart"))
@@ -0,0 +1,29 @@
"""Tests de godot_map_asset_dir (pura, offline)."""
import os
import sys
import pytest
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from core.godot_map_asset_dir import godot_map_asset_dir # noqa: E402
def test_all_kinds():
assert godot_map_asset_dir("sprite") == "sprites"
assert godot_map_asset_dir("pixelart") == "sprites"
assert godot_map_asset_dir("tileset") == "tilesets"
assert godot_map_asset_dir("vfx") == "vfx"
assert godot_map_asset_dir("sfx") == "audio/sfx"
assert godot_map_asset_dir("music") == "audio/music"
assert godot_map_asset_dir("model") == "models"
def test_case_insensitive():
assert godot_map_asset_dir("PixelArt") == "sprites"
assert godot_map_asset_dir(" MODEL ") == "models"
def test_unknown_kind_raises():
with pytest.raises(ValueError):
godot_map_asset_dir("hologram")