Add new game assets and update asset paths in code

This commit is contained in:
zwitschi committed 2026-10-31 15:20:08 +01:00
1 parent 53e98cf3fe
commit 69484866db
20 files changed
+228 -36

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 597 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 503 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 521 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 927 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 921 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+10 -10
View File
@@ -5,16 +5,16 @@ extends CanvasLayer
const Rules := preload("res://src/core/rules.gd") const Rules := preload("res://src/core/rules.gd")
const Strings := preload("res://src/core/strings.gd") const Strings := preload("res://src/core/strings.gd")
const T_VIGNETTE := preload("res://assets/textures/vignette.png") const T_VIGNETTE := preload("res://assets/textures/ai/game_ready/vignette.png")
const T_ICON_FLASH := preload("res://assets/textures/icons/flashlight_on.png") const T_ICON_FLASH := preload("res://assets/textures/ai/game_ready/icons/flashlight_on.png")
const T_ICON_DOOR := preload("res://assets/textures/icons/door_locked.png") const T_ICON_DOOR := preload("res://assets/textures/ai/game_ready/icons/door_locked.png")
const T_ICON_WAND := preload("res://assets/textures/icons/wand.png") const T_ICON_WAND := preload("res://assets/textures/ai/game_ready/icons/wand.png")
const T_ICON_REC := preload("res://assets/textures/icons/rec_dot.png") const T_ICON_REC := preload("res://assets/textures/ai/game_ready/icons/rec_dot.png")
const T_CARD_ID := preload("res://assets/textures/cards/id_card.png") const T_CARD_ID := preload("res://assets/textures/ai/game_ready/cards/id_card.png")
const T_CARD_WRIST := preload("res://assets/textures/cards/wristband.png") const T_CARD_WRIST := preload("res://assets/textures/ai/game_ready/cards/wristband.png")
const T_CARD_TICKET := preload("res://assets/textures/cards/ticket.png") const T_CARD_TICKET := preload("res://assets/textures/ai/game_ready/cards/ticket.png")
const T_CARD_CRED := preload("res://assets/textures/cards/credential.png") const T_CARD_CRED := preload("res://assets/textures/ai/game_ready/cards/credential.png")
const T_CARD_GUEST := preload("res://assets/textures/cards/guest_list.png") const T_CARD_GUEST := preload("res://assets/textures/ai/game_ready/cards/guest_list.png")
const CARD_TEXTURES := { const CARD_TEXTURES := {
Rules.AccessKind.ID: T_CARD_ID, Rules.AccessKind.ID: T_CARD_ID,
Rules.AccessKind.WRISTBAND: T_CARD_WRIST, Rules.AccessKind.WRISTBAND: T_CARD_WRIST,
+5 -5
View File
@@ -1,11 +1,11 @@
class_name WorldBuilder class_name WorldBuilder
## Procedurally assembles a minimal Hollywood Boulevard entrance prototype: ## Procedurally assembles a minimal Hollywood Boulevard entrance prototype:
const T_BRICK := preload("res://assets/textures/brick_facade.png") const T_BRICK := preload("res://assets/textures/ai/game_ready/brick_facade.png")
const T_CONCRETE := preload("res://assets/textures/concrete.png") const T_CONCRETE := preload("res://assets/textures/ai/game_ready/concrete.png")
const T_ASPHALT := preload("res://assets/textures/asphalt.png") const T_ASPHALT := preload("res://assets/textures/ai/game_ready/asphalt.png")
const T_AWNING := preload("res://assets/textures/awning.png") const T_AWNING := preload("res://assets/textures/ai/game_ready/awning.png")
const T_SKYLINE := preload("res://assets/textures/skyline.png") const T_SKYLINE := preload("res://assets/textures/ai/game_ready/skyline.png")
## steps, building facade, restricted door, sidewalk, pavement, and street edge. ## steps, building facade, restricted door, sidewalk, pavement, and street edge.
## No art assets required; all geometry is BoxMesh with flat materials. ## No art assets required; all geometry is BoxMesh with flat materials.
+96 -11
View File
@@ -12,6 +12,12 @@ import generate_textures
ROOT = Path(__file__).resolve().parent.parent ROOT = Path(__file__).resolve().parent.parent
IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".webp", ".bmp", ".tif", ".tiff"} IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".webp", ".bmp", ".tif", ".tiff"}
DEFAULT_ROLE_IMAGE_MODEL = "openai/gpt-5-image-mini"
API_ASPECTS = {
"texture": "1:1", "tile": "1:1", "icon": "1:1", "app_icon": "1:1",
"overlay": "1:1", "card": "3:2", "banner": "3:2", "backdrop": "3:2",
"background": "3:2",
}
PROFILES = { PROFILES = {
"texture": {"target": (512, 512), "min": (256, 256), "max": (1024, 1024), "texture": {"target": (512, 512), "min": (256, 256), "max": (1024, 1024),
@@ -24,15 +30,20 @@ PROFILES = {
"app_icon": {"target": (1024, 1024), "min": (512, 512), "max": (1024, 1024), "app_icon": {"target": (1024, 1024), "min": (512, 512), "max": (1024, 1024),
"aspect": 1.0, "alpha": None, "view": "square, centered composition"}, "aspect": 1.0, "alpha": None, "view": "square, centered composition"},
"overlay": {"target": (512, 512), "min": (256, 256), "max": (1024, 1024), "overlay": {"target": (512, 512), "min": (256, 256), "max": (1024, 1024),
"aspect": 1.0, "alpha": None, "view": "screen-space overlay"}, "aspect": 1.0, "alpha": None, "view": "screen-space overlay",
"min_contrast": 5.0},
"card": {"target": (256, 128), "min": (128, 64), "max": (1024, 512), "card": {"target": (256, 128), "min": (128, 64), "max": (1024, 512),
"aspect": 2.0, "alpha": None, "view": "flat, front-facing card"}, "aspect": 2.0, "alpha": None, "view": "flat, front-facing card"},
"banner": {"target": (1024, 256), "min": (512, 128), "max": (2048, 512), "banner": {"target": (1024, 256), "min": (512, 128), "max": (2048, 512),
"aspect": 4.0, "alpha": None, "view": "flat, front-facing banner"}, "aspect": 4.0, "alpha": None, "view": "flat, front-facing banner"},
"backdrop": {"target": (1024, 256), "min": (1024, 256), "max": (8192, 2048), "backdrop": {"target": (1024, 256), "min": (1024, 256), "max": (8192, 2048),
"aspect": 4.0, "alpha": None, "view": "straight-on wide backdrop"}, "aspect": 4.0, "alpha": True,
"view": "straight-on wide backdrop on transparency"},
"background": {"target": (1920, 1080), "min": (1280, 720), "max": (8192, 4320), "background": {"target": (1920, 1080), "min": (1280, 720), "max": (8192, 4320),
"aspect": 16 / 9, "alpha": None, "view": "intended game camera and horizon"}, "aspect": 16 / 9, "alpha": None, "view": "intended game camera and horizon"},
"screenshot": {"target": (1280, 720), "min": (1280, 720), "max": (3840, 2160),
"aspect": 16 / 9, "alpha": None, "view": "gameplay frame, venue, and UI state",
"min_contrast": 5.0},
} }
ROLE_GUIDANCE = { ROLE_GUIDANCE = {
@@ -41,10 +52,10 @@ ROLE_GUIDANCE = {
"icon": "single centered flat UI glyph, clean silhouette, transparent background, no mockup or perspective", "icon": "single centered flat UI glyph, clean silhouette, transparent background, no mockup or perspective",
"app_icon": "square game application icon, centered composition, no perspective", "app_icon": "square game application icon, centered composition, no perspective",
"overlay": "screen-space overlay texture with transparent edges, no perspective", "overlay": "screen-space overlay texture with transparent edges, no perspective",
"card": "single front-facing flat game UI card, no perspective, no surrounding mockup", "card": "single wide landscape game UI card filling the frame, front-facing and flat, no perspective, no surrounding mockup or whitespace",
"banner": "wide front-facing game banner texture, no perspective, no surrounding scene", "banner": "wide front-facing game banner texture, keep important details in a narrow horizontal band through the image center for final cropping, no perspective, no surrounding scene",
"backdrop": "wide seamless-looking game environment backdrop, straight-on view, no foreground objects", "backdrop": "wide isolated skyline silhouette across a low horizon in a narrow horizontal band through the image center, transparent background, no foreground objects",
"background": "wide 16:9 game background environment with a coherent horizon and straight camera view", "background": "wide 16:9 game background with a coherent horizon and straight camera view, keep important details in the central horizontal band for final cropping",
} }
@@ -137,6 +148,8 @@ def resize_image(source, output, width, height, fit="contain", background=(0, 0,
def infer_role(path): def infer_role(path):
parts = [part.lower() for part in path.parts] parts = [part.lower() for part in path.parts]
stem = path.stem.lower() stem = path.stem.lower()
if "screenshots" in parts or "screenshot" in stem:
return "screenshot"
if "icons" in parts: if "icons" in parts:
return "icon" return "icon"
if "cards" in parts: if "cards" in parts:
@@ -193,6 +206,8 @@ def audit_image(path, role="auto", seam_threshold=18.0):
f"wrong aspect ratio: {actual_aspect:.3f}; expected about {aspect:.3f}") f"wrong aspect ratio: {actual_aspect:.3f}; expected about {aspect:.3f}")
if profile.get("alpha") is True and not alpha: if profile.get("alpha") is True and not alpha:
errors.append("transparency required but image is opaque") errors.append("transparency required but image is opaque")
if alpha and image.convert("RGBA").getchannel("A").getbbox() is None:
errors.append("image is fully transparent")
elif profile.get("alpha") is False and alpha: elif profile.get("alpha") is False and alpha:
warnings.append( warnings.append(
"image has an alpha channel although this role is normally opaque") "image has an alpha channel although this role is normally opaque")
@@ -204,20 +219,27 @@ def audit_image(path, role="auto", seam_threshold=18.0):
if edge_difference > seam_threshold: if edge_difference > seam_threshold:
errors.append( errors.append(
f"tile edges differ by {edge_difference:.1f} (limit {seam_threshold:.1f})") f"tile edges differ by {edge_difference:.1f} (limit {seam_threshold:.1f})")
contrast = None
if profile.get("min_contrast") is not None:
contrast = ImageStat.Stat(image.convert("L")).stddev[0]
if contrast < profile["min_contrast"]:
errors.append(
f"near-blank frame: contrast {contrast:.1f} is below {profile['min_contrast']:.1f}")
manual_review = [ manual_review = [
f"Confirm {profile.get('view', 'intended')} perspective/projection visually; " f"Confirm {profile.get('view', 'intended')} perspective/projection visually; "
"automated checks cannot infer it reliably." "automated checks cannot infer it reliably."
] ]
return { return {
"path": str(path), "role": chosen_role, "size": [width, height], "path": str(path), "role": chosen_role, "size": [width, height],
"alpha": alpha, "edge_difference": edge_difference, "alpha": alpha, "edge_difference": edge_difference, "contrast": contrast,
"quality": "bad" if errors else "good", "quality": "bad" if errors else "good",
"errors": errors, "warnings": warnings, "manual_review": manual_review, "errors": errors, "warnings": warnings, "manual_review": manual_review,
} }
except (OSError, ValueError) as exc: except (OSError, ValueError) as exc:
return { return {
"path": str(path), "role": chosen_role, "size": None, "alpha": None, "path": str(path), "role": chosen_role, "size": None, "alpha": None,
"edge_difference": None, "quality": "bad", "errors": [f"cannot read image: {exc}"], "edge_difference": None, "contrast": None, "quality": "bad",
"errors": [f"cannot read image: {exc}"],
"warnings": [], "manual_review": [], "warnings": [], "manual_review": [],
} }
@@ -246,13 +268,19 @@ def _generate(args):
"alpha") is True "alpha") is True
prompt = f"{args.prompt.strip()}. {ROLE_GUIDANCE[args.role]}." prompt = f"{args.prompt.strip()}. {ROLE_GUIDANCE[args.role]}."
image = generate_textures.generate_image( image = generate_textures.generate_image(
prompt, args.model, args.request_size, transparent) prompt, args.model, args.request_size, transparent,
aspect_ratio=API_ASPECTS[args.role])
foreground = getattr(args, "foreground", None)
if foreground:
image = colorize_alpha(image, foreground)
if args.role == "icon" and has_alpha(image): if args.role == "icon" and has_alpha(image):
image = resize_image_from_image( image = resize_image_from_image(
image, width, height, "contain", (0, 0, 0, 0)) image, width, height, "contain", (0, 0, 0, 0))
else: else:
image = ImageOps.fit(image, (width, height), image = ImageOps.fit(image, (width, height),
method=Image.Resampling.LANCZOS) method=Image.Resampling.LANCZOS)
if args.role == "tile":
image = make_seamless(image)
save_image(image, resolve_path(args.output)) save_image(image, resolve_path(args.output))
@@ -273,6 +301,53 @@ def resize_image_from_image(image, width, height, fit, background):
return result return result
def colorize_alpha(image, color):
if not has_alpha(image):
raise ValueError(
"foreground color requires an image with transparency")
result = Image.new("RGBA", image.size, color[:3])
result.putalpha(image.convert("RGBA").getchannel("A"))
return result
def make_seamless(image, band=32):
original_mode = image.mode
result = image.convert("RGBA")
width, height = result.size
pixels = result.load()
band = min(band, width // 4, height // 4)
def blend_edges(first_index, last_index, length, across_length, horizontal):
for offset in range(band):
weight = 1.0 - offset / band
first = first_index(offset)
last = last_index(offset)
for across in range(across_length):
first_pos = (first, across) if horizontal else (across, first)
last_pos = (last, across) if horizontal else (across, last)
first_color = pixels[first_pos]
last_color = pixels[last_pos]
average = tuple((a + b) / 2 for a,
b in zip(first_color, last_color))
pixels[first_pos] = tuple(round(a * (1 - weight) + b * weight)
for a, b in zip(first_color, average))
pixels[last_pos] = tuple(round(a * (1 - weight) + b * weight)
for a, b in zip(last_color, average))
blend_edges(lambda offset: offset, lambda offset: width - 1 - offset,
width, height, True)
blend_edges(lambda offset: offset, lambda offset: height - 1 - offset,
height, width, False)
return result if has_alpha(image) else result.convert(original_mode)
def seamless_image(source, output):
with Image.open(source) as image:
image.load()
result = make_seamless(image)
save_image(result, output)
def build_parser(): def build_parser():
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Create and validate game image assets.") description="Create and validate game image assets.")
@@ -283,13 +358,16 @@ def build_parser():
generate.add_argument("--prompt", required=True) generate.add_argument("--prompt", required=True)
generate.add_argument("--output", required=True, generate.add_argument("--output", required=True,
help="output path, relative to the project root or absolute") help="output path, relative to the project root or absolute")
generate.add_argument("--role", choices=PROFILES, required=True) generate.add_argument("--role", choices=API_ASPECTS, required=True)
generate.add_argument( generate.add_argument(
"--model", default=generate_textures.DEFAULT_IMAGE_MODEL) "--model", default=DEFAULT_ROLE_IMAGE_MODEL)
generate.add_argument("--request-size", type=int, generate.add_argument("--request-size", type=int,
default=generate_textures.GEN_SIZE) default=generate_textures.GEN_SIZE)
generate.add_argument("--width", type=int) generate.add_argument("--width", type=int)
generate.add_argument("--height", type=int) generate.add_argument("--height", type=int)
generate.add_argument(
"--foreground", type=parse_color,
help="replace visible RGB with this color while preserving alpha")
alpha = generate.add_mutually_exclusive_group() alpha = generate.add_mutually_exclusive_group()
alpha.add_argument("--transparent", dest="transparent", alpha.add_argument("--transparent", dest="transparent",
action="store_true") action="store_true")
@@ -320,6 +398,13 @@ def build_parser():
resize.set_defaults(handler=lambda args: resize_image(resolve_path(args.input), resolve_path(args.output), resize.set_defaults(handler=lambda args: resize_image(resolve_path(args.input), resolve_path(args.output),
args.width, args.height, args.fit, args.background)) args.width, args.height, args.fit, args.background))
seamless = commands.add_parser(
"seamless", help="blend opposite edges of a texture for tiling")
seamless.add_argument("input")
seamless.add_argument("output")
seamless.set_defaults(handler=lambda args: seamless_image(
resolve_path(args.input), resolve_path(args.output)))
audit = commands.add_parser( audit = commands.add_parser(
"audit", help="classify images as good/bad using their game-asset role") "audit", help="classify images as good/bad using their game-asset role")
audit.add_argument("paths", nargs="+", help="image files or directories") audit.add_argument("paths", nargs="+", help="image files or directories")
+17 -8
View File
@@ -373,23 +373,32 @@ def _api_key() -> str:
def generate_image(prompt: str, model: str, size: int, def generate_image(prompt: str, model: str, size: int,
transparent: bool = False) -> Image.Image: transparent: bool = False,
aspect_ratio: str = "1:1") -> Image.Image:
"""Call the OpenRouter image API and return the decoded PIL image. """Call the OpenRouter image API and return the decoded PIL image.
Requests a square image at `size` px and decodes the base64 PNG/JPEG/WebP Requests a square image at `size` px or a role-shaped image at the matching
bytes returned in `data[].b64_json`. The requested size is clamped to at resolution tier and decodes `data[].b64_json`. Seedream's square requests
least GEN_SIZE because some models (e.g. seedream) reject small outputs; retain the existing GEN_SIZE floor. When `transparent` is true, the API is
callers downscale the result to MAX_DIM afterwards. When `transparent` is asked for a transparent PNG background.
true, the API is asked for a transparent background (PNG alpha).
""" """
size = max(GEN_SIZE, int(size)) size = max(1, int(size))
if model == DEFAULT_IMAGE_MODEL:
size = max(GEN_SIZE, size)
body = { body = {
"model": model, "model": model,
"prompt": prompt, "prompt": prompt,
"size": f"{size}x{size}",
"output_format": "png", "output_format": "png",
"n": 1, "n": 1,
} }
if aspect_ratio == "1:1":
body["size"] = f"{size}x{size}"
else:
body["aspect_ratio"] = aspect_ratio
body["resolution"] = next(
(tier for limit, tier in ((512, "512"), (768, "768"), (1024, "1K"),
(1536, "1.5K"), (2048, "2K"), (4096, "4K"))
if size <= limit), "4K")
if transparent: if transparent:
body["background"] = "transparent" body["background"] = "transparent"
req = urllib.request.Request( req = urllib.request.Request(
+100 -2
View File
@@ -3,7 +3,7 @@ import unittest
from pathlib import Path from pathlib import Path
from unittest.mock import patch from unittest.mock import patch
from PIL import Image from PIL import Image, ImageDraw
import asset_pipeline import asset_pipeline
@@ -51,6 +51,38 @@ class AssetPipelineTests(unittest.TestCase):
self.assertEqual(asset_pipeline.audit_image( self.assertEqual(asset_pipeline.audit_image(
bad, "tile")["quality"], "bad") bad, "tile")["quality"], "bad")
def test_seamless_transform_makes_tile_edges_continuous(self):
image_path = self.root / "tile.png"
output = self.root / "seamless.png"
image = Image.new("RGB", (512, 512))
for x in range(512):
for y in range(512):
image.putpixel((x, y), (x // 2, y // 2, (x + y) // 4))
image.save(image_path)
asset_pipeline.seamless_image(image_path, output)
self.assertEqual(asset_pipeline.audit_image(
output, "tile")["quality"], "good")
def test_tile_generation_applies_seamless_transform(self):
output = self.root / "generated_tile.png"
args = type("Args", (), {
"prompt": "brick", "role": "tile", "output": str(output),
"width": None, "height": None, "transparent": False,
"model": "test-model", "request_size": 1024,
})()
generated = Image.new("RGB", (512, 512), (80, 80, 80))
for y in range(512):
generated.putpixel((0, y), (255, 0, 0))
with patch.object(asset_pipeline.generate_textures, "generate_image",
return_value=generated):
asset_pipeline._generate(args)
self.assertEqual(asset_pipeline.audit_image(
output, "tile")["quality"], "good")
def test_background_audit_rejects_small_image(self): def test_background_audit_rejects_small_image(self):
image_path = self.root / "small.png" image_path = self.root / "small.png"
Image.new("RGB", (640, 360)).save(image_path) Image.new("RGB", (640, 360)).save(image_path)
@@ -63,11 +95,27 @@ class AssetPipelineTests(unittest.TestCase):
def test_grayscale_vignette_is_valid_without_alpha(self): def test_grayscale_vignette_is_valid_without_alpha(self):
image_path = self.root / "vignette.png" image_path = self.root / "vignette.png"
Image.new("L", (512, 512), 0).save(image_path) image = Image.new("L", (512, 512), 0)
draw = ImageDraw.Draw(image)
for radius in range(255, -1, -1):
value = int(255 * (1 - radius / 255))
draw.ellipse((256 - radius, 256 - radius,
256 + radius, 256 + radius), fill=value)
image.save(image_path)
self.assertEqual(asset_pipeline.audit_image( self.assertEqual(asset_pipeline.audit_image(
image_path, "overlay")["quality"], "good") image_path, "overlay")["quality"], "good")
def test_overlay_audit_rejects_blank_transparent_image(self):
image_path = self.root / "blank_overlay.png"
Image.new("RGBA", (512, 512), (0, 0, 0, 0)).save(image_path)
result = asset_pipeline.audit_image(image_path, "overlay")
self.assertEqual(result["quality"], "bad")
self.assertTrue(any("near-blank frame" in error
for error in result["errors"]))
def test_icon_audit_rejects_opaque_rgba_images(self): def test_icon_audit_rejects_opaque_rgba_images(self):
image_path = self.root / "opaque_icon.png" image_path = self.root / "opaque_icon.png"
Image.new("RGBA", (64, 64), (255, 255, 255, 255)).save(image_path) Image.new("RGBA", (64, 64), (255, 255, 255, 255)).save(image_path)
@@ -78,6 +126,16 @@ class AssetPipelineTests(unittest.TestCase):
self.assertTrue(any("transparency required" in error self.assertTrue(any("transparency required" in error
for error in result["errors"])) for error in result["errors"]))
def test_backdrop_audit_requires_transparency(self):
image_path = self.root / "skyline.png"
Image.new("RGB", (1024, 256), (20, 30, 40)).save(image_path)
result = asset_pipeline.audit_image(image_path, "backdrop")
self.assertEqual(result["quality"], "bad")
self.assertTrue(any("transparency required" in error
for error in result["errors"]))
def test_generation_reuses_existing_backend_and_targets_role_dimensions(self): def test_generation_reuses_existing_backend_and_targets_role_dimensions(self):
output = self.root / "background.png" output = self.root / "background.png"
args = type("Args", (), { args = type("Args", (), {
@@ -93,9 +151,43 @@ class AssetPipelineTests(unittest.TestCase):
self.assertIn("Hollywood at night", backend.call_args.args[0]) self.assertIn("Hollywood at night", backend.call_args.args[0])
self.assertIn("wide 16:9", backend.call_args.args[0]) self.assertIn("wide 16:9", backend.call_args.args[0])
self.assertEqual(backend.call_args.kwargs["aspect_ratio"], "3:2")
with Image.open(output) as image: with Image.open(output) as image:
self.assertEqual(image.size, (1920, 1080)) self.assertEqual(image.size, (1920, 1080))
def test_generation_can_recolor_transparent_glyph(self):
output = self.root / "icon.png"
args = type("Args", (), {
"prompt": "flashlight", "role": "icon", "output": str(output),
"width": None, "height": None, "transparent": True,
"model": "test-model", "request_size": 1024,
"foreground": (255, 255, 255, 255),
})()
generated = Image.new("RGBA", (64, 64), (0, 0, 0, 0))
generated.putpixel((32, 32), (20, 20, 20, 255))
with patch.object(asset_pipeline.generate_textures, "generate_image",
return_value=generated):
asset_pipeline._generate(args)
with Image.open(output) as image:
self.assertEqual(image.getpixel((32, 32)), (255, 255, 255, 255))
self.assertEqual(image.getpixel((0, 0))[3], 0)
def test_screenshot_audit_accepts_game_frame_and_rejects_blank_capture(self):
valid = self.root / "frame.png"
blank = self.root / "blank.png"
frame = Image.new("RGB", (1280, 720), (0, 0, 0))
ImageDraw.Draw(frame).rectangle(
(640, 0, 1279, 719), fill=(255, 255, 255))
frame.save(valid)
Image.new("RGB", (1280, 720), (0, 0, 0)).save(blank)
self.assertEqual(asset_pipeline.audit_image(
valid, "screenshot")["quality"], "good")
self.assertEqual(asset_pipeline.audit_image(
blank, "screenshot")["quality"], "bad")
def test_role_inference_uses_game_asset_paths(self): def test_role_inference_uses_game_asset_paths(self):
self.assertEqual(asset_pipeline.infer_role( self.assertEqual(asset_pipeline.infer_role(
Path("assets/textures/icons/wand.png")), "icon") Path("assets/textures/icons/wand.png")), "icon")
@@ -104,6 +196,12 @@ class AssetPipelineTests(unittest.TestCase):
self.assertEqual(asset_pipeline.infer_role( self.assertEqual(asset_pipeline.infer_role(
Path("assets/textures/skyline.png")), "backdrop") Path("assets/textures/skyline.png")), "backdrop")
def test_role_generator_defaults_to_validated_image_model(self):
args = asset_pipeline.build_parser().parse_args([
"generate", "--prompt", "test", "--role", "tile", "--output", "tile.png"])
self.assertEqual(args.model, asset_pipeline.DEFAULT_ROLE_IMAGE_MODEL)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()