feat(gamedev): comfyui_build_trap_hazard_workflow — trampas/peligros de escenario

Builder ComfyUI puro (dict API format) para UNA trampa/peligro JUGABLE de nivel
(pinchos, sierra giratoria, foso de lava, placa de presion, llamas, trampa de
flechas, charco acido, descarga electrica): objeto de peligro aislado y centrado
a perspectiva de juego (side/top-down/iso via view), fondo limpio recortable a
alpha, estilo consistente para poblar niveles.

Hermano de comfyui_build_prop_object/structure/foliage_set_workflow: mismo patron
que compone comfyui_build_txt2img_workflow + comfyui_inject_lora (estilo opcional)
+ Image Rembg (alpha si transparent). Diferenciado de prop_object (peligro con
hitbox de dano vs decoracion inerte) y enemy_creature (trampa vs enemigo vivo);
el negativo rechaza character/person/creature/multiple objects.

Gotcha documentado: para hazards puramente etereos (llamas/electricidad/gas) usar
transparent=False + comfyui_matting_luma_to_alpha (conserva el falloff), no Rembg.

12 tests offline en verde. Probado e2e en GPU con SD1.5 — spiked floor trap side
512x512 RGBA, mecanismo de peligro centrado recortado a alpha real (alpha extrema
0-255, prompt_id ab1b1560).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-27 02:10:26 +02:00
parent 0ce1c31fb9
commit fa94f7a235
4 changed files with 519 additions and 0 deletions
@@ -0,0 +1,129 @@
---
name: comfyui_build_trap_hazard_workflow
kind: function
lang: py
domain: ml
version: "1.0.0"
purity: pure
signature: "def comfyui_build_trap_hazard_workflow(hazard: str, *, view: str = \"side\", style: str = \"game hazard trap\", checkpoint: str = \"dreamshaper_8.safetensors\", size: int = 512, transparent: bool = True, seed: int = 0, lora: str | None = None, lora_strength: float = 1.0, rembg_model: str = \"u2net\", negative: str | None = None, steps: int = 28, cfg: float = 7.0, sampler_name: str = \"dpmpp_2m\", scheduler: str = \"karras\", filename_prefix: str = \"trap_hazard\") -> dict"
description: "Construye el dict (API format) del workflow de UNA trampa/peligro de escenario de juego 2D (pinchos del suelo, sierra giratoria, foso de lava, placa de presion, columna de llamas, trampa de flechas, charco acido, descarga electrica, prensa, estaca cayendo): UN objeto de peligro JUGABLE aislado y centrado a perspectiva de juego (lateral/cenital via view), fondo limpio uniforme recortable a alpha, estilo consistente para poblar niveles. Diferenciado de comfyui_build_prop_object (peligro jugable con hitbox de daño vs objeto inerte de decoracion) y de comfyui_build_enemy_creature (trampa vs enemigo vivo). Compone comfyui_build_txt2img_workflow + comfyui_inject_lora (estilo opcional) + Image Rembg (fondo transparente si transparent). Hermano de comfyui_build_prop_object/structure/foliage_set_workflow. Pura, sin red ni I/O. class_types verificados contra /object_info."
tags: [comfyui, ml, gamedev, gamedev-2d, trap, hazard, danger, scenery, environment, level, rembg, workflow]
uses_functions: [comfyui_build_txt2img_workflow_py_ml, comfyui_inject_lora_py_ml]
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
params:
- name: hazard
desc: "Descripcion del peligro (ej. 'spiked floor trap', 'spinning saw blade', 'lava pit', 'pressure plate', 'flame jet', 'arrow trap', 'acid pool', 'electric trap', 'falling spike', 'swinging blade', 'bear trap'). Se inserta en un prompt scaffold de trampa. No puede estar vacio."
- name: view
desc: "Perspectiva del juego con la que se muestra el peligro. Se inserta como '{view} view' en el prompt (ej. 'side', 'top-down', 'isometric', 'front', '3/4'). Por defecto 'side' (la mas comun para trampas de plataformas). Vacio/None -> sin clausula de vista. keyword-only."
- name: style
desc: "Descriptor de estilo que mantiene consistentes los peligros del set (ej. 'game hazard trap', 'pixel art hazard', 'low poly stylized trap', 'cartoon danger', 'dark fantasy trap'). Pasa el MISMO view + style + checkpoint + lora a todas las trampas del nivel para coherencia. keyword-only."
- name: checkpoint
desc: "Checkpoint del servidor. 'dreamshaper_8.safetensors' (SD1.5, holgado en 8GB lowvram) por defecto; 'juggernaut_xl_v11.safetensors' para SDXL (mas VRAM, subir size). keyword-only."
- name: size
desc: "Lado del cuadrado en px (width = height = size). 512 SD1.5 por defecto. keyword-only."
- name: transparent
desc: "Si True inyecta Image Rembg y el PNG sale con alpha (fondo recortado, listo para soltar sobre el nivel). False = peligro opaco sobre fondo plano, recortable luego (recomendado para hazards etereos: llamas/electricidad/gas + luma-to-alpha). keyword-only."
- name: seed
desc: "Semilla del KSampler. Misma seed + mismo hazard/view/style -> misma trampa. keyword-only."
- name: lora
desc: "LoRA de estilo opcional en models/loras (ej. 'pixel_art_sd15.safetensors', 'stylized_game_assets_xl.safetensors'). None = sin LoRA. keyword-only."
- name: lora_strength
desc: "Fuerza del LoRA sobre model y clip. Se clampa a [0.0, 2.0]. keyword-only."
- name: rembg_model
desc: "Modelo Rembg ('u2net' general, 'isnet-anime' para anime). Solo se usa si transparent=True. keyword-only."
- name: negative
desc: "Prompt negativo. None usa el negativo por defecto pensado para trampas (un peligro aislado y entero, sin personajes/criaturas vivas, sin multiples objetos, fondo limpio, sin texto/recorte). keyword-only."
- name: steps
desc: "Pasos del KSampler. keyword-only."
- name: cfg
desc: "CFG del KSampler. keyword-only."
- name: sampler_name
desc: "Sampler del KSampler. keyword-only."
- name: scheduler
desc: "Scheduler del KSampler. keyword-only."
- name: filename_prefix
desc: "Prefijo del PNG en output/. keyword-only."
output: "dict en API format listo para comfyui_submit_workflow: base txt2img cuadrada con prompt scaffold de trampa ('{hazard}, {view} view, {style}, single hazard object, trap, dangerous, centered, plain background, game asset, high detail') + LoRA de estilo opcional + Image Rembg (si transparent). UNA trampa; para poblar un nivel -> llamar por cada hazard con mismo view/style/checkpoint/lora; contact-sheet de los peligros -> montar los PNG con comfyui_build_grid."
tested: true
tests: ["golden transparent: clases CheckpointLoaderSimple/KSampler/VAEDecode/SaveImage/Image Rembg; hazard + 'single hazard object' + 'trap' + 'centered' + 'game asset' + 'plain background' + 'side view' en prompt; SaveImage <- Rembg; transparency True", "edge transparent=False: sin Rembg, SaveImage <- VAEDecode", "edge size: width==height==768 (cuadrado)", "edge hazard al inicio del scaffold", "edge view reflejado como '{view} view'", "edge view vacio: sin clausula 'view' colgando", "edge style del set en prompt", "edge negativo anti personaje/criatura viva + multiple objects (diferenciado de enemy_creature/prop_object)", "edge lora: LoraLoader presente con strength", "edge transparent default True", "error hazard vacio -> ValueError", "determinismo"]
test_file_path: "python/functions/ml/comfyui_build_trap_hazard_workflow_test.py"
file_path: "python/functions/ml/comfyui_build_trap_hazard_workflow.py"
---
## Ejemplo
```python
import sys, os
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
from ml.comfyui_build_trap_hazard_workflow import comfyui_build_trap_hazard_workflow
# Una trampa de escenario con fondo transparente (alpha), lista para soltar en el nivel.
wf = comfyui_build_trap_hazard_workflow(
"spiked floor trap",
view="side",
style="game hazard trap",
transparent=True,
seed=7,
)
# Poblar un nivel: variar `hazard` con el MISMO view/style/checkpoint/(lora) para coherencia.
# for h in ["spinning saw blade", "lava pit", "pressure plate", "flame jet", "acid pool"]:
# wf = comfyui_build_trap_hazard_workflow(h, view="side", style="game hazard trap", seed=7)
# comfyui_submit_workflow(wf) # -> comfyui_wait_result -> comfyui_fetch_output_image
# Contact-sheet de los peligros: montar los PNG resultantes con comfyui_build_grid.
```
O lanzable directo con: `python/.venv/bin/python python/functions/ml/comfyui_build_trap_hazard_workflow.py` (imprime nodos + class_types del ejemplo). Nota: `./fn run` no admite esta firma (parámetros keyword-only con `*`), igual que los builders hermanos; usa el `__main__` del `.py`.
## Cuando usarla
Cuando necesites trampas/peligros JUGABLES para poblar niveles de un juego (plataformas,
mazmorras, roguelike, acción): pinchos del suelo, sierras giratorias, fosos de lava,
placas de presión, columnas de llamas, trampas de flechas, charcos de ácido, descargas
eléctricas, prensas, estacas cayendo. A diferencia de `comfyui_build_prop_object_workflow`
(un objeto de decoración INERTE: barril, cofre, antorcha que solo ambienta), aquí el asset
es un PELIGRO con el que el motor le asigna hitbox de daño, estado activo/inactivo y
animación de disparo/golpe. Y a diferencia de `comfyui_build_enemy_creature_workflow`
(enemigo VIVO), una trampa es un objeto/mecanismo, no una criatura — el negativo por
defecto rechaza `character / person / creature` para que no salga un enemigo. Fija `view`
(`side` por defecto) para la perspectiva del nivel y pasa el MISMO `view` + `style` +
`checkpoint` + (`lora`) a todas las trampas del set para que combinen; varía solo
`hazard`. `transparent` recorta el fondo (alpha) listo para soltar sobre el nivel. Para un
atlas/contact-sheet de los peligros, genera cada uno y monta los PNG con
`comfyui_build_grid`.
## Gotchas
- **Hazard (peligro jugable) != prop_object (decoración inerte) != enemy_creature
(enemigo vivo)**: si lo que quieres es un objeto de escena que solo ambienta (barril,
cofre, planta), usa `comfyui_build_prop_object_workflow`; si quieres una criatura
enemiga, usa `comfyui_build_enemy_creature_workflow`. Este builder genera el MECANISMO
de peligro (`single hazard object, trap, dangerous`); el negativo por defecto rechaza
`character / person / creature / multiple objects` para no degradar a un enemigo ni a
una escena con varios objetos.
- **El recorte usa Rembg, NO luma-to-alpha por defecto**: la mayoría de trampas son masas
sólidas con silueta definida (pinchos metálicos, sierra, placa de presión, foso); rembg
las recorta limpio. Pero si la trampa es **puramente etérea** (columna de llamas, arco
eléctrico, nube de gas) y quieres conservar el falloff translúcido para blend aditivo en
el motor, pon `transparent=False` y recorta con `comfyui_matting_luma_to_alpha` (o
on-black + luma) fuera de este builder, igual que hacen
`comfyui_build_status_effect_icon` / `decal_overlay`. Con rembg un efecto puramente
translúcido perdería los bordes suaves.
- **`view` fija la perspectiva del nivel**: `side` (default) para trampas de plataformas;
`top-down` para mazmorras cenitales; `isometric` para niveles iso; `front`/`3/4` para
vistas frontales. Mantén el mismo `view` que el resto de assets del nivel.
- **Coherencia del set = mismos parámetros**: si cambias `view`/`style`/`checkpoint`/`lora`/
`seed` entre trampas, el nivel deja de combinar. Fija esos y varía solo `hazard`.
- **SDXL pide más VRAM y resolución**: con `checkpoint="juggernaut_xl_v11.safetensors"`
sube `size` a 768/1024; con dreamshaper_8 (SD1.5) deja 512 (holgado en 8GB lowvram). Si
hay OOM, baja `size` o usa SD1.5.
- Si el modelo mete un personaje, varios objetos o una escena completa, el negativo por
defecto ya empuja a "single hazard object / no people / no creature"; refuerza `style`
con "single isolated trap mechanism" si insiste.
- `transparent=False` deja el peligro opaco sobre fondo plano: útil si prefieres recortar
fuera del workflow o el motor compone sobre un fondo sólido.
- Es una función **pura**: solo arma el dict. La generación real (GPU) la hacen
`comfyui_submit_workflow` + `comfyui_wait_result` + `comfyui_fetch_output_image`.
@@ -0,0 +1,245 @@
"""Construye el workflow ComfyUI de UNA trampa/peligro de escenario de juego (API format).
Trampa o peligro de nivel (pinchos del suelo, sierra giratoria, foso de lava, placa de
presion, columna de llamas, trampa de flechas, charco acido, descarga electrica, sierra
de pared, prensa aplastante, estaca cayendo, brasero...): UN objeto de peligro AISLADO y
centrado, con la perspectiva del juego (lateral o cenital via view), fondo limpio y
uniforme recortable a alpha, estilo consistente para poblar niveles. Es el builder
hermano de comfyui_build_prop_object_workflow / comfyui_build_structure_workflow: mismo
patron (PURO, dict API format) que compone funciones existentes del registry, no reescribe
el grafo.
Diferencia con prop_object (clave para no duplicar): un *prop* es un objeto de decoracion
inerte (barril, cofre, antorcha, planta, roca) que ambienta la escena pero no afecta al
jugador. Un *hazard* es un peligro JUGABLE que daña o mata: el motor le asigna una hitbox
de daño, animacion de disparo/golpe y estado activo/inactivo. Por eso el scaffold empuja
"hazard, trap, dangerous" y el negativo rechaza "character, person, creature" para que
salga el peligro y no un enemigo (un enemigo vivo es comfyui_build_enemy_creature).
Cableado:
CheckpointLoaderSimple -> [LoraLoader opcional de estilo] -> KSampler
-> CLIPTextEncode (prompt scaffold de hazard) ...
-> VAEDecode -> [Image Rembg opcional] -> SaveImage
Compone:
- comfyui_build_txt2img_workflow -> base txt2img cuadrada
- comfyui_inject_lora -> LoRA de estilo opcional (consistencia del set)
- 'Image Rembg (Remove Background)' (helper local) -> fondo transparente (alpha)
Por que Rembg y NO comfyui_matting_luma_to_alpha por defecto: la mayoria de trampas son
objetos SOLIDOS con silueta definida (pinchos metalicos, sierra, placa de presion, foso);
rembg recorta limpio la silueta dejando alpha, listo para soltar sobre el nivel. La
luma-to-alpha es para translucidos sobre negro (humo/fuego/magia). Si la trampa es
puramente etérea (una columna de llamas, un arco electrico, una nube de gas) y quieres
conservar el falloff translucido, pon transparent=False y recorta con
comfyui_matting_luma_to_alpha (o un on-black + luma) fuera de este builder, igual que en
comfyui_build_status_effect_icon / decal_overlay.
Por que un solo peligro centrado y fondo plano: una trampa se inserta como sprite/objeto
en el motor; el scaffold empuja a "single hazard object, centered, plain background, game
asset" y el negativo por defecto rechaza "multiple objects, character, person, creature,
cropped, out of frame" para mantener UN peligro entero y recortable.
class_types/inputs verificados contra /object_info del servidor (8GB lowvram):
CheckpointLoaderSimple, CLIPTextEncode, EmptyLatentImage, KSampler, VAEDecode, SaveImage,
LoraLoader, 'Image Rembg (Remove Background)' (transparency BOOLEAN).
Funcion pura: sin red, sin I/O. No muta dicts de entrada (copia profunda en el helper de
rembg). Determinista para los mismos argumentos.
"""
from __future__ import annotations
import copy
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
# Negativo por defecto pensado para trampas/peligros: UN peligro completo y aislado, fondo
# limpio, sin multiples objetos, sin personajes/criaturas vivas (eso es enemy_creature),
# sin texto/marcas ni recortes. No filtra ningun tipo de trampa (pinchos, sierra, lava,
# acido, electricidad... son validos).
_HAZARD_NEGATIVE = (
"character, person, people, creature, monster, enemy, hero, "
"multiple objects, cluttered scene, full level, tileset, "
"blurry, lowres, deformed, bad perspective, "
"text, watermark, signature, logo, photo, photorealistic, "
"cropped, cut off, out of frame, jpeg artifacts"
)
def _inject_rembg(workflow: dict, model: str) -> dict:
"""Inserta 'Image Rembg (Remove Background)' (transparency=True) entre VAEDecode y SaveImage.
Mismo helper que usan comfyui_build_prop_object_workflow / structure: el nodo recorta
la silueta del peligro dejando alpha. Repunta SaveImage.images a la salida del Rembg.
"""
wf = copy.deepcopy(workflow)
vaedecode_id = next(
(nid for nid, n in wf.items() if n.get("class_type") == "VAEDecode"), None
)
save_id = next((nid for nid, n in wf.items() if n.get("class_type") == "SaveImage"), None)
if vaedecode_id is None or save_id is None:
raise ValueError(
"comfyui_build_trap_hazard_workflow: no se encontro VAEDecode/SaveImage para Rembg"
)
numeric = [int(k) for k in wf.keys() if str(k).isdigit()]
rembg_id = str((max(numeric) + 1) if numeric else len(wf) + 1)
wf[rembg_id] = {
"class_type": "Image Rembg (Remove Background)",
"inputs": {
"images": [vaedecode_id, 0],
"transparency": True,
"model": model,
"post_processing": False,
"only_mask": False,
"alpha_matting": False,
"alpha_matting_foreground_threshold": 240,
"alpha_matting_background_threshold": 10,
"alpha_matting_erode_size": 10,
"background_color": "none",
},
}
wf[save_id]["inputs"]["images"] = [rembg_id, 0]
return wf
def comfyui_build_trap_hazard_workflow(
hazard: str,
*,
view: str = "side",
style: str = "game hazard trap",
checkpoint: str = "dreamshaper_8.safetensors",
size: int = 512,
transparent: bool = True,
seed: int = 0,
lora: str | None = None,
lora_strength: float = 1.0,
rembg_model: str = "u2net",
negative: str | None = None,
steps: int = 28,
cfg: float = 7.0,
sampler_name: str = "dpmpp_2m",
scheduler: str = "karras",
filename_prefix: str = "trap_hazard",
) -> dict:
"""Construye el dict (API format) del workflow de una trampa/peligro de escenario.
Args:
hazard: descripcion del peligro (ej. "spiked floor trap", "spinning saw blade",
"lava pit", "pressure plate", "flame jet", "arrow trap", "acid pool",
"electric trap", "falling spike", "swinging blade", "bear trap"). Se inserta
en un prompt scaffold de trampa. No puede estar vacio.
view: perspectiva del juego con la que se muestra el peligro. Se inserta como
"{view} view" en el prompt (ej. "side", "top-down", "isometric", "front",
"3/4"). Por defecto "side" (la mas comun para trampas de plataformas).
Vacio/None -> sin clausula de vista. keyword-only.
style: descriptor de estilo que mantiene consistentes los peligros del set (ej.
"game hazard trap", "pixel art hazard", "low poly stylized trap", "cartoon
danger", "dark fantasy trap"). Pasa el MISMO view + style + checkpoint +
(lora) a todas las trampas del nivel para coherencia visual. keyword-only.
checkpoint: checkpoint del servidor. 'dreamshaper_8.safetensors' (SD1.5, holgado
en 8GB lowvram) por defecto; 'juggernaut_xl_v11.safetensors' para SDXL (mas
VRAM, subir size a 768/1024). keyword-only.
size: lado del cuadrado en px (width = height = size). 512 SD1.5 por defecto.
keyword-only.
transparent: si True inyecta Image Rembg y el PNG sale con alpha (fondo recortado,
listo para soltar sobre el nivel). Si False deja el peligro opaco sobre fondo
plano, recortable luego por el caller/pipeline (recomendado para hazards
etereos: llamas/electricidad/gas + luma-to-alpha). keyword-only.
seed: semilla del KSampler. Misma seed + mismo hazard/view/style -> misma trampa.
keyword-only.
lora: LoRA de estilo opcional en models/loras (ej.
'pixel_art_sd15.safetensors', 'stylized_game_assets_xl.safetensors'). None =
sin LoRA. keyword-only.
lora_strength: fuerza del LoRA sobre model y clip. Se clampa a [0.0, 2.0].
keyword-only.
rembg_model: modelo Rembg ('u2net' general, 'isnet-anime' para anime). Solo se usa
si transparent=True. keyword-only.
negative: prompt negativo. None usa el negativo por defecto pensado para trampas
(un peligro aislado y entero, sin personajes/criaturas vivas, sin multiples
objetos, fondo limpio, sin texto/recorte). keyword-only.
steps, cfg, sampler_name, scheduler, filename_prefix: parametros de generacion.
keyword-only.
Returns:
dict en API format listo para comfyui_submit_workflow: txt2img base cuadrada con
prompt scaffold de trampa ('{hazard}, {view} view, {style}, single hazard object,
centered, plain background, game asset, ...') + LoRA de estilo opcional + Image
Rembg (si transparent). Es UNA trampa; para poblar un nivel -> llamar por cada
hazard con el mismo view/style/checkpoint/(lora). Montar el set con
comfyui_build_grid si se quiere un contact-sheet de los peligros.
Raises:
ValueError: si hazard esta vacio, o si la base no tiene VAEDecode/SaveImage donde
inyectar el Rembg (propagado por el helper).
"""
from ml.comfyui_build_txt2img_workflow import comfyui_build_txt2img_workflow
if not hazard or not hazard.strip():
raise ValueError(
"comfyui_build_trap_hazard_workflow: 'hazard' no puede estar vacio"
)
hazard = hazard.strip()
view = (view or "").strip()
lora_strength = max(0.0, min(2.0, float(lora_strength)))
neg = _HAZARD_NEGATIVE if negative is None else negative
# Prompt scaffold de hazard: UN peligro aislado (no un personaje), centrado, fondo
# plano, listo como asset de juego recortable. El "{view} view" fija la perspectiva
# del nivel. "hazard, trap, dangerous" empuja al objeto de peligro, no a un enemigo.
view_clause = f"{view} view, " if view else ""
positive = (
f"{hazard}, {view_clause}{style}, single hazard object, "
"trap, dangerous, centered, plain background, game asset, "
"high detail"
)
wf = comfyui_build_txt2img_workflow(
checkpoint,
positive,
neg,
steps=steps,
cfg=cfg,
width=size,
height=size,
seed=seed,
sampler_name=sampler_name,
scheduler=scheduler,
filename_prefix=filename_prefix,
)
if lora:
from ml.comfyui_inject_lora import comfyui_inject_lora
wf = comfyui_inject_lora(
wf, lora, strength_model=lora_strength, strength_clip=lora_strength
)
if transparent:
wf = _inject_rembg(wf, rembg_model)
return wf
if __name__ == "__main__":
import json
wf = comfyui_build_trap_hazard_workflow(
"spiked floor trap",
view="side",
style="game hazard trap",
transparent=True,
seed=7,
)
print(
json.dumps(
{
"nodes": list(wf),
"classes": sorted({n["class_type"] for n in wf.values()}),
},
indent=2,
)
)
@@ -0,0 +1,144 @@
"""Tests offline de comfyui_build_trap_hazard_workflow (estructura del dict, sin GPU)."""
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from ml.comfyui_build_trap_hazard_workflow import ( # noqa: E402
comfyui_build_trap_hazard_workflow,
)
def _classes(wf):
return sorted({n["class_type"] for n in wf.values()})
def _by_class(wf, cls):
return [n for n in wf.values() if n["class_type"] == cls]
def _id_of(wf, cls):
return next(nid for nid, n in wf.items() if n["class_type"] == cls)
def _pos_with(wf, needle):
return next(
n for n in wf.values()
if n["class_type"] == "CLIPTextEncode" and needle in n["inputs"]["text"]
)
def test_golden_transparent_recipe():
wf = comfyui_build_trap_hazard_workflow(
"spiked floor trap", view="side", transparent=True, seed=7
)
cls = _classes(wf)
# Cadena base txt2img + Rembg para alpha.
assert "CheckpointLoaderSimple" in cls
assert "KSampler" in cls
assert "VAEDecode" in cls
assert "SaveImage" in cls
assert "Image Rembg (Remove Background)" in cls
# Prompt scaffold de hazard.
pos = _pos_with(wf, "spiked floor trap")
text = pos["inputs"]["text"]
assert text.startswith("spiked floor trap")
assert "single hazard object" in text
assert "trap" in text
assert "centered" in text
assert "plain background" in text
assert "game asset" in text
assert "side view" in text
# SaveImage cuelga del Rembg, no del VAEDecode.
rembg_id = _id_of(wf, "Image Rembg (Remove Background)")
save = _by_class(wf, "SaveImage")[0]
assert save["inputs"]["images"][0] == rembg_id
# transparency activa en el nodo Rembg.
rembg = _by_class(wf, "Image Rembg (Remove Background)")[0]
assert rembg["inputs"]["transparency"] is True
def test_edge_transparent_false_no_rembg():
wf = comfyui_build_trap_hazard_workflow("lava pit", transparent=False)
assert "Image Rembg (Remove Background)" not in _classes(wf)
vae_id = _id_of(wf, "VAEDecode")
save = _by_class(wf, "SaveImage")[0]
assert save["inputs"]["images"][0] == vae_id
def test_edge_size_square():
wf = comfyui_build_trap_hazard_workflow("spinning saw blade", size=768)
latent = _by_class(wf, "EmptyLatentImage")[0]
assert latent["inputs"]["width"] == 768
assert latent["inputs"]["height"] == 768
def test_edge_hazard_at_start_of_scaffold():
wf = comfyui_build_trap_hazard_workflow("acid pool")
pos = _pos_with(wf, "acid pool")
assert pos["inputs"]["text"].startswith("acid pool")
def test_edge_view_reflected():
wf = comfyui_build_trap_hazard_workflow("pressure plate", view="top-down")
pos = _pos_with(wf, "pressure plate")
assert "top-down view" in pos["inputs"]["text"]
def test_edge_view_empty_no_dangling_clause():
wf = comfyui_build_trap_hazard_workflow("flame jet", view="")
pos = _pos_with(wf, "flame jet")
text = pos["inputs"]["text"]
# Sin clausula "view" colgando cuando view es vacio.
assert " view," not in text
assert "single hazard object" in text
def test_edge_style_reflected():
wf = comfyui_build_trap_hazard_workflow(
"arrow trap", style="pixel art hazard"
)
pos = _pos_with(wf, "arrow trap")
assert "pixel art hazard" in pos["inputs"]["text"]
def test_edge_negative_rejects_living_character():
wf = comfyui_build_trap_hazard_workflow("electric trap")
neg = next(
n for n in wf.values()
if n["class_type"] == "CLIPTextEncode"
and "electric trap" not in n["inputs"]["text"]
)
ntext = neg["inputs"]["text"]
# Diferenciado de enemy_creature: rechaza personaje/criatura vivos.
assert "character" in ntext
assert "creature" in ntext
assert "multiple objects" in ntext
def test_edge_lora_injected():
wf = comfyui_build_trap_hazard_workflow(
"bear trap", lora="pixel_art_sd15.safetensors", lora_strength=0.8
)
loras = _by_class(wf, "LoraLoader")
assert len(loras) == 1
assert loras[0]["inputs"]["lora_name"] == "pixel_art_sd15.safetensors"
assert loras[0]["inputs"]["strength_model"] == 0.8
def test_edge_transparent_default_true():
wf = comfyui_build_trap_hazard_workflow("swinging blade")
assert "Image Rembg (Remove Background)" in _classes(wf)
def test_error_empty_hazard():
import pytest
with pytest.raises(ValueError):
comfyui_build_trap_hazard_workflow(" ")
def test_determinism():
a = comfyui_build_trap_hazard_workflow("spiked floor trap", seed=7)
b = comfyui_build_trap_hazard_workflow("spiked floor trap", seed=7)
assert a == b