Baseline: Saumwacht 0.15.0 (Phase 20A) vor dem Umbau zu Champions of Luneraria
Unveränderter Ausgangsstand inklusive Masterprompt und Konzeptbildern unter assets/. .godot/ und docs/screenshots/ bleiben laut .gitignore unversioniert; letztere sind unter ../backups gesichert. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
.godot/
|
||||||
|
*.tmp
|
||||||
|
docs/screenshots/
|
||||||
|
test-results/
|
||||||
81
README.md
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
# Saumwacht · Die grüne Kluft
|
||||||
|
|
||||||
|
Ein Tower-Defense-Autobattler für Godot 4 mit **Glutpakt und Wurzelbund**, Rivalen-KI, 20-Wellen-Expedition und Acht-Wellen-Kurzpartie. Zwanzig spielbare Wächter, zehn Wellengegner, Arbeiter und Kernkristalle besitzen ausgearbeitete 3D-Modelle.
|
||||||
|
|
||||||
|
## Start
|
||||||
|
|
||||||
|
**Demo 0.15.0:** Im übergeordneten `dist` das passende [Windows-ZIP](../dist/Saumwacht-Windows-x64-0.15.0.zip) oder [Linux-ZIP](../dist/Saumwacht-Linux-x64-0.15.0.zip) vollständig entpacken. Windows: `Saumwacht.exe`; Linux: `Spielen.sh`. Godot und Blender werden zum Spielen nicht benötigt. Bei Vulkan-Problemen `Kompatibilitaet.bat` bzw. `Kompatibilitaet.sh` nutzen.
|
||||||
|
|
||||||
|
[Aktueller Stand: Phase 20A · Hauptmenü und Bundauswahl](../docs/phase-20a-hauptmenue.md). 0.15.0 bringt einen neuen Spieleinstieg vor einer 3D-Bühne an der Grünen Kluft, eine Bundwahl über gestaltete Karten und ein gemeinsames Menü-Theme; erstmals sind auch [Funkenlanze](../docs/phase-18-funkenlanze.md) und [Glasballiste](../docs/phase-19-glasballiste.md) im Paket. [Frühere Integrationsschritte](../docs/phase-17-integration-und-demo.md).
|
||||||
|
|
||||||
|
Aus dem Quellprojekt: `./play.sh`, `godot --path .` oder `project.godot` in Godot 4.7.2 öffnen und F5 drücken.
|
||||||
|
|
||||||
|
**Neue Partie** öffnet die Bundwahl mit Partielänge; Rivalenbund, KI-Stufe und Bauzeit liegen unter *Weitere Einstellungen*. Nach **Partie beginnen** hilft **Startformation**: drei Wächter für 185 Gold beim Wurzelbund, 180 Gold beim Glutpakt. **Leertaste / Kampf starten** beginnt den Autokampf. Standardmäßig gibt es keinen Baudruck; auch die Abrechnung wartet auf dich. Das frühere Zeitlimit lässt sich im Menü zuschalten.
|
||||||
|
|
||||||
|
Die [Landschaft](../docs/phase-10-grafik-und-landschaft.md), die [Beleuchtung und die Menüs](../docs/phase-13-grafik-und-menu.md) sind cineastisch überarbeitet: warmer Schlüssel, kühles Fülllicht, Glaspanels und ein neues Titelmenü.
|
||||||
|
|
||||||
|
Die [Ringborke](../docs/phase-11-ringborke.md), der [Funkenleser](../docs/phase-12-arbeiter.md), die [Basaltbastion](../docs/phase-15-basaltbastion.md), das [Quellgeflecht](../docs/phase-16-quellgeflecht.md), die [Funkenlanze](../docs/phase-18-funkenlanze.md) und die [Glasballiste](../docs/phase-19-glasballiste.md) besitzen gewichtete Skelette. Der Arbeiter zeigt Werkzeugkontakt, Aufnehmen, Einfüllen und Rückkehr; **V → Funkenleser** öffnet den Betrachter. Die Basaltbastion nimmt zum Kampfbeginn ihre Schutzhaltung ein; empfangene Heilung zeigt einen Effektpuls, die eigene Fähigkeit ihre zugehörige Animation. Der Kristall reagiert auf Aktivierung und Schaden. Die [Engine-Prüfung](../docs/engine-pruefung-2026-09-19.md) bleibt die Grundlage für einen späteren Vergleich.
|
||||||
|
|
||||||
|
## Bedienung
|
||||||
|
|
||||||
|
| Eingabe | Aktion |
|
||||||
|
| --- | --- |
|
||||||
|
| 1–5 / Baukarte | Wächter und Modellvorschau wählen |
|
||||||
|
| Linksklick auf freies Feld | Einen Wächter kaufen, platzieren und auswählen |
|
||||||
|
| Shift + Linksklick | Mehrere Wächter platzieren |
|
||||||
|
| Linksklick auf Wächter | Aufwertung, Versetzen, Verkauf und Zielwahl öffnen |
|
||||||
|
| Rechtsklick / Esc | Bauauftrag oder Versetzen abbrechen |
|
||||||
|
| Rückgängig | Letzte erfolgreiche Bauaktion zurücknehmen |
|
||||||
|
| Leertaste | Kampf starten / nächste Bauphase |
|
||||||
|
| C / Basis ansehen | Arbeiter und Kristall ansehen / zur Front zurückkehren |
|
||||||
|
| Tab | Eigene Front / Rivalenfront |
|
||||||
|
| P / 1×–2× | Pause / Kampfgeschwindigkeit |
|
||||||
|
| Mausrad / Q–E | Zoom / Kamera drehen |
|
||||||
|
| Mittlere Maustaste ziehen / R | Kamera verschieben / zurücksetzen |
|
||||||
|
| F1 | Optionen, Steuerung, neue Partie und Beenden |
|
||||||
|
| V | Modelle groß ansehen und Bewegungen abspielen |
|
||||||
|
| M / F11 | Stumm / Vollbild |
|
||||||
|
|
||||||
|
Gold kauft Wächter, Aufwertungen und Arbeiter. Jeder aktive Arbeiter liefert **zehn Funken pro Bauphase**; neue Arbeiter erst ab der folgenden Welle. Ab Welle 3 kannst du Funken für Söldner ausgeben: Sie greifen den Rivalen an und steigern dauerhaft dein Goldeinkommen. Die Anzeige nennt nur freie, noch nicht reservierte Funken. Gekaufte Wächter kehren nach dem Kampf vollständig geheilt in ihre Formation zurück.
|
||||||
|
|
||||||
|
Der Wurzelbund nutzt Heilung, Ranken und Keimbrand. Seine Heiler müssen nahe an der Front stehen. Der Glutpakt kombiniert Schutz, Brand und Flächenschaden. Nach der letzten Welle gewinnt der Kristall mit mehr HP; ein zerstörter Kern beendet das Duell früher.
|
||||||
|
|
||||||
|
## Umfang und Prüfung
|
||||||
|
|
||||||
|
[Hauptmenü 20A](../docs/phase-20a-hauptmenue.md) · [Mindestpolygonregel](../docs/pruefung-mindestpolygonregel.md) · [Änderungen, Modelle und Balance](../docs/phase-10-grafik-und-landschaft.md) · [Grafik & Menü](../docs/phase-13-grafik-und-menu.md) · [Arbeiter](../docs/phase-12-arbeiter.md) · [Builds](docs/phase20-builds.md) · [QA](docs/QA.md) · [Spielansicht](docs/screenshots/phase20/after/ingame-build-1920x1080.png) · [Menü](docs/screenshots/phase20/after/title-1920x1080.png).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
godot --path . --resolution 1920x1080 res://tools/verify_menu.tscn
|
||||||
|
godot --path . --resolution 1920x1080 res://tools/menu_review.tscn
|
||||||
|
godot --headless --path . res://tests/test_runner.tscn
|
||||||
|
python3 tools/verify_arbeiter.py
|
||||||
|
godot --headless --path . res://tools/verify_arbeiter.tscn
|
||||||
|
godot --headless --path . res://tools/verify_ability_events.tscn
|
||||||
|
godot --path . res://tools/verify_material_sides.tscn
|
||||||
|
python3 tools/verify_glasballiste.py
|
||||||
|
godot --headless --path . res://tools/verify_glass.tscn
|
||||||
|
python3 tools/build_demo.py --check-only
|
||||||
|
godot --headless --path . res://tools/verify_phase5.tscn -- --phase9
|
||||||
|
godot --headless --path . -- --faction-test
|
||||||
|
godot --path . -- --faction-test --ui-only
|
||||||
|
godot --headless --path . -- --expedition-test
|
||||||
|
godot --headless --path . -- --smoke-test
|
||||||
|
```
|
||||||
|
|
||||||
|
`--faction-test` prüft Fähigkeiten, Fraktionsbesitz, Wirtschaft, Bedienung und vier normale KI-Partien. Mit Grafikausgabe entstehen Screenshots unter `user://phase10-captures`. `--ui-only` lässt die vier vollständigen Partien weg. Die alten Prüfmodi setzen ausdrücklich Glutpakt und zeitgesteuerte Bauphasen, um die bisherigen Ergebnisse zu vergleichen.
|
||||||
|
|
||||||
|
Die Grafik verwendet Forward+; der beiliegende OpenGL-Modus zeigt dieselbe Geometrie ohne SSAO und Glow. Ringborke, Funkenleser, Basaltbastion, Quellgeflecht, Funkenlanze und Glasballiste nutzen gewichtete Skelette; die anderen Figuren bewegen weiterhin starre Gelenkgruppen. Die Illustrationen sind Stilvorlagen für eigenständige 3D-Modelle. Balance ist anhand reproduzierbarer KI-Partien eingegrenzt; menschliche Spieltests sind weiterhin wichtig. Kein Speichern laufender Partien, kein Multiplayer. Windows ist exportiert; ein nativer Windows-Test steht aus.
|
||||||
|
|
||||||
|
## Assetquellen
|
||||||
|
|
||||||
|
Das Menü-Diorama entsteht aus [build_menu_stage.py](assets-source/phase20/build_menu_stage.py) (Blender) mit [GLB](assets/environment/menu-stage.glb) und `assets/menu_stage_manifest.json`; Embleme und Steuerungs-Icons liegen als SVG unter `assets/ui`, das Menü-Theme in `scripts/ui/theme`.
|
||||||
|
|
||||||
|
Die Glasballiste entsteht vollständig aus ihrem [Generator](assets-source/phase19/build_glasballiste.py); daraus folgen [Blender-Szene](assets-source/phase19/glasballiste.blend), [GLB](assets/characters/glasballiste.glb) und Manifest. [Modellbericht](../docs/phase-19-glasballiste.md).
|
||||||
|
|
||||||
|
Der Funkenleser liegt als [Blender-Szene](assets-source/phase12/arbeiter.blend) mit editierbaren Einzelteilen und Arbeitsclips vor. Der [GLB-Export](assets/characters/arbeiter.glb) enthält das gewichtete Modell. [Modellbericht](../docs/phase-12-arbeiter.md).
|
||||||
|
|
||||||
|
Die Ringborke liegt als [Blender-Szene](assets-source/phase11/ringborke.blend) mit editierbaren Einzelteilen und Animationen vor. Der [GLB-Export](assets/characters/ringborke.glb) enthält das gewichtete Modell und seine Clips. [Modellbericht und Wiederaufbau](../docs/phase-11-ringborke.md).
|
||||||
|
|
||||||
|
Die Kartenquelle liegt unter `assets-source/phase10/verdant-rift.blend`, der Spielexport unter `assets/environment`. 59 Figuren- und Bauwerk-GLBs liegen unter `assets/models`, bearbeitbare Blender-Szenen unter `assets-source/phase4/blend`. Glutpakt und Wurzelbund haben je zehn Modelle im höheren Detailgrad; Nietenkonvent und Nebelchor sind noch nicht spielbar. Die [Blender-Pipeline](assets-source/phase4/README.md) liefert außerdem neun eigenständige Skripte. [Direkte Referenzvergleiche](docs/screenshots/phase4/reference-gallery.html) und `godot --path . res://tools/model_gallery.tscn` öffnen die Modellwerkstatt.
|
||||||
|
|
||||||
|
Die bisherigen Einstellungen bleiben im Benutzerordner `Saumwacht · Glutpakt` erhalten. Exportdateien enthalten nur Spieldaten und benötigte Manifeste; Quellen, Dokumentation und Entwicklungswerkzeuge werden nicht mit ausgeliefert.
|
||||||
0
assets-source/.gdignore
Normal file
61
assets-source/phase10/bake_bark.py
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
"""Render authored, periodic 4D wood grain at native 4096px in Blender.
|
||||||
|
This is a procedural material bake, not an enlargement of a bitmap.
|
||||||
|
The torus coordinate mapping makes the material seamless on all four edges.
|
||||||
|
"""
|
||||||
|
import bpy, math, json
|
||||||
|
from pathlib import Path
|
||||||
|
PROJECT=Path(__file__).resolve().parents[2]
|
||||||
|
out=PROJECT/'assets/materials/phase10'
|
||||||
|
scene=bpy.data.scenes.new('Bark_PBR_4K');bpy.context.window.scene=scene
|
||||||
|
scene.render.engine='CYCLES';scene.cycles.samples=1
|
||||||
|
scene.render.resolution_x=4096;scene.render.resolution_y=4096;scene.render.resolution_percentage=100
|
||||||
|
scene.render.image_settings.file_format='PNG';scene.render.image_settings.color_mode='RGB';scene.render.image_settings.color_depth='8'
|
||||||
|
scene.render.image_settings.compression=90;scene.render.dither_intensity=0
|
||||||
|
scene.view_settings.view_transform='Standard';scene.view_settings.look='None'
|
||||||
|
scene.render.film_transparent=False
|
||||||
|
bpy.ops.mesh.primitive_plane_add(size=2)
|
||||||
|
plane=bpy.context.object;plane.name='Bark_material_bake_surface'
|
||||||
|
mat=bpy.data.materials.new('SW_4K_Sculpted_Oak');mat.use_nodes=True;plane.data.materials.append(mat)
|
||||||
|
n=mat.node_tree.nodes;n.clear();links=mat.node_tree.links
|
||||||
|
def node(kind): return n.new(kind)
|
||||||
|
def mathnode(op,a,b=None):
|
||||||
|
obj=node('ShaderNodeMath');obj.operation=op
|
||||||
|
if isinstance(a,(float,int)): obj.inputs[0].default_value=a
|
||||||
|
else: links.new(a,obj.inputs[0])
|
||||||
|
if b is not None:
|
||||||
|
if isinstance(b,(float,int)):obj.inputs[1].default_value=b
|
||||||
|
else:links.new(b,obj.inputs[1])
|
||||||
|
return obj.outputs[0]
|
||||||
|
uv=node('ShaderNodeTexCoord');split=node('ShaderNodeSeparateXYZ');links.new(uv.outputs['UV'],split.inputs[0])
|
||||||
|
u=mathnode('MULTIPLY',split.outputs['X'],math.tau)
|
||||||
|
v=mathnode('MULTIPLY',split.outputs['Y'],math.tau)
|
||||||
|
co=node('ShaderNodeCombineXYZ')
|
||||||
|
links.new(mathnode('MULTIPLY',mathnode('COSINE',u),10),co.inputs['X'])
|
||||||
|
links.new(mathnode('MULTIPLY',mathnode('SINE',u),10),co.inputs['Y'])
|
||||||
|
links.new(mathnode('MULTIPLY',mathnode('COSINE',v),.7),co.inputs['Z'])
|
||||||
|
grain=node('ShaderNodeTexNoise');grain.noise_dimensions='4D'
|
||||||
|
links.new(co.outputs[0],grain.inputs['Vector']);links.new(mathnode('MULTIPLY',mathnode('SINE',v),.7),grain.inputs['W'])
|
||||||
|
grain.inputs['Scale'].default_value=2.6;grain.inputs['Detail'].default_value=7;grain.inputs['Roughness'].default_value=.73
|
||||||
|
height=node('ShaderNodeValToRGB')
|
||||||
|
height.color_ramp.elements[0].position=.27;height.color_ramp.elements[0].color=(.07,.07,.07,1)
|
||||||
|
height.color_ramp.elements[1].position=.67;height.color_ramp.elements[1].color=(.8,.8,.8,1)
|
||||||
|
height.color_ramp.elements.new(.43).color=(.14,.14,.14,1)
|
||||||
|
height.color_ramp.elements.new(.51).color=(.55,.55,.55,1)
|
||||||
|
links.new(grain.outputs['Fac'],height.inputs[0])
|
||||||
|
emit=node('ShaderNodeEmission');output=node('ShaderNodeOutputMaterial');links.new(emit.outputs[0],output.inputs['Surface'])
|
||||||
|
links.new(height.outputs[0],emit.inputs['Color'])
|
||||||
|
bpy.ops.object.camera_add(location=(0,0,3));camera=bpy.context.object;camera.data.type='ORTHO';camera.data.ortho_scale=2;scene.camera=camera
|
||||||
|
scene.render.filepath=str(out/'oak-height-4k.png');bpy.ops.render.render(write_still=True)
|
||||||
|
colors=node('ShaderNodeValToRGB')
|
||||||
|
def linear(hexcolor):
|
||||||
|
rgb=[int(hexcolor[i:i+2],16)/255 for i in (0,2,4)]
|
||||||
|
return tuple(c/12.92 if c<=.04045 else ((c+.055)/1.055)**2.4 for c in rgb)+(1,)
|
||||||
|
colors.color_ramp.elements[0].position=.0;colors.color_ramp.elements[0].color=linear('302B23')
|
||||||
|
colors.color_ramp.elements[1].position=1;colors.color_ramp.elements[1].color=linear('B09A6D')
|
||||||
|
colors.color_ramp.elements.new(.18).color=linear('4B3C2A')
|
||||||
|
colors.color_ramp.elements.new(.51).color=linear('766041')
|
||||||
|
colors.color_ramp.elements.new(.74).color=linear('94855D')
|
||||||
|
links.new(height.outputs[0],colors.inputs[0]);links.new(colors.outputs[0],emit.inputs['Color'])
|
||||||
|
scene.render.filepath=str(out/'oak-albedo-4k.png');bpy.ops.render.render(write_still=True)
|
||||||
|
bpy.data.libraries.write(str(PROJECT/'assets-source/phase10/oak-material-4k.blend'),{scene},fake_user=True,compress=True)
|
||||||
|
print('PBR_BAKE_PASS: two native 4096 x 4096 8-bit runtime maps; original seamless procedural material.')
|
||||||
274
assets-source/phase10/build_sanctuary.py
Normal file
|
|
@ -0,0 +1,274 @@
|
||||||
|
"""Saumwacht: the Verdant Rift, an editable Blender environment in real metres.
|
||||||
|
|
||||||
|
The two 6x8 gameplay grids remain unobstructed. All vegetation, bridges,
|
||||||
|
architectural details and rock strata are actual meshes, not a backdrop.
|
||||||
|
Run: ALSOFT_DRIVERS=null blender -b --python-exit-code 1 --python this_file.py
|
||||||
|
"""
|
||||||
|
from pathlib import Path
|
||||||
|
import sys, math, random, json, hashlib
|
||||||
|
import bpy
|
||||||
|
from mathutils import Vector, noise
|
||||||
|
|
||||||
|
PROJECT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(PROJECT/'assets-source/phase4/blender'))
|
||||||
|
from common import mesh_object, ellipsoid, cylinder, cone, srgb_to_linear
|
||||||
|
from reference_high_detail import hd_box, hd_curve, hd_ring, hd_rod, hd_finish
|
||||||
|
|
||||||
|
R = random.Random(10092026)
|
||||||
|
scene = bpy.data.scenes.new('The_Verdant_Rift')
|
||||||
|
bpy.context.window.scene = scene
|
||||||
|
scene.unit_settings.system = 'METRIC'
|
||||||
|
scene['description'] = 'Two intact 12 x 40 metre paths; forest sanctuary above a river gorge.'
|
||||||
|
M = {}
|
||||||
|
for key, color, rough, metal in [
|
||||||
|
('limestone','918875',.89,0),('cliff','666D68',.92,0),
|
||||||
|
('soil','404F39',.97,0),('bark','534332',.93,0),
|
||||||
|
('leaf_dark','314B38',.89,0),('leaf_mid','526A42',.85,0),
|
||||||
|
('leaf_light','8B9655',.79,0),('brass','BEA36D',.53,.65),
|
||||||
|
('teal','69CEB9',.36,.2),('coral','DF9A7A',.36,.2),
|
||||||
|
('flower','C4BB93',.72,0),('paving','707367',.92,0)]:
|
||||||
|
mat=bpy.data.materials.new('ENV_'+key); mat.use_nodes=True
|
||||||
|
c=tuple(srgb_to_linear(int(color[i:i+2],16)/255) for i in (0,2,4))
|
||||||
|
mat.diffuse_color=(*c,1)
|
||||||
|
bs=mat.node_tree.nodes.get('Principled BSDF')
|
||||||
|
bs.inputs['Base Color'].default_value=(*c,1)
|
||||||
|
bs.inputs['Roughness'].default_value=rough
|
||||||
|
bs.inputs['Metallic'].default_value=metal
|
||||||
|
if key in ['teal','coral']:
|
||||||
|
bs.inputs['Emission Color'].default_value=(*c,1)
|
||||||
|
bs.inputs['Emission Strength'].default_value=.65
|
||||||
|
M[key]=mat
|
||||||
|
|
||||||
|
def p(x,z,h=0): return (x,-z,h)
|
||||||
|
def box(name,x,z,h,sx,sz,sh,key='limestone',radius=.035,angle=0):
|
||||||
|
return hd_box(name,p(x,z,h),(sx,sz,sh),M[key],radius,(0,0,angle))
|
||||||
|
|
||||||
|
def rock(name,x,z,h,sx,sz,sh,seed=0):
|
||||||
|
# Angular, eroded stratified rock, authored rings rather than round spheres.
|
||||||
|
rng=random.Random(seed+917);verts=[];faces=[];sides=14;levels=13
|
||||||
|
phase=rng.uniform(0,6.28)
|
||||||
|
for row in range(levels):
|
||||||
|
t=row/(levels-1)
|
||||||
|
shoulder=.55+.45*math.sin(math.pi*t)**.3
|
||||||
|
ledge=[1.0,.93,1.05,.96][row%4]
|
||||||
|
for col in range(sides):
|
||||||
|
angle=math.tau*col/sides+phase
|
||||||
|
n=noise.noise(Vector((math.cos(angle)*2,math.sin(angle)*2,t*3+seed)))
|
||||||
|
radius=shoulder*ledge*(1+n*.16)
|
||||||
|
px=math.cos(angle)*sx*radius
|
||||||
|
py=math.sin(angle)*sz*radius
|
||||||
|
pz=(t*2-1)*sh+n*.07*sh
|
||||||
|
verts.append((px,py,pz))
|
||||||
|
for row in range(levels-1):
|
||||||
|
for col in range(sides):
|
||||||
|
a=row*sides+col;b=row*sides+(col+1)%sides
|
||||||
|
faces.append((a,b,b+sides,a+sides))
|
||||||
|
faces.append(tuple(reversed(range(sides))))
|
||||||
|
faces.append(tuple((levels-1)*sides+k for k in range(sides)))
|
||||||
|
obj=mesh_object(name,verts,faces,M['cliff']);obj.location=p(x,z,h)
|
||||||
|
hd_finish(obj,.035,2,False)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def leaf_mesh(name,center,radius,seed):
|
||||||
|
"""An airy spray of individually curved, closed six-sided leaves."""
|
||||||
|
rng=random.Random(seed); verts=[]; faces=[]
|
||||||
|
for i in range(90):
|
||||||
|
direction=Vector((rng.uniform(-1,1),rng.uniform(-1,1),rng.uniform(-.6,.7))).normalized()
|
||||||
|
point=Vector(center)+direction*radius*rng.random()**.38
|
||||||
|
tangent=Vector((rng.uniform(-1,1),rng.uniform(-1,1),rng.uniform(-.4,.4))).normalized()
|
||||||
|
side=tangent.cross(Vector((0,0,1))).normalized()
|
||||||
|
length=rng.uniform(.18,.35); width=length*.47; base=len(verts)
|
||||||
|
verts.extend([tuple(point-tangent*length),tuple(point-side*width),tuple(point+tangent*length),
|
||||||
|
tuple(point+side*width),tuple(point+Vector((0,0,length*.22))),tuple(point-Vector((0,0,.018)))])
|
||||||
|
faces.extend([tuple(base+j for j in f) for f in [(0,1,4),(1,2,4),(2,3,4),(3,0,4),(1,0,5),(2,1,5),(3,2,5),(0,3,5)]])
|
||||||
|
return mesh_object(name,verts,faces,M[['leaf_dark','leaf_mid','leaf_light'][seed%3]])
|
||||||
|
|
||||||
|
def tree(x,z,size,seed):
|
||||||
|
rng=random.Random(seed); base=Vector(p(x,z,-.3))
|
||||||
|
lean=Vector((rng.uniform(-.5,.5),rng.uniform(-.5,.5),0))*size
|
||||||
|
height=size*3.8
|
||||||
|
trunk=[base,base+Vector((0,0,height*.28)),base+lean*.7+Vector((0,0,height*.68)),base+lean+Vector((0,0,height))]
|
||||||
|
hd_curve('Tree_%d_Trunk'%seed,trunk,[size*.38,size*.25,size*.15,.035],M['bark'],18,9)
|
||||||
|
for j in range(7):
|
||||||
|
a=j*math.tau/7
|
||||||
|
root=base+Vector((math.cos(a),math.sin(a),0))*size*1.3
|
||||||
|
root.z=-.21
|
||||||
|
hd_curve('Tree_%d_Root_%d'%(seed,j),[root,base+Vector((math.cos(a)*.3,math.sin(a)*.3,.3))*size,trunk[1]],[.03,size*.14,.12*size],M['bark'],10,6)
|
||||||
|
for j in range(11):
|
||||||
|
a=j*2.399+seed
|
||||||
|
branch=base+lean*.5+Vector((0,0,height*(.4+j*.035)))
|
||||||
|
length=size*(1.7-j*.065)
|
||||||
|
end=branch+Vector((math.cos(a)*length,math.sin(a)*length,size*(.9+j*.055)))
|
||||||
|
hd_curve('Tree_%d_Branch_%d'%(seed,j),[branch,(branch+end)*.5+Vector((0,0,.18)),end],[size*.11,size*.065,.012],M['bark'],10,6)
|
||||||
|
leaf_mesh('Tree_%d_Canopy_%d'%(seed,j),end,size*.68,seed*100+j)
|
||||||
|
leaf_mesh('Tree_%d_Spray_%d'%(seed,j),end*.55+branch*.45,size*.5,seed*200+j)
|
||||||
|
|
||||||
|
def fern(x,z,scale,seed):
|
||||||
|
rng=random.Random(seed); verts=[]; faces=[]
|
||||||
|
base=Vector(p(x,z,-.08))
|
||||||
|
for j in range(7):
|
||||||
|
a=math.tau*j/7+seed
|
||||||
|
direction=Vector((math.cos(a),math.sin(a),0))
|
||||||
|
side=direction.cross(Vector((0,0,1)))
|
||||||
|
length=scale*rng.uniform(.65,1.05)
|
||||||
|
for k in range(1,8):
|
||||||
|
t=k/8
|
||||||
|
center=base+direction*length*t+Vector((0,0,math.sin(t*math.pi)*length*.6))
|
||||||
|
for sign in [-1,1]:
|
||||||
|
end=center+side*sign*length*.25*math.sin(t*math.pi)+direction*length*.09
|
||||||
|
i=len(verts)
|
||||||
|
verts.extend([tuple(center-direction*.025),tuple(end),tuple(center+direction*.08),tuple(center+Vector((0,0,.015)))])
|
||||||
|
faces.extend([(i,i+1,i+3),(i+1,i+2,i+3),(i+2,i,i+3),(i+2,i+1,i)])
|
||||||
|
mesh_object('Fern_'+str(seed),verts,faces,M['leaf_mid' if seed%2 else 'leaf_light'])
|
||||||
|
|
||||||
|
def ruin_pillar(x,z,height,team,seed):
|
||||||
|
box('Pillar_Plinth',x,z,.10,1.30,1.30,.38)
|
||||||
|
box('Pillar_Foot',x,z,.36,1.04,1.04,.16,'brass',.025)
|
||||||
|
for level in range(int(height/.5)):
|
||||||
|
box('Pillar_Block',x,z,.63+level*.48,.8,.8,.46,angle=(seed%3-1)*.018)
|
||||||
|
box('Pillar_Capital',x,z,height+.2,1.06,1.06,.24)
|
||||||
|
box('Pillar_Cap',x,z,height+.38,.94,.94,.12,'brass',.018)
|
||||||
|
# Deep vertical grooves and inset illuminated rune bars on all four faces.
|
||||||
|
for sx,sz in [(1,0),(-1,0),(0,1),(0,-1)]:
|
||||||
|
for off in [-.21,.21]:
|
||||||
|
box('Pillar_Groove',x+sx*.408+sz*off,z+sz*.408+sx*off,height*.52+.25,.034 if sx else .065,.065 if sx else .034,height*.63,'brass',.008)
|
||||||
|
box('Pillar_Inscription',x+sx*.425,z+sz*.425,height*.67,.038 if sx else .095,.095 if sx else .038,.4,'teal' if team==0 else 'coral',.009)
|
||||||
|
|
||||||
|
# A sculpted, continuous ravine. The causeways are flat and above the terrain.
|
||||||
|
nx,nz=224,256
|
||||||
|
verts=[]; faces=[]
|
||||||
|
for j in range(nz+1):
|
||||||
|
z=-52+j*104/nz
|
||||||
|
for i in range(nx+1):
|
||||||
|
x=-48+i*96/nx
|
||||||
|
v=noise.noise_vector(Vector((x*.16,z*.12,3)))
|
||||||
|
river=math.exp(-(abs(x)/3.4)**4)
|
||||||
|
h=-.42-5.1*river + v.x*.34
|
||||||
|
outer=max(0,abs(x)-29)/11
|
||||||
|
h+=outer*(1.7+v.y*2.7)
|
||||||
|
if 3 < abs(x) < 16 and abs(z)<21: h=min(h,-.24)
|
||||||
|
shore=max(0,min(1,(math.sqrt((x/48)**2+(z/52)**2)-.70)/.27))
|
||||||
|
shore=shore*shore*(3-2*shore)
|
||||||
|
h=h*(1-shore)-5.5*shore
|
||||||
|
verts.append(p(x,z,h))
|
||||||
|
for j in range(nz):
|
||||||
|
for i in range(nx):
|
||||||
|
k=j*(nx+1)+i
|
||||||
|
faces.append((k,k+nx+1,k+nx+2,k+1))
|
||||||
|
terrain=mesh_object('Sculpted_River_Gorge',verts,faces,M['soil'])
|
||||||
|
for face in terrain.data.polygons: face.use_smooth=True
|
||||||
|
solid=terrain.modifiers.new('Closed bedrock','SOLIDIFY'); solid.thickness=2
|
||||||
|
bpy.context.view_layer.objects.active=terrain
|
||||||
|
bpy.ops.object.modifier_apply(modifier=solid.name)
|
||||||
|
|
||||||
|
for team,c in enumerate([-9,9]):
|
||||||
|
# Granite retaining wall: jointed masonry above real rock walls.
|
||||||
|
box('Causeway_Foundation',c,0,-.65,12.6,41,1.1,'cliff',.13)
|
||||||
|
for side in [-1,1]:
|
||||||
|
for j in range(21):
|
||||||
|
z=-20+j*2
|
||||||
|
box('Edge_Coping',c+side*6.2,z,-.04,.48,1.94,.26,'limestone',.06)
|
||||||
|
if j%3==0:
|
||||||
|
box('Boundary_Socket',c+side*6.18,z,.12,.58,.7,.25,'limestone',.05)
|
||||||
|
box('Boundary_Inlay',c+side*6.18,z,.25,.12,.40,.02,'teal' if team==0 else 'coral',.008)
|
||||||
|
# Side buttresses with stepped lower construction.
|
||||||
|
if j%4==0:
|
||||||
|
box('Wall_Buttress',c+side*6.4,z,-1.0,.75,1.15,2.4,'limestone',.09)
|
||||||
|
box('Wall_Buttress_Foot',c+side*6.4,z,-2.15,1,1.4,.3,'limestone',.06)
|
||||||
|
for j in range(11):
|
||||||
|
rock('Stratified_Cliff',c+side*6.7,-21+j*4,-2.55,1.05,2.45,2.45,100+team*30+j)
|
||||||
|
# Quiet broad paving, recessed bevels and minor rotation keep it natural.
|
||||||
|
for row in range(20):
|
||||||
|
for col in range(6):
|
||||||
|
box('Paving',c-5+col*2,-19+row*2,-.082,1.975,1.975,.15,'paving',.018)
|
||||||
|
# Portals at the arrival end, clear 10m passage under a ruined arch.
|
||||||
|
for s in [-1,1]:
|
||||||
|
ruin_pillar(c+s*5.25,-20.7,3.1,team,team*2+s)
|
||||||
|
for t in range(3):
|
||||||
|
box('Portal_Corbel',c+s*(5.1-t*.32),-20.7,3.42+t*.26,1.2,1.15,.32,'limestone',.045)
|
||||||
|
# Tall spire and layered ring patio around the crystal, outside workers.
|
||||||
|
for radius,height,depth in [(3.8,-.06,.12),(3.5,.025,.055)]:
|
||||||
|
obj=cylinder('Sanctum_Patio',p(c,18.2,height),radius,depth,M['limestone'],96)
|
||||||
|
hd_finish(obj,.025,2)
|
||||||
|
for rad in [3.25,3.5]: hd_ring('Sanctum_Inlay',p(c,18.2,.067),rad,.025,M['brass'],segments=128)
|
||||||
|
for s in [-1,1]:
|
||||||
|
ruin_pillar(c+s*4.45,20.6,2.6,team,40+s)
|
||||||
|
for i in range(8):
|
||||||
|
x=c-4.2+(i%4)*2.8; z=13.7+(i//4)*2.2
|
||||||
|
box('Gatherer_Station',x,z+.55,.05,1.6,1.0,.12,'limestone',.05)
|
||||||
|
box('Gatherer_Channel',x,z+.55,.12,1.16,.06,.025,'teal' if team==0 else 'coral',.01)
|
||||||
|
# Rear stairs communicate an entrance without covering the arena.
|
||||||
|
for step in range(6): box('Sanctum_Stairs',c,21.3+step*.35,-.08-step*.08,5.6,.45,.2,'limestone',.035)
|
||||||
|
|
||||||
|
# Ruined footbridges connect the courtyards over the gorge.
|
||||||
|
for z in [-22.5,23.6]:
|
||||||
|
for j in range(13):
|
||||||
|
x=-5.1+j*.85
|
||||||
|
h=.25+math.sin((j/12)*math.pi)*.5
|
||||||
|
box('Bridge_Voussoir',x,z,h-.52,.86,2.3,1,'limestone',.075)
|
||||||
|
box('Bridge_Paving',x,z,h,.83,2.28,.12,'paving',.018)
|
||||||
|
for side in [-1,1]:
|
||||||
|
box('Bridge_Parapet',x,z+side*1.2,h+.25,.81,.18,.50,'limestone',.035)
|
||||||
|
|
||||||
|
# Vegetation frames the board; no trees or grass on either gameplay grid.
|
||||||
|
for i in range(30):
|
||||||
|
side=-1 if i%2 else 1
|
||||||
|
x=side*R.uniform(19.5,29.5); z=R.uniform(-29,29)
|
||||||
|
tree(x,z,R.uniform(.78,1.35),i+1)
|
||||||
|
for i in range(100):
|
||||||
|
x=R.choice([-1,1])*R.uniform(17.0,30); z=R.uniform(-31,31)
|
||||||
|
rock('Forest_Boulder',x,z,-.1,R.uniform(.25,1.3),R.uniform(.35,1.5),R.uniform(.3,1.1),i)
|
||||||
|
for i in range(370):
|
||||||
|
x=R.choice([-1,1])*R.uniform(16.3,29); z=R.uniform(-31,31)
|
||||||
|
fern(x,z,R.uniform(.30,.85),i)
|
||||||
|
for i in range(28):
|
||||||
|
z=R.uniform(-29,29); x=R.choice([-1,1])*R.uniform(2.1,2.9)
|
||||||
|
rock('River_Bank',x,z,-3.2,R.uniform(.6,1),R.uniform(1,2),R.uniform(1.2,2.8),300+i)
|
||||||
|
for i in range(14):
|
||||||
|
x=R.choice([-1,1])*R.uniform(17,22); z=R.uniform(-28,28)
|
||||||
|
for j in range(3):
|
||||||
|
box('Fallen_Ruin',x+j*.32,z+j*.42,.12+j*.1,1.5,.8,.55,'limestone',.07,angle=i*.4)
|
||||||
|
|
||||||
|
# Meadow tussocks on the outer banks, absent from all gameplay cells.
|
||||||
|
verts=[];faces=[]
|
||||||
|
for i in range(2200):
|
||||||
|
x=R.choice([-1,1])*R.uniform(16.6,30.5);z=R.uniform(-32,32)
|
||||||
|
for j in range(5):
|
||||||
|
h=R.uniform(.13,.38);w=R.uniform(.012,.03);a=R.uniform(0,math.tau)
|
||||||
|
base=Vector(p(x+R.uniform(-.11,.11),z+R.uniform(-.11,.11),-.24))
|
||||||
|
side=Vector((math.cos(a),math.sin(a),0))*w
|
||||||
|
bend=Vector((math.sin(a),math.cos(a),0))*h*.36
|
||||||
|
mid=base+Vector((0,0,h*.56))+bend*.3
|
||||||
|
end=base+Vector((0,0,h))+bend
|
||||||
|
n=len(verts)
|
||||||
|
verts.extend([tuple(base-side),tuple(base+side),tuple(mid+side*.7),tuple(mid-side*.7),tuple(end),tuple(mid+Vector((0,0,.012)))])
|
||||||
|
faces.extend([(n,n+1,n+2,n+3),(n+3,n+2,n+4),(n+1,n,n+5),(n+2,n+1,n+5),(n+4,n+2,n+5),(n+3,n+4,n+5),(n,n+3,n+5)])
|
||||||
|
mesh_object('Meadow_11000_Curved_Blades',verts,faces,M['leaf_mid'])
|
||||||
|
|
||||||
|
# Editable source retains every object. Runtime merges by material and chunk
|
||||||
|
# for culling, greatly reducing the number of draws compared with source parts.
|
||||||
|
source=PROJECT/'assets-source/phase10/verdant-rift.blend'
|
||||||
|
bpy.data.libraries.write(str(source),{scene},fake_user=True,compress=True)
|
||||||
|
objects=[o for o in scene.objects if o.type=='MESH']
|
||||||
|
source_count=len(objects)
|
||||||
|
groups={}
|
||||||
|
for obj in objects:
|
||||||
|
center=obj.matrix_world.translation
|
||||||
|
key=(int(math.floor(center.x/16)),int(math.floor(center.y/16)),obj.data.materials[0].name)
|
||||||
|
groups.setdefault(key,[]).append(obj)
|
||||||
|
for key,parts in groups.items():
|
||||||
|
bpy.ops.object.select_all(action='DESELECT')
|
||||||
|
for obj in parts: obj.select_set(True)
|
||||||
|
bpy.context.view_layer.objects.active=parts[0]
|
||||||
|
bpy.ops.object.join()
|
||||||
|
parts[0].name='Chunk_%d_%d_%s'%key
|
||||||
|
bpy.ops.object.select_all(action='SELECT')
|
||||||
|
out=PROJECT/'assets/environment/verdant-rift.glb'
|
||||||
|
bpy.ops.export_scene.gltf(filepath=str(out),export_format='GLB',use_selection=True,export_apply=True,export_yup=True,export_cameras=False,export_lights=False,export_animations=False)
|
||||||
|
tris=0
|
||||||
|
for obj in scene.objects:
|
||||||
|
if obj.type=='MESH':
|
||||||
|
obj.data.calc_loop_triangles();tris+=len(obj.data.loop_triangles)
|
||||||
|
record={'name':'Die grüne Kluft','source_objects':source_count,'runtime_chunks':len(groups),'triangles':tris,'seed':10092026,'blender':bpy.app.version_string,'sha256':hashlib.sha256(out.read_bytes()).hexdigest(),'playable_grids':2,'cell_size_m':2,'grid_cells':[6,8]}
|
||||||
|
(PROJECT/'assets-source/phase10/environment-manifest.json').write_text(json.dumps(record,indent=2)+'\n')
|
||||||
|
print('ENVIRONMENT_EXPORTED',json.dumps(record),flush=True)
|
||||||
15
assets-source/phase10/environment-manifest.json
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"name": "Die gr\u00fcne Kluft",
|
||||||
|
"source_objects": 2586,
|
||||||
|
"runtime_chunks": 62,
|
||||||
|
"triangles": 1607600,
|
||||||
|
"seed": 10092026,
|
||||||
|
"blender": "5.2.1 LTS",
|
||||||
|
"sha256": "bebe2d4193500df1cf54970adf0e240607430d66e42fd9ab2e67368166a6d052",
|
||||||
|
"playable_grids": 2,
|
||||||
|
"cell_size_m": 2,
|
||||||
|
"grid_cells": [
|
||||||
|
6,
|
||||||
|
8
|
||||||
|
]
|
||||||
|
}
|
||||||
39
assets-source/phase10/finalize_sources.py
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import bpy,json,sys
|
||||||
|
from pathlib import Path
|
||||||
|
p=Path(__file__).resolve().parents[2];source=p/'assets-source/phase4'
|
||||||
|
sys.path.insert(0,str(source/'blender'))
|
||||||
|
from reference_sculpt_detail import add_offline_materials
|
||||||
|
for path in sorted((source/'records').glob('*.json')):
|
||||||
|
record=json.loads(path.read_text())
|
||||||
|
if record['faction'] not in ['wurzelbund','glutpakt','rift'] and record['role'] not in ['worker','core']:continue
|
||||||
|
with bpy.data.libraries.load(record['blend_path'],link=False) as (frm,to):to.scenes=frm.scenes
|
||||||
|
objects=[o for scene in to.scenes for o in scene.objects if o.type=='MESH']
|
||||||
|
add_offline_materials(objects,p)
|
||||||
|
# Keep source image references relative to the .blend location.
|
||||||
|
bpy.data.libraries.write(record['blend_path'],set(to.scenes),path_remap='RELATIVE_ALL',fake_user=True,compress=True)
|
||||||
|
record['offline_sculpt_parts']=sum(bool(o.get('sculpt_surface')) for o in objects)
|
||||||
|
record['offline_render_triangles']=0
|
||||||
|
for obj in objects:
|
||||||
|
obj.data.calc_loop_triangles();record['offline_render_triangles']+=len(obj.data.loop_triangles)*(16 if obj.get('sculpt_surface') else 1)
|
||||||
|
path.write_text(json.dumps(record,ensure_ascii=False,indent=2)+'\n')
|
||||||
|
for scene in to.scenes:bpy.data.scenes.remove(scene)
|
||||||
|
for obj in objects:
|
||||||
|
if not obj.users:bpy.data.objects.remove(obj)
|
||||||
|
from pipeline import write_manifest
|
||||||
|
write_manifest()
|
||||||
|
print('BLENDER_PBR_SOURCE_PASS: 33 independent scenes with editable high-resolution geometry and material nodes.')
|
||||||
|
|
||||||
|
# Environment source keeps its original 2586 independently editable objects.
|
||||||
|
map_path=p/'assets-source/phase10/verdant-rift.blend'
|
||||||
|
with bpy.data.libraries.load(str(map_path),link=False) as (frm,to):to.scenes=frm.scenes
|
||||||
|
materials={m for scene in to.scenes for obj in scene.objects if obj.type=='MESH' for m in obj.data.materials if m}
|
||||||
|
for mat in materials:
|
||||||
|
if mat.get('phase10_pbr') or not any(k in mat.name for k in ['limestone','cliff','paving']):continue
|
||||||
|
nodes=mat.node_tree.nodes;links=mat.node_tree.links;bsdf=nodes.get('Principled BSDF')
|
||||||
|
texture=nodes.new('ShaderNodeTexImage');texture.image=bpy.data.images.load(str(p/'assets/materials/phase10/limestone-albedo.png'),check_existing=True);texture.projection='BOX';texture.projection_blend=.3
|
||||||
|
coord=nodes.new('ShaderNodeTexCoord');links.new(coord.outputs['Object'],texture.inputs['Vector'])
|
||||||
|
bump=nodes.new('ShaderNodeBump');bump.inputs['Strength'].default_value=.3;bump.inputs['Distance'].default_value=.04
|
||||||
|
links.new(texture.outputs['Color'],bump.inputs['Height']);links.new(bump.outputs['Normal'],bsdf.inputs['Normal'])
|
||||||
|
mat['phase10_pbr']=True
|
||||||
|
bpy.data.libraries.write(str(map_path),set(to.scenes),path_remap='RELATIVE_ALL',fake_user=True,compress=True)
|
||||||
|
print('ENVIRONMENT_PBR_SOURCE_PASS')
|
||||||
22
assets-source/phase10/material-provenance.json
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"method": "Built-in image_gen (stone); Blender procedural render/bake (oak).",
|
||||||
|
"stone": {
|
||||||
|
"file": "assets/materials/phase10/limestone-albedo.png",
|
||||||
|
"width": 1254,
|
||||||
|
"height": 1254,
|
||||||
|
"note": "Tool returned 1254px despite the prompt requesting 4096px. This file is not advertised as 4K."
|
||||||
|
},
|
||||||
|
"oak": {
|
||||||
|
"files": [
|
||||||
|
"assets/materials/phase10/oak-albedo-4k.png",
|
||||||
|
"assets/materials/phase10/oak-height-4k.png"
|
||||||
|
],
|
||||||
|
"width": 4096,
|
||||||
|
"height": 4096,
|
||||||
|
"source": "assets-source/phase10/bake_bark.py",
|
||||||
|
"note": "Native Blender renders of a periodic 4D-noise material. No bitmap enlargement. 16-bit intermediates retained in assets-source/phase10; 8-bit runtime maps use GPU compression and mipmaps."
|
||||||
|
},
|
||||||
|
"unused_attempt": {
|
||||||
|
"result": "Image generation service returned usage_limit_reached; no generated bark image used."
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
assets-source/phase10/oak-albedo-4k-16bit.png
Normal file
|
After Width: | Height: | Size: 75 MiB |
BIN
assets-source/phase10/oak-height-4k-16bit.png
Normal file
|
After Width: | Height: | Size: 79 MiB |
BIN
assets-source/phase10/oak-material-4k.blend
Normal file
BIN
assets-source/phase10/revisions/before-graphics.tar.gz
Normal file
BIN
assets-source/phase10/verdant-rift.blend
Normal file
32
assets-source/phase11/bake_cutwood.py
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
"""Native 4K PBR maps from an authored annual-ring surface; no bitmap upscaling.
|
||||||
|
Blender writes portable RGB PNGs. Colour is explicitly encoded as sRGB; the
|
||||||
|
normal and roughness images contain linear data. All maps share the same UVs.
|
||||||
|
"""
|
||||||
|
from pathlib import Path
|
||||||
|
import bpy,numpy as np,json
|
||||||
|
P=Path(__file__).resolve().parents[2];out=P/'assets/materials/phase11';out.mkdir(exist_ok=True)
|
||||||
|
N=4096
|
||||||
|
axis=np.linspace(-1,1,N,dtype=np.float32);x=axis[None,:];y=axis[:,None]
|
||||||
|
r=np.sqrt(x*x+y*y);a=np.arctan2(y,x)
|
||||||
|
phase=r*12+.13*np.sin(a*3+r*9)+.07*np.sin(a*7-r*8)
|
||||||
|
edge=np.abs(np.sin(phase*np.pi));line=np.clip((.27-edge)/.15,0,1);line=line*line*(3-2*line)
|
||||||
|
fibres=np.sin(a*113+r*38)*np.sin(r*290+a*5)
|
||||||
|
split=np.zeros((N,N),dtype=np.float32)
|
||||||
|
for k in range(3):
|
||||||
|
ca=.36+k*2.12;d=np.abs(np.arctan2(np.sin(a-ca-.012*np.sin(r*24)),np.cos(a-ca-.012*np.sin(r*24))))
|
||||||
|
split=np.maximum(split,np.clip((.014-d)/.011,0,1)*np.clip((r-(.48+.1*k))/(.91-.48-.1*k),0,1))
|
||||||
|
height=.54-.17*line-.35*split+.018*fibres
|
||||||
|
rgba=np.ones((N,N,4),dtype=np.float32)
|
||||||
|
def save(name,data,srgb=False):
|
||||||
|
if data.ndim==2:data=data[:,:,None]
|
||||||
|
if srgb:data=np.where(data<=.0031308,12.92*data,1.055*np.maximum(data,0)**(1/2.4)-.055)
|
||||||
|
rgba[:,:,:3]=np.clip(data,0,1)
|
||||||
|
im=bpy.data.images.new(name,width=N,height=N,alpha=False,float_buffer=False)
|
||||||
|
im.colorspace_settings.name='Non-Color';im.pixels.foreach_set(rgba.ravel());im.filepath_raw=str(out/(name+'.png'));im.file_format='PNG';im.save();bpy.data.images.remove(im)
|
||||||
|
colour=(1.2-.64*line)*(1+.07*fibres)*(1-.8*split)
|
||||||
|
save('ringborke-cut-albedo-4k',colour[:,:,None]*np.array([.265,.147,.066],dtype=np.float32),True)
|
||||||
|
gy,gx=np.gradient(height);normal=np.dstack((-gx*42,-gy*42,np.ones_like(height)));normal/=np.linalg.norm(normal,axis=2)[:,:,None]
|
||||||
|
save('ringborke-cut-normal-4k',normal*.5+.5)
|
||||||
|
save('ringborke-cut-roughness-4k',.78+.14*line+.06*split)
|
||||||
|
(P/'assets-source/phase11/material-provenance.json').write_text(json.dumps({'method':'Original analytic Blender/Python surface bake','size':[N,N],'upscaled':False,'albedo_encoding':'sRGB','normal_encoding':'linear, tangent space +Y','roughness_encoding':'linear','generator':'assets-source/phase11/bake_cutwood.py','inputs':'Annual ring field, warped fibres and radial growth checks; no external images'},indent=2)+'\n')
|
||||||
|
print('CUTWOOD_BAKE_PASS: three native 4096px material maps',flush=True)
|
||||||
340
assets-source/phase11/build_ringborke.py
Normal file
|
|
@ -0,0 +1,340 @@
|
||||||
|
"""Ringborke: authored surfaces, weighted skeleton and portable animation clips.
|
||||||
|
Blender Z up, -Y forward. Run in a fresh background Blender process.
|
||||||
|
The existing Phase 4 sources remain intact. No downloaded assets are used.
|
||||||
|
"""
|
||||||
|
from pathlib import Path
|
||||||
|
import bpy, bmesh, math, random, json, hashlib
|
||||||
|
from mathutils import Vector, Quaternion
|
||||||
|
from math import sin, cos, pi
|
||||||
|
P=Path(__file__).resolve().parents[2]
|
||||||
|
OUT=P/'assets/characters'; OUT.mkdir(exist_ok=True)
|
||||||
|
SRC=Path(__file__).resolve().parent
|
||||||
|
random.seed(41)
|
||||||
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||||
|
S=.82
|
||||||
|
def coord(v):
|
||||||
|
return Vector((v[0]*1.06,v[1],v[2]*.82 if v[2]<.8 else v[2]-.144))*S
|
||||||
|
parts=[]
|
||||||
|
|
||||||
|
def material(name, color, rough=.85, bark=False, emission=False):
|
||||||
|
m=bpy.data.materials.new('RB_'+name);m.diffuse_color=(*color,1);m.use_nodes=True
|
||||||
|
n=m.node_tree.nodes;l=m.node_tree.links;b=n.get('Principled BSDF');b.inputs['Base Color'].default_value=(*color,1);b.inputs['Roughness'].default_value=rough
|
||||||
|
if emission:
|
||||||
|
b.inputs['Emission Color'].default_value=(*color,1);b.inputs['Emission Strength'].default_value=.8
|
||||||
|
else:
|
||||||
|
v=n.new('ShaderNodeVertexColor');v.layer_name='SurfaceColor';l.new(v.outputs['Color'],b.inputs['Base Color'])
|
||||||
|
if name=='Cutwood':
|
||||||
|
for suffix,target in [('albedo','Base Color'),('roughness','Roughness'),('normal','Normal')]:
|
||||||
|
tex=n.new('ShaderNodeTexImage');tex.image=bpy.data.images.load(str(P/('assets/materials/phase11/ringborke-cut-'+suffix+'-4k.png')),check_existing=True)
|
||||||
|
tex.image.colorspace_settings.name='sRGB' if suffix=='albedo' else 'Non-Color'
|
||||||
|
if suffix=='normal':
|
||||||
|
normal=n.new('ShaderNodeNormalMap');normal.inputs['Strength'].default_value=.6;l.new(tex.outputs['Color'],normal.inputs['Color']);l.new(normal.outputs['Normal'],b.inputs[target])
|
||||||
|
else:l.new(tex.outputs['Color'],b.inputs[target])
|
||||||
|
if bark:
|
||||||
|
t=n.new('ShaderNodeTexImage');t.image=bpy.data.images.load(str(P/'assets/materials/phase10/oak-height-4k.png'),check_existing=True);t.image.colorspace_settings.name='Non-Color'
|
||||||
|
bump=n.new('ShaderNodeBump');bump.inputs['Strength'].default_value=.4;bump.inputs['Distance'].default_value=.035;l.new(t.outputs['Color'],bump.inputs['Height']);l.new(bump.outputs['Normal'],b.inputs['Normal'])
|
||||||
|
return m
|
||||||
|
M={
|
||||||
|
'bark':material('Bark',(.18,.105,.055),bark=True),
|
||||||
|
'heart':material('Heartwood',(.38,.23,.115),bark=True),
|
||||||
|
'cut':material('Cutwood',(.55,.35,.17)),
|
||||||
|
'dark':material('Crevice',(.052,.035,.018)),
|
||||||
|
'moss':material('Moss',(.18,.28,.052)),
|
||||||
|
'leaf':material('Leaf',(.31,.40,.085),.72),
|
||||||
|
'flower':material('Petal',(.65,.26,.36),.65),
|
||||||
|
'eye':material('Saplight',(.35,.85,.06),.5,emission=True)}
|
||||||
|
|
||||||
|
def mesh(name, verts, faces, kind, bone, uv=None, colors=None, weights=None, smooth=True):
|
||||||
|
me=bpy.data.meshes.new(name);me.from_pydata([coord(v) for v in verts],[],faces);me.update()
|
||||||
|
ob=bpy.data.objects.new(name,me);bpy.context.collection.objects.link(ob);me.materials.append(M[kind]);parts.append(ob)
|
||||||
|
for f in me.polygons:f.use_smooth=smooth
|
||||||
|
tex=me.uv_layers.new(name='UVMap');col=me.color_attributes.new(name='SurfaceColor',type='FLOAT_COLOR',domain='CORNER')
|
||||||
|
base=M[kind].diffuse_color[:3]
|
||||||
|
for f in me.polygons:
|
||||||
|
for li in f.loop_indices:
|
||||||
|
i=me.loops[li].vertex_index;v=verts[i]
|
||||||
|
tex.data[li].uv=uv[i] if uv else (v[0]*.7+v[1]*.3,v[2]*.8)
|
||||||
|
fac=.89+.11*sin(v[0]*19+v[2]*8)+.035*cos(v[1]*71+v[2]*32)
|
||||||
|
c=colors[i] if colors else tuple(max(.004,x*fac) for x in base)
|
||||||
|
col.data[li].color=(*c,1)
|
||||||
|
groups={}
|
||||||
|
for i in range(len(verts)):
|
||||||
|
ws=weights[i] if weights else {bone:1.0}
|
||||||
|
total=sum(ws.values())
|
||||||
|
for key,w in ws.items():
|
||||||
|
if key not in groups:groups[key]=ob.vertex_groups.new(name=key)
|
||||||
|
groups[key].add([i],w/total,'REPLACE')
|
||||||
|
return ob
|
||||||
|
|
||||||
|
def mixweights(t,bones):
|
||||||
|
if len(bones)==1:return {bones[0]:1}
|
||||||
|
t=max(0,min(.999999,t))*(len(bones)-1);i=int(t);w=t-i
|
||||||
|
return {bones[i]:1-w,bones[i+1]:w}
|
||||||
|
|
||||||
|
def tube(name,points,radii,kind,bones,sides=16,steps=32,flatten=1,groove=.08):
|
||||||
|
"""Closed organic sweep, smooth interpolated centerline, longitudinal grain relief."""
|
||||||
|
points=[Vector(x) for x in points];vs=[];uv=[];ws=[];loops=[];fs=[]
|
||||||
|
def point(t):
|
||||||
|
a=min(len(points)-2,int(t*(len(points)-1)));u=t*(len(points)-1)-a
|
||||||
|
p0=points[max(a-1,0)];p1=points[a];p2=points[a+1];p3=points[min(a+2,len(points)-1)]
|
||||||
|
return .5*((2*p1)+(-p0+p2)*u+(2*p0-5*p1+4*p2-p3)*u*u+(-p0+3*p1-3*p2+p3)*u*u*u),radii[a]*(1-u)+radii[a+1]*u
|
||||||
|
for j in range(steps+1):
|
||||||
|
t=j/steps;p,r=point(t);tan=(point(min(1,t+.002))[0]-point(max(0,t-.002))[0]).normalized();u=tan.cross(Vector((0,1,0))).normalized();v=tan.cross(u)
|
||||||
|
loop=[]
|
||||||
|
for k in range(sides):
|
||||||
|
a=2*pi*k/sides;mod=1+groove*(sin(a*7+t*3)+.4*sin(a*13-t*9))
|
||||||
|
vs.append(p+r*mod*(u*cos(a)+v*sin(a)*flatten));uv.append((k/sides,t*1.7));ws.append(mixweights(t,bones));loop.append(len(vs)-1)
|
||||||
|
loops.append(loop)
|
||||||
|
for a,b in zip(loops,loops[1:]):
|
||||||
|
for k in range(sides):q=(k+1)%sides;fs.append((a[k],a[q],b[q],b[k]))
|
||||||
|
fs.extend([tuple(reversed(loops[0])),tuple(loops[-1])])
|
||||||
|
return mesh(name,vs,fs,kind,bones[0],uv,weights=ws)
|
||||||
|
|
||||||
|
def ell(name,c,r,kind,bone,segments=20,rings=10):
|
||||||
|
vs=[(c[0],c[1],c[2]+r[2])];fs=[]
|
||||||
|
for j in range(1,rings):
|
||||||
|
th=pi*j/rings
|
||||||
|
for k in range(segments):
|
||||||
|
a=2*pi*k/segments;f=1+.045*sin(a*5+j*1.7)
|
||||||
|
vs.append((c[0]+r[0]*sin(th)*cos(a)*f,c[1]+r[1]*sin(th)*sin(a)*f,c[2]+r[2]*cos(th)))
|
||||||
|
vs.append((c[0],c[1],c[2]-r[2]));end=len(vs)-1
|
||||||
|
for k in range(segments):fs.append((0,1+k,1+(k+1)%segments))
|
||||||
|
for j in range(rings-2):
|
||||||
|
for k in range(segments):a=1+j*segments+k;b=1+j*segments+(k+1)%segments;fs.append((a,a+segments,b+segments,b))
|
||||||
|
for k in range(segments):fs.append((end,end-1-k,end-1-(k+1)%segments))
|
||||||
|
return mesh(name,vs,fs,kind,bone)
|
||||||
|
|
||||||
|
def shield(name,c,rx,rz,depth,bone,angle=0):
|
||||||
|
# Thick irregular cut log: no raised concentric torus ornaments.
|
||||||
|
n=96;radial=26;vs=[];uv=[];cols=[];fs=[]
|
||||||
|
def world(x,y,z):return(c[0]+x*cos(angle)-z*sin(angle),c[1]+y,c[2]+x*sin(angle)+z*cos(angle))
|
||||||
|
vs.append(world(0,-depth*.54,0));uv.append((.5,.5));cols.append((.42,.27,.135))
|
||||||
|
for j in range(1,radial+1):
|
||||||
|
r=j/radial
|
||||||
|
for k in range(n):
|
||||||
|
a=2*pi*k/n;edge=1+.038*sin(5*a+1.1)+.028*sin(9*a-.4)
|
||||||
|
rings=r*12+.10*sin(3*a+r*7)+.07*sin(7*a-r*9)
|
||||||
|
grain=abs(sin(rings*pi));groove=max(0,(.22-grain)/.22)
|
||||||
|
# Four narrow radial checks, restricted to the edge of the heartwood.
|
||||||
|
crack=max(max(0,1-abs(math.atan2(sin(a-q),cos(a-q)))/.025) for q in [.35,2.1,3.9,5.6])*max(0,(r-.53)/.47)
|
||||||
|
y=-depth*.55+.018*r*r+.008*groove+.028*crack
|
||||||
|
vs.append(world(rx*r*edge*cos(a),y,rz*r*edge*sin(a)));uv.append((.5+.5*r*cos(a),.5+.5*r*sin(a)))
|
||||||
|
val=(.91-.22*groove-.60*crack)*(1-.12*r)
|
||||||
|
cols.append(tuple(v*val for v in (.53,.35,.185)))
|
||||||
|
for k in range(n):fs.append((0,1+(k+1)%n,1+k))
|
||||||
|
for j in range(radial-1):
|
||||||
|
for k in range(n):a=1+j*n+k;b=1+j*n+(k+1)%n;fs.append((a,b,b+n,a+n))
|
||||||
|
front=len(vs)-n;back=len(vs)
|
||||||
|
for k in range(n):
|
||||||
|
a=2*pi*k/n;e=1+.038*sin(5*a+1.1)+.028*sin(9*a-.4);vs.append(world(rx*.95*e*cos(a),depth*.5,rz*.95*e*sin(a)));uv.append((k/n,0));cols.append((.13,.08,.038))
|
||||||
|
for k in range(n):q=(k+1)%n;fs.append((front+k,front+q,back+q,back+k))
|
||||||
|
fs.append(tuple(back+k for k in reversed(range(n))))
|
||||||
|
ob=mesh(name,vs,[tuple(reversed(f)) for f in fs],'cut',bone,uv,cols)
|
||||||
|
# Continuous chipped bark mantle around the log; longitudinal ridges, not beads.
|
||||||
|
vv=[];ff=[];tt=[]
|
||||||
|
profile=[(.94,-.56),(1.025,-.49),(1.07,-.18),(1.045,.36),(.94,.63),(.87,.42)]
|
||||||
|
for j,(radius,y) in enumerate(profile):
|
||||||
|
for k in range(n):
|
||||||
|
a=2*pi*k/n;edge=1+.038*sin(5*a+1.1)+.028*sin(9*a-.4)
|
||||||
|
ridge=1+.028*sin(a*19+.4*j)+.015*sin(a*31-.2*j)
|
||||||
|
vv.append(world(rx*radius*edge*ridge*cos(a),depth*y+.008*sin(a*13),rz*radius*edge*ridge*sin(a)))
|
||||||
|
tt.append((k/n*2,j/5))
|
||||||
|
for j in range(len(profile)):
|
||||||
|
for k in range(n):
|
||||||
|
q=(k+1)%n;h=(j+1)%len(profile);ff.append((j*n+k,j*n+q,h*n+q,h*n+k))
|
||||||
|
mesh(name+'_BarkMantle',vv,[tuple(reversed(f)) for f in ff],'bark',bone,tt)
|
||||||
|
return ob
|
||||||
|
|
||||||
|
def leaf(name,start,end,width,bone,kind='leaf'):
|
||||||
|
a=Vector(start);b=Vector(end);d=b-a;u=d.cross(Vector((0,1,0))).normalized();v=d.normalized().cross(u);vs=[];fs=[]
|
||||||
|
for side in [-1,1]:
|
||||||
|
for j in range(9):
|
||||||
|
t=j/8;w=width*sin(pi*t)**.7
|
||||||
|
for k in [-1,0,1]:vs.append(a+d*t+u*w*k+v*(sin(pi*t)*(.025+.028*(1-abs(k))))+Vector((0,side*.003,0)))
|
||||||
|
for layer in range(2):
|
||||||
|
for j in range(8):
|
||||||
|
for k in range(2):
|
||||||
|
i=layer*27+j*3+k;quad=(i,i+1,i+4,i+3);fs.append(quad if layer else tuple(reversed(quad)))
|
||||||
|
for j in range(8):
|
||||||
|
i=j*3;fs.extend([(i,i+3,i+30,i+27),(i+2,i+29,i+32,i+5)])
|
||||||
|
fs.extend([(0,27,28,29,2,1),(24,25,26,53,52,51)])
|
||||||
|
return mesh(name,vs,fs,kind,bone)
|
||||||
|
|
||||||
|
# Thick connected trunk and flowing, softly weighted fibres around moving joints.
|
||||||
|
tube('Trunk',[(0,.08,.70),(-.02,.08,1.03),(0,.05,1.42),(0,.06,1.69)],[.28,.36,.46,.30],'bark',['Pelvis','Spine','Chest'],32,52,flatten=.77)
|
||||||
|
for k in range(11):
|
||||||
|
a=k*2*pi/11
|
||||||
|
tube('Trunk_Rib_%02d'%k,[(.23*cos(a),.08+.22*sin(a),.73),(.30*cos(a+.12),.03+.29*sin(a+.12),1.08),(.41*cos(a-.10),.03+.31*sin(a-.10),1.42),(.22*cos(a),.07+.19*sin(a),1.73)],[.055,.075,.09,.038],'heart' if k%3==0 else 'bark',['Pelvis','Spine','Chest'],10,30,groove=.14)
|
||||||
|
# Chest shield is recessed between shoulders; the face remains visible above it.
|
||||||
|
shield('Breast_Heart',(.05,-.365,1.28),.33,.38,.14,'Chest',-.14)
|
||||||
|
# Neck and carved angular mask, eyebrows and an actual dark recessed face.
|
||||||
|
tube('Neck',[(0,.07,1.58),(0,-.02,1.86),(.01,-.06,2.05)],[.21,.24,.19],'bark',['Chest','Head'],20,24,flatten=.8)
|
||||||
|
ell('Face_Recess',(0,-.23,1.90),(.22,.095,.18),'dark','Head')
|
||||||
|
for s,side in [(-1,'L'),(1,'R')]:
|
||||||
|
tube('Brow_'+side,[(s*.025,-.332,1.96),(s*.125,-.329,2.025),(s*.24,-.23,2.035)],[.068,.064,.045],'heart',['Head'],8,14,flatten=.7)
|
||||||
|
tube('Cheek_'+side,[(s*.22,-.245,1.98),(s*.21,-.318,1.88),(s*.12,-.31,1.79)],[.065,.045,.035],'bark',['Head'],8,14)
|
||||||
|
ell('Eye_'+side,(s*.105,-.326,1.934),(.052,.019,.018),'eye','Head',16,8)
|
||||||
|
for s,side in [(-1,'L'),(1,'R')]:
|
||||||
|
vs=[(s*.01,-.34,1.98),(s*.115,-.39,2.04),(s*.25,-.25,2.065),(s*.235,-.30,1.975),(s*.09,-.36,1.967),
|
||||||
|
(s*.01,-.29,1.98),(s*.115,-.30,2.04),(s*.25,-.18,2.065),(s*.235,-.23,1.975),(s*.09,-.28,1.967)]
|
||||||
|
fs=[(0,1,2,3,4),(9,8,7,6,5)]+[(k,(k+1)%5,(k+1)%5+5,k+5) for k in range(5)]
|
||||||
|
mesh('CarvedBrow_'+side,vs,fs if s>0 else [tuple(reversed(f)) for f in fs],'bark','Head',smooth=False)
|
||||||
|
tube('Nose_Ridge',[(0,-.335,2.01),(0,-.38,1.91),(0,-.35,1.83)],[.038,.051,.028],'heart',['Head'],8,14)
|
||||||
|
# Crown: tapered broken wood spires and a large asymmetrical rear stump.
|
||||||
|
shield('Back_Crown',(-.10,.07,1.95),.26,.29,.28,'Chest',-.30)
|
||||||
|
for k in range(5):
|
||||||
|
a=k*pi*.4
|
||||||
|
tube('Crown_Branch_%d'%k,[(.16*cos(a),.11+.17*sin(a),1.95),(.28*cos(a),.13+.22*sin(a),2.16+(.06 if k%2 else 0)),(.38*cos(a),.16+.27*sin(a),2.29+(k%2)*.11)],[.075,.045,.009],'bark',['Head'],10,16)
|
||||||
|
for s,side in [(-1,'L'),(1,'R')]:
|
||||||
|
arm=['UpperArm.'+side,'Forearm.'+side,'Hand.'+side]
|
||||||
|
tube('Arm_Fibres_'+side,[(s*.39,.04,1.65),(s*.68,.035,1.55),(s*.85,-.045,1.20),(s*.87,-.20,.85),(s*.89,-.24,.68)],[.22,.27,.21,.21,.18],'bark',arm,24,44,flatten=.86)
|
||||||
|
for k in range(5):
|
||||||
|
a=2*pi*k/5
|
||||||
|
tube('Arm_Rib_'+side+str(k),[(s*.50+.15*cos(a),.04+.15*sin(a),1.62),(s*.78+.16*cos(a),-.02+.16*sin(a),1.28),(s*.88+.15*cos(a),-.17+.15*sin(a),.88)],[.055,.048,.035],'heart' if k==1 else 'bark',arm[:2],9,24)
|
||||||
|
shield('Shoulder_'+side,(s*.63,-.17,1.60),.36 if s<0 else .30,.39 if s<0 else .33,.20,arm[0],s*-.16)
|
||||||
|
shield('Forearm_Plate_'+side,(s*.91,-.355,.97),.19,.25,.15,arm[1],s*.16)
|
||||||
|
ell('Palm_'+side,(s*.88,-.25,.70),(.22,.17,.16),'bark',arm[2])
|
||||||
|
for finger in range(3):
|
||||||
|
x=s*.88+(finger-1)*.12
|
||||||
|
tube('Finger_'+side+str(finger),[(x,-.30,.69),(x+s*.035,-.37,.55),(x+s*.03,-.40,.46)],[.065,.052,.027],'heart',[arm[2],'Finger%d.%s'%(finger,side)],10,16)
|
||||||
|
tube('Thumb_'+side,[(s*.76,-.21,.73),(s*.68,-.35,.65),(s*.71,-.42,.60)],[.075,.053,.026],'bark',[arm[2],'Thumb.'+side],10,16)
|
||||||
|
leg=['Thigh.'+side,'Shin.'+side,'Foot.'+side]
|
||||||
|
tube('Leg_'+side,[(s*.24,.07,.86),(s*.34,.035,.64),(s*.42,-.025,.38),(s*.43,-.025,.16)],[.22,.23,.18,.15],'bark',leg,24,42,flatten=.87)
|
||||||
|
for k in range(4):
|
||||||
|
a=2*pi*k/4
|
||||||
|
tube('Leg_Ridge_'+side+str(k),[(s*.25+.14*cos(a),.05+.13*sin(a),.84),(s*.40+.16*cos(a),.02+.14*sin(a),.43),(s*.46+.14*cos(a),-.04+.12*sin(a),.14)],[.06,.05,.035],'heart' if k==3 else 'bark',leg,9,24)
|
||||||
|
shield('Knee_'+side,(s*.39,-.19,.43),.14,.16,.08,leg[1],s*.22)
|
||||||
|
for toe in range(3):
|
||||||
|
x=s*.43+(toe-1)*.13
|
||||||
|
tube('Toe_'+side+str(toe),[(s*.43,-.03,.19),(x,-.20,.10),(x+(toe-1)*.03,-.40,.045)],[.095,.081,.020],'heart',[leg[2],'Toe.'+side],12,22)
|
||||||
|
tube('Heel_'+side,[(s*.43,.035,.20),(s*.49,.18,.11),(s*.55,.23,.04)],[.095,.064,.015],'bark',[leg[2]],10,14)
|
||||||
|
# Distinct layered back plates: tangential, following the torso rather than floating discs.
|
||||||
|
for row in range(4):
|
||||||
|
for k in range(7):
|
||||||
|
a=.1+k*pi/6;z=.90+row*.20;r=.31+.07*sin(row/3*pi)
|
||||||
|
pts=[(r*.84*cos(a),.08+r*.72*sin(a),z-.13),((r+.045)*cos(a),.08+(r+.045)*.75*sin(a),z),((r-.035)*cos(a),.08+(r-.035)*.75*sin(a),z+.19)]
|
||||||
|
tube('Back_Plate_%d_%d'%(row,k),pts,[.05,.105,.015],'bark',['Spine' if row<2 else 'Chest'],8,16,flatten=.42,groove=.12)
|
||||||
|
# Moss grows in upper seams. Small leaf clusters make porous growth, never a green body ball.
|
||||||
|
for j in range(190):
|
||||||
|
zone=j%3
|
||||||
|
if zone==0:
|
||||||
|
s=-1 if j%2 else 1;c=(s*(.45+random.random()*.25),-.15+random.random()*.25,(1.97 if s<0 else 1.92)+random.random()*.055);bone='UpperArm.'+('L' if s<0 else 'R')
|
||||||
|
elif zone==1:
|
||||||
|
a=random.uniform(.15,pi-.15);c=(.34*cos(a),.1+.28*sin(a),random.uniform(1.06,1.65));bone='Chest'
|
||||||
|
else:c=(random.uniform(-.22,.22),random.uniform(-.03,.25),2.225+random.random()*.04);bone='Head'
|
||||||
|
ell('Moss_%03d'%j,c,(.045+random.random()*.025,.036,.019),'moss',bone,8,4)
|
||||||
|
# Small overlapping foliage breaks up the shoulder and torso seams.
|
||||||
|
for j in range(84):
|
||||||
|
s=-1 if j%2 else 1
|
||||||
|
if j<52:
|
||||||
|
x=s*random.uniform(.40,.82);y=random.uniform(-.23,.13);z=1.96 if s<0 else 1.91;bone='UpperArm.'+('L' if s<0 else 'R')
|
||||||
|
else:
|
||||||
|
x=s*random.uniform(.29,.39);y=-.27;z=random.uniform(.98,1.56);bone='Spine' if z<1.2 else 'Chest'
|
||||||
|
leaf('SeamLeaf_%02d'%j,(x,y,z),(x+s*.065,y-.045,z+random.uniform(.035,.095)),random.uniform(.025,.048),bone,'moss')
|
||||||
|
for j in range(32):
|
||||||
|
s=-1 if j%2 else 1;bone='Head' if j<14 else 'Chest';z=2.15 if j<14 else 1.49;x=s*(.19+random.random()*.14);y=.11 if j<14 else .24
|
||||||
|
leaf('Sprout_%02d'%j,(x,y,z),(x+s*random.uniform(.08,.19),y-.07,z+random.uniform(.10,.24)),random.uniform(.03,.055),bone)
|
||||||
|
for j,(x,y,z,bone) in enumerate([(-.46,-.21,1.93,'UpperArm.L'),(.54,-.23,1.83,'UpperArm.R'),(-.23,-.28,1.41,'Chest'),(.26,-.29,1.57,'Chest')]):
|
||||||
|
for k in range(5):
|
||||||
|
a=2*pi*k/5;leaf('Blossom_%d_%d'%(j,k),(x+.007*cos(a),y,z+.007*sin(a)),(x+.045*cos(a),y-.015,z+.045*sin(a)),.014,bone,'flower')
|
||||||
|
ell('BlossomHeart'+str(j),(x,y-.014,z),(.009,.009,.009),'cut',bone,8,4)
|
||||||
|
|
||||||
|
# Weld closed leaf tips and consistently orient every disconnected solid before skinning.
|
||||||
|
# BMesh preserves UV corners, colours and deform groups during this cleanup.
|
||||||
|
for ob in parts:
|
||||||
|
bm=bmesh.new();bm.from_mesh(ob.data)
|
||||||
|
bmesh.ops.remove_doubles(bm,verts=list(bm.verts),dist=.0000005)
|
||||||
|
bmesh.ops.triangulate(bm,faces=list(bm.faces))
|
||||||
|
bmesh.ops.dissolve_degenerate(bm,edges=list(bm.edges),dist=.0000001)
|
||||||
|
bmesh.ops.recalc_face_normals(bm,faces=list(bm.faces))
|
||||||
|
if bm.calc_volume(signed=True)<0:bmesh.ops.reverse_faces(bm,faces=list(bm.faces))
|
||||||
|
bm.to_mesh(ob.data);bm.free();ob.data.update()
|
||||||
|
|
||||||
|
# Skeleton, anatomically placed hinges. Flexible fibre meshes blend adjacent joints.
|
||||||
|
bpy.ops.object.armature_add(enter_editmode=True);rig=bpy.context.object;rig.name='RingborkeRig';ed=rig.data.edit_bones;ed.remove(ed[0]);defs={}
|
||||||
|
def bone(name,head,tail,parent=None):
|
||||||
|
b=ed.new(name);b.head=coord(head);b.tail=coord(tail)
|
||||||
|
if parent:b.parent=ed[parent]
|
||||||
|
defs[name]=(head,tail,parent)
|
||||||
|
bone('Root',(0,0,0),(0,0,.2));bone('Pelvis',(0,.06,.72),(0,.05,1.02),'Root');bone('Spine',(0,.05,1.02),(0,.05,1.38),'Pelvis');bone('Chest',(0,.05,1.38),(0,.02,1.72),'Spine');bone('Head',(0,.02,1.72),(0,-.02,2.15),'Chest')
|
||||||
|
for s,side in [(-1,'L'),(1,'R')]:
|
||||||
|
bone('UpperArm.'+side,(s*.47,.04,1.63),(s*.84,-.04,1.19),'Chest');bone('Forearm.'+side,(s*.84,-.04,1.19),(s*.88,-.24,.73),'UpperArm.'+side);bone('Hand.'+side,(s*.88,-.24,.73),(s*.88,-.35,.59),'Forearm.'+side)
|
||||||
|
for f in range(3):
|
||||||
|
x=s*.88+(f-1)*.12;bone('Finger%d.%s'%(f,side),(x,-.32,.66),(x+s*.03,-.40,.46),'Hand.'+side)
|
||||||
|
bone('Thumb.'+side,(s*.76,-.21,.73),(s*.71,-.42,.60),'Hand.'+side)
|
||||||
|
bone('Thigh.'+side,(s*.25,.06,.79),(s*.41,-.02,.42),'Pelvis');bone('Shin.'+side,(s*.41,-.02,.42),(s*.43,-.02,.15),'Thigh.'+side);bone('Foot.'+side,(s*.43,-.02,.15),(s*.43,-.25,.07),'Shin.'+side);bone('Toe.'+side,(s*.43,-.25,.07),(s*.43,-.43,.045),'Foot.'+side)
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
for ob in parts:
|
||||||
|
mod=ob.modifiers.new('Weighted living wood','ARMATURE');mod.object=rig;ob.parent=rig
|
||||||
|
# Bake explicit character poses into editable actions at 30fps. Global axes are
|
||||||
|
# converted to each bone's rest basis. Imported engine clips need no Python.
|
||||||
|
clips={'idle':(3.2,True),'start':(.4,False),'walk':(1.4,True),'stop':(.4,False),'attack_a':(1.25,False),'attack_b':(1.25,False),'hit':(.48,False),'ability':(1.8,False),'death':(2.1,False)}
|
||||||
|
def curve(t,keys):
|
||||||
|
for i in range(len(keys)-1):
|
||||||
|
a,x=keys[i];b,y=keys[i+1]
|
||||||
|
if t<=b:
|
||||||
|
u=max(0,(t-a)/(b-a));u=u*u*(3-2*u);return x*(1-u)+y*u
|
||||||
|
return keys[-1][1]
|
||||||
|
def animate(name,t,dur):
|
||||||
|
rotations={};locs={}
|
||||||
|
def rot(b,x=0,y=0,z=0):rotations[b]=(x,y,z)
|
||||||
|
if name=='idle':
|
||||||
|
q=sin(2*pi*t/dur);rot('Spine',.012*q);rot('Chest',-.018*q,0,.009*q);rot('Head',.016*q,0,-.015*q)
|
||||||
|
for s,side in [(-1,'L'),(1,'R')]:rot('Forearm.'+side,.013*q,0,s*.007*q)
|
||||||
|
elif name in ('walk','start','stop'):
|
||||||
|
fade=1 if name=='walk' else sin(pi*.5*t/dur) if name=='start' else cos(pi*.5*t/dur)
|
||||||
|
phase=t/1.4*2*pi;roll=sin(phase)*fade
|
||||||
|
rot('Pelvis',.015,0,.035*roll);rot('Spine',-.045*fade,0,-.05*roll);rot('Chest',.018*fade,0,.025*roll);rot('Head',.035*fade,0,-.02*roll)
|
||||||
|
for s,side in [(-1,'L'),(1,'R')]:
|
||||||
|
q=sin(phase+(0 if s<0 else pi));lift=max(0,q);rot('Thigh.'+side,.34*q*fade);rot('Shin.'+side,-.40*lift*fade);rot('Foot.'+side,(-.34*q+.40*lift)*fade);rot('Toe.'+side,.12*max(0,-q)*fade)
|
||||||
|
rot('UpperArm.'+side,-.13*q*fade,0,s*.02*fade);rot('Forearm.'+side,-.045*fade-.025*lift)
|
||||||
|
locs['Root']=(0,0,(-.012+.012*cos(phase*2))*fade)
|
||||||
|
elif name.startswith('attack'):
|
||||||
|
# Contact is at normalized attack progress 0.72, aligned by runtime to damage.
|
||||||
|
q=curve(t/dur,[(0,0),(.32,-.35),(.54,-.55),(.72,1),(.80,.85),(1,0)])
|
||||||
|
left=name=='attack_a';side='L' if left else 'R';other='R' if left else 'L';s=-1 if left else 1
|
||||||
|
rot('Spine',-.11*q,0,s*.09*q);rot('Chest',-.10*q,0,s*.14*q);rot('Head',.09*q,0,-s*.10*q)
|
||||||
|
rot('UpperArm.'+side,-.88*q,(-.14 if left else .08)*q,s*.13*q);rot('Forearm.'+side,-.20*q);rot('Hand.'+side,.16*q);rot('UpperArm.'+other,.11*q,0,-s*.10*q)
|
||||||
|
for f in range(3):rot('Finger%d.%s'%(f,side),-.25*max(0,q))
|
||||||
|
rot('Thigh.'+side,.06*q);rot('Shin.'+side,-.06*q);locs['Root']=(0,-.04*q,-.015*max(0,q))
|
||||||
|
elif name=='hit':
|
||||||
|
q=curve(t,[(0,0),(.08,1),(.22,.5),(.48,0)]);rot('Spine',.12*q,0,-.08*q);rot('Head',.13*q,0,.06*q);rot('Forearm.L',-.10*q);rot('Forearm.R',-.08*q)
|
||||||
|
elif name=='ability':
|
||||||
|
q=curve(t,[(0,0),(.35,.5),(.75,1),(1.15,.85),(1.8,0)]);rot('Spine',.08*q);rot('Chest',.10*q);rot('Head',-.15*q)
|
||||||
|
for s,side in [(-1,'L'),(1,'R')]:rot('UpperArm.'+side,-.22*q,0,s*.38*q);rot('Forearm.'+side,-.32*q);rot('Hand.'+side,.15*q)
|
||||||
|
elif name=='death':
|
||||||
|
q=curve(t,[(0,0),(.2,.05),(.7,.38),(1.30,1),(1.55,.93),(2.1,1)])
|
||||||
|
rot('Root',-.95*q,0,.13*q);locs['Root']=(0,-.15*q,-.12*q);rot('Pelvis',-.22*q);rot('Spine',-.24*q);rot('Head',-.20*q)
|
||||||
|
for s,side in [(-1,'L'),(1,'R')]:rot('UpperArm.'+side,-.32*q,0,s*.17*q);rot('Forearm.'+side,-.50*q);rot('Thigh.'+side,.26*q);rot('Shin.'+side,-.44*q)
|
||||||
|
for pb in rig.pose.bones:
|
||||||
|
pb.rotation_mode='QUATERNION';r=rotations.get(pb.name,(0,0,0));q=Quaternion((0,0,1),r[2])@Quaternion((0,1,0),r[1])@Quaternion((1,0,0),r[0]);basis=pb.bone.matrix_local.to_quaternion();pb.rotation_quaternion=basis.inverted()@q@basis
|
||||||
|
pb.location=pb.bone.matrix_local.to_3x3().inverted()@(Vector(locs.get(pb.name,(0,0,0)))*S)
|
||||||
|
for name,(dur,loop) in clips.items():
|
||||||
|
rig.animation_data_clear();action=bpy.data.actions.new(name);action.use_fake_user=True;rig.animation_data_create();rig.animation_data.action=action
|
||||||
|
for frame in range(round(dur*30)+1):
|
||||||
|
animate(name,frame/30,dur)
|
||||||
|
for pb in rig.pose.bones:pb.keyframe_insert('rotation_quaternion',frame=frame);pb.keyframe_insert('location',frame=frame)
|
||||||
|
action['loop']=loop
|
||||||
|
rig.animation_data_clear()
|
||||||
|
rig.animation_data_create()
|
||||||
|
rig.animation_data.action=bpy.data.actions['idle']
|
||||||
|
for pb in rig.pose.bones:pb.rotation_quaternion=Quaternion();pb.location=(0,0,0)
|
||||||
|
bpy.context.scene.render.fps=30;bpy.context.scene.frame_set(0)
|
||||||
|
# Save the editable source with separate parts and material nodes before export joining.
|
||||||
|
bpy.ops.wm.save_as_mainfile(filepath=str(SRC/'ringborke.blend'))
|
||||||
|
bpy.ops.file.make_paths_relative()
|
||||||
|
bpy.ops.wm.save_as_mainfile(filepath=str(SRC/'ringborke.blend'))
|
||||||
|
bpy.ops.object.select_all(action='DESELECT')
|
||||||
|
for ob in parts:ob.select_set(True)
|
||||||
|
bpy.context.view_layer.objects.active=parts[0];bpy.ops.object.join();body=bpy.context.object;body.name='RingborkeBody'
|
||||||
|
# One skinned mesh; material surfaces and bone weights remain separate and portable.
|
||||||
|
body.select_set(True);rig.select_set(True);bpy.context.view_layer.objects.active=rig
|
||||||
|
bpy.ops.export_scene.gltf(filepath=str(OUT/'ringborke.glb'),export_format='GLB',use_selection=True,export_animations=True,export_animation_mode='ACTIONS',export_force_sampling=True,export_frame_range=False,export_anim_slide_to_zero=True,export_skins=True,export_all_influences=False,export_def_bones=True,export_anim_single_armature=True,export_materials='EXPORT',export_yup=True)
|
||||||
|
raw=(OUT/'ringborke.glb').read_bytes();import struct
|
||||||
|
doc=json.loads(raw[20:20+struct.unpack_from('<I',raw,12)[0]])
|
||||||
|
triangles=sum(doc['accessors'][q['indices']]['count']//3 for m in doc['meshes'] for q in m['primitives'])
|
||||||
|
mins=[min(v.co[i] for v in body.data.vertices) for i in range(3)];maxs=[max(v.co[i] for v in body.data.vertices) for i in range(3)]
|
||||||
|
record={'id':'w04','role':'tank','resource_path':'res://assets/characters/ringborke.glb','triangles':triangles,'mesh_parts':1,'parts':['RingborkeBody'],'bounds_godot':{'min':[mins[0],mins[2],-maxs[1]],'max':[maxs[0],maxs[2],-mins[1]]},'skeletal':True,'bones':len(defs),'clips':list(clips),'sha256':hashlib.sha256(raw).hexdigest(),'source':'assets-source/phase11/ringborke.blend'}
|
||||||
|
(P/'assets/phase11_manifest.json').write_text(json.dumps({'assets':[record]},indent=2)+'\n')
|
||||||
|
(SRC/'ringborke-audit.json').write_text(json.dumps({'record':record,'exported_clips':[a.get('name') for a in doc.get('animations',[])],'skins':len(doc.get('skins',[])),'source_parts':len(parts),'source_units':'metres','seed':41},indent=2)+'\n')
|
||||||
|
print('RINGBORKE_BUILD',triangles,'triangles',len(defs),'bones',len(doc.get('animations',[])),'clips',flush=True)
|
||||||
13
assets-source/phase11/material-provenance.json
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"method": "Original analytic Blender/Python surface bake",
|
||||||
|
"size": [
|
||||||
|
4096,
|
||||||
|
4096
|
||||||
|
],
|
||||||
|
"upscaled": false,
|
||||||
|
"albedo_encoding": "sRGB",
|
||||||
|
"normal_encoding": "linear, tangent space +Y",
|
||||||
|
"roughness_encoding": "linear",
|
||||||
|
"generator": "assets-source/phase11/bake_cutwood.py",
|
||||||
|
"inputs": "Annual ring field, warped fibres and radial growth checks; no external images"
|
||||||
|
}
|
||||||
54
assets-source/phase11/ringborke-audit.json
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
{
|
||||||
|
"record": {
|
||||||
|
"id": "w04",
|
||||||
|
"role": "tank",
|
||||||
|
"resource_path": "res://assets/characters/ringborke.glb",
|
||||||
|
"triangles": 118064,
|
||||||
|
"mesh_parts": 1,
|
||||||
|
"parts": [
|
||||||
|
"RingborkeBody"
|
||||||
|
],
|
||||||
|
"bounds_godot": {
|
||||||
|
"min": [
|
||||||
|
-0.9736061096191406,
|
||||||
|
0.010707742534577847,
|
||||||
|
-0.3616310954093933
|
||||||
|
],
|
||||||
|
"max": [
|
||||||
|
0.977129340171814,
|
||||||
|
1.8529540300369263,
|
||||||
|
0.3701480031013489
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"skeletal": true,
|
||||||
|
"bones": 27,
|
||||||
|
"clips": [
|
||||||
|
"idle",
|
||||||
|
"start",
|
||||||
|
"walk",
|
||||||
|
"stop",
|
||||||
|
"attack_a",
|
||||||
|
"attack_b",
|
||||||
|
"hit",
|
||||||
|
"ability",
|
||||||
|
"death"
|
||||||
|
],
|
||||||
|
"sha256": "a9c61bcb7ae957c3e6b035ef994db3c8736b7a5f89d62724162dd606b76895b8",
|
||||||
|
"source": "assets-source/phase11/ringborke.blend"
|
||||||
|
},
|
||||||
|
"exported_clips": [
|
||||||
|
"ability",
|
||||||
|
"attack_a",
|
||||||
|
"attack_b",
|
||||||
|
"death",
|
||||||
|
"hit",
|
||||||
|
"idle",
|
||||||
|
"start",
|
||||||
|
"stop",
|
||||||
|
"walk"
|
||||||
|
],
|
||||||
|
"skins": 1,
|
||||||
|
"source_parts": 442,
|
||||||
|
"source_units": "metres",
|
||||||
|
"seed": 41
|
||||||
|
}
|
||||||
BIN
assets-source/phase11/ringborke.blend
Normal file
BIN
assets-source/phase11/ringborke.blend1
Normal file
63
assets-source/phase12/arbeiter-audit.json
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
{
|
||||||
|
"record": {
|
||||||
|
"id": "worker",
|
||||||
|
"role": "worker",
|
||||||
|
"name": "Funkenleser",
|
||||||
|
"resource_path": "res://assets/characters/arbeiter.glb",
|
||||||
|
"triangles": 7184,
|
||||||
|
"mesh_parts": 1,
|
||||||
|
"parts": [
|
||||||
|
"ArbeiterBody"
|
||||||
|
],
|
||||||
|
"bounds_godot": {
|
||||||
|
"min": [
|
||||||
|
-0.4699999988079071,
|
||||||
|
-0.10096755623817444,
|
||||||
|
-0.3050000071525574
|
||||||
|
],
|
||||||
|
"max": [
|
||||||
|
0.4281836748123169,
|
||||||
|
1.2300000190734863,
|
||||||
|
0.31158649921417236
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"skeletal": true,
|
||||||
|
"bones": 27,
|
||||||
|
"clips": [
|
||||||
|
"idle",
|
||||||
|
"start",
|
||||||
|
"walk",
|
||||||
|
"stop",
|
||||||
|
"strike",
|
||||||
|
"pickup",
|
||||||
|
"deposit",
|
||||||
|
"return",
|
||||||
|
"hit",
|
||||||
|
"death"
|
||||||
|
],
|
||||||
|
"work_cycle": [
|
||||||
|
"strike",
|
||||||
|
"pickup",
|
||||||
|
"deposit",
|
||||||
|
"return"
|
||||||
|
],
|
||||||
|
"sha256": "6b61389e6806cf130376665bcf791c5948e070a49593702faebaafad52d7d6d2",
|
||||||
|
"source": "assets-source/phase12/arbeiter.blend"
|
||||||
|
},
|
||||||
|
"exported_clips": [
|
||||||
|
"death",
|
||||||
|
"deposit",
|
||||||
|
"hit",
|
||||||
|
"idle",
|
||||||
|
"pickup",
|
||||||
|
"return",
|
||||||
|
"start",
|
||||||
|
"stop",
|
||||||
|
"strike",
|
||||||
|
"walk"
|
||||||
|
],
|
||||||
|
"skins": 1,
|
||||||
|
"source_parts": 78,
|
||||||
|
"source_units": "metres",
|
||||||
|
"seed": 52
|
||||||
|
}
|
||||||
BIN
assets-source/phase12/arbeiter.blend
Normal file
BIN
assets-source/phase12/arbeiter.blend1
Normal file
597
assets-source/phase12/build_arbeiter.py
Normal file
|
|
@ -0,0 +1,597 @@
|
||||||
|
"""Funkenleser: compact weighted worker with tool-contact work clips.
|
||||||
|
Blender Z up, -Y forward. Run in a fresh background Blender process.
|
||||||
|
Replaces the Phase 4 worker presentation; gameplay values stay unchanged.
|
||||||
|
"""
|
||||||
|
from pathlib import Path
|
||||||
|
import bpy, bmesh, math, random, json, hashlib
|
||||||
|
from mathutils import Vector, Quaternion
|
||||||
|
from math import sin, cos, pi
|
||||||
|
P = Path(__file__).resolve().parents[2]
|
||||||
|
OUT = P / 'assets/characters'
|
||||||
|
OUT.mkdir(exist_ok=True)
|
||||||
|
SRC = Path(__file__).resolve().parent
|
||||||
|
random.seed(52)
|
||||||
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||||
|
S = 1.0 # authored directly at ~1.15 m game height
|
||||||
|
|
||||||
|
def coord(v):
|
||||||
|
return Vector(v) * S
|
||||||
|
|
||||||
|
parts = []
|
||||||
|
|
||||||
|
def material(name, color, rough=.8, metal=0.0, emission=None, alpha=1.0, glass=False):
|
||||||
|
m = bpy.data.materials.new('AB_' + name)
|
||||||
|
m.diffuse_color = (*color, alpha)
|
||||||
|
m.use_nodes = True
|
||||||
|
n = m.node_tree.nodes
|
||||||
|
l = m.node_tree.links
|
||||||
|
b = n.get('Principled BSDF')
|
||||||
|
b.inputs['Base Color'].default_value = (*color, 1)
|
||||||
|
b.inputs['Roughness'].default_value = rough
|
||||||
|
b.inputs['Metallic'].default_value = metal
|
||||||
|
if alpha < 1.0 or glass:
|
||||||
|
b.inputs['Alpha'].default_value = alpha
|
||||||
|
m.blend_method = 'BLEND'
|
||||||
|
b.inputs['Transmission Weight'].default_value = .55 if glass else 0.0
|
||||||
|
b.inputs['IOR'].default_value = 1.45
|
||||||
|
if emission is not None:
|
||||||
|
b.inputs['Emission Color'].default_value = (*emission, 1)
|
||||||
|
b.inputs['Emission Strength'].default_value = 1.4
|
||||||
|
else:
|
||||||
|
v = n.new('ShaderNodeVertexColor')
|
||||||
|
v.layer_name = 'SurfaceColor'
|
||||||
|
l.new(v.outputs['Color'], b.inputs['Base Color'])
|
||||||
|
if name in ('Leather', 'Cloth', 'Clay'):
|
||||||
|
# Fine authored grain without external bitmaps.
|
||||||
|
noise = n.new('ShaderNodeTexNoise')
|
||||||
|
noise.inputs['Scale'].default_value = 48 if name != 'Clay' else 22
|
||||||
|
noise.inputs['Detail'].default_value = 3.2
|
||||||
|
bump = n.new('ShaderNodeBump')
|
||||||
|
bump.inputs['Strength'].default_value = .28 if name == 'Leather' else .16
|
||||||
|
bump.inputs['Distance'].default_value = .012
|
||||||
|
l.new(noise.outputs['Fac'], bump.inputs['Height'])
|
||||||
|
l.new(bump.outputs['Normal'], b.inputs['Normal'])
|
||||||
|
return m
|
||||||
|
|
||||||
|
M = {
|
||||||
|
'clay': material('Clay', (.36, .26, .18), .88),
|
||||||
|
'cloth': material('Cloth', (.16, .19, .23), .92),
|
||||||
|
'hood': material('Hood', (.12, .15, .14), .9),
|
||||||
|
'leather': material('Leather', (.30, .16, .08), .72),
|
||||||
|
'boot': material('Boot', (.10, .07, .05), .78),
|
||||||
|
'glove': material('Glove', (.32, .22, .12), .7),
|
||||||
|
'metal': material('Metal', (.42, .45, .48), .35, .85),
|
||||||
|
'brass': material('Brass', (.55, .40, .16), .4, .75),
|
||||||
|
'darkmetal': material('DarkMetal', (.12, .13, .14), .45, .7),
|
||||||
|
'glass': material('Glass', (.55, .78, .72), .12, 0.0, alpha=.38, glass=True),
|
||||||
|
'spark': material('Spark', (.55, .95, .40), .4, emission=(.45, .95, .35)),
|
||||||
|
'lamp': material('Lamp', (1.0, .88, .55), .3, emission=(1.0, .82, .40)),
|
||||||
|
'lens': material('Lens', (.35, .55, .50), .18, metal=.2),
|
||||||
|
'skin': material('Skin', (.48, .36, .26), .75),
|
||||||
|
}
|
||||||
|
|
||||||
|
def mesh(name, verts, faces, kind, bone, uv=None, colors=None, weights=None, smooth=True):
|
||||||
|
me = bpy.data.meshes.new(name)
|
||||||
|
me.from_pydata([coord(v) for v in verts], [], faces)
|
||||||
|
me.update()
|
||||||
|
ob = bpy.data.objects.new(name, me)
|
||||||
|
bpy.context.collection.objects.link(ob)
|
||||||
|
me.materials.append(M[kind])
|
||||||
|
parts.append(ob)
|
||||||
|
for f in me.polygons:
|
||||||
|
f.use_smooth = smooth
|
||||||
|
tex = me.uv_layers.new(name='UVMap')
|
||||||
|
col = me.color_attributes.new(name='SurfaceColor', type='FLOAT_COLOR', domain='CORNER')
|
||||||
|
base = M[kind].diffuse_color[:3]
|
||||||
|
for f in me.polygons:
|
||||||
|
for li in f.loop_indices:
|
||||||
|
i = me.loops[li].vertex_index
|
||||||
|
v = verts[i]
|
||||||
|
tex.data[li].uv = uv[i] if uv else (v[0] * 1.4 + v[1] * .5, v[2] * 1.2)
|
||||||
|
fac = .92 + .08 * sin(v[0] * 31 + v[2] * 17) + .03 * cos(v[1] * 53)
|
||||||
|
c = colors[i] if colors else tuple(max(.01, x * fac) for x in base)
|
||||||
|
col.data[li].color = (*c, 1)
|
||||||
|
groups = {}
|
||||||
|
for i in range(len(verts)):
|
||||||
|
ws = weights[i] if weights else {bone: 1.0}
|
||||||
|
total = sum(ws.values())
|
||||||
|
for key, w in ws.items():
|
||||||
|
if key not in groups:
|
||||||
|
groups[key] = ob.vertex_groups.new(name=key)
|
||||||
|
groups[key].add([i], w / total, 'REPLACE')
|
||||||
|
return ob
|
||||||
|
|
||||||
|
def mixweights(t, bones):
|
||||||
|
if len(bones) == 1:
|
||||||
|
return {bones[0]: 1}
|
||||||
|
t = max(0, min(.999999, t)) * (len(bones) - 1)
|
||||||
|
i = int(t)
|
||||||
|
w = t - i
|
||||||
|
return {bones[i]: 1 - w, bones[i + 1]: w}
|
||||||
|
|
||||||
|
def tube(name, points, radii, kind, bones, sides=12, steps=20, flatten=1.0, groove=.04):
|
||||||
|
points = [Vector(x) for x in points]
|
||||||
|
vs, uv, ws, loops, fs = [], [], [], [], []
|
||||||
|
|
||||||
|
def point(t):
|
||||||
|
a = min(len(points) - 2, int(t * (len(points) - 1)))
|
||||||
|
u = t * (len(points) - 1) - a
|
||||||
|
p0, p1, p2, p3 = points[max(a - 1, 0)], points[a], points[a + 1], points[min(a + 2, len(points) - 1)]
|
||||||
|
p = .5 * ((2 * p1) + (-p0 + p2) * u + (2 * p0 - 5 * p1 + 4 * p2 - p3) * u * u + (-p0 + 3 * p1 - 3 * p2 + p3) * u * u * u)
|
||||||
|
return p, radii[a] * (1 - u) + radii[a + 1] * u
|
||||||
|
|
||||||
|
for j in range(steps + 1):
|
||||||
|
t = j / steps
|
||||||
|
p, r = point(t)
|
||||||
|
tan = (point(min(1, t + .002))[0] - point(max(0, t - .002))[0]).normalized()
|
||||||
|
axis = Vector((1, 0, 0)) if abs(tan.y) > .94 else Vector((0, 1, 0))
|
||||||
|
u = tan.cross(axis).normalized()
|
||||||
|
v = tan.cross(u)
|
||||||
|
loop = []
|
||||||
|
for k in range(sides):
|
||||||
|
a = 2 * pi * k / sides
|
||||||
|
mod = 1 + groove * (sin(a * 5 + t * 4) + .3 * sin(a * 9 - t * 6))
|
||||||
|
vs.append(p + r * mod * (u * cos(a) + v * sin(a) * flatten))
|
||||||
|
uv.append((k / sides, t))
|
||||||
|
ws.append(mixweights(t, bones))
|
||||||
|
loop.append(len(vs) - 1)
|
||||||
|
loops.append(loop)
|
||||||
|
for a, b in zip(loops, loops[1:]):
|
||||||
|
for k in range(sides):
|
||||||
|
q = (k + 1) % sides
|
||||||
|
fs.append((a[k], a[q], b[q], b[k]))
|
||||||
|
fs.extend([tuple(reversed(loops[0])), tuple(loops[-1])])
|
||||||
|
return mesh(name, vs, fs, kind, bones[0], uv, weights=ws)
|
||||||
|
|
||||||
|
def ell(name, c, r, kind, bone, segments=14, rings=8):
|
||||||
|
vs = [(c[0], c[1], c[2] + r[2])]
|
||||||
|
fs = []
|
||||||
|
for j in range(1, rings):
|
||||||
|
th = pi * j / rings
|
||||||
|
for k in range(segments):
|
||||||
|
a = 2 * pi * k / segments
|
||||||
|
f = 1 + .03 * sin(a * 4 + j)
|
||||||
|
vs.append((c[0] + r[0] * sin(th) * cos(a) * f, c[1] + r[1] * sin(th) * sin(a) * f, c[2] + r[2] * cos(th)))
|
||||||
|
vs.append((c[0], c[1], c[2] - r[2]))
|
||||||
|
end = len(vs) - 1
|
||||||
|
for k in range(segments):
|
||||||
|
fs.append((0, 1 + k, 1 + (k + 1) % segments))
|
||||||
|
for j in range(rings - 2):
|
||||||
|
for k in range(segments):
|
||||||
|
a = 1 + j * segments + k
|
||||||
|
b = 1 + j * segments + (k + 1) % segments
|
||||||
|
fs.append((a, a + segments, b + segments, b))
|
||||||
|
for k in range(segments):
|
||||||
|
fs.append((end, end - 1 - k, end - 1 - (k + 1) % segments))
|
||||||
|
return mesh(name, vs, fs, kind, bone)
|
||||||
|
|
||||||
|
def box(name, c, size, kind, bone, rot_y=0.0, smooth=False):
|
||||||
|
sx, sy, sz = size[0] * .5, size[1] * .5, size[2] * .5
|
||||||
|
local = [(-sx, -sy, -sz), (sx, -sy, -sz), (sx, sy, -sz), (-sx, sy, -sz),
|
||||||
|
(-sx, -sy, sz), (sx, -sy, sz), (sx, sy, sz), (-sx, sy, sz)]
|
||||||
|
ca, sa = cos(rot_y), sin(rot_y)
|
||||||
|
vs = [(c[0] + x * ca - z * sa, c[1] + y, c[2] + x * sa + z * ca) for x, y, z in local]
|
||||||
|
faces = [(0, 1, 2, 3), (4, 7, 6, 5), (0, 4, 5, 1), (1, 5, 6, 2), (2, 6, 7, 3), (3, 7, 4, 0)]
|
||||||
|
return mesh(name, vs, faces, kind, bone, smooth=smooth)
|
||||||
|
|
||||||
|
def frame(name, c, w, h, depth, kind, bone):
|
||||||
|
"""Open rectangular frame for the spark reservoir."""
|
||||||
|
t = .018
|
||||||
|
box(name + '_T', (c[0], c[1], c[2] + h * .5), (w, depth, t), kind, bone)
|
||||||
|
box(name + '_B', (c[0], c[1], c[2] - h * .5), (w, depth, t), kind, bone)
|
||||||
|
box(name + '_L', (c[0] - w * .5, c[1], c[2]), (t, depth, h), kind, bone)
|
||||||
|
box(name + '_R', (c[0] + w * .5, c[1], c[2]), (t, depth, h), kind, bone)
|
||||||
|
|
||||||
|
def fabric(name, rows, kind, bones, depth=.012, folds=.008, cols=18):
|
||||||
|
"""Tailored closed panel: (height, half-width, front, curvature) rows.
|
||||||
|
|
||||||
|
The two surfaces, side welt and blended weights form one deforming garment;
|
||||||
|
folds are geometric, so the silhouette survives export without shader tricks.
|
||||||
|
"""
|
||||||
|
vs, fs, ws, uv = [], [], [], []
|
||||||
|
for back in (0, 1):
|
||||||
|
for j, (z, width, front, curve) in enumerate(rows):
|
||||||
|
t = j / (len(rows) - 1)
|
||||||
|
for k in range(cols + 1):
|
||||||
|
u = k / cols * 2 - 1
|
||||||
|
y = front + curve * u * u + folds * sin(u * pi * 3 + t) * sin(pi * t)
|
||||||
|
vs.append((u * width, y + back * depth, z + .008 * cos(u * pi) * t))
|
||||||
|
ws.append(mixweights(t, bones)); uv.append((k / cols, 1-t))
|
||||||
|
stride = cols + 1; layer = len(rows) * stride
|
||||||
|
for back in (0, 1):
|
||||||
|
for j in range(len(rows) - 1):
|
||||||
|
for k in range(cols):
|
||||||
|
a = back * layer + j * stride + k
|
||||||
|
q = (a, a+1, a+stride+1, a+stride)
|
||||||
|
fs.append(q if back else tuple(reversed(q)))
|
||||||
|
boundary = list(range(stride)) + [j*stride+cols for j in range(1,len(rows))]
|
||||||
|
boundary += list(range(layer-2,layer-stride-1,-1)) + [j*stride for j in range(len(rows)-2,0,-1)]
|
||||||
|
for a,b in zip(boundary, boundary[1:]+boundary[:1]):
|
||||||
|
fs.append((a,b,b+layer,a+layer))
|
||||||
|
return mesh(name, vs, fs, kind, bones[0], uv, weights=ws)
|
||||||
|
|
||||||
|
def boot(name, s, side):
|
||||||
|
"""Rounded last, broad sole and fitted ankle rather than stacked cubes."""
|
||||||
|
bone = 'Foot.' + side
|
||||||
|
ell(name, (s*.13,-.053,.071), (.087,.145,.068), 'boot', bone, 24, 10)
|
||||||
|
# A closed, flattened swept sole has a real broad contact surface.
|
||||||
|
vs, fs = [], []
|
||||||
|
for z, scale in [(0.006,.96),(.018,1),(.037,.97)]:
|
||||||
|
for k in range(32):
|
||||||
|
a = 2*pi*k/32
|
||||||
|
vs.append((s*.13 + .086*cos(a)*scale,-.055+.147*sin(a)*scale,z))
|
||||||
|
for j in range(2):
|
||||||
|
for k in range(32):
|
||||||
|
q=(k+1)%32;fs.append((j*32+k,j*32+q,(j+1)*32+q,(j+1)*32+k))
|
||||||
|
fs += [tuple(reversed(range(32))),tuple(range(64,96))]
|
||||||
|
mesh('Sole_'+side,vs,fs,'darkmetal',bone)
|
||||||
|
tube('Ankle_'+side,[(s*.13,-.01,.06),(s*.125,-.008,.14),(s*.12,-.003,.19)],[.068,.076,.071],'boot',['Foot.'+side,'Shin.'+side],18,14,flatten=.91,groove=.01)
|
||||||
|
tube('Cuff_'+side,[(s*.125,-.008,.151),(s*.12,-.003,.182)],[.080,.077],'leather',['Shin.'+side],18,6,flatten=.91,groove=.01)
|
||||||
|
for z in [.080,.099,.118]:
|
||||||
|
tube('Lace_'+side+str(z),[(s*.13-.04,-.141,z),(s*.13,-.153,z+.007),(s*.13+.04,-.141,z)],[.005,.006,.005],'leather',[bone],6,8,groove=0)
|
||||||
|
|
||||||
|
# ---- Body: stocky gatherer, readable silhouette from all sides ----
|
||||||
|
tube('Tunic',[(0,.015,.40),(0,.015,.51),(0,.018,.65),(0,.016,.77),(0,.01,.82)],[.175,.204,.206,.184,.119],'cloth',['Pelvis','Spine','Chest'],28,32,flatten=.77,groove=.018)
|
||||||
|
fabric('Apron',[(.735,.095,-.153,.021),(.70,.112,-.166,.019),(.63,.154,-.172,.026),(.54,.179,-.182,.033),(.45,.173,-.175,.031),(.36,.163,-.166,.021),(.285,.148,-.16,.017)],'leather',['Chest','Spine','Pelvis'],depth=.014,folds=.012)
|
||||||
|
for s, side in [(-1,'L'),(1,'R')]:
|
||||||
|
tube('ApronWelt_'+side,[(s*.094,-.155,.737),(s*.112,-.149,.70),(s*.154,-.148,.63),(s*.179,-.151,.54),(s*.173,-.146,.45),(s*.163,-.147,.36),(s*.148,-.147,.285)],[.005]*7,'boot',['Chest','Spine','Pelvis'],6,30,groove=0)
|
||||||
|
tube('Harness_'+side,[(s*.085,-.155,.728),(s*.13,-.115,.805),(s*.145,.002,.84),(s*.13,.14,.785),(s*.105,.179,.59)],[.026,.025,.025,.023,.022],'leather',['Chest'],12,28,flatten=.28,groove=0)
|
||||||
|
ell('ApronRivet_'+side,(s*.084,-.163,.716),(.009,.006,.009),'brass','Chest',10,5)
|
||||||
|
for j in range(8):
|
||||||
|
z=.31+j*.035;x=s*(.151+.020*sin((z-.31)*9))
|
||||||
|
tube('ApronStitch_'+side+str(j),[(x,-.152,z),(x,-.154,z+.009)],[.0017,.0017],'brass',['Pelvis' if z<.48 else 'Spine'],5,2,groove=0)
|
||||||
|
tube('ApronHem',[(-.148,-.147,.285),(-.07,-.17,.291),(0,-.163,.293),(.07,-.167,.291),(.148,-.147,.285)],[.006]*5,'boot',['Pelvis'],8,26,groove=0)
|
||||||
|
# Curved chest pocket and stitched flap conform to the belly.
|
||||||
|
pocket=fabric('ToolPocket',[(.535,.067,-.20,.012),(.50,.070,-.219,.016),(.455,.062,-.221,.014),(.443,.046,-.208,.010)],'leather',['Spine','Pelvis'],depth=.008,folds=.002,cols=12)
|
||||||
|
for v in pocket.data.vertices:v.co.x+=.058
|
||||||
|
tube('PocketLip',[(0,-.197,.535),(.055,-.208,.538),(.12,-.195,.535)],[.005,.006,.005],'boot',['Spine'],8,14,groove=0)
|
||||||
|
for s in [-1,1]:
|
||||||
|
tube('Belt_'+str(s),[(0,-.178,.545),(s*.18,-.119,.548),(s*.204,.045,.555),(s*.14,.166,.565),(0,.178,.565)],[.016]*5,'leather',['Spine'],12,25,flatten=.44,groove=0)
|
||||||
|
|
||||||
|
# Framed glass reservoir on the back for gathered sparks.
|
||||||
|
frame('ReservoirFrame', (0, .22, .72), .28, .34, .16, 'brass', 'Chest')
|
||||||
|
box('ReservoirGlass', (0, .22, .72), (.24, .12, .30), 'glass', 'Chest')
|
||||||
|
ell('ReservoirLiquid', (0, .20, .64), (.10, .05, .08), 'spark', 'Chest', 10, 6)
|
||||||
|
for k, z in enumerate([.58, .72, .86]):
|
||||||
|
box('ReservoirBrace_%d' % k, (0, .22, z), (.30, .17, .025), 'brass', 'Chest')
|
||||||
|
|
||||||
|
# Head: hood, lamp, goggles, recessed face.
|
||||||
|
ell('Neck', (0, .01, .875), (.075, .077, .088), 'skin', 'Head',20,10)
|
||||||
|
tube('Collar',[(0,.015,.818),(0,.008,.868)],[.112,.094],'hood',['Chest','Head'],24,10,flatten=.79,groove=.014)
|
||||||
|
ell('Head', (0, -.014, 1.008), (.127, .128, .167), 'skin', 'Head',24,14)
|
||||||
|
ell('Hood', (0, .040, 1.032), (.155, .154, .190), 'hood', 'Head',24,14)
|
||||||
|
ell('FaceRecess', (0, -.115, .997), (.112, .058, .12), 'boot', 'Head',24,12)
|
||||||
|
ell('Face', (0, -.14, 1.00), (.090, .051, .108), 'skin', 'Head',24,12)
|
||||||
|
ell('Chin', (0, -.140, .931), (.064, .043, .040), 'skin', 'Head',16,8)
|
||||||
|
for s, side in [(-1,'L'),(1,'R')]:
|
||||||
|
tube('HoodWelt_'+side,[(s*.03,-.100,.889),(s*.115,-.125,.956),(s*.127,-.139,1.041),(s*.096,-.119,1.115)],[.011,.017,.013,.012],'cloth',['Head'],10,18,groove=.015)
|
||||||
|
ell('Cheek_'+side,(s*.065,-.162,.986),(.038,.031,.045),'skin','Head',16,8)
|
||||||
|
for s, side in [(-1, 'L'), (1, 'R')]:
|
||||||
|
ell('GoggleSocket_' + side, (s * .07, -.15, 1.05), (.05, .03, .04), 'darkmetal', 'Head', 10, 6)
|
||||||
|
ell('GoggleLens_' + side, (s * .07, -.17, 1.05), (.038, .018, .03), 'lens', 'Head', 10, 6)
|
||||||
|
ell('GoggleGlint_' + side, (s * .055, -.185, 1.06), (.01, .006, .008), 'lamp', 'Head', 8, 4)
|
||||||
|
box('GoggleBridge', (0, -.16, 1.05), (.05, .03, .025), 'darkmetal', 'Head')
|
||||||
|
ell('Cap', (0, .020, 1.135), (.158, .162, .086), 'cloth', 'Head',28,12)
|
||||||
|
fabric('CapPeak',[(1.133,.135,-.072,.015),(1.125,.163,-.14,.009),(1.108,.164,-.196,.018),(1.102,.129,-.222,.024)],'hood',['Head'],depth=.013,folds=.001,cols=24)
|
||||||
|
tube('CapSeam',[(-.151,.022,1.153),(-.098,.018,1.197),(0,.010,1.221),(.101,.018,1.197),(.151,.022,1.153)],[.004]*5,'hood',['Head'],6,26,groove=0)
|
||||||
|
tube('LampHousing',[(0,-.101,1.17),(0,-.158,1.17)],[.045,.044],'brass',['Head'],20,8,groove=0)
|
||||||
|
ell('Lamp', (0, -.171, 1.17), (.034, .014, .034), 'lamp', 'Head', 20, 8)
|
||||||
|
ell('Nose',(0,-.188,1.006),(.021,.031,.030),'skin','Head',14,8)
|
||||||
|
tube('Mouth',[(-.030,-.185,.957),(0,-.188,.953),(.030,-.185,.957)],[.0025,.003,.0025],'boot',['Head'],6,10,groove=0)
|
||||||
|
|
||||||
|
# Arms, gloves, fingers and tools.
|
||||||
|
for s, side in [(-1, 'L'), (1, 'R')]:
|
||||||
|
arm = ['UpperArm.' + side, 'Forearm.' + side, 'Hand.' + side]
|
||||||
|
tube('Sleeve_' + side, [(s*.17,.01,.797),(s*.24,.00,.75),(s*.30,-.02,.64),(s*.33,-.075,.54),(s*.34,-.10,.478)], [.08,.088,.073,.072,.057], 'cloth', [arm[0], arm[1]], 18, 32,groove=.018)
|
||||||
|
ell('ElbowPatch_' + side, (s * .311, .014, .644), (.068, .048, .071), 'leather', arm[1], 14, 8)
|
||||||
|
tube('SleeveCuff_'+side,[(s*.332,-.083,.523),(s*.341,-.103,.482)],[.066,.062],'leather',[arm[1]],18,8,groove=.014)
|
||||||
|
ell('Glove_' + side, (s * .348, -.12, .44), (.067, .055, .064), 'glove', arm[2],20,10)
|
||||||
|
for finger in range(3):
|
||||||
|
z=.432-finger*.029; hx=s*(.345+finger*.006)
|
||||||
|
tube('Finger_%s%d' % (side, finger), [(hx+s*.047,-.119,z+.002),(hx+s*.050,-.151,z),(hx+s*.024,-.177,z-.006),(hx-s*.010,-.173,z-.012)], [.016,.018,.015,.010], 'glove', [arm[2], 'Finger%d.%s' % (finger, side)], 10, 14,groove=.014)
|
||||||
|
tube('Thumb_' + side, [(s*.306,-.109,.459),(s*.310,-.158,.438),(s*.341,-.174,.429)], [.026,.020,.014], 'glove', [arm[2], 'Thumb.' + side], 10, 14,groove=.014)
|
||||||
|
|
||||||
|
# Left: gathering fork. Right: slender probe.
|
||||||
|
tube('ForkHandle', [(-.35, -.14, .40), (-.38, -.22, .22), (-.40, -.26, .08)], [.025, .022, .02], 'leather', ['Hand.L'], 8, 12)
|
||||||
|
for tine in range(3):
|
||||||
|
x = -.40 + (tine - 1) * .045
|
||||||
|
tube('ForkTine_%d' % tine, [(x, -.26, .08), (x, -.30, -.02), (x, -.28, -.10)], [.014, .012, .004], 'metal', ['Hand.L'], 8, 10)
|
||||||
|
box('ForkGuard', (-.40, -.25, .10), (.14, .04, .03), 'brass', 'Hand.L')
|
||||||
|
tube('Probe', [(.35, -.14, .40), (.38, -.20, .24), (.40, -.22, .12)], [.018, .014, .008], 'metal', ['Hand.R'], 8, 10)
|
||||||
|
ell('ProbeTip', (.40, -.23, .10), (.018, .018, .02), 'spark', 'Hand.R', 8, 4)
|
||||||
|
|
||||||
|
# Legs and boots with cuffs and toe ridges.
|
||||||
|
for s, side in [(-1, 'L'), (1, 'R')]:
|
||||||
|
leg = ['Thigh.' + side, 'Shin.' + side, 'Foot.' + side]
|
||||||
|
tube('Trousers_' + side, [(s*.10,.01,.435),(s*.113,.005,.32),(s*.12,0,.24),(s*.13,-.01,.15)], [.095,.098,.078,.068], 'cloth', [leg[0],leg[1]], 18, 26,groove=.018)
|
||||||
|
ell('KneePatch_'+side,(s*.12,-.066,.25),(.058,.027,.067),'leather',leg[1],14,8)
|
||||||
|
boot('Boot_'+side,s,side)
|
||||||
|
|
||||||
|
# Weld, triangulate and orient closed solids before skinning.
|
||||||
|
for ob in parts:
|
||||||
|
bm = bmesh.new()
|
||||||
|
bm.from_mesh(ob.data)
|
||||||
|
bmesh.ops.remove_doubles(bm, verts=list(bm.verts), dist=.0000005)
|
||||||
|
bmesh.ops.triangulate(bm, faces=list(bm.faces))
|
||||||
|
bmesh.ops.dissolve_degenerate(bm, edges=list(bm.edges), dist=.0000001)
|
||||||
|
bmesh.ops.recalc_face_normals(bm, faces=list(bm.faces))
|
||||||
|
if bm.calc_volume(signed=True) < 0:
|
||||||
|
bmesh.ops.reverse_faces(bm, faces=list(bm.faces))
|
||||||
|
bm.to_mesh(ob.data)
|
||||||
|
bm.free()
|
||||||
|
ob.data.update()
|
||||||
|
|
||||||
|
# Skeleton: anatomically placed hinges for a compact gatherer.
|
||||||
|
bpy.ops.object.armature_add(enter_editmode=True)
|
||||||
|
rig = bpy.context.object
|
||||||
|
rig.name = 'ArbeiterRig'
|
||||||
|
ed = rig.data.edit_bones
|
||||||
|
ed.remove(ed[0])
|
||||||
|
defs = {}
|
||||||
|
|
||||||
|
def bone(name, head, tail, parent=None):
|
||||||
|
b = ed.new(name)
|
||||||
|
b.head = coord(head)
|
||||||
|
b.tail = coord(tail)
|
||||||
|
if parent:
|
||||||
|
b.parent = ed[parent]
|
||||||
|
defs[name] = (head, tail, parent)
|
||||||
|
|
||||||
|
bone('Root', (0, 0, 0), (0, 0, .12))
|
||||||
|
bone('Pelvis', (0, .01, .40), (0, .02, .58), 'Root')
|
||||||
|
bone('Spine', (0, .02, .58), (0, .02, .74), 'Pelvis')
|
||||||
|
bone('Chest', (0, .02, .74), (0, .01, .88), 'Spine')
|
||||||
|
bone('Head', (0, .01, .88), (0, 0, 1.16), 'Chest')
|
||||||
|
for s, side in [(-1, 'L'), (1, 'R')]:
|
||||||
|
bone('UpperArm.' + side, (s * .20, .01, .78), (s * .30, -.02, .64), 'Chest')
|
||||||
|
bone('Forearm.' + side, (s * .30, -.02, .64), (s * .34, -.10, .48), 'UpperArm.' + side)
|
||||||
|
bone('Hand.' + side, (s * .34, -.10, .48), (s * .35, -.15, .38), 'Forearm.' + side)
|
||||||
|
for f in range(3):
|
||||||
|
x = s * .35 + (f - 1) * .035
|
||||||
|
bone('Finger%d.%s' % (f, side), (x, -.15, .40), (x + s * .01, -.19, .33), 'Hand.' + side)
|
||||||
|
bone('Thumb.' + side, (s * .30, -.10, .44), (s * .27, -.17, .37), 'Hand.' + side)
|
||||||
|
bone('Thigh.' + side, (s * .10, .01, .42), (s * .12, 0, .24), 'Pelvis')
|
||||||
|
bone('Shin.' + side, (s * .12, 0, .24), (s * .13, -.01, .08), 'Thigh.' + side)
|
||||||
|
bone('Foot.' + side, (s * .13, -.01, .08), (s * .13, -.16, .03), 'Shin.' + side)
|
||||||
|
bone('Toe.' + side, (s * .13, -.16, .03), (s * .13, -.20, .03), 'Foot.' + side)
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
for ob in parts:
|
||||||
|
mod = ob.modifiers.new('Weighted gatherer', 'ARMATURE')
|
||||||
|
mod.object = rig
|
||||||
|
ob.parent = rig
|
||||||
|
|
||||||
|
# Pose clips at 30 fps. Work cycle: strike (tool contact) → pickup → deposit → return.
|
||||||
|
clips = {
|
||||||
|
'idle': (2.8, True),
|
||||||
|
'start': (.35, False),
|
||||||
|
'walk': (1.1, True),
|
||||||
|
'stop': (.35, False),
|
||||||
|
'strike': (1.30, False),
|
||||||
|
'pickup': (1.15, False),
|
||||||
|
'deposit': (1.20, False),
|
||||||
|
'return': (.95, False),
|
||||||
|
'hit': (.42, False),
|
||||||
|
'death': (1.9, False),
|
||||||
|
}
|
||||||
|
|
||||||
|
def curve(t, keys):
|
||||||
|
for i in range(len(keys) - 1):
|
||||||
|
a, x = keys[i]
|
||||||
|
b, y = keys[i + 1]
|
||||||
|
if t <= b:
|
||||||
|
u = max(0, (t - a) / (b - a))
|
||||||
|
u = u * u * (3 - 2 * u)
|
||||||
|
return x * (1 - u) + y * u
|
||||||
|
return keys[-1][1]
|
||||||
|
|
||||||
|
def animate(name, t, dur):
|
||||||
|
rotations = {}
|
||||||
|
locs = {}
|
||||||
|
|
||||||
|
def rot(b, x=0, y=0, z=0):
|
||||||
|
rotations[b] = (x, y, z)
|
||||||
|
|
||||||
|
if name == 'idle':
|
||||||
|
q = sin(2 * pi * t / dur)
|
||||||
|
rot('Spine', .01 * q)
|
||||||
|
rot('Chest', -.014 * q, 0, .008 * q)
|
||||||
|
rot('Head', .02 * q, 0, -.012 * q)
|
||||||
|
for s, side in [(-1, 'L'), (1, 'R')]:
|
||||||
|
rot('Forearm.' + side, .02 * q, 0, s * .01 * q)
|
||||||
|
rot('Finger0.' + side, .04 * q)
|
||||||
|
elif name in ('walk', 'start', 'stop'):
|
||||||
|
fade = 1 if name == 'walk' else sin(pi * .5 * t / dur) if name == 'start' else cos(pi * .5 * t / dur)
|
||||||
|
phase = t / 1.1 * 2 * pi
|
||||||
|
roll = sin(phase) * fade
|
||||||
|
rot('Pelvis', .01, 0, .03 * roll)
|
||||||
|
rot('Spine', -.03 * fade, 0, -.04 * roll)
|
||||||
|
rot('Chest', .012 * fade, 0, .02 * roll)
|
||||||
|
rot('Head', .02 * fade, 0, -.015 * roll)
|
||||||
|
for s, side in [(-1, 'L'), (1, 'R')]:
|
||||||
|
q = sin(phase + (0 if s < 0 else pi))
|
||||||
|
lift = max(0, q)
|
||||||
|
rot('Thigh.' + side, .28 * q * fade)
|
||||||
|
rot('Shin.' + side, -.34 * lift * fade)
|
||||||
|
rot('Foot.' + side, (-.28 * q + .34 * lift) * fade)
|
||||||
|
rot('Toe.' + side, .10 * max(0, -q) * fade)
|
||||||
|
rot('UpperArm.' + side, -.08 * q * fade, 0, s * .03 * fade)
|
||||||
|
rot('Forearm.' + side, -.12 * fade - .04 * lift)
|
||||||
|
# Reservoir lag: chest sways against the carried load.
|
||||||
|
locs['Root'] = (0, 0, (-.008 + .008 * cos(phase * 2)) * fade)
|
||||||
|
elif name == 'strike':
|
||||||
|
# Contact at 0.62 — the fork tip reaches the ore at the damage frame.
|
||||||
|
q = curve(t / dur, [(0, 0), (.28, -.40), (.48, -.62), (.62, 1), (.72, .82), (1, 0)])
|
||||||
|
rot('Spine', -.14 * q, .08 * q, .06 * q)
|
||||||
|
rot('Chest', -.10 * q, .06 * q, .10 * q)
|
||||||
|
rot('Head', .12 * q, -.05 * q, -.06 * q)
|
||||||
|
rot('UpperArm.L', -.95 * q, -.10 * q, .12 * q)
|
||||||
|
rot('Forearm.L', -.35 * q)
|
||||||
|
rot('Hand.L', .22 * q)
|
||||||
|
rot('UpperArm.R', .18 * q, 0, -.08 * q)
|
||||||
|
rot('Forearm.R', -.15 * q)
|
||||||
|
for f in range(3):
|
||||||
|
rot('Finger%d.L' % f, -.30 * max(0, q))
|
||||||
|
rot('Thigh.L', .08 * q)
|
||||||
|
rot('Thigh.R', -.04 * q)
|
||||||
|
locs['Root'] = (0, -.03 * q, -.01 * max(0, q))
|
||||||
|
elif name == 'pickup':
|
||||||
|
q = curve(t / dur, [(0, 0), (.35, 1), (.70, .85), (1, 0)])
|
||||||
|
rot('Spine', .38 * q)
|
||||||
|
rot('Chest', .22 * q)
|
||||||
|
rot('Head', .18 * q)
|
||||||
|
rot('Thigh.L', .32 * q)
|
||||||
|
rot('Thigh.R', .28 * q)
|
||||||
|
rot('Shin.L', -.48 * q)
|
||||||
|
rot('Shin.R', -.42 * q)
|
||||||
|
rot('Foot.L', .20 * q)
|
||||||
|
rot('Foot.R', .18 * q)
|
||||||
|
rot('UpperArm.L', -.55 * q, 0, .18 * q)
|
||||||
|
rot('Forearm.L', -.40 * q)
|
||||||
|
rot('UpperArm.R', -.48 * q, 0, -.18 * q)
|
||||||
|
rot('Forearm.R', -.38 * q)
|
||||||
|
for s, side in [(-1, 'L'), (1, 'R')]:
|
||||||
|
for f in range(3):
|
||||||
|
rot('Finger%d.%s' % (f, side), -.45 * max(0, q))
|
||||||
|
locs['Root'] = (0, -.02 * q, -.08 * q)
|
||||||
|
elif name == 'deposit':
|
||||||
|
# Lift the gathered spark into the back reservoir.
|
||||||
|
q = curve(t / dur, [(0, 0), (.30, .7), (.55, 1), (.80, .6), (1, 0)])
|
||||||
|
rot('Spine', -.08 * q, -.25 * q, 0)
|
||||||
|
rot('Chest', -.05 * q, -.20 * q, 0)
|
||||||
|
rot('Head', -.10 * q, .15 * q, 0)
|
||||||
|
rot('UpperArm.R', -.70 * q, -.35 * q, -.25 * q)
|
||||||
|
rot('Forearm.R', -.90 * q)
|
||||||
|
rot('Hand.R', .40 * q)
|
||||||
|
rot('UpperArm.L', .15 * q, 0, .10 * q)
|
||||||
|
for f in range(3):
|
||||||
|
rot('Finger%d.R' % f, -.55 * max(0, q))
|
||||||
|
locs['Root'] = (0, .01 * q, .02 * q)
|
||||||
|
elif name == 'return':
|
||||||
|
# Step back to the station and settle into the ready stance.
|
||||||
|
q = curve(t / dur, [(0, 0), (.40, 1), (1, 0)])
|
||||||
|
rot('Spine', .06 * q, 0, -.04 * q)
|
||||||
|
rot('Thigh.L', -.18 * q)
|
||||||
|
rot('Thigh.R', .14 * q)
|
||||||
|
rot('Shin.L', .12 * q)
|
||||||
|
rot('Foot.L', -.10 * q)
|
||||||
|
rot('UpperArm.L', .10 * q)
|
||||||
|
rot('UpperArm.R', .08 * q)
|
||||||
|
rot('Head', .04 * q)
|
||||||
|
locs['Root'] = (0, .04 * q, 0)
|
||||||
|
elif name == 'hit':
|
||||||
|
q = curve(t, [(0, 0), (.07, 1), (.20, .45), (.42, 0)])
|
||||||
|
rot('Spine', .14 * q, 0, -.08 * q)
|
||||||
|
rot('Head', .15 * q, 0, .06 * q)
|
||||||
|
rot('Forearm.L', -.12 * q)
|
||||||
|
rot('Forearm.R', -.10 * q)
|
||||||
|
elif name == 'death':
|
||||||
|
q = curve(t, [(0, 0), (.18, .08), (.65, .42), (1.20, 1), (1.50, .92), (1.9, 1)])
|
||||||
|
rot('Root', -.85 * q, 0, .10 * q)
|
||||||
|
locs['Root'] = (0, -.12 * q, -.10 * q)
|
||||||
|
rot('Pelvis', -.18 * q)
|
||||||
|
rot('Spine', -.22 * q)
|
||||||
|
rot('Head', -.18 * q)
|
||||||
|
for s, side in [(-1, 'L'), (1, 'R')]:
|
||||||
|
rot('UpperArm.' + side, -.28 * q, 0, s * .14 * q)
|
||||||
|
rot('Forearm.' + side, -.40 * q)
|
||||||
|
rot('Thigh.' + side, .22 * q)
|
||||||
|
rot('Shin.' + side, -.38 * q)
|
||||||
|
for pb in rig.pose.bones:
|
||||||
|
pb.rotation_mode = 'QUATERNION'
|
||||||
|
r = rotations.get(pb.name, (0, 0, 0))
|
||||||
|
q = Quaternion((0, 0, 1), r[2]) @ Quaternion((0, 1, 0), r[1]) @ Quaternion((1, 0, 0), r[0])
|
||||||
|
basis = pb.bone.matrix_local.to_quaternion()
|
||||||
|
pb.rotation_quaternion = basis.inverted() @ q @ basis
|
||||||
|
pb.location = pb.bone.matrix_local.to_3x3().inverted() @ (Vector(locs.get(pb.name, (0, 0, 0))) * S)
|
||||||
|
|
||||||
|
for name, (dur, loop) in clips.items():
|
||||||
|
rig.animation_data_clear()
|
||||||
|
action = bpy.data.actions.new(name)
|
||||||
|
action.use_fake_user = True
|
||||||
|
rig.animation_data_create()
|
||||||
|
rig.animation_data.action = action
|
||||||
|
for frame in range(round(dur * 30) + 1):
|
||||||
|
animate(name, frame / 30, dur)
|
||||||
|
for pb in rig.pose.bones:
|
||||||
|
pb.keyframe_insert('rotation_quaternion', frame=frame)
|
||||||
|
pb.keyframe_insert('location', frame=frame)
|
||||||
|
action['loop'] = loop
|
||||||
|
rig.animation_data_clear()
|
||||||
|
rig.animation_data_create()
|
||||||
|
rig.animation_data.action = bpy.data.actions['idle']
|
||||||
|
for pb in rig.pose.bones:
|
||||||
|
pb.rotation_quaternion = Quaternion()
|
||||||
|
pb.location = (0, 0, 0)
|
||||||
|
bpy.context.scene.render.fps = 30
|
||||||
|
bpy.context.scene.frame_set(0)
|
||||||
|
|
||||||
|
bpy.ops.wm.save_as_mainfile(filepath=str(SRC / 'arbeiter.blend'))
|
||||||
|
bpy.ops.file.make_paths_relative()
|
||||||
|
bpy.ops.wm.save_as_mainfile(filepath=str(SRC / 'arbeiter.blend'))
|
||||||
|
bpy.ops.object.select_all(action='DESELECT')
|
||||||
|
for ob in parts:
|
||||||
|
ob.select_set(True)
|
||||||
|
bpy.context.view_layer.objects.active = parts[0]
|
||||||
|
bpy.ops.object.join()
|
||||||
|
body = bpy.context.object
|
||||||
|
body.name = 'ArbeiterBody'
|
||||||
|
body.select_set(True)
|
||||||
|
rig.select_set(True)
|
||||||
|
bpy.context.view_layer.objects.active = rig
|
||||||
|
bpy.ops.export_scene.gltf(
|
||||||
|
filepath=str(OUT / 'arbeiter.glb'),
|
||||||
|
export_format='GLB',
|
||||||
|
use_selection=True,
|
||||||
|
export_animations=True,
|
||||||
|
export_animation_mode='ACTIONS',
|
||||||
|
export_force_sampling=True,
|
||||||
|
export_frame_range=False,
|
||||||
|
export_anim_slide_to_zero=True,
|
||||||
|
export_skins=True,
|
||||||
|
export_all_influences=False,
|
||||||
|
export_def_bones=True,
|
||||||
|
export_anim_single_armature=True,
|
||||||
|
export_materials='EXPORT',
|
||||||
|
export_yup=True,
|
||||||
|
)
|
||||||
|
raw = (OUT / 'arbeiter.glb').read_bytes()
|
||||||
|
import struct
|
||||||
|
doc = json.loads(raw[20:20 + struct.unpack_from('<I', raw, 12)[0]])
|
||||||
|
triangles = sum(doc['accessors'][q['indices']]['count'] // 3 for m in doc['meshes'] for q in m['primitives'])
|
||||||
|
mins = [min(v.co[i] for v in body.data.vertices) for i in range(3)]
|
||||||
|
maxs = [max(v.co[i] for v in body.data.vertices) for i in range(3)]
|
||||||
|
record = {
|
||||||
|
'id': 'worker',
|
||||||
|
'role': 'worker',
|
||||||
|
'name': 'Funkenleser',
|
||||||
|
'resource_path': 'res://assets/characters/arbeiter.glb',
|
||||||
|
'triangles': triangles,
|
||||||
|
'mesh_parts': 1,
|
||||||
|
'parts': ['ArbeiterBody'],
|
||||||
|
'bounds_godot': {
|
||||||
|
'min': [mins[0], mins[2], -maxs[1]],
|
||||||
|
'max': [maxs[0], maxs[2], -mins[1]],
|
||||||
|
},
|
||||||
|
'skeletal': True,
|
||||||
|
'bones': len(defs),
|
||||||
|
'clips': list(clips),
|
||||||
|
'work_cycle': ['strike', 'pickup', 'deposit', 'return'],
|
||||||
|
'sha256': hashlib.sha256(raw).hexdigest(),
|
||||||
|
'source': 'assets-source/phase12/arbeiter.blend',
|
||||||
|
}
|
||||||
|
manifest = {'assets': [record]}
|
||||||
|
# Core presentation metadata for activation / operation / damage reactions.
|
||||||
|
manifest['cores'] = [
|
||||||
|
{'id': 'core_team0', 'activate_clip': 'activate', 'operate_clip': 'operate', 'damage_clip': 'damage'},
|
||||||
|
{'id': 'core_team1', 'activate_clip': 'activate', 'operate_clip': 'operate', 'damage_clip': 'damage'},
|
||||||
|
]
|
||||||
|
(P / 'assets/phase12_manifest.json').write_text(json.dumps(manifest, indent=2) + '\n')
|
||||||
|
(SRC / 'arbeiter-audit.json').write_text(json.dumps({
|
||||||
|
'record': record,
|
||||||
|
'exported_clips': [a.get('name') for a in doc.get('animations', [])],
|
||||||
|
'skins': len(doc.get('skins', [])),
|
||||||
|
'source_parts': len(parts),
|
||||||
|
'source_units': 'metres',
|
||||||
|
'seed': 52,
|
||||||
|
}, indent=2) + '\n')
|
||||||
|
print('ARBEITER_BUILD', triangles, 'triangles', len(defs), 'bones', len(doc.get('animations', [])), 'clips', flush=True)
|
||||||
297
assets-source/phase14/build_compare_props.py
Normal file
|
|
@ -0,0 +1,297 @@
|
||||||
|
"""Portable compare props: rock, tree and arena tile for the Godot/Unreal stage.
|
||||||
|
Blender Z up, -Y forward. Same style family as the Verdant Rift sanctuary.
|
||||||
|
"""
|
||||||
|
from pathlib import Path
|
||||||
|
import bpy, bmesh, math, random, json, hashlib
|
||||||
|
from mathutils import Vector
|
||||||
|
from math import sin, cos, pi
|
||||||
|
P = Path(__file__).resolve().parents[2]
|
||||||
|
OUT = P / 'assets/compare'
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
SRC = Path(__file__).resolve().parent
|
||||||
|
random.seed(77)
|
||||||
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||||
|
|
||||||
|
def material(name, color, rough=.85):
|
||||||
|
m = bpy.data.materials.new('CV_' + name)
|
||||||
|
m.diffuse_color = (*color, 1)
|
||||||
|
m.use_nodes = True
|
||||||
|
b = m.node_tree.nodes.get('Principled BSDF')
|
||||||
|
b.inputs['Base Color'].default_value = (*color, 1)
|
||||||
|
b.inputs['Roughness'].default_value = rough
|
||||||
|
n = m.node_tree.nodes
|
||||||
|
l = m.node_tree.links
|
||||||
|
v = n.new('ShaderNodeVertexColor')
|
||||||
|
v.layer_name = 'SurfaceColor'
|
||||||
|
l.new(v.outputs['Color'], b.inputs['Base Color'])
|
||||||
|
return m
|
||||||
|
|
||||||
|
M = {
|
||||||
|
'cliff': material('Cliff', (.42, .40, .38), .92),
|
||||||
|
'limestone': material('Limestone', (.62, .58, .50), .88),
|
||||||
|
'bark': material('Bark', (.22, .14, .08), .9),
|
||||||
|
'leaf': material('Leaf', (.28, .42, .12), .78),
|
||||||
|
'leaf2': material('LeafLight', (.40, .55, .18), .75),
|
||||||
|
'paving': material('Paving', (.38, .36, .34), .82),
|
||||||
|
}
|
||||||
|
|
||||||
|
def mesh(name, verts, faces, kind, uv=None, colors=None, smooth=True):
|
||||||
|
me = bpy.data.meshes.new(name)
|
||||||
|
me.from_pydata(verts, [], faces)
|
||||||
|
me.update()
|
||||||
|
ob = bpy.data.objects.new(name, me)
|
||||||
|
bpy.context.collection.objects.link(ob)
|
||||||
|
me.materials.append(M[kind])
|
||||||
|
for f in me.polygons:
|
||||||
|
f.use_smooth = smooth
|
||||||
|
tex = me.uv_layers.new(name='UVMap')
|
||||||
|
col = me.color_attributes.new(name='SurfaceColor', type='FLOAT_COLOR', domain='CORNER')
|
||||||
|
base = M[kind].diffuse_color[:3]
|
||||||
|
for f in me.polygons:
|
||||||
|
for li in f.loop_indices:
|
||||||
|
i = me.loops[li].vertex_index
|
||||||
|
v = verts[i]
|
||||||
|
tex.data[li].uv = uv[i] if uv else (v[0] * .5, (v[1] + v[2]) * .5)
|
||||||
|
fac = .90 + .10 * sin(v[0] * 13 + v[2] * 7) + .04 * cos(v[1] * 29)
|
||||||
|
c = colors[i] if colors else tuple(max(.01, x * fac) for x in base)
|
||||||
|
col.data[li].color = (*c, 1)
|
||||||
|
return ob
|
||||||
|
|
||||||
|
def finish(ob):
|
||||||
|
bm = bmesh.new()
|
||||||
|
bm.from_mesh(ob.data)
|
||||||
|
bmesh.ops.remove_doubles(bm, verts=list(bm.verts), dist=.0001)
|
||||||
|
bmesh.ops.triangulate(bm, faces=list(bm.faces))
|
||||||
|
bmesh.ops.recalc_face_normals(bm, faces=list(bm.faces))
|
||||||
|
if bm.calc_volume(signed=True) < 0:
|
||||||
|
bmesh.ops.reverse_faces(bm, faces=list(bm.faces))
|
||||||
|
bm.to_mesh(ob.data)
|
||||||
|
bm.free()
|
||||||
|
ob.data.update()
|
||||||
|
|
||||||
|
def export(ob, name, record_id, role):
|
||||||
|
bpy.ops.object.select_all(action='DESELECT')
|
||||||
|
ob.select_set(True)
|
||||||
|
bpy.context.view_layer.objects.active = ob
|
||||||
|
path = OUT / (name + '.glb')
|
||||||
|
bpy.ops.export_scene.gltf(
|
||||||
|
filepath=str(path), export_format='GLB', use_selection=True,
|
||||||
|
export_animations=False, export_materials='EXPORT', export_yup=True)
|
||||||
|
raw = path.read_bytes()
|
||||||
|
import struct
|
||||||
|
doc = json.loads(raw[20:20 + struct.unpack_from('<I', raw, 12)[0]])
|
||||||
|
triangles = sum(doc['accessors'][q['indices']]['count'] // 3 for m in doc['meshes'] for q in m['primitives'])
|
||||||
|
mins = [min(v.co[i] for v in ob.data.vertices) for i in range(3)]
|
||||||
|
maxs = [max(v.co[i] for v in ob.data.vertices) for i in range(3)]
|
||||||
|
return {
|
||||||
|
'id': record_id,
|
||||||
|
'role': role,
|
||||||
|
'resource_path': 'res://assets/compare/' + name + '.glb',
|
||||||
|
'triangles': triangles,
|
||||||
|
'bounds_godot': {
|
||||||
|
'min': [mins[0], mins[2], -maxs[1]],
|
||||||
|
'max': [maxs[0], maxs[2], -mins[1]],
|
||||||
|
},
|
||||||
|
'sha256': hashlib.sha256(raw).hexdigest(),
|
||||||
|
'source': 'assets-source/phase14/build_compare_props.py',
|
||||||
|
}
|
||||||
|
|
||||||
|
def rock():
|
||||||
|
# Layered sanctuary boulder with eroded ledges.
|
||||||
|
rng = random.Random(11)
|
||||||
|
vs, fs = [], []
|
||||||
|
rings = 7
|
||||||
|
sides = 18
|
||||||
|
for j in range(rings):
|
||||||
|
t = j / (rings - 1)
|
||||||
|
r = (.95 - .55 * abs(t - .45) * 1.6) * (1 + .12 * sin(t * 9))
|
||||||
|
y = -0.15 + t * 1.35
|
||||||
|
for k in range(sides):
|
||||||
|
a = 2 * pi * k / sides
|
||||||
|
wobble = 1 + .16 * sin(a * 3 + j) + .08 * sin(a * 7 - j * 2)
|
||||||
|
vs.append((r * wobble * cos(a) * 1.15, y + .04 * sin(a * 5), r * wobble * sin(a) * .85))
|
||||||
|
for j in range(rings - 1):
|
||||||
|
for k in range(sides):
|
||||||
|
a = j * sides + k
|
||||||
|
b = j * sides + (k + 1) % sides
|
||||||
|
fs.append((a, b, b + sides, a + sides))
|
||||||
|
vs.append((0, -.15, 0))
|
||||||
|
vs.append((0, 1.35, 0))
|
||||||
|
bot, top = len(vs) - 2, len(vs) - 1
|
||||||
|
for k in range(sides):
|
||||||
|
fs.append((bot, (k + 1) % sides, k))
|
||||||
|
a = (rings - 1) * sides + k
|
||||||
|
b = (rings - 1) * sides + (k + 1) % sides
|
||||||
|
fs.append((top, a, b))
|
||||||
|
ob = mesh('CompareRock', vs, fs, 'cliff', smooth=True)
|
||||||
|
# Limestone ledges
|
||||||
|
for j in range(3):
|
||||||
|
lv, lf = [], []
|
||||||
|
y = .25 + j * .38
|
||||||
|
rr = .72 - j * .08
|
||||||
|
for k in range(sides):
|
||||||
|
a = 2 * pi * k / sides
|
||||||
|
w = 1 + .1 * sin(a * 4 + j)
|
||||||
|
lv.append((rr * w * cos(a) * 1.1, y, rr * w * sin(a) * .8))
|
||||||
|
lv.append((rr * w * cos(a) * 1.1, y + .08, rr * w * sin(a) * .8))
|
||||||
|
for k in range(sides):
|
||||||
|
a = k * 2
|
||||||
|
b = ((k + 1) % sides) * 2
|
||||||
|
lf.append((a, b, b + 1, a + 1))
|
||||||
|
ledge = mesh('CompareRock_Ledge%d' % j, lv, lf, 'limestone', smooth=True)
|
||||||
|
finish(ledge)
|
||||||
|
ledge.select_set(True)
|
||||||
|
ob.select_set(True)
|
||||||
|
bpy.context.view_layer.objects.active = ob
|
||||||
|
bpy.ops.object.join()
|
||||||
|
ob = bpy.context.object
|
||||||
|
ob.name = 'CompareRock'
|
||||||
|
finish(ob)
|
||||||
|
return ob
|
||||||
|
|
||||||
|
def tree():
|
||||||
|
# Compact sanctuary tree: tapered trunk, branch stubs, leaf clusters.
|
||||||
|
rng = random.Random(19)
|
||||||
|
parts = []
|
||||||
|
# Trunk
|
||||||
|
tv, tf = [], []
|
||||||
|
sides, steps = 10, 8
|
||||||
|
for j in range(steps + 1):
|
||||||
|
t = j / steps
|
||||||
|
y = t * 2.4
|
||||||
|
r = .22 * (1 - t * .55) * (1 + .08 * sin(t * 8))
|
||||||
|
for k in range(sides):
|
||||||
|
a = 2 * pi * k / sides
|
||||||
|
tv.append((r * cos(a) * (1 + .1 * sin(a * 3 + t * 5)), y, r * sin(a)))
|
||||||
|
for j in range(steps):
|
||||||
|
for k in range(sides):
|
||||||
|
a = j * sides + k
|
||||||
|
b = j * sides + (k + 1) % sides
|
||||||
|
tf.append((a, b, b + sides, a + sides))
|
||||||
|
tv.append((0, 0, 0))
|
||||||
|
tv.append((0, 2.4, 0))
|
||||||
|
bot, top = len(tv) - 2, len(tv) - 1
|
||||||
|
for k in range(sides):
|
||||||
|
tf.append((bot, (k + 1) % sides, k))
|
||||||
|
a = steps * sides + k
|
||||||
|
b = steps * sides + (k + 1) % sides
|
||||||
|
tf.append((top, a, b))
|
||||||
|
trunk = mesh('CompareTree_Trunk', tv, tf, 'bark')
|
||||||
|
finish(trunk)
|
||||||
|
parts.append(trunk)
|
||||||
|
# Branches + leaf clusters
|
||||||
|
for j in range(6):
|
||||||
|
a = j * 2.3
|
||||||
|
y0 = 1.1 + (j % 3) * .35
|
||||||
|
length = .7 + .2 * (j % 2)
|
||||||
|
bv, bf = [], []
|
||||||
|
for s in range(5):
|
||||||
|
t = s / 4
|
||||||
|
r = .07 * (1 - t * .7)
|
||||||
|
x = cos(a) * length * t
|
||||||
|
z = sin(a) * length * t
|
||||||
|
y = y0 + t * .35
|
||||||
|
for k in range(6):
|
||||||
|
aa = 2 * pi * k / 6
|
||||||
|
bv.append((x + r * cos(aa), y + r * sin(aa), z + r * cos(aa) * .5))
|
||||||
|
for s in range(4):
|
||||||
|
for k in range(6):
|
||||||
|
i0 = s * 6 + k
|
||||||
|
i1 = s * 6 + (k + 1) % 6
|
||||||
|
bf.append((i0, i1, i1 + 6, i0 + 6))
|
||||||
|
branch = mesh('CompareTree_Branch%d' % j, bv, bf, 'bark')
|
||||||
|
finish(branch)
|
||||||
|
parts.append(branch)
|
||||||
|
# Leaf sphere cluster at branch tip
|
||||||
|
cx, cy, cz = cos(a) * length, y0 + .35, sin(a) * length
|
||||||
|
lv, lf = [], []
|
||||||
|
lv.append((cx, cy + .28, cz))
|
||||||
|
for ring in range(1, 5):
|
||||||
|
th = pi * ring / 5
|
||||||
|
for k in range(8):
|
||||||
|
aa = 2 * pi * k / 8
|
||||||
|
f = 1 + .12 * sin(aa * 3 + ring)
|
||||||
|
lv.append((cx + .32 * f * sin(th) * cos(aa), cy + .28 * cos(th) * .8, cz + .32 * f * sin(th) * sin(aa)))
|
||||||
|
lv.append((cx, cy - .28, cz))
|
||||||
|
end = len(lv) - 1
|
||||||
|
for k in range(8):
|
||||||
|
lf.append((0, 1 + k, 1 + (k + 1) % 8))
|
||||||
|
for ring in range(3):
|
||||||
|
for k in range(8):
|
||||||
|
a0 = 1 + ring * 8 + k
|
||||||
|
b0 = 1 + ring * 8 + (k + 1) % 8
|
||||||
|
lf.append((a0, a0 + 8, b0 + 8, b0))
|
||||||
|
for k in range(8):
|
||||||
|
lf.append((end, end - 1 - k, end - 1 - (k + 1) % 8))
|
||||||
|
leaf = mesh('CompareTree_Leaf%d' % j, lv, lf, 'leaf' if j % 2 else 'leaf2')
|
||||||
|
finish(leaf)
|
||||||
|
parts.append(leaf)
|
||||||
|
bpy.ops.object.select_all(action='DESELECT')
|
||||||
|
for p in parts:
|
||||||
|
p.select_set(True)
|
||||||
|
bpy.context.view_layer.objects.active = parts[0]
|
||||||
|
bpy.ops.object.join()
|
||||||
|
ob = bpy.context.object
|
||||||
|
ob.name = 'CompareTree'
|
||||||
|
finish(ob)
|
||||||
|
return ob
|
||||||
|
|
||||||
|
def arena_tile():
|
||||||
|
vs = [(-2, 0, -2), (2, 0, -2), (2, 0, 2), (-2, 0, 2),
|
||||||
|
(-2, -.18, -2), (2, -.18, -2), (2, -.18, 2), (-2, -.18, 2)]
|
||||||
|
fs = [(0, 1, 2, 3), (4, 7, 6, 5), (0, 4, 5, 1), (1, 5, 6, 2), (2, 6, 7, 3), (3, 7, 4, 0)]
|
||||||
|
ob = mesh('CompareArenaTile', vs, fs, 'paving', smooth=False)
|
||||||
|
finish(ob)
|
||||||
|
return ob
|
||||||
|
|
||||||
|
records = []
|
||||||
|
ob = rock()
|
||||||
|
records.append(export(ob, 'compare-rock', 'compare_rock', 'prop_rock'))
|
||||||
|
ob = tree()
|
||||||
|
records.append(export(ob, 'compare-tree', 'compare_tree', 'prop_tree'))
|
||||||
|
ob = arena_tile()
|
||||||
|
records.append(export(ob, 'compare-arena-tile', 'compare_arena_tile', 'prop_arena'))
|
||||||
|
|
||||||
|
# Also re-export the three hero assets with stable compare IDs (copies of existing GLBs).
|
||||||
|
import shutil
|
||||||
|
for src_name, rec_id, role in [
|
||||||
|
('characters/ringborke.glb', 'compare_ringborke', 'figure_pattern'),
|
||||||
|
('characters/arbeiter.glb', 'compare_arbeiter', 'figure_worker'),
|
||||||
|
('models/core_team0.glb', 'compare_core', 'prop_core'),
|
||||||
|
]:
|
||||||
|
src = P / 'assets' / src_name
|
||||||
|
dst_name = src_name.split('/')[-1].replace('.glb', '') + '-compare.glb'
|
||||||
|
dst = OUT / dst_name
|
||||||
|
shutil.copy2(src, dst)
|
||||||
|
raw = dst.read_bytes()
|
||||||
|
records.append({
|
||||||
|
'id': rec_id,
|
||||||
|
'role': role,
|
||||||
|
'resource_path': 'res://assets/compare/' + dst_name,
|
||||||
|
'source_asset': 'res://assets/' + src_name,
|
||||||
|
'sha256': hashlib.sha256(raw).hexdigest(),
|
||||||
|
})
|
||||||
|
|
||||||
|
manifest = {
|
||||||
|
'scene': 'godot_unreal_compare',
|
||||||
|
'camera': {
|
||||||
|
'projection': 'orthogonal',
|
||||||
|
'game_size': 4.6,
|
||||||
|
'game_position': [0.0, 7.2, 6.8],
|
||||||
|
'game_look_at': [0.0, 0.5, 0.0],
|
||||||
|
'game_yaw': 0.2,
|
||||||
|
'close_size': 2.1,
|
||||||
|
'close_position': [1.3, 1.5, 1.9],
|
||||||
|
'close_look_at': [0.0, 0.9, 0.15],
|
||||||
|
},
|
||||||
|
'lighting': {
|
||||||
|
'key': {'rotation_degrees': [-40, -30, 0], 'color': '#ffd4a0', 'energy': 1.55},
|
||||||
|
'fill': {'rotation_degrees': [-32, 148, 0], 'color': '#a8c4e8', 'energy': 0.48},
|
||||||
|
'ambient': '#a8b4c8',
|
||||||
|
'tonemap': 'ACES',
|
||||||
|
},
|
||||||
|
'assets': records,
|
||||||
|
}
|
||||||
|
(SRC / 'compare-manifest.json').write_text(json.dumps(manifest, indent=2) + '\n')
|
||||||
|
(P / 'assets/compare/compare-manifest.json').write_text(json.dumps(manifest, indent=2) + '\n')
|
||||||
|
print('COMPARE_PROPS', [(r['id'], r.get('triangles')) for r in records], flush=True)
|
||||||
134
assets-source/phase14/compare-manifest.json
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
{
|
||||||
|
"scene": "godot_unreal_compare",
|
||||||
|
"camera": {
|
||||||
|
"projection": "orthogonal",
|
||||||
|
"game_size": 4.6,
|
||||||
|
"game_position": [
|
||||||
|
0.0,
|
||||||
|
7.2,
|
||||||
|
6.8
|
||||||
|
],
|
||||||
|
"game_look_at": [
|
||||||
|
0.0,
|
||||||
|
0.5,
|
||||||
|
0.0
|
||||||
|
],
|
||||||
|
"game_yaw": 0.2,
|
||||||
|
"close_size": 2.1,
|
||||||
|
"close_position": [
|
||||||
|
1.3,
|
||||||
|
1.5,
|
||||||
|
1.9
|
||||||
|
],
|
||||||
|
"close_look_at": [
|
||||||
|
0.0,
|
||||||
|
0.9,
|
||||||
|
0.15
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lighting": {
|
||||||
|
"key": {
|
||||||
|
"rotation_degrees": [
|
||||||
|
-40,
|
||||||
|
-30,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"color": "#ffd4a0",
|
||||||
|
"energy": 1.55
|
||||||
|
},
|
||||||
|
"fill": {
|
||||||
|
"rotation_degrees": [
|
||||||
|
-32,
|
||||||
|
148,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"color": "#a8c4e8",
|
||||||
|
"energy": 0.48
|
||||||
|
},
|
||||||
|
"ambient": "#a8b4c8",
|
||||||
|
"tonemap": "ACES"
|
||||||
|
},
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"id": "compare_rock",
|
||||||
|
"role": "prop_rock",
|
||||||
|
"resource_path": "res://assets/compare/compare-rock.glb",
|
||||||
|
"triangles": 360,
|
||||||
|
"bounds_godot": {
|
||||||
|
"min": [
|
||||||
|
-1.0129448175430298,
|
||||||
|
-0.760317325592041,
|
||||||
|
-1.350000023841858
|
||||||
|
],
|
||||||
|
"max": [
|
||||||
|
1.195098876953125,
|
||||||
|
0.8693029284477234,
|
||||||
|
0.1893923133611679
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"sha256": "6fe3b527975f227f9040782e24ea7c87693e038e8ad09cc2a5b50bf9221414a8",
|
||||||
|
"source": "assets-source/phase14/build_compare_props.py"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "compare_tree",
|
||||||
|
"role": "prop_tree",
|
||||||
|
"resource_path": "res://assets/compare/compare-tree.glb",
|
||||||
|
"triangles": 852,
|
||||||
|
"bounds_godot": {
|
||||||
|
"min": [
|
||||||
|
-0.9815748333930969,
|
||||||
|
-1.0770471096038818,
|
||||||
|
-2.430000066757202
|
||||||
|
],
|
||||||
|
"max": [
|
||||||
|
1.071698784828186,
|
||||||
|
1.0116279125213623,
|
||||||
|
-0.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"sha256": "80aa80d8bd71a1ed1837df4c43d80367dd4c7643e5e7ca41bee3e8e4ac35c533",
|
||||||
|
"source": "assets-source/phase14/build_compare_props.py"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "compare_arena_tile",
|
||||||
|
"role": "prop_arena",
|
||||||
|
"resource_path": "res://assets/compare/compare-arena-tile.glb",
|
||||||
|
"triangles": 12,
|
||||||
|
"bounds_godot": {
|
||||||
|
"min": [
|
||||||
|
-2.0,
|
||||||
|
-2.0,
|
||||||
|
-0.0
|
||||||
|
],
|
||||||
|
"max": [
|
||||||
|
2.0,
|
||||||
|
2.0,
|
||||||
|
0.18000000715255737
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"sha256": "c172a34a6a67bf247323bb66fc0132c87602deb179e277e16160c4b793eca9ae",
|
||||||
|
"source": "assets-source/phase14/build_compare_props.py"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "compare_ringborke",
|
||||||
|
"role": "figure_pattern",
|
||||||
|
"resource_path": "res://assets/compare/ringborke-compare.glb",
|
||||||
|
"source_asset": "res://assets/characters/ringborke.glb",
|
||||||
|
"sha256": "a9c61bcb7ae957c3e6b035ef994db3c8736b7a5f89d62724162dd606b76895b8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "compare_arbeiter",
|
||||||
|
"role": "figure_worker",
|
||||||
|
"resource_path": "res://assets/compare/arbeiter-compare.glb",
|
||||||
|
"source_asset": "res://assets/characters/arbeiter.glb",
|
||||||
|
"sha256": "6b61389e6806cf130376665bcf791c5948e070a49593702faebaafad52d7d6d2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "compare_core",
|
||||||
|
"role": "prop_core",
|
||||||
|
"resource_path": "res://assets/compare/core_team0-compare.glb",
|
||||||
|
"source_asset": "res://assets/models/core_team0.glb",
|
||||||
|
"sha256": "81a5305fc55482e6ca19524b72787d585211643d8842840efb4bdaf844660181"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
54
assets-source/phase15/basaltbastion-audit.json
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
{
|
||||||
|
"record": {
|
||||||
|
"id": "g04",
|
||||||
|
"role": "tank",
|
||||||
|
"name": "Basaltbastion",
|
||||||
|
"resource_path": "res://assets/characters/basaltbastion.glb",
|
||||||
|
"triangles": 17824,
|
||||||
|
"mesh_parts": 1,
|
||||||
|
"parts": [
|
||||||
|
"BasaltBody"
|
||||||
|
],
|
||||||
|
"bounds_godot": {
|
||||||
|
"min": [
|
||||||
|
-0.8817209601402283,
|
||||||
|
-0.019999999552965164,
|
||||||
|
-0.46514952182769775
|
||||||
|
],
|
||||||
|
"max": [
|
||||||
|
0.8817209601402283,
|
||||||
|
2.2275519371032715,
|
||||||
|
0.3866296112537384
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"skeletal": true,
|
||||||
|
"bones": 17,
|
||||||
|
"clips": [
|
||||||
|
"idle",
|
||||||
|
"start",
|
||||||
|
"walk",
|
||||||
|
"stop",
|
||||||
|
"attack_a",
|
||||||
|
"attack_b",
|
||||||
|
"hit",
|
||||||
|
"ability",
|
||||||
|
"death"
|
||||||
|
],
|
||||||
|
"sha256": "af7ec19ee2714f4bd884b61af15b225b79be7efea7aefa73a706fe981cafdbbe",
|
||||||
|
"source": "assets-source/phase15/basaltbastion.blend"
|
||||||
|
},
|
||||||
|
"exported_clips": [
|
||||||
|
"ability",
|
||||||
|
"attack_a",
|
||||||
|
"attack_b",
|
||||||
|
"death",
|
||||||
|
"hit",
|
||||||
|
"idle",
|
||||||
|
"start",
|
||||||
|
"stop",
|
||||||
|
"walk"
|
||||||
|
],
|
||||||
|
"skins": 1,
|
||||||
|
"source_parts": 112,
|
||||||
|
"seed": 63
|
||||||
|
}
|
||||||
BIN
assets-source/phase15/basaltbastion.blend
Normal file
BIN
assets-source/phase15/basaltbastion.blend1
Normal file
496
assets-source/phase15/build_basaltbastion.py
Normal file
|
|
@ -0,0 +1,496 @@
|
||||||
|
"""Basaltbastion: weighted Glutpakt tank with magma seams and authored clips.
|
||||||
|
Blender Z up, -Y forward. Run in a fresh background Blender process.
|
||||||
|
"""
|
||||||
|
from pathlib import Path
|
||||||
|
import bpy, bmesh, math, random, json, hashlib
|
||||||
|
from mathutils import Vector, Quaternion
|
||||||
|
from math import sin, cos, pi
|
||||||
|
P = Path(__file__).resolve().parents[2]
|
||||||
|
OUT = P / 'assets/characters'
|
||||||
|
OUT.mkdir(exist_ok=True)
|
||||||
|
SRC = Path(__file__).resolve().parent
|
||||||
|
random.seed(63)
|
||||||
|
bpy.context.preferences.filepaths.save_version = 0
|
||||||
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||||
|
S = 1.0
|
||||||
|
parts = []
|
||||||
|
|
||||||
|
def coord(v):
|
||||||
|
return Vector(v) * S
|
||||||
|
|
||||||
|
def material(name, color, rough=.8, metal=0.0, emission=None):
|
||||||
|
m = bpy.data.materials.new('BB_' + name)
|
||||||
|
m.diffuse_color = (*color, 1)
|
||||||
|
m.use_nodes = True
|
||||||
|
n = m.node_tree.nodes
|
||||||
|
l = m.node_tree.links
|
||||||
|
b = n.get('Principled BSDF')
|
||||||
|
b.inputs['Base Color'].default_value = (*color, 1)
|
||||||
|
b.inputs['Roughness'].default_value = rough
|
||||||
|
b.inputs['Metallic'].default_value = metal
|
||||||
|
if emission is not None:
|
||||||
|
b.inputs['Emission Color'].default_value = (*emission, 1)
|
||||||
|
b.inputs['Emission Strength'].default_value = .65
|
||||||
|
else:
|
||||||
|
v = n.new('ShaderNodeVertexColor')
|
||||||
|
v.layer_name = 'SurfaceColor'
|
||||||
|
l.new(v.outputs['Color'], b.inputs['Base Color'])
|
||||||
|
noise = n.new('ShaderNodeTexNoise')
|
||||||
|
noise.inputs['Scale'].default_value = 18
|
||||||
|
noise.inputs['Detail'].default_value = 3.5
|
||||||
|
bump = n.new('ShaderNodeBump')
|
||||||
|
bump.inputs['Strength'].default_value = .35
|
||||||
|
bump.inputs['Distance'].default_value = .02
|
||||||
|
l.new(noise.outputs['Fac'], bump.inputs['Height'])
|
||||||
|
l.new(bump.outputs['Normal'], b.inputs['Normal'])
|
||||||
|
return m
|
||||||
|
|
||||||
|
M = {
|
||||||
|
'basalt': material('Basalt', (.040, .049, .059), .92),
|
||||||
|
'basalt2': material('BasaltLight', (.085, .101, .119), .88),
|
||||||
|
'crack': material('Magma', (.78, .19, .022), .55, emission=(.95, .11, .009)),
|
||||||
|
'iron': material('Iron', (.28, .30, .33), .4, .8),
|
||||||
|
'brass': material('Brass', (.55, .38, .14), .38, .7),
|
||||||
|
'ember': material('Ember', (.95, .32, .045), .4, emission=(1.0, .24, .018)),
|
||||||
|
}
|
||||||
|
|
||||||
|
def mesh(name, verts, faces, kind, bone, uv=None, colors=None, weights=None, smooth=True):
|
||||||
|
me = bpy.data.meshes.new(name)
|
||||||
|
me.from_pydata([coord(v) for v in verts], [], faces)
|
||||||
|
me.update()
|
||||||
|
ob = bpy.data.objects.new(name, me)
|
||||||
|
bpy.context.collection.objects.link(ob)
|
||||||
|
me.materials.append(M[kind])
|
||||||
|
parts.append(ob)
|
||||||
|
for f in me.polygons:
|
||||||
|
f.use_smooth = smooth
|
||||||
|
tex = me.uv_layers.new(name='UVMap')
|
||||||
|
col = me.color_attributes.new(name='SurfaceColor', type='FLOAT_COLOR', domain='CORNER')
|
||||||
|
base = M[kind].diffuse_color[:3]
|
||||||
|
for f in me.polygons:
|
||||||
|
for li in f.loop_indices:
|
||||||
|
i = me.loops[li].vertex_index
|
||||||
|
v = verts[i]
|
||||||
|
tex.data[li].uv = uv[i] if uv else (v[0] * .8 + v[1] * .2, v[2] * .9)
|
||||||
|
fac = .88 + .12 * sin(v[0] * 17 + v[2] * 11) + .04 * cos(v[1] * 41)
|
||||||
|
c = colors[i] if colors else tuple(max(.01, x * fac) for x in base)
|
||||||
|
col.data[li].color = (*c, 1)
|
||||||
|
groups = {}
|
||||||
|
for i in range(len(verts)):
|
||||||
|
ws = weights[i] if weights else {bone: 1.0}
|
||||||
|
total = sum(ws.values())
|
||||||
|
for key, w in ws.items():
|
||||||
|
if key not in groups:
|
||||||
|
groups[key] = ob.vertex_groups.new(name=key)
|
||||||
|
groups[key].add([i], w / total, 'REPLACE')
|
||||||
|
return ob
|
||||||
|
|
||||||
|
def mixweights(t, bones):
|
||||||
|
if len(bones) == 1:
|
||||||
|
return {bones[0]: 1}
|
||||||
|
t = max(0, min(.999999, t)) * (len(bones) - 1)
|
||||||
|
i = int(t)
|
||||||
|
w = t - i
|
||||||
|
return {bones[i]: 1 - w, bones[i + 1]: w}
|
||||||
|
|
||||||
|
def tube(name, points, radii, kind, bones, sides=20, steps=36, flatten=1.0, groove=.06):
|
||||||
|
points = [Vector(x) for x in points]
|
||||||
|
vs, uv, ws, loops, fs = [], [], [], [], []
|
||||||
|
|
||||||
|
def point(t):
|
||||||
|
a = min(len(points) - 2, int(t * (len(points) - 1)))
|
||||||
|
u = t * (len(points) - 1) - a
|
||||||
|
p0, p1, p2, p3 = points[max(a - 1, 0)], points[a], points[a + 1], points[min(a + 2, len(points) - 1)]
|
||||||
|
p = .5 * ((2 * p1) + (-p0 + p2) * u + (2 * p0 - 5 * p1 + 4 * p2 - p3) * u * u + (-p0 + 3 * p1 - 3 * p2 + p3) * u * u * u)
|
||||||
|
return p, radii[a] * (1 - u) + radii[a + 1] * u
|
||||||
|
|
||||||
|
for j in range(steps + 1):
|
||||||
|
t = j / steps
|
||||||
|
p, r = point(t)
|
||||||
|
tan = (point(min(1, t + .002))[0] - point(max(0, t - .002))[0]).normalized()
|
||||||
|
u = tan.cross(Vector((0, 1, 0))).normalized()
|
||||||
|
v = tan.cross(u)
|
||||||
|
loop = []
|
||||||
|
for k in range(sides):
|
||||||
|
a = 2 * pi * k / sides
|
||||||
|
mod = 1 + groove * (sin(a * 6 + t * 5) + .35 * sin(a * 11 - t * 7))
|
||||||
|
vs.append(p + r * mod * (u * cos(a) + v * sin(a) * flatten))
|
||||||
|
uv.append((k / sides, t))
|
||||||
|
ws.append(mixweights(t, bones))
|
||||||
|
loop.append(len(vs) - 1)
|
||||||
|
loops.append(loop)
|
||||||
|
for a, b in zip(loops, loops[1:]):
|
||||||
|
for k in range(sides):
|
||||||
|
q = (k + 1) % sides
|
||||||
|
fs.append((a[k], a[q], b[q], b[k]))
|
||||||
|
fs.extend([tuple(reversed(loops[0])), tuple(loops[-1])])
|
||||||
|
return mesh(name, vs, fs, kind, bones[0], uv, weights=ws)
|
||||||
|
|
||||||
|
def ell(name, c, r, kind, bone, segments=22, rings=12):
|
||||||
|
vs = [(c[0], c[1], c[2] + r[2])]
|
||||||
|
fs = []
|
||||||
|
for j in range(1, rings):
|
||||||
|
th = pi * j / rings
|
||||||
|
for k in range(segments):
|
||||||
|
a = 2 * pi * k / segments
|
||||||
|
f = 1 + .05 * sin(a * 4 + j * 1.3)
|
||||||
|
vs.append((c[0] + r[0] * sin(th) * cos(a) * f, c[1] + r[1] * sin(th) * sin(a) * f, c[2] + r[2] * cos(th)))
|
||||||
|
vs.append((c[0], c[1], c[2] - r[2]))
|
||||||
|
end = len(vs) - 1
|
||||||
|
for k in range(segments):
|
||||||
|
fs.append((0, 1 + k, 1 + (k + 1) % segments))
|
||||||
|
for j in range(rings - 2):
|
||||||
|
for k in range(segments):
|
||||||
|
a = 1 + j * segments + k
|
||||||
|
b = 1 + j * segments + (k + 1) % segments
|
||||||
|
fs.append((a, a + segments, b + segments, b))
|
||||||
|
for k in range(segments):
|
||||||
|
fs.append((end, end - 1 - k, end - 1 - (k + 1) % segments))
|
||||||
|
return mesh(name, vs, fs, kind, bone)
|
||||||
|
|
||||||
|
def plate(name, c, w, h, d, bone, tilt=0.0, cant=0.0, yaw=0.0, kind='basalt2'):
|
||||||
|
"""Chipped basalt lamina: irregular ten-sided outline and broad fracture facets.
|
||||||
|
|
||||||
|
Three offset perimeter rings create a real bevel and broken corners. Each
|
||||||
|
slab has a stable seeded outline, rather than a box with random shading.
|
||||||
|
"""
|
||||||
|
from mathutils import Euler
|
||||||
|
rng = random.Random(name)
|
||||||
|
rot = Euler((tilt, cant, yaw)).to_matrix()
|
||||||
|
n = 10
|
||||||
|
outline = []
|
||||||
|
for k in range(n):
|
||||||
|
a = 2 * pi * k / n + .08
|
||||||
|
f = rng.uniform(.88, 1.08)
|
||||||
|
outline.append((cos(a) * w * .5 * f, sin(a) * h * .5 * f))
|
||||||
|
vs, fs, colors = [], [], []
|
||||||
|
base = M[kind].diffuse_color[:3]
|
||||||
|
for ring, (scale, y) in enumerate([(.80, d * .5), (1, d * .06), (.83, -d * .5)]):
|
||||||
|
for k, (x, z) in enumerate(outline):
|
||||||
|
p = rot @ Vector((x * scale, y + rng.uniform(-.03, .03) * d, z * scale)) + Vector(c)
|
||||||
|
vs.append(p)
|
||||||
|
f = [.68, .83, 1.04][ring] * rng.uniform(.93, 1.09)
|
||||||
|
colors.append(tuple(v * f for v in base))
|
||||||
|
fs.append(tuple(reversed(range(n))))
|
||||||
|
for ring in range(2):
|
||||||
|
for k in range(n):
|
||||||
|
j = (k + 1) % n
|
||||||
|
fs.append((ring * n + k, ring * n + j, (ring + 1) * n + j, (ring + 1) * n + k))
|
||||||
|
# Off-center ridge gives three large readable fracture planes on the face.
|
||||||
|
p = rot @ Vector((w * .03, -d * .54, h * -.05)) + Vector(c)
|
||||||
|
vs.append(p)
|
||||||
|
colors.append(tuple(v * 1.06 for v in base))
|
||||||
|
for k in range(n):
|
||||||
|
fs.append((2 * n + k, 2 * n + (k + 1) % n, 3 * n))
|
||||||
|
return mesh(name, vs, fs, kind, bone, colors=colors, smooth=False)
|
||||||
|
|
||||||
|
def seam(name, points, bone, width=.007):
|
||||||
|
"""Narrow fissure in a recessed dark channel, never a surface rope."""
|
||||||
|
radii = [width * (.65 if k in (0, len(points) - 1) else 1) for k in range(len(points))]
|
||||||
|
tube(name + '_recess', [(x, y + .003, z) for x, y, z in points],
|
||||||
|
[r * 2.6 for r in radii], 'basalt', [bone], 6, 12, groove=0)
|
||||||
|
return tube(name, points, radii, 'crack', [bone], 6, 12, groove=0)
|
||||||
|
|
||||||
|
# ---- Compact, asymmetric guardian built from interlocking basalt strata ----
|
||||||
|
# Deep core volumes stay recessed under the overlapping hard plates.
|
||||||
|
plate('Rib_Core', (0, .035, 1.09), .89, .72, .53, 'Spine', kind='basalt')
|
||||||
|
plate('Pelvic_Core', (0, .015, .72), .70, .43, .45, 'Pelvis', kind='basalt')
|
||||||
|
# Broad diagonal chest strata, with gaps instead of a rectangular breastplate.
|
||||||
|
plate('Pectoral_L', (-.20, -.19, 1.26), .56, .38, .28, 'Chest', cant=-.18, yaw=-.12)
|
||||||
|
plate('Pectoral_R', (.21, -.18, 1.22), .53, .40, .26, 'Chest', cant=.23, yaw=.13)
|
||||||
|
plate('Sternum_Keystone', (-.01, -.22, 1.02), .31, .36, .26, 'Spine', cant=-.11)
|
||||||
|
plate('Rib_Low_L', (-.22, -.17, .91), .38, .30, .20, 'Spine', cant=-.20, yaw=-.20, kind='basalt')
|
||||||
|
plate('Rib_Low_R', (.23, -.17, .87), .39, .30, .22, 'Spine', cant=.26, yaw=.16)
|
||||||
|
plate('Hip_Keystone', (0, -.13, .69), .54, .25, .30, 'Pelvis', cant=.10)
|
||||||
|
for s, side in [(-1, 'L'), (1, 'R')]:
|
||||||
|
for j in range(3):
|
||||||
|
plate('Flank_%s%d' % (side, j), (s * (.34 - j * .025), .08, 1.14 - j * .18),
|
||||||
|
.24, .27, .36, 'Chest' if j == 0 else 'Spine', cant=s * .25, yaw=s * .32, kind='basalt')
|
||||||
|
seam('Sternum_Fault', [(-.045, -.343, 1.39), (-.025, -.335, 1.27), (.016, -.326, 1.19), (-.016, -.36, 1.10)], 'Chest', .006)
|
||||||
|
seam('Lower_Fault', [(.055, -.36, 1.11), (.071, -.365, 1.00), (.042, -.35, .90)], 'Spine', .006)
|
||||||
|
seam('Chest_Fault_L', [(-.38, -.27, 1.35), (-.25, -.321, 1.30), (-.14, -.326, 1.31)], 'Chest', .005)
|
||||||
|
seam('Chest_Fault_R', [(.14, -.309, 1.13), (.23, -.310, 1.17), (.38, -.28, 1.12)], 'Chest', .005)
|
||||||
|
|
||||||
|
# The recessed neck and slanted mask make the head part of the stone mass.
|
||||||
|
plate('Neck', (0, .045, 1.47), .35, .30, .33, 'Chest', kind='basalt')
|
||||||
|
plate('Cranium', (0, .015, 1.73), .53, .51, .46, 'Head', cant=-.035, kind='basalt')
|
||||||
|
plate('Temple_L', (-.185, -.13, 1.76), .20, .33, .18, 'Head', cant=-.13, yaw=-.22)
|
||||||
|
plate('Temple_R', (.185, -.13, 1.76), .21, .34, .18, 'Head', cant=.17, yaw=.18)
|
||||||
|
plate('Jaw', (0, -.175, 1.57), .35, .21, .22, 'Head', cant=.025)
|
||||||
|
plate('Nose_Ridge', (0, -.225, 1.72), .075, .27, .13, 'Head', kind='basalt2')
|
||||||
|
for s, side in [(-1, 'L'), (1, 'R')]:
|
||||||
|
plate('Eye_Recess_' + side, (s * .115, -.208, 1.785), .174, .080, .08, 'Head', cant=s * .10, kind='basalt')
|
||||||
|
plate('Eye_' + side, (s * .115, -.253, 1.786), .111, .023, .012, 'Head', cant=s * .11, kind='ember')
|
||||||
|
plate('Brow_' + side, (s * .113, -.214, 1.849), .252, .10, .13, 'Head', cant=s * .14)
|
||||||
|
plate('Cheek_' + side, (s * .118, -.211, 1.679), .18, .17, .13, 'Head', cant=-s * .30)
|
||||||
|
# Broken geological crown: no symmetric animal horns.
|
||||||
|
plate('Crown_Shard_L', (-.16, .08, 1.98), .21, .38, .28, 'Head', cant=-.23, tilt=.14)
|
||||||
|
plate('Crown_Shard_R', (.13, .10, 1.92), .23, .24, .29, 'Head', cant=.28, tilt=.12, kind='basalt')
|
||||||
|
plate('Crown_Shard_Back', (.02, .185, 1.87), .25, .31, .23, 'Head', cant=-.10, tilt=-.20)
|
||||||
|
seam('Jaw_Fissure', [(.020, -.29, 11.63), (.001, -.298, 1.58), (.022, -.26, 1.53)], 'Head', .004)
|
||||||
|
|
||||||
|
# Arms have dark narrow joints, layered mineral deltoids and hefty split fists.
|
||||||
|
for s, side in [(-1, 'L'), (1, 'R')]:
|
||||||
|
upper, fore, hand = ['UpperArm.' + side, 'Forearm.' + side, 'Hand.' + side]
|
||||||
|
tube('UpperArm_Core_' + side, [(s * .39, .02, 1.30), (s * .57, -.02, 1.06)], [.145, .12], 'basalt', [upper], 10, 12, groove=.035)
|
||||||
|
tube('Elbow_Sleeve_' + side, [(s * .55, -.012, 1.12), (s * .61, -.055, .98)], [.105, .10], 'basalt', [upper, fore], 10, 8, groove=.04)
|
||||||
|
plate('UpperArm_Shell_' + side, (s * .52, -.01, 1.18), .31, .37, .30, upper, cant=s * .20, kind='basalt')
|
||||||
|
large = 1.18 if side == 'L' else .92
|
||||||
|
plate('Shoulder_Cap_' + side, (s * .50, .005, 1.40), .50 * large, .34 * large, .48, upper, cant=s * .20)
|
||||||
|
plate('Shoulder_Upper_Stratum_' + side, (s * .50, .035, 1.53), .35 * large, .17, .37, upper, cant=s * .22, kind='basalt')
|
||||||
|
plate('Shoulder_Outer_Stratum_' + side, (s * .65, .015, 11.33), .27, .31, .33, upper, cant=-s * .20)
|
||||||
|
plate('Forearm_Core_' + side, (s * .64, -.09, .91), .31, .36, .31, fore, cant=s * .13, kind='basalt')
|
||||||
|
plate('Gauntlet_Front_' + side, (s * .665, -.20, .90), .32, .33, .22, fore, cant=s * .18)
|
||||||
|
plate('Gauntlet_Outer_' + side, (s * .77, -.08, .89), .19, .30, .30, fore, cant=-s * .12, kind='basalt')
|
||||||
|
plate('Fist_' + side, (s * .72, -.19, .665), .37, .29, .34, hand, cant=-s * .08)
|
||||||
|
for k in range(3):
|
||||||
|
plate('Knuckle_%s%d' % (side, k), (s * .72 + (k - 1) * .10, -.342, .69), .11, .12, .10, hand, cant=.08 * (k - 1), kind='basalt')
|
||||||
|
plate('Thumb_' + side, (s * .58, -.19, .65), .13, .20, .20, hand, cant=s * .20)
|
||||||
|
seam('Forearm_Fault_' + side, [(s * .65, -.308, 1.025), (s * .685, -.312, .92), (s * .65, -.306, .83)], fore, .0055)
|
||||||
|
plate('Bastion_Shoulder_Shard', (-.64, .08, 1.61), .20, .35, .32, 'UpperArm.L', cant=-.24, tilt=.12)
|
||||||
|
|
||||||
|
# Broad feet touch the floor; segmented leg armor does not bridge rotating joints.
|
||||||
|
for s, side in [(-1, 'L'), (1, 'R')]:
|
||||||
|
thigh, shin, foot = ['Thigh.' + side, 'Shin.' + side, 'Foot.' + side]
|
||||||
|
plate('Thigh_' + side, (s * .225, .01, .56), .35, .39, .36, thigh, cant=s * .04, kind='basalt')
|
||||||
|
plate('Thigh_Stratum_' + side, (s * .23, -.12, .62), .31, .22, .18, thigh, cant=s * .15)
|
||||||
|
tube('Knee_Joint_' + side, [(s * .24, 0, .47), (s * .245, -.006, .36)], [.125, .12], 'basalt', [thigh, shin], 10, 8, groove=.025)
|
||||||
|
plate('Shin_' + side, (s * .25, -.01, .255), .33, .36, .33, shin, cant=-s * .04)
|
||||||
|
plate('Knee_' + side, (s * .24, -.15, .43), .27, .21, .18, shin, cant=s * .12, kind='basalt')
|
||||||
|
plate('Foot_' + side, (s * .25, -.10, .086), .40, .135, .49, foot, kind='basalt2')
|
||||||
|
for k in range(2):
|
||||||
|
plate('Toe_%s%d' % (side, k), (s * .25 + (k - .5) * .15, -.285, .065), .18, .10, .17, foot, cant=.08 * (k - .5), kind='basalt')
|
||||||
|
seam('Shin_Fault_' + side, [(s * .23, -.177, .36), (s * .26, -.186, .26), (s * .24, -.16, .17)], shin, .005)
|
||||||
|
|
||||||
|
# Back reads as stacked columnar basalt; the vent glows between strata.
|
||||||
|
for j in range(4):
|
||||||
|
plate('Back_Stratum_%d' % j, ((-.03 if j % 2 else .02), .235, .86 + j * .86 + j * .16),
|
||||||
|
.62 - j * .025, .24, .28, 'Spine' if j < 2 else 'Chest', tilt=.10, cant=.10 * (-1) ** j, kind='basalt')
|
||||||
|
for s, side in [(-1, 'L'), (1, 'R')]:
|
||||||
|
seam('Back_Vent_' + side, [(s * .08, .365, 1.02), (s * .11, .38, 1.17), (s * .07, .37, 1.32)], 'Chest', .006)
|
||||||
|
|
||||||
|
# Cleanup and skinning.
|
||||||
|
for ob in parts:
|
||||||
|
bm = bmesh.new()
|
||||||
|
bm.from_mesh(ob.data)
|
||||||
|
bmesh.ops.remove_doubles(bm, verts=list(bm.verts), dist=.0000005)
|
||||||
|
bmesh.ops.triangulate(bm, faces=list(bm.faces))
|
||||||
|
bmesh.ops.dissolve_degenerate(bm, edges=list(bm.edges), dist=.0000001)
|
||||||
|
bmesh.ops.recalc_face_normals(bm, faces=list(bm.faces))
|
||||||
|
if bm.calc_volume(signed=True) < 0:
|
||||||
|
bmesh.ops.reverse_faces(bm, faces=list(bm.faces))
|
||||||
|
bm.to_mesh(ob.data)
|
||||||
|
bm.free()
|
||||||
|
ob.data.update()
|
||||||
|
|
||||||
|
bpy.ops.object.armature_add(enter_editmode=True)
|
||||||
|
rig = bpy.context.object
|
||||||
|
rig.name = 'BasaltRig'
|
||||||
|
ed = rig.data.edit_bones
|
||||||
|
ed.remove(ed[0])
|
||||||
|
defs = {}
|
||||||
|
|
||||||
|
def bone(name, head, tail, parent=None):
|
||||||
|
b = ed.new(name)
|
||||||
|
b.head = coord(head)
|
||||||
|
b.tail = coord(tail)
|
||||||
|
if parent:
|
||||||
|
b.parent = ed[parent]
|
||||||
|
defs[name] = (head, tail, parent)
|
||||||
|
|
||||||
|
bone('Root', (0, 0, 0), (0, 0, .15))
|
||||||
|
bone('Pelvis', (0, .02, .70), (0, .02, .95), 'Root')
|
||||||
|
bone('Spine', (0, .02, .95), (0, .02, 1.25), 'Pelvis')
|
||||||
|
bone('Chest', (0, .02, 1.25), (0, .04, 1.50), 'Spine')
|
||||||
|
bone('Head', (0, .04, 1.50), (0, 0, 1.95), 'Chest')
|
||||||
|
for s, side in [(-1, 'L'), (1, 'R')]:
|
||||||
|
bone('UpperArm.' + side, (s * .38, .02, 1.30), (s * .58, -.02, 1.05), 'Chest')
|
||||||
|
bone('Forearm.' + side, (s * .58, -.02, 1.05), (s * .68, -.12, .78), 'UpperArm.' + side)
|
||||||
|
bone('Hand.' + side, (s * .68, -.12, .78), (s * .72, -.22, .65), 'Forearm.' + side)
|
||||||
|
bone('Thigh.' + side, (s * .20, .02, .72), (s * .24, 0, .42), 'Pelvis')
|
||||||
|
bone('Shin.' + side, (s * .24, 0, .42), (s * .25, -.02, .12), 'Thigh.' + side)
|
||||||
|
bone('Foot.' + side, (s * .25, -.02, .12), (s * .25, -.22, .06), 'Shin.' + side)
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
for ob in parts:
|
||||||
|
mod = ob.modifiers.new('Weighted basalt', 'ARMATURE')
|
||||||
|
mod.object = rig
|
||||||
|
ob.parent = rig
|
||||||
|
|
||||||
|
clips = {'idle': (3.0, True), 'start': (.4, False), 'walk': (1.5, True), 'stop': (.4, False),
|
||||||
|
'attack_a': (1.3, False), 'attack_b': (1.3, False), 'hit': (.5, False), 'ability': (1.7, False), 'death': (2.0, False)}
|
||||||
|
|
||||||
|
def curve(t, keys):
|
||||||
|
for i in range(len(keys) - 1):
|
||||||
|
a, x = keys[i]
|
||||||
|
b, y = keys[i + 1]
|
||||||
|
if t <= b:
|
||||||
|
u = max(0, (t - a) / (b - a))
|
||||||
|
u = u * u * (3 - 2 * u)
|
||||||
|
return x * (1 - u) + y * u
|
||||||
|
return keys[-1][1]
|
||||||
|
|
||||||
|
def animate(name, t, dur):
|
||||||
|
rotations = {}
|
||||||
|
locs = {}
|
||||||
|
|
||||||
|
def rot(b, x=0, y=0, z=0):
|
||||||
|
rotations[b] = (x, y, z)
|
||||||
|
|
||||||
|
if name == 'idle':
|
||||||
|
q = sin(2 * pi * t / dur)
|
||||||
|
rot('Spine', .01 * q)
|
||||||
|
rot('Chest', -.015 * q, 0, .008 * q)
|
||||||
|
rot('Head', .018 * q, 0, -.01 * q)
|
||||||
|
for s, side in [(-1, 'L'), (1, 'R')]:
|
||||||
|
rot('Forearm.' + side, .015 * q)
|
||||||
|
elif name in ('walk', 'start', 'stop'):
|
||||||
|
fade = 1 if name == 'walk' else sin(pi * .5 * t / dur) if name == 'start' else cos(pi * .5 * t / dur)
|
||||||
|
phase = t / 1.5 * 2 * pi
|
||||||
|
roll = sin(phase) * fade
|
||||||
|
rot('Pelvis', .012, 0, .03 * roll)
|
||||||
|
rot('Spine', -.04 * fade, 0, -.04 * roll)
|
||||||
|
rot('Chest', .015 * fade, 0, .02 * roll)
|
||||||
|
rot('Head', .03 * fade, 0, -.015 * roll)
|
||||||
|
for s, side in [(-1, 'L'), (1, 'R')]:
|
||||||
|
q = sin(phase + (0 if s < 0 else pi))
|
||||||
|
lift = max(0, q)
|
||||||
|
rot('Thigh.' + side, .30 * q * fade)
|
||||||
|
rot('Shin.' + side, -.36 * lift * fade)
|
||||||
|
rot('Foot.' + side, (-.30 * q + .36 * lift) * fade)
|
||||||
|
rot('UpperArm.' + side, -.10 * q * fade, 0, s * .02 * fade)
|
||||||
|
rot('Forearm.' + side, -.08 * fade)
|
||||||
|
locs['Root'] = (0, 0, (-.01 + .01 * cos(phase * 2)) * fade)
|
||||||
|
elif name == 'attack_a':
|
||||||
|
# Left magma punch, contact at 0.70
|
||||||
|
q = curve(t / dur, [(0, 0), (.30, -.35), (.52, -.55), (.70, 1), (.80, .82), (1, 0)])
|
||||||
|
rot('Spine', -.10 * q, .08 * q, .06 * q)
|
||||||
|
rot('Chest', -.08 * q, .06 * q, .10 * q)
|
||||||
|
rot('Head', .10 * q, 0, -.06 * q)
|
||||||
|
rot('UpperArm.L', -.95 * q, -.08 * q, .10 * q)
|
||||||
|
rot('Forearm.L', -.30 * q)
|
||||||
|
rot('Hand.L', .20 * q)
|
||||||
|
rot('UpperArm.R', .15 * q)
|
||||||
|
rot('Thigh.L', .05 * q)
|
||||||
|
locs['Root'] = (0, -.04 * q, -.01 * max(0, q))
|
||||||
|
elif name == 'attack_b':
|
||||||
|
# Right slam, contact at 0.70
|
||||||
|
q = curve(t / dur, [(0, 0), (.30, -.35), (.52, -.55), (.70, 1), (.80, .82), (1, 0)])
|
||||||
|
rot('Spine', -.10 * q, -.08 * q, -.06 * q)
|
||||||
|
rot('Chest', -.08 * q, -.06 * q, -.10 * q)
|
||||||
|
rot('Head', .10 * q, 0, .06 * q)
|
||||||
|
rot('UpperArm.R', -.95 * q, .08 * q, -.10 * q)
|
||||||
|
rot('Forearm.R', -.30 * q)
|
||||||
|
rot('Hand.R', .20 * q)
|
||||||
|
rot('UpperArm.L', .15 * q)
|
||||||
|
rot('Thigh.R', .05 * q)
|
||||||
|
locs['Root'] = (0, -.04 * q, -.01 * max(0, q))
|
||||||
|
elif name == 'hit':
|
||||||
|
q = curve(t, [(0, 0), (.08, 1), (.22, .5), (.5, 0)])
|
||||||
|
rot('Spine', .10 * q, 0, -.06 * q)
|
||||||
|
rot('Head', .12 * q, 0, .05 * q)
|
||||||
|
elif name == 'ability':
|
||||||
|
# Defensive stance: arms cross, magma vents flare.
|
||||||
|
q = curve(t / dur, [(0, 0), (.35, 1), (1.1, .9), (1.7, 0)])
|
||||||
|
rot('Spine', .06 * q)
|
||||||
|
rot('Chest', .04 * q)
|
||||||
|
rot('UpperArm.L', -.45 * q, -.30 * q, .55 * q)
|
||||||
|
rot('Forearm.L', -.70 * q)
|
||||||
|
rot('UpperArm.R', -.45 * q, .30 * q, -.55 * q)
|
||||||
|
rot('Forearm.R', -.70 * q)
|
||||||
|
rot('Head', -.08 * q)
|
||||||
|
locs['Root'] = (0, 0, -.03 * q)
|
||||||
|
elif name == 'death':
|
||||||
|
q = curve(t, [(0, 0), (.2, .08), (.7, .45), (1.25, 1), (1.55, .92), (2.0, 1)])
|
||||||
|
rot('Root', -.80 * q, 0, .10 * q)
|
||||||
|
locs['Root'] = (0, -.12 * q, -.10 * q)
|
||||||
|
rot('Pelvis', -.15 * q)
|
||||||
|
rot('Spine', -.20 * q)
|
||||||
|
rot('Head', -.16 * q)
|
||||||
|
for s, side in [(-1, 'L'), (1, 'R')]:
|
||||||
|
rot('UpperArm.' + side, -.25 * q, 0, s * .12 * q)
|
||||||
|
rot('Forearm.' + side, -.35 * q)
|
||||||
|
rot('Thigh.' + side, .20 * q)
|
||||||
|
rot('Shin.' + side, -.32 * q)
|
||||||
|
for pb in rig.pose.bones:
|
||||||
|
pb.rotation_mode = 'QUATERNION'
|
||||||
|
r = rotations.get(pb.name, (0, 0, 0))
|
||||||
|
q = Quaternion((0, 0, 1), r[2]) @ Quaternion((0, 1, 0), r[1]) @ Quaternion((1, 0, 0), r[0])
|
||||||
|
basis = pb.bone.matrix_local.to_quaternion()
|
||||||
|
pb.rotation_quaternion = basis.inverted() @ q @ basis
|
||||||
|
pb.location = pb.bone.matrix_local.to_3x3().inverted() @ (Vector(locs.get(pb.name, (0, 0, 0))) * S)
|
||||||
|
|
||||||
|
for name, (dur, loop) in clips.items():
|
||||||
|
rig.animation_data_clear()
|
||||||
|
action = bpy.data.actions.new(name)
|
||||||
|
action.use_fake_user = True
|
||||||
|
rig.animation_data_create()
|
||||||
|
rig.animation_data.action = action
|
||||||
|
for frame in range(round(dur * 30) + 1):
|
||||||
|
animate(name, frame / 30, dur)
|
||||||
|
for pb in rig.pose.bones:
|
||||||
|
pb.keyframe_insert('rotation_quaternion', frame=frame)
|
||||||
|
pb.keyframe_insert('location', frame=frame)
|
||||||
|
action['loop'] = loop
|
||||||
|
rig.animation_data_clear()
|
||||||
|
rig.animation_data_create()
|
||||||
|
rig.animation_data.action = bpy.data.actions['idle']
|
||||||
|
for pb in rig.pose.bones:
|
||||||
|
pb.rotation_quaternion = Quaternion()
|
||||||
|
pb.location = (0, 0, 0)
|
||||||
|
bpy.context.scene.render.fps = 30
|
||||||
|
bpy.context.scene.frame_set(0)
|
||||||
|
|
||||||
|
bpy.ops.wm.save_as_mainfile(filepath=str(SRC / 'basaltbastion.blend'))
|
||||||
|
bpy.ops.file.make_paths_relative()
|
||||||
|
bpy.ops.wm.save_as_mainfile(filepath=str(SRC / 'basaltbastion.blend'))
|
||||||
|
bpy.ops.object.select_all(action='DESELECT')
|
||||||
|
for ob in parts:
|
||||||
|
ob.select_set(True)
|
||||||
|
bpy.context.view_layer.objects.active = parts[0]
|
||||||
|
bpy.ops.object.join()
|
||||||
|
body = bpy.context.object
|
||||||
|
body.name = 'BasaltBody'
|
||||||
|
body.select_set(True)
|
||||||
|
rig.select_set(True)
|
||||||
|
bpy.context.view_layer.objects.active = rig
|
||||||
|
bpy.ops.export_scene.gltf(
|
||||||
|
filepath=str(OUT / 'basaltbastion.glb'),
|
||||||
|
export_format='GLB', use_selection=True,
|
||||||
|
export_animations=True, export_animation_mode='ACTIONS',
|
||||||
|
export_force_sampling=True, export_frame_range=False,
|
||||||
|
export_anim_slide_to_zero=True, export_skins=True,
|
||||||
|
export_all_influences=False, export_def_bones=True,
|
||||||
|
export_anim_single_armature=True, export_materials='EXPORT', export_yup=True,
|
||||||
|
)
|
||||||
|
raw = (OUT / 'basaltbastion.glb').read_bytes()
|
||||||
|
import struct
|
||||||
|
doc = json.loads(raw[20:20 + struct.unpack_from('<I', raw, 12)[0]])
|
||||||
|
triangles = sum(doc['accessors'][q['indices']]['count'] // 3 for m in doc['meshes'] for q in m['primitives'])
|
||||||
|
mins = [min(v.co[i] for v in body.data.vertices) for i in range(3)]
|
||||||
|
maxs = [max(v.co[i] for v in body.data.vertices) for i in range(3)]
|
||||||
|
record = {
|
||||||
|
'id': 'g04',
|
||||||
|
'role': 'tank',
|
||||||
|
'name': 'Basaltbastion',
|
||||||
|
'resource_path': 'res://assets/characters/basaltbastion.glb',
|
||||||
|
'triangles': triangles,
|
||||||
|
'mesh_parts': 1,
|
||||||
|
'parts': ['BasaltBody'],
|
||||||
|
'bounds_godot': {'min': [mins[0], mins[2], -maxs[1]], 'max': [maxs[0], maxs[2], -mins[1]]},
|
||||||
|
'skeletal': True,
|
||||||
|
'bones': len(defs),
|
||||||
|
'clips': list(clips),
|
||||||
|
'sha256': hashlib.sha256(raw).hexdigest(),
|
||||||
|
'source': 'assets-source/phase15/basaltbastion.blend',
|
||||||
|
}
|
||||||
|
# Merge into phase12 manifest as a second asset (or write phase15).
|
||||||
|
manifest_path = P / 'assets/phase15_manifest.json'
|
||||||
|
manifest_path.write_text(json.dumps({'assets': [record]}, indent=2) + '\n')
|
||||||
|
(SRC / 'basaltbastion-audit.json').write_text(json.dumps({
|
||||||
|
'record': record,
|
||||||
|
'exported_clips': [a.get('name') for a in doc.get('animations', [])],
|
||||||
|
'skins': len(doc.get('skins', [])),
|
||||||
|
'source_parts': len(parts),
|
||||||
|
'seed': 63,
|
||||||
|
}, indent=2) + '\n')
|
||||||
|
print('BASALT_BUILD', triangles, 'triangles', len(defs), 'bones', len(doc.get('animations', [])), 'clips', flush=True)
|
||||||
470
assets-source/phase16/build_quellgeflecht.py
Normal file
|
|
@ -0,0 +1,470 @@
|
||||||
|
"""Quellgeflecht: living Wurzelbund healer with weighted vine body and spring core.
|
||||||
|
Blender Z up, -Y forward. Run in a fresh background Blender process.
|
||||||
|
"""
|
||||||
|
from pathlib import Path
|
||||||
|
import bpy, bmesh, math, random, json, hashlib
|
||||||
|
from mathutils import Vector, Quaternion
|
||||||
|
from math import sin, cos, pi
|
||||||
|
P = Path(__file__).resolve().parents[2]
|
||||||
|
OUT = P / 'assets/characters'
|
||||||
|
OUT.mkdir(exist_ok=True)
|
||||||
|
SRC = Path(__file__).resolve().parent
|
||||||
|
random.seed(71)
|
||||||
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||||
|
S = 1.0
|
||||||
|
parts = []
|
||||||
|
|
||||||
|
def coord(v):
|
||||||
|
return Vector(v) * S
|
||||||
|
|
||||||
|
def material(name, color, rough=.8, metal=0.0, emission=None):
|
||||||
|
m = bpy.data.materials.new('QG_' + name)
|
||||||
|
m.diffuse_color = (*color, 1)
|
||||||
|
m.use_nodes = True
|
||||||
|
n = m.node_tree.nodes
|
||||||
|
l = m.node_tree.links
|
||||||
|
b = n.get('Principled BSDF')
|
||||||
|
b.inputs['Base Color'].default_value = (*color, 1)
|
||||||
|
b.inputs['Roughness'].default_value = rough
|
||||||
|
b.inputs['Metallic'].default_value = metal
|
||||||
|
if emission is not None:
|
||||||
|
b.inputs['Emission Color'].default_value = (*emission, 1)
|
||||||
|
b.inputs['Emission Strength'].default_value = 1.5
|
||||||
|
else:
|
||||||
|
v = n.new('ShaderNodeVertexColor')
|
||||||
|
v.layer_name = 'SurfaceColor'
|
||||||
|
l.new(v.outputs['Color'], b.inputs['Base Color'])
|
||||||
|
noise = n.new('ShaderNodeTexNoise')
|
||||||
|
noise.inputs['Scale'].default_value = 22
|
||||||
|
noise.inputs['Detail'].default_value = 3.0
|
||||||
|
bump = n.new('ShaderNodeBump')
|
||||||
|
bump.inputs['Strength'].default_value = .22
|
||||||
|
bump.inputs['Distance'].default_value = .015
|
||||||
|
l.new(noise.outputs['Fac'], bump.inputs['Height'])
|
||||||
|
l.new(bump.outputs['Normal'], b.inputs['Normal'])
|
||||||
|
return m
|
||||||
|
|
||||||
|
M = {
|
||||||
|
'bark': material('Bark', (.22, .16, .10), .88),
|
||||||
|
'vine': material('Vine', (.28, .42, .18), .78),
|
||||||
|
'moss': material('Moss', (.34, .52, .16), .82),
|
||||||
|
'leaf': material('Leaf', (.42, .62, .22), .7),
|
||||||
|
'petal': material('Petal', (.72, .48, .55), .65),
|
||||||
|
'spring': material('Spring', (.45, .85, .78), .35, emission=(.35, .9, .75)),
|
||||||
|
'wood': material('Wood', (.40, .28, .14), .8),
|
||||||
|
}
|
||||||
|
|
||||||
|
def mesh(name, verts, faces, kind, bone, uv=None, colors=None, weights=None, smooth=True):
|
||||||
|
me = bpy.data.meshes.new(name)
|
||||||
|
me.from_pydata([coord(v) for v in verts], [], faces)
|
||||||
|
me.update()
|
||||||
|
ob = bpy.data.objects.new(name, me)
|
||||||
|
bpy.context.collection.objects.link(ob)
|
||||||
|
me.materials.append(M[kind])
|
||||||
|
parts.append(ob)
|
||||||
|
for f in me.polygons:
|
||||||
|
f.use_smooth = smooth
|
||||||
|
tex = me.uv_layers.new(name='UVMap')
|
||||||
|
col = me.color_attributes.new(name='SurfaceColor', type='FLOAT_COLOR', domain='CORNER')
|
||||||
|
base = M[kind].diffuse_color[:3]
|
||||||
|
for f in me.polygons:
|
||||||
|
for li in f.loop_indices:
|
||||||
|
i = me.loops[li].vertex_index
|
||||||
|
v = verts[i]
|
||||||
|
tex.data[li].uv = uv[i] if uv else (v[0] * .9 + v[1] * .1, v[2] * 1.1)
|
||||||
|
fac = .90 + .10 * sin(v[0] * 21 + v[2] * 9) + .04 * cos(v[1] * 37)
|
||||||
|
c = colors[i] if colors else tuple(max(.01, x * fac) for x in base)
|
||||||
|
col.data[li].color = (*c, 1)
|
||||||
|
groups = {}
|
||||||
|
for i in range(len(verts)):
|
||||||
|
ws = weights[i] if weights else {bone: 1.0}
|
||||||
|
total = sum(ws.values())
|
||||||
|
for key, w in ws.items():
|
||||||
|
if key not in groups:
|
||||||
|
groups[key] = ob.vertex_groups.new(name=key)
|
||||||
|
groups[key].add([i], w / total, 'REPLACE')
|
||||||
|
return ob
|
||||||
|
|
||||||
|
def mixweights(t, bones):
|
||||||
|
if len(bones) == 1:
|
||||||
|
return {bones[0]: 1}
|
||||||
|
t = max(0, min(.999999, t)) * (len(bones) - 1)
|
||||||
|
i = int(t)
|
||||||
|
w = t - i
|
||||||
|
return {bones[i]: 1 - w, bones[i + 1]: w}
|
||||||
|
|
||||||
|
def tube(name, points, radii, kind, bones, sides=16, steps=28, flatten=1.0, groove=.05):
|
||||||
|
points = [Vector(x) for x in points]
|
||||||
|
vs, uv, ws, loops, fs = [], [], [], [], []
|
||||||
|
|
||||||
|
def point(t):
|
||||||
|
a = min(len(points) - 2, int(t * (len(points) - 1)))
|
||||||
|
u = t * (len(points) - 1) - a
|
||||||
|
p0, p1, p2, p3 = points[max(a - 1, 0)], points[a], points[a + 1], points[min(a + 2, len(points) - 1)]
|
||||||
|
p = .5 * ((2 * p1) + (-p0 + p2) * u + (2 * p0 - 5 * p1 + 4 * p2 - p3) * u * u + (-p0 + 3 * p1 - 3 * p2 + p3) * u * u * u)
|
||||||
|
return p, radii[a] * (1 - u) + radii[a + 1] * u
|
||||||
|
|
||||||
|
for j in range(steps + 1):
|
||||||
|
t = j / steps
|
||||||
|
p, r = point(t)
|
||||||
|
tan = (point(min(1, t + .002))[0] - point(max(0, t - .002))[0]).normalized()
|
||||||
|
u = tan.cross(Vector((0, 1, 0))).normalized()
|
||||||
|
v = tan.cross(u)
|
||||||
|
loop = []
|
||||||
|
for k in range(sides):
|
||||||
|
a = 2 * pi * k / sides
|
||||||
|
mod = 1 + groove * (sin(a * 5 + t * 6) + .3 * sin(a * 9 - t * 5))
|
||||||
|
vs.append(p + r * mod * (u * cos(a) + v * sin(a) * flatten))
|
||||||
|
uv.append((k / sides, t))
|
||||||
|
ws.append(mixweights(t, bones))
|
||||||
|
loop.append(len(vs) - 1)
|
||||||
|
loops.append(loop)
|
||||||
|
for a, b in zip(loops, loops[1:]):
|
||||||
|
for k in range(sides):
|
||||||
|
q = (k + 1) % sides
|
||||||
|
fs.append((a[k], a[q], b[q], b[k]))
|
||||||
|
fs.extend([tuple(reversed(loops[0])), tuple(loops[-1])])
|
||||||
|
return mesh(name, vs, fs, kind, bones[0], uv, weights=ws)
|
||||||
|
|
||||||
|
def ell(name, c, r, kind, bone, segments=18, rings=10):
|
||||||
|
vs = [(c[0], c[1], c[2] + r[2])]
|
||||||
|
fs = []
|
||||||
|
for j in range(1, rings):
|
||||||
|
th = pi * j / rings
|
||||||
|
for k in range(segments):
|
||||||
|
a = 2 * pi * k / segments
|
||||||
|
f = 1 + .06 * sin(a * 3 + j)
|
||||||
|
vs.append((c[0] + r[0] * sin(th) * cos(a) * f, c[1] + r[1] * sin(th) * sin(a) * f, c[2] + r[2] * cos(th)))
|
||||||
|
vs.append((c[0], c[1], c[2] - r[2]))
|
||||||
|
end = len(vs) - 1
|
||||||
|
for k in range(segments):
|
||||||
|
fs.append((0, 1 + k, 1 + (k + 1) % segments))
|
||||||
|
for j in range(rings - 2):
|
||||||
|
for k in range(segments):
|
||||||
|
a = 1 + j * segments + k
|
||||||
|
b = 1 + j * segments + (k + 1) % segments
|
||||||
|
fs.append((a, a + segments, b + segments, b))
|
||||||
|
for k in range(segments):
|
||||||
|
fs.append((end, end - 1 - k, end - 1 - (k + 1) % segments))
|
||||||
|
return mesh(name, vs, fs, kind, bone)
|
||||||
|
|
||||||
|
def leaf(name, start, end, width, bone, kind='leaf'):
|
||||||
|
a = Vector(start)
|
||||||
|
b = Vector(end)
|
||||||
|
d = b - a
|
||||||
|
u = d.cross(Vector((0, 1, 0))).normalized() if abs(d.y) < .9 else d.cross(Vector((1, 0, 0))).normalized()
|
||||||
|
v = d.normalized().cross(u)
|
||||||
|
vs, fs = [], []
|
||||||
|
for side in [-1, 1]:
|
||||||
|
for j in range(7):
|
||||||
|
t = j / 6
|
||||||
|
w = width * sin(pi * t) ** .65
|
||||||
|
for k in [-1, 0, 1]:
|
||||||
|
vs.append(a + d * t + u * w * k + v * (sin(pi * t) * .03) + Vector((0, side * .004, 0)))
|
||||||
|
for layer in range(2):
|
||||||
|
for j in range(6):
|
||||||
|
for k in range(2):
|
||||||
|
i = layer * 21 + j * 3 + k
|
||||||
|
quad = (i, i + 1, i + 4, i + 3)
|
||||||
|
fs.append(quad if layer else tuple(reversed(quad)))
|
||||||
|
for j in range(6):
|
||||||
|
i = j * 3
|
||||||
|
fs.extend([(i, i + 3, i + 24, i + 21), (i + 2, i + 23, i + 26, i + 5)])
|
||||||
|
fs.extend([(0, 21, 22, 23, 2, 1), (18, 19, 20, 41, 40, 39)])
|
||||||
|
return mesh(name, vs, fs, kind, bone)
|
||||||
|
|
||||||
|
# ---- Body: braided living spring ----
|
||||||
|
# Root braids
|
||||||
|
for k in range(5):
|
||||||
|
a = 2 * pi * k / 5
|
||||||
|
tube('RootBraid_%d' % k,
|
||||||
|
[(.16 * cos(a), .12 * sin(a), .02), (.22 * cos(a + .3), .18 * sin(a + .3), .18),
|
||||||
|
(.14 * cos(a + .5), .10 * sin(a + .5), .38), (.08 * cos(a), .05 * sin(a), .52)],
|
||||||
|
[.07, .08, .06, .05], 'bark', ['Root', 'Pelvis'], 10, 16)
|
||||||
|
ell('Base_Wood', (0, 0, .08), (.22, .18, .08), 'wood', 'Root', 16, 8)
|
||||||
|
# Trunk weave
|
||||||
|
tube('Trunk', [(0, .02, .40), (0, .0, .75), (.02, -.02, 1.10), (0, 0, 1.38)], [.18, .22, .20, .15], 'vine', ['Pelvis', 'Spine', 'Chest'], 18, 32, flatten=.85)
|
||||||
|
for k in range(8):
|
||||||
|
a = k * 2 * pi / 8
|
||||||
|
tube('TrunkWeave_%d' % k,
|
||||||
|
[(.12 * cos(a), .08 + .10 * sin(a), .48), (.18 * cos(a + .2), .02 + .16 * sin(a + .2), .85),
|
||||||
|
(.16 * cos(a - .1), .0 + .14 * sin(a - .1), 1.15), (.08 * cos(a), .04 + .08 * sin(a), 1.36)],
|
||||||
|
[.04, .05, .04, .02], 'vine' if k % 2 else 'moss', ['Pelvis', 'Spine', 'Chest'], 8, 20)
|
||||||
|
# Spring orb in chest
|
||||||
|
ell('SpringCore', (0, -.12, 1.05), (.14, .10, .14), 'spring', 'Spine', 16, 10)
|
||||||
|
ell('SpringHalo', (0, -.10, 1.05), (.20, .12, .20), 'spring', 'Spine', 14, 8)
|
||||||
|
# Crown / head: woven nest with drooping leaves and a glowing bud
|
||||||
|
ell('Head_Nest', (0, 0, 1.55), (.22, .20, .18), 'bark', 'Head', 18, 10)
|
||||||
|
ell('Head_Bud', (0, -.08, 1.68), (.10, .08, .12), 'spring', 'Head', 14, 8)
|
||||||
|
for k in range(7):
|
||||||
|
a = 2 * pi * k / 7
|
||||||
|
leaf('CrownLeaf_%d' % k,
|
||||||
|
(.12 * cos(a), .08 * sin(a), 1.62),
|
||||||
|
(.28 * cos(a), .18 * sin(a), 1.82 + .04 * (k % 2)),
|
||||||
|
.06, 'Head', 'leaf' if k % 2 else 'moss')
|
||||||
|
for k in range(4):
|
||||||
|
a = 2 * pi * k / 4 + .3
|
||||||
|
leaf('Petal_%d' % k,
|
||||||
|
(.04 * cos(a), -.06 + .04 * sin(a), 1.72),
|
||||||
|
(.12 * cos(a), -.14 + .05 * sin(a), 1.84),
|
||||||
|
.035, 'Head', 'petal')
|
||||||
|
|
||||||
|
# Arms: long healing vines with leaf fans and droplet tips
|
||||||
|
for s, side in [(-1, 'L'), (1, 'R')]:
|
||||||
|
arm = ['UpperArm.' + side, 'Forearm.' + side, 'Hand.' + side]
|
||||||
|
tube('Arm_' + side,
|
||||||
|
[(s * .22, 0, 1.28), (s * .42, -.08, 1.10), (s * .55, -.18, .88), (s * .58, -.28, .72)],
|
||||||
|
[.08, .07, .055, .04], 'vine', arm, 12, 24, flatten=.9)
|
||||||
|
# Leaf fan at wrist
|
||||||
|
for j in range(4):
|
||||||
|
a = j * 1.2
|
||||||
|
leaf('HandLeaf_%s%d' % (side, j),
|
||||||
|
(s * .58, -.28, .72),
|
||||||
|
(s * (.72 + .04 * j), -.36 - .02 * j, .62 + .06 * sin(a)),
|
||||||
|
.04, arm[2], 'leaf')
|
||||||
|
ell('Drop_' + side, (s * .58, -.32, .66), (.035, .03, .045), 'spring', arm[2], 10, 6)
|
||||||
|
# Shoulder leaves
|
||||||
|
for j in range(3):
|
||||||
|
leaf('ShoulderLeaf_%s%d' % (side, j),
|
||||||
|
(s * .20, .02, 1.34),
|
||||||
|
(s * (.34 + .05 * j), .10, 1.48 + .04 * j),
|
||||||
|
.05, 'Chest', 'moss')
|
||||||
|
|
||||||
|
# Back canopy of vines and moss
|
||||||
|
for k in range(10):
|
||||||
|
a = k * .55
|
||||||
|
z = .9 + (k % 4) * .16
|
||||||
|
tube('BackVine_%d' % k,
|
||||||
|
[(.05 * cos(a), .12, z), (.18 * cos(a), .22, z + .18), (.24 * cos(a), .18, z + .32)],
|
||||||
|
[.03, .035, .01], 'vine', ['Chest' if z > 1.05 else 'Spine'], 8, 14)
|
||||||
|
leaf('BackLeaf_%d' % k,
|
||||||
|
(.18 * cos(a), .22, z + .18),
|
||||||
|
(.30 * cos(a), .12, z + .30),
|
||||||
|
.05, 'Chest' if z > 1.05 else 'Spine', 'leaf')
|
||||||
|
for j in range(24):
|
||||||
|
a = j * 1.7
|
||||||
|
z = .5 + (j % 8) * .12
|
||||||
|
ell('MossClump_%02d' % j,
|
||||||
|
(.12 * cos(a) * (1 + .05 * j), .08 * sin(a), z),
|
||||||
|
(.035, .028, .02), 'moss', 'Spine' if z < 1.0 else 'Chest', 8, 5)
|
||||||
|
|
||||||
|
# Cleanup and skin
|
||||||
|
for ob in parts:
|
||||||
|
bm = bmesh.new()
|
||||||
|
bm.from_mesh(ob.data)
|
||||||
|
bmesh.ops.remove_doubles(bm, verts=list(bm.verts), dist=.0000005)
|
||||||
|
bmesh.ops.triangulate(bm, faces=list(bm.faces))
|
||||||
|
bmesh.ops.dissolve_degenerate(bm, edges=list(bm.edges), dist=.0000001)
|
||||||
|
bmesh.ops.recalc_face_normals(bm, faces=list(bm.faces))
|
||||||
|
if bm.calc_volume(signed=True) < 0:
|
||||||
|
bmesh.ops.reverse_faces(bm, faces=list(bm.faces))
|
||||||
|
bm.to_mesh(ob.data)
|
||||||
|
bm.free()
|
||||||
|
ob.data.update()
|
||||||
|
|
||||||
|
bpy.ops.object.armature_add(enter_editmode=True)
|
||||||
|
rig = bpy.context.object
|
||||||
|
rig.name = 'QuellRig'
|
||||||
|
ed = rig.data.edit_bones
|
||||||
|
ed.remove(ed[0])
|
||||||
|
defs = {}
|
||||||
|
|
||||||
|
def bone(name, head, tail, parent=None):
|
||||||
|
b = ed.new(name)
|
||||||
|
b.head = coord(head)
|
||||||
|
b.tail = coord(tail)
|
||||||
|
if parent:
|
||||||
|
b.parent = ed[parent]
|
||||||
|
defs[name] = (head, tail, parent)
|
||||||
|
|
||||||
|
bone('Root', (0, 0, 0), (0, 0, .12))
|
||||||
|
bone('Pelvis', (0, 0, .40), (0, 0, .75), 'Root')
|
||||||
|
bone('Spine', (0, 0, .75), (0, 0, 1.10), 'Pelvis')
|
||||||
|
bone('Chest', (0, 0, 1.10), (0, 0, 1.40), 'Spine')
|
||||||
|
bone('Head', (0, 0, 1.40), (0, 0, 1.75), 'Chest')
|
||||||
|
for s, side in [(-1, 'L'), (1, 'R')]:
|
||||||
|
bone('UpperArm.' + side, (s * .22, 0, 1.28), (s * .42, -.08, 1.10), 'Chest')
|
||||||
|
bone('Forearm.' + side, (s * .42, -.08, 1.10), (s * .55, -.18, .88), 'UpperArm.' + side)
|
||||||
|
bone('Hand.' + side, (s * .55, -.18, .88), (s * .58, -.28, .68), 'Forearm.' + side)
|
||||||
|
bone('Thigh.' + side, (s * .10, .04, .40), (s * .12, .02, .22), 'Pelvis')
|
||||||
|
bone('Shin.' + side, (s * .12, .02, .22), (s * .12, 0, .06), 'Thigh.' + side)
|
||||||
|
bone('Foot.' + side, (s * .12, 0, .06), (s * .12, -.12, .03), 'Shin.' + side)
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
for ob in parts:
|
||||||
|
mod = ob.modifiers.new('Weighted vine', 'ARMATURE')
|
||||||
|
mod.object = rig
|
||||||
|
ob.parent = rig
|
||||||
|
|
||||||
|
clips = {'idle': (3.2, True), 'start': (.4, False), 'walk': (1.4, True), 'stop': (.4, False),
|
||||||
|
'attack_a': (1.3, False), 'attack_b': (1.3, False), 'hit': (.48, False), 'ability': (1.9, False), 'death': (2.0, False)}
|
||||||
|
|
||||||
|
def curve(t, keys):
|
||||||
|
for i in range(len(keys) - 1):
|
||||||
|
a, x = keys[i]
|
||||||
|
b, y = keys[i + 1]
|
||||||
|
if t <= b:
|
||||||
|
u = max(0, (t - a) / (b - a))
|
||||||
|
u = u * u * (3 - 2 * u)
|
||||||
|
return x * (1 - u) + y * u
|
||||||
|
return keys[-1][1]
|
||||||
|
|
||||||
|
def animate(name, t, dur):
|
||||||
|
rotations = {}
|
||||||
|
locs = {}
|
||||||
|
|
||||||
|
def rot(b, x=0, y=0, z=0):
|
||||||
|
rotations[b] = (x, y, z)
|
||||||
|
|
||||||
|
if name == 'idle':
|
||||||
|
q = sin(2 * pi * t / dur)
|
||||||
|
q2 = sin(2 * pi * t / dur * 2)
|
||||||
|
rot('Spine', .015 * q)
|
||||||
|
rot('Chest', -.02 * q, 0, .01 * q)
|
||||||
|
rot('Head', .025 * q, 0, -.012 * q)
|
||||||
|
for s, side in [(-1, 'L'), (1, 'R')]:
|
||||||
|
rot('Forearm.' + side, .03 * q, 0, s * .02 * q2)
|
||||||
|
locs['Root'] = (0, 0, .004 * q)
|
||||||
|
elif name in ('walk', 'start', 'stop'):
|
||||||
|
fade = 1 if name == 'walk' else sin(pi * .5 * t / dur) if name == 'start' else cos(pi * .5 * t / dur)
|
||||||
|
phase = t / 1.4 * 2 * pi
|
||||||
|
roll = sin(phase) * fade
|
||||||
|
rot('Pelvis', .01, 0, .04 * roll)
|
||||||
|
rot('Spine', -.03 * fade, 0, -.05 * roll)
|
||||||
|
rot('Chest', .02 * fade, 0, .03 * roll)
|
||||||
|
rot('Head', .04 * fade, 0, -.02 * roll)
|
||||||
|
for s, side in [(-1, 'L'), (1, 'R')]:
|
||||||
|
q = sin(phase + (0 if s < 0 else pi))
|
||||||
|
lift = max(0, q)
|
||||||
|
rot('Thigh.' + side, .28 * q * fade)
|
||||||
|
rot('Shin.' + side, -.32 * lift * fade)
|
||||||
|
rot('Foot.' + side, (-.28 * q + .32 * lift) * fade)
|
||||||
|
rot('UpperArm.' + side, -.12 * q * fade, 0, s * .03 * fade)
|
||||||
|
rot('Forearm.' + side, -.15 * fade - .05 * lift, 0, s * .04 * fade)
|
||||||
|
locs['Root'] = (0, 0, (-.008 + .008 * cos(phase * 2)) * fade)
|
||||||
|
elif name == 'attack_a':
|
||||||
|
# Left tendril pulse, contact at 0.70
|
||||||
|
q = curve(t / dur, [(0, 0), (.32, -.30), (.54, -.50), (.70, 1), (.82, .80), (1, 0)])
|
||||||
|
rot('Spine', -.08 * q, .06 * q, .05 * q)
|
||||||
|
rot('Chest', -.06 * q, .05 * q, .08 * q)
|
||||||
|
rot('Head', .08 * q, 0, -.05 * q)
|
||||||
|
rot('UpperArm.L', -.85 * q, -.10 * q, .15 * q)
|
||||||
|
rot('Forearm.L', -.40 * q, 0, .10 * q)
|
||||||
|
rot('Hand.L', .25 * q)
|
||||||
|
rot('UpperArm.R', .12 * q)
|
||||||
|
elif name == 'attack_b':
|
||||||
|
q = curve(t / dur, [(0, 0), (.32, -.30), (.54, -.50), (.70, 1), (.82, .80), (1, 0)])
|
||||||
|
rot('Spine', -.08 * q, -.06 * q, -.05 * q)
|
||||||
|
rot('Chest', -.06 * q, -.05 * q, -.08 * q)
|
||||||
|
rot('Head', .08 * q, 0, .05 * q)
|
||||||
|
rot('UpperArm.R', -.85 * q, .10 * q, -.15 * q)
|
||||||
|
rot('Forearm.R', -.40 * q, 0, -.10 * q)
|
||||||
|
rot('Hand.R', .25 * q)
|
||||||
|
rot('UpperArm.L', .12 * q)
|
||||||
|
elif name == 'hit':
|
||||||
|
q = curve(t, [(0, 0), (.08, 1), (.22, .5), (.48, 0)])
|
||||||
|
rot('Spine', .12 * q, 0, -.07 * q)
|
||||||
|
rot('Head', .14 * q, 0, .05 * q)
|
||||||
|
rot('Forearm.L', -.12 * q)
|
||||||
|
rot('Forearm.R', -.10 * q)
|
||||||
|
elif name == 'ability':
|
||||||
|
# Multi-heal: arms open, core flares, leaves lift
|
||||||
|
q = curve(t / dur, [(0, 0), (.35, .7), (.70, 1), (1.3, .85), (1.9, 0)])
|
||||||
|
rot('Spine', .10 * q)
|
||||||
|
rot('Chest', .08 * q)
|
||||||
|
rot('Head', -.18 * q)
|
||||||
|
for s, side in [(-1, 'L'), (1, 'R')]:
|
||||||
|
rot('UpperArm.' + side, -.35 * q, 0, s * .55 * q)
|
||||||
|
rot('Forearm.' + side, -.50 * q, 0, s * .25 * q)
|
||||||
|
rot('Hand.' + side, .20 * q)
|
||||||
|
locs['Root'] = (0, 0, .04 * q)
|
||||||
|
elif name == 'death':
|
||||||
|
q = curve(t, [(0, 0), (.2, .08), (.7, .45), (1.25, 1), (1.55, .90), (2.0, 1)])
|
||||||
|
rot('Root', -.55 * q, 0, .08 * q)
|
||||||
|
locs['Root'] = (0, -.08 * q, -.08 * q)
|
||||||
|
rot('Spine', -.20 * q)
|
||||||
|
rot('Chest', -.15 * q)
|
||||||
|
rot('Head', -.25 * q)
|
||||||
|
for s, side in [(-1, 'L'), (1, 'R')]:
|
||||||
|
rot('UpperArm.' + side, -.40 * q, 0, s * .25 * q)
|
||||||
|
rot('Forearm.' + side, -.55 * q)
|
||||||
|
for pb in rig.pose.bones:
|
||||||
|
pb.rotation_mode = 'QUATERNION'
|
||||||
|
r = rotations.get(pb.name, (0, 0, 0))
|
||||||
|
q = Quaternion((0, 0, 1), r[2]) @ Quaternion((0, 1, 0), r[1]) @ Quaternion((1, 0, 0), r[0])
|
||||||
|
basis = pb.bone.matrix_local.to_quaternion()
|
||||||
|
pb.rotation_quaternion = basis.inverted() @ q @ basis
|
||||||
|
pb.location = pb.bone.matrix_local.to_3x3().inverted() @ (Vector(locs.get(pb.name, (0, 0, 0))) * S)
|
||||||
|
|
||||||
|
for name, (dur, loop) in clips.items():
|
||||||
|
rig.animation_data_clear()
|
||||||
|
action = bpy.data.actions.new(name)
|
||||||
|
action.use_fake_user = True
|
||||||
|
rig.animation_data_create()
|
||||||
|
rig.animation_data.action = action
|
||||||
|
for frame in range(round(dur * 30) + 1):
|
||||||
|
animate(name, frame / 30, dur)
|
||||||
|
for pb in rig.pose.bones:
|
||||||
|
pb.keyframe_insert('rotation_quaternion', frame=frame)
|
||||||
|
pb.keyframe_insert('location', frame=frame)
|
||||||
|
action['loop'] = loop
|
||||||
|
rig.animation_data_clear()
|
||||||
|
rig.animation_data_create()
|
||||||
|
rig.animation_data.action = bpy.data.actions['idle']
|
||||||
|
for pb in rig.pose.bones:
|
||||||
|
pb.rotation_quaternion = Quaternion()
|
||||||
|
pb.location = (0, 0, 0)
|
||||||
|
bpy.context.scene.render.fps = 30
|
||||||
|
bpy.context.scene.frame_set(0)
|
||||||
|
|
||||||
|
bpy.ops.wm.save_as_mainfile(filepath=str(SRC / 'quellgeflecht.blend'))
|
||||||
|
bpy.ops.file.make_paths_relative()
|
||||||
|
bpy.ops.wm.save_as_mainfile(filepath=str(SRC / 'quellgeflecht.blend'))
|
||||||
|
bpy.ops.object.select_all(action='DESELECT')
|
||||||
|
for ob in parts:
|
||||||
|
ob.select_set(True)
|
||||||
|
bpy.context.view_layer.objects.active = parts[0]
|
||||||
|
bpy.ops.object.join()
|
||||||
|
body = bpy.context.object
|
||||||
|
body.name = 'QuellBody'
|
||||||
|
body.select_set(True)
|
||||||
|
rig.select_set(True)
|
||||||
|
bpy.context.view_layer.objects.active = rig
|
||||||
|
bpy.ops.export_scene.gltf(
|
||||||
|
filepath=str(OUT / 'quellgeflecht.glb'),
|
||||||
|
export_format='GLB', use_selection=True,
|
||||||
|
export_animations=True, export_animation_mode='ACTIONS',
|
||||||
|
export_force_sampling=True, export_frame_range=False,
|
||||||
|
export_anim_slide_to_zero=True, export_skins=True,
|
||||||
|
export_all_influences=False, export_def_bones=True,
|
||||||
|
export_anim_single_armature=True, export_materials='EXPORT', export_yup=True,
|
||||||
|
)
|
||||||
|
raw = (OUT / 'quellgeflecht.glb').read_bytes()
|
||||||
|
import struct
|
||||||
|
doc = json.loads(raw[20:20 + struct.unpack_from('<I', raw, 12)[0]])
|
||||||
|
triangles = sum(doc['accessors'][q['indices']]['count'] // 3 for m in doc['meshes'] for q in m['primitives'])
|
||||||
|
mins = [min(v.co[i] for v in body.data.vertices) for i in range(3)]
|
||||||
|
maxs = [max(v.co[i] for v in body.data.vertices) for i in range(3)]
|
||||||
|
record = {
|
||||||
|
'id': 'w10',
|
||||||
|
'role': 'support',
|
||||||
|
'name': 'Quellgeflecht',
|
||||||
|
'resource_path': 'res://assets/characters/quellgeflecht.glb',
|
||||||
|
'triangles': triangles,
|
||||||
|
'mesh_parts': 1,
|
||||||
|
'parts': ['QuellBody'],
|
||||||
|
'bounds_godot': {'min': [mins[0], mins[2], -maxs[1]], 'max': [maxs[0], maxs[2], -mins[1]]},
|
||||||
|
'skeletal': True,
|
||||||
|
'bones': len(defs),
|
||||||
|
'clips': list(clips),
|
||||||
|
'sha256': hashlib.sha256(raw).hexdigest(),
|
||||||
|
'source': 'assets-source/phase16/quellgeflecht.blend',
|
||||||
|
}
|
||||||
|
(P / 'assets/phase16_manifest.json').write_text(json.dumps({'assets': [record]}, indent=2) + '\n')
|
||||||
|
(SRC / 'quellgeflecht-audit.json').write_text(json.dumps({
|
||||||
|
'record': record,
|
||||||
|
'exported_clips': [a.get('name') for a in doc.get('animations', [])],
|
||||||
|
'skins': len(doc.get('skins', [])),
|
||||||
|
'source_parts': len(parts),
|
||||||
|
'seed': 71,
|
||||||
|
}, indent=2) + '\n')
|
||||||
|
print('QUELL_BUILD', triangles, 'triangles', len(defs), 'bones', len(doc.get('animations', [])), 'clips', flush=True)
|
||||||
54
assets-source/phase16/quellgeflecht-audit.json
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
{
|
||||||
|
"record": {
|
||||||
|
"id": "w10",
|
||||||
|
"role": "support",
|
||||||
|
"name": "Quellgeflecht",
|
||||||
|
"resource_path": "res://assets/characters/quellgeflecht.glb",
|
||||||
|
"triangles": 14276,
|
||||||
|
"mesh_parts": 1,
|
||||||
|
"parts": [
|
||||||
|
"QuellBody"
|
||||||
|
],
|
||||||
|
"bounds_godot": {
|
||||||
|
"min": [
|
||||||
|
-0.8399999737739563,
|
||||||
|
-0.011425986886024475,
|
||||||
|
-0.2583521902561188
|
||||||
|
],
|
||||||
|
"max": [
|
||||||
|
0.8399999737739563,
|
||||||
|
1.8600000143051147,
|
||||||
|
0.42399999499320984
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"skeletal": true,
|
||||||
|
"bones": 17,
|
||||||
|
"clips": [
|
||||||
|
"idle",
|
||||||
|
"start",
|
||||||
|
"walk",
|
||||||
|
"stop",
|
||||||
|
"attack_a",
|
||||||
|
"attack_b",
|
||||||
|
"hit",
|
||||||
|
"ability",
|
||||||
|
"death"
|
||||||
|
],
|
||||||
|
"sha256": "52de3d57191b85a0053cc3049f298801d07c17266c08364cfa0da68e94b4614a",
|
||||||
|
"source": "assets-source/phase16/quellgeflecht.blend"
|
||||||
|
},
|
||||||
|
"exported_clips": [
|
||||||
|
"ability",
|
||||||
|
"attack_a",
|
||||||
|
"attack_b",
|
||||||
|
"death",
|
||||||
|
"hit",
|
||||||
|
"idle",
|
||||||
|
"start",
|
||||||
|
"stop",
|
||||||
|
"walk"
|
||||||
|
],
|
||||||
|
"skins": 1,
|
||||||
|
"source_parts": 92,
|
||||||
|
"seed": 71
|
||||||
|
}
|
||||||
BIN
assets-source/phase16/quellgeflecht.blend
Normal file
BIN
assets-source/phase16/quellgeflecht.blend1
Normal file
540
assets-source/phase18/build_funkenlanze.py
Normal file
|
|
@ -0,0 +1,540 @@
|
||||||
|
"""Funkenlanze: an editable, weighted mineral marksman and crystal lance rifle.
|
||||||
|
|
||||||
|
Run: blender --background --python assets-source/phase18/build_funkenlanze.py
|
||||||
|
Blender is Z-up, -Y-forward. Arm contact is solved analytically per sample, then
|
||||||
|
baked to FK animation: no runtime IK, rigid-joint approximation, or constraints.
|
||||||
|
"""
|
||||||
|
from pathlib import Path
|
||||||
|
import bpy, bmesh, math, json, hashlib, struct
|
||||||
|
from math import sin, cos, pi, sqrt
|
||||||
|
from mathutils import Vector, Quaternion, Matrix
|
||||||
|
|
||||||
|
P = Path(__file__).resolve().parents[2]
|
||||||
|
SRC = Path(__file__).resolve().parent
|
||||||
|
OUT = P / 'assets/characters'
|
||||||
|
OUT.mkdir(exist_ok=True)
|
||||||
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||||
|
bpy.context.preferences.filepaths.save_version = 0
|
||||||
|
parts, defs, contact_samples = [], {}, []
|
||||||
|
|
||||||
|
|
||||||
|
def material(name, color, rough=.6, metal=0, emission=0):
|
||||||
|
m = bpy.data.materials.new('FL_' + name)
|
||||||
|
m.diffuse_color = (*color, 1)
|
||||||
|
m.use_nodes = True
|
||||||
|
n, links = m.node_tree.nodes, m.node_tree.links
|
||||||
|
bs = n.get('Principled BSDF')
|
||||||
|
bs.inputs['Base Color'].default_value = (*color, 1)
|
||||||
|
bs.inputs['Metallic'].default_value = metal
|
||||||
|
bs.inputs['Roughness'].default_value = rough
|
||||||
|
vertex = n.new('ShaderNodeVertexColor')
|
||||||
|
vertex.layer_name = 'SurfaceColor'
|
||||||
|
links.new(vertex.outputs['Color'], bs.inputs['Base Color'])
|
||||||
|
if emission:
|
||||||
|
bs.inputs['Emission Color'].default_value = (*color, 1)
|
||||||
|
bs.inputs['Emission Strength'].default_value = emission
|
||||||
|
else:
|
||||||
|
tex = n.new('ShaderNodeTexNoise')
|
||||||
|
tex.inputs['Scale'].default_value = 65 if metal else 26
|
||||||
|
tex.inputs['Detail'].default_value = 2
|
||||||
|
bump = n.new('ShaderNodeBump')
|
||||||
|
bump.inputs['Strength'].default_value = .14 if metal else .24
|
||||||
|
bump.inputs['Distance'].default_value = .007
|
||||||
|
links.new(tex.outputs['Fac'], bump.inputs['Height'])
|
||||||
|
links.new(bump.outputs['Normal'], bs.inputs['Normal'])
|
||||||
|
return m
|
||||||
|
|
||||||
|
|
||||||
|
M = {
|
||||||
|
'stone': material('Basalt', (.125, .145, .17), .86),
|
||||||
|
'facet': material('Obsidian', (.225, .245, .28), .64, .13),
|
||||||
|
'gold': material('BurnishedBronze', (.68, .405, .145), .31, .76),
|
||||||
|
'edge': material('BronzeEdge', (.92, .665, .28), .27, .70),
|
||||||
|
'steel': material('Iron', (.27, .30, .335), .33, .78),
|
||||||
|
'dark': material('Joint', (.055, .061, .07), .75, .20),
|
||||||
|
'ember': material('Ember', (1.0, .235, .018), .34, .15, 1.05),
|
||||||
|
'crystal': material('Crystal', (1.0, .47, .045), .22, .2, .7),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def mesh(name, vs, fs, kind, bone, weights=None, smooth=True, uv=None):
|
||||||
|
me = bpy.data.meshes.new(name)
|
||||||
|
me.from_pydata(vs, [], fs)
|
||||||
|
me.update()
|
||||||
|
ob = bpy.data.objects.new(name, me)
|
||||||
|
bpy.context.collection.objects.link(ob)
|
||||||
|
parts.append(ob)
|
||||||
|
me.materials.append(M[kind])
|
||||||
|
col = me.color_attributes.new(name='SurfaceColor', type='FLOAT_COLOR', domain='CORNER')
|
||||||
|
tex = me.uv_layers.new(name='UVMap')
|
||||||
|
base = M[kind].diffuse_color[:3]
|
||||||
|
for face in me.polygons:
|
||||||
|
face.use_smooth = smooth
|
||||||
|
for li in face.loop_indices:
|
||||||
|
vi = me.loops[li].vertex_index
|
||||||
|
x, y, z = vs[vi]
|
||||||
|
# Broad, deliberate surface variation survives export without textures.
|
||||||
|
variation = .93 + .055 * sin(17*x + 11*z) + .025 * cos(24*y - 7*z)
|
||||||
|
col.data[li].color = (*(min(1, c*variation) for c in base), 1)
|
||||||
|
tex.data[li].uv = uv[vi] if uv else (x*1.7 + y*.3, z*1.5)
|
||||||
|
groups = {}
|
||||||
|
for i in range(len(vs)):
|
||||||
|
ws = weights[i] if weights else {bone: 1.0}
|
||||||
|
total = sum(ws.values())
|
||||||
|
for key, weight in ws.items():
|
||||||
|
if weight <= 1e-8:
|
||||||
|
continue
|
||||||
|
groups.setdefault(key, ob.vertex_groups.get(key) or ob.vertex_groups.new(name=key))
|
||||||
|
groups[key].add([i], weight / total, 'REPLACE')
|
||||||
|
return ob
|
||||||
|
|
||||||
|
|
||||||
|
def blend(a, b, t):
|
||||||
|
t = max(0, min(1, t))
|
||||||
|
return {a: 1-t, b: t} if a != b else {a: 1}
|
||||||
|
|
||||||
|
|
||||||
|
def catmull(points, t):
|
||||||
|
seg = min(len(points)-2, int(t*(len(points)-1)))
|
||||||
|
u = t*(len(points)-1)-seg
|
||||||
|
a, b, c, d = [points[max(0, min(len(points)-1, q))] for q in (seg-1,seg,seg+1,seg+2)]
|
||||||
|
return .5*((2*b)+(-a+c)*u+(2*a-5*b+4*c-d)*u*u+(-a+3*b-3*c+d)*u*u*u)
|
||||||
|
|
||||||
|
|
||||||
|
def tube(name, points, radii, kind, bone, sides=16, steps=20,
|
||||||
|
flatten=1, weight_fn=None, groove=.015, smooth=True):
|
||||||
|
pts = [Vector(x) for x in points]
|
||||||
|
vs, fs, ws, uv = [], [], [], []
|
||||||
|
for j in range(steps+1):
|
||||||
|
t = j/steps
|
||||||
|
p = catmull(pts, t)
|
||||||
|
seg = min(len(radii)-2, int(t*(len(radii)-1)))
|
||||||
|
a = t*(len(radii)-1)-seg
|
||||||
|
radius = radii[seg]*(1-a)+radii[seg+1]*a
|
||||||
|
tangent = (catmull(pts, min(1,t+.002))-catmull(pts,max(0,t-.002))).normalized()
|
||||||
|
ref = Vector((0,1,0)) if abs(tangent.y)<.9 else Vector((1,0,0))
|
||||||
|
u = tangent.cross(ref).normalized()
|
||||||
|
v = tangent.cross(u)
|
||||||
|
for k in range(sides):
|
||||||
|
angle = 2*pi*k/sides
|
||||||
|
detail = 1 + groove*cos(5*angle+t*4)
|
||||||
|
vs.append(p + radius*detail*(u*cos(angle)+v*sin(angle)*flatten))
|
||||||
|
ws.append(weight_fn(t) if weight_fn else {bone:1})
|
||||||
|
uv.append((k/sides,t))
|
||||||
|
if j:
|
||||||
|
for k in range(sides):
|
||||||
|
a, b = (j-1)*sides+k, (j-1)*sides+(k+1)%sides
|
||||||
|
fs.append((a,b,b+sides,a+sides))
|
||||||
|
fs += [tuple(reversed(range(sides))), tuple(range(steps*sides,(steps+1)*sides))]
|
||||||
|
return mesh(name,vs,fs,kind,bone,ws,smooth,uv)
|
||||||
|
|
||||||
|
|
||||||
|
def ell(name, c, r, kind, bone, segments=20, rings=12, smooth=True):
|
||||||
|
vs, fs = [(c[0],c[1],c[2]+r[2])], []
|
||||||
|
for j in range(1,rings):
|
||||||
|
th = pi*j/rings
|
||||||
|
for k in range(segments):
|
||||||
|
a = 2*pi*k/segments
|
||||||
|
vs.append((c[0]+r[0]*sin(th)*cos(a),c[1]+r[1]*sin(th)*sin(a),c[2]+r[2]*cos(th)))
|
||||||
|
vs.append((c[0],c[1],c[2]-r[2]))
|
||||||
|
for k in range(segments):
|
||||||
|
fs.append((0,1+k,1+(k+1)%segments))
|
||||||
|
for j in range(rings-2):
|
||||||
|
for k in range(segments):
|
||||||
|
a=1+j*segments+k; b=1+j*segments+(k+1)%segments
|
||||||
|
fs.append((a,a+segments,b+segments,b))
|
||||||
|
for k in range(segments):
|
||||||
|
fs.append((len(vs)-1,len(vs)-2-k,len(vs)-2-(k+1)%segments))
|
||||||
|
return mesh(name,vs,fs,kind,bone,smooth=smooth)
|
||||||
|
|
||||||
|
|
||||||
|
def shield(name, outline, center, depth, kind, bone):
|
||||||
|
"""Closed tapered plate with bevel belt and a proud central ridge, never a box."""
|
||||||
|
c = Vector(center)
|
||||||
|
front = [Vector(p) for p in outline]
|
||||||
|
vs = front + [p.lerp(c,.13)+Vector((0,-depth*.32,0)) for p in front]
|
||||||
|
vs += [c+Vector((0,-depth,0))]
|
||||||
|
vs += [p+Vector((0,depth*.22,0)) for p in front]
|
||||||
|
n=len(front); fs=[]
|
||||||
|
for i in range(n):
|
||||||
|
q=(i+1)%n
|
||||||
|
fs += [(i,q,n+q,n+i),(n+i,n+q,2*n),(i,2*n+1+i,2*n+1+q,q)]
|
||||||
|
fs.append(tuple(reversed(range(2*n+1,3*n+1))))
|
||||||
|
return mesh(name,vs,fs,kind,bone,smooth=False)
|
||||||
|
|
||||||
|
|
||||||
|
def ring(name, p, axis, radius, width, kind, bone, sides=16):
|
||||||
|
axis=Vector(axis).normalized(); p=Vector(p)
|
||||||
|
return tube(name,[p-axis*width/2,p+axis*width/2],[radius,radius],kind,bone,sides,3,groove=0,smooth=False)
|
||||||
|
|
||||||
|
|
||||||
|
def shard(name, points, radius, kind, bone):
|
||||||
|
return tube(name,points,[radius,.74*radius,.002],kind,bone,6,5,groove=0,smooth=False)
|
||||||
|
|
||||||
|
|
||||||
|
def ik_elbow(a,c,l1,l2,pole):
|
||||||
|
delta=c-a; d=delta.length
|
||||||
|
if not abs(l1-l2)+.00001 < d < l1+l2-.00001:
|
||||||
|
raise RuntimeError('Unreachable arm contact: %.6f for %.6f' % (d,l1+l2))
|
||||||
|
line=delta/d
|
||||||
|
v=Vector(pole)-a
|
||||||
|
perpendicular=(v-line*v.dot(line)).normalized()
|
||||||
|
along=(l1*l1-l2*l2+d*d)/(2*d)
|
||||||
|
return a+line*along+perpendicular*sqrt(max(0,l1*l1-along*along))
|
||||||
|
|
||||||
|
|
||||||
|
SHOULDERS={'L':Vector((-.265,.035,1.49)),'R':Vector((.265,.035,1.49))}
|
||||||
|
WRISTS={'L':Vector((.12,-.57,1.275)),'R':Vector((.12,-.27,1.25))}
|
||||||
|
POLES={'L':Vector((-.72,-.24,1.12)),'R':Vector((.78,-.12,1.16))}
|
||||||
|
ELBOWS={side:ik_elbow(SHOULDERS[side],WRISTS[side],.405,.395,POLES[side]) for side in ('L','R')}
|
||||||
|
WEAPON_ORIGIN=Vector((.12,-.30,1.34))
|
||||||
|
|
||||||
|
# Continuous abdomen/chest with smoothly shared pelvis, spine, and chest weights.
|
||||||
|
sections=[(.71,.13,.105),(.80,.185,.13),(.95,.17,.12),(1.09,.195,.14),
|
||||||
|
(1.22,.245,.165),(1.39,.28,.165),(1.50,.245,.135),(1.55,.12,.09)]
|
||||||
|
vs,fs,ws=[],[],[]
|
||||||
|
for j in range(43):
|
||||||
|
t=j/42; f=t*(len(sections)-1); idx=min(len(sections)-2,int(f)); u=f-idx
|
||||||
|
z,rx,ry=[sections[idx][k]*(1-u)+sections[idx+1][k]*u for k in range(3)]
|
||||||
|
for k in range(24):
|
||||||
|
a=2*pi*k/24; mod=1+.025*cos(a*4)
|
||||||
|
vs.append((rx*cos(a)*mod,.025+ry*sin(a),z))
|
||||||
|
ws.append(blend('Pelvis','Spine',(z-.81)/.32) if z<1.13 else blend('Spine','Chest',(z-1.13)/.30))
|
||||||
|
if j:
|
||||||
|
for k in range(24):
|
||||||
|
a=(j-1)*24+k; b=(j-1)*24+(k+1)%24
|
||||||
|
fs.append((a,b,b+24,a+24))
|
||||||
|
fs += [tuple(reversed(range(24))),tuple(range(42*24,43*24))]
|
||||||
|
mesh('Continuous_Mineral_Torso',vs,fs,'stone','Spine',ws)
|
||||||
|
for s,side in [(-1,'L'),(1,'R')]:
|
||||||
|
outline=[(s*.015,-.145,1.42),(s*.22,-.125,1.51),(s*.275,-.10,1.36),
|
||||||
|
(s*.205,-.17,1.20),(s*.035,-.19,1.07)]
|
||||||
|
shield('Bronze_Pectoral_Rim_'+side,outline,(s*.135,-.17,1.32),.065,'gold','Chest')
|
||||||
|
inset=[Vector(p).lerp(Vector((s*.14,-.19,1.32)),.11)+Vector((0,-.025,0)) for p in outline]
|
||||||
|
shield('Obsidian_Pectoral_'+side,inset,(s*.135,-.217,1.32),.06,'facet','Chest')
|
||||||
|
tube('Rib_Conduit_'+side,[(s*.205,-.13,1.16),(s*.15,-.145,1.06),(s*.11,-.12,.97)],
|
||||||
|
[.018,.024,.011],'ember','Spine',10,16,weight_fn=lambda t:blend('Chest','Spine',t))
|
||||||
|
outline=[(s*.03,-.10,.94),(s*.205,-.085,1.00),(s*.25,-.085,.85),(s*.16,-.14,.69)]
|
||||||
|
shield('Hip_Petal_'+side,outline,(s*.14,-.13,.85),.045,'gold','Pelvis')
|
||||||
|
shield('Hip_Dark_Inlay_'+side,[Vector(v).lerp(Vector((s*.14,-.155,.86)),.25) for v in outline],
|
||||||
|
(s*.14,-.17,.86),.018,'facet','Pelvis')
|
||||||
|
shard('Sternum_Ember',[(0,-.21,1.44),(0,-.242,1.28),(0,-.215,1.11)],.036,'ember','Chest')
|
||||||
|
tube('Neck_Flexible_Core',[(0,.025,1.49),(0,.025,1.60),(0,-.012,1.68)],
|
||||||
|
[.08,.076,.062],'dark','Neck',16,18,weight_fn=lambda t:blend('Chest','Head',t))
|
||||||
|
ring('Gorget',(0,.018,1.565),(0,0,1),.112,.045,'gold','Neck')
|
||||||
|
|
||||||
|
# Helmet: swept mineral hood and pointed visor framed in burnished bronze.
|
||||||
|
ell('Helmet_Core',(0,.017,1.73),(.135,.123,.181),'stone','Head',20,12,False)
|
||||||
|
helmet=[(-.14,-.061,1.82),(-.125,-.09,1.68),(0,-.112,1.585),(.125,-.09,1.68),(.14,-.061,1.82),(0,-.096,1.91)]
|
||||||
|
shield('Helmet_Bronze_Frame',helmet,(0,-.15,1.755),.057,'gold','Head')
|
||||||
|
inner=[Vector(p).lerp(Vector((0,-.17,1.76)),.18)+Vector((0,-.018,0)) for p in helmet]
|
||||||
|
shield('Helmet_Dark_Face',inner,(0,-.19,1.76),.032,'facet','Head')
|
||||||
|
for s,side in [(-1,'L'),(1,'R')]:
|
||||||
|
tube('Visor_'+side,[(s*.013,-.224,1.755),(s*.057,-.198,1.772),(s*.096,-.15,1.781)],
|
||||||
|
[.012,.013,.006],'ember','Head',8,9,flatten=.75,groove=0)
|
||||||
|
shard('Swept_Crest_'+side,[(s*.09,-.02,1.81),(s*.185,.05,1.925),(s*.205,.12,2.02)],.058,'gold','Head')
|
||||||
|
shard('Crest_Dark_Inlay_'+side,[(s*.11,.01,1.84),(s*.168,.055,1.93),(s*.20,.12,2.01)],.027,'facet','Head')
|
||||||
|
shard('Visor_Central_Seam',[(0,-.212,1.81),(0,-.221,1.75),(0,-.157,1.64)],.018,'ember','Head')
|
||||||
|
|
||||||
|
# Arms are one continuous deforming core each; armor leaves the bend zones clear.
|
||||||
|
for s,side in [(-1,'L'),(1,'R')]:
|
||||||
|
a,b,c=SHOULDERS[side],ELBOWS[side],WRISTS[side]
|
||||||
|
upper,fore,hand='UpperArm.'+side,'Forearm.'+side,'Hand.'+side
|
||||||
|
points=[a,a.lerp(b,.42),b,b.lerp(c,.50),c]
|
||||||
|
def arm_weights(t,upper=upper,fore=fore,hand=hand):
|
||||||
|
if t<.12:return blend('Chest',upper,t/.12)
|
||||||
|
if t<.43:return {upper:1}
|
||||||
|
if t<.60:return blend(upper,fore,(t-.43)/.17)
|
||||||
|
if t<.91:return {fore:1}
|
||||||
|
return blend(fore,hand,(t-.91)/.09)
|
||||||
|
tube('Continuous_Arm_'+side,points,[.079,.076,.055,.074,.052],'stone',upper,20,42,weight_fn=arm_weights)
|
||||||
|
ell('Elbow_Recess_'+side,b,(.067,.067,.067),'dark',fore,16,10)
|
||||||
|
ring('Elbow_Bearing_'+side,b+Vector((s*.025,0,0)),(s,0,0),.075,.018,'gold',fore)
|
||||||
|
shoulder_outline=[(s*.19,-.05,1.54),(s*.29,-.08,1.62),(s*.43,-.03,1.48),(s*.37,-.09,1.405),(s*.22,-.095,1.43)]
|
||||||
|
shield('Shoulder_Mantle_'+side,shoulder_outline,(s*.29,-.11,1.49),.05,'gold','Clavicle.'+side)
|
||||||
|
for start,end,key,radius,kind in [(a.lerp(b,.20),a.lerp(b,.73),upper,.082,'facet'),
|
||||||
|
(b.lerp(c,.18),b.lerp(c,.80),fore,.078,'facet')]:
|
||||||
|
tube('Tapered_Armor_'+key,[start,start.lerp(end,.45),end],[radius*.75,radius,radius*.65],kind,key,8,9,groove=0,smooth=False)
|
||||||
|
ring('Wrist_Bronze_Cuff_'+side,c.lerp(b,.13),c-b,.066,.037,'gold',fore)
|
||||||
|
# Closed hand around the receiver/support socket, with individually separated fingers.
|
||||||
|
ell('Palm_'+side,c+Vector((0,-.018,.027)),(.059,.067,.043),'facet',hand,16,10)
|
||||||
|
for k in range(4):
|
||||||
|
x=c.x+(k-1.5)*.026
|
||||||
|
tube('Grip_Finger_%s%d'%(side,k),[(x,c.y+.009,c.z+.049),(x,c.y-.049,c.z+.068),
|
||||||
|
(x,c.y-.072,c.z+.036),(x,c.y-.052,c.z+.005)],
|
||||||
|
[.012,.014,.013,.010],'facet','Index.'+side if k==0 else hand,10,10,groove=0)
|
||||||
|
tube('Grip_Thumb_'+side,[c+Vector((s*.047,.014,.018)),c+Vector((s*.065,-.022,.034)),
|
||||||
|
c+Vector((s*.036,-.055,.053))],[.016,.018,.012],'gold',hand,10,10,groove=0)
|
||||||
|
|
||||||
|
# Long tapered leg cores and separate articulated boots.
|
||||||
|
for s,side in [(-1,'L'),(1,'R')]:
|
||||||
|
hip=Vector((s*.13,.025,.83)); knee=Vector((s*.19,-.04,.48)); ankle=Vector((s*.20,.02,.135))
|
||||||
|
thigh,shin,foot='Thigh.'+side,'Shin.'+side,'Foot.'+side
|
||||||
|
def leg_weights(t,thigh=thigh,shin=shin,foot=foot):
|
||||||
|
if t<.42:return {thigh:1}
|
||||||
|
if t<.61:return blend(thigh,shin,(t-.42)/.19)
|
||||||
|
if t<.92:return {shin:1}
|
||||||
|
return blend(shin,foot,(t-.92)/.08)
|
||||||
|
tube('Continuous_Leg_'+side,[hip,hip.lerp(knee,.46),knee,knee.lerp(ankle,.55),ankle],
|
||||||
|
[.103,.105,.066,.078,.047],'stone',thigh,20,42,weight_fn=leg_weights)
|
||||||
|
for j,(center,w,h,bone) in enumerate([(hip.lerp(knee,.42),.13,.24,thigh),(knee.lerp(ankle,.54),.10,.255,shin)]):
|
||||||
|
x,y,z=center; outline=[(x-w*.42,y-.069,z+h*.48),(x+w*.46,y-.068,z+h*.31),
|
||||||
|
(x+w*.37,y-.072,z-h*.33),(x,y-.097,z-h*.55),(x-w*.52,y-.07,z-h*.17)]
|
||||||
|
shield('Leg_Bronze_Edge_%s%d'%(side,j),outline,(x,y-.102,z),.031,'gold',bone)
|
||||||
|
shield('Leg_Obsidian_Inlay_%s%d'%(side,j),[Vector(p).lerp(Vector((x,y-.12,z)),.18) for p in outline],
|
||||||
|
(x,y-.126,z),.028,'facet',bone)
|
||||||
|
ring('Knee_Bearing_'+side,knee+Vector((s*.015,0,0)),(1,0,0),.077,.021,'gold',shin)
|
||||||
|
ell('Knee_Glow_'+side,knee+Vector((0,-.065,0)),(.026,.016,.027),'ember',shin,12,8)
|
||||||
|
tube('Boot_'+side,[(s*.20,.06,.095),(s*.20,-.065,.073),(s*.20,-.205,.043)],
|
||||||
|
[.066,.085,.025],'facet',foot,10,14,flatten=.7,groove=0,smooth=False)
|
||||||
|
shield('Boot_Instep_'+side,[(s*.20-.055,-.015,.136),(s*.20+.055,-.015,.136),
|
||||||
|
(s*.20+.037,-.13,.090),(s*.20,-.19,.066),(s*.20-.037,-.13,.090)],
|
||||||
|
(s*.20,-.073,.129),.012,'gold',foot)
|
||||||
|
|
||||||
|
# Back capacitor is a crescent cradle with a visible but restrained ember core.
|
||||||
|
ell('Back_Reactor_Cradle',(0,.19,1.34),(.17,.07,.18),'gold','Chest',20,12,False)
|
||||||
|
ell('Back_Reactor_Obsidian',(0,.245,1.34),(.135,.045,.14),'facet','Chest',20,10,False)
|
||||||
|
ell('Back_Reactor_Ember',(0,.277,1.34),(.074,.018,.084),'ember','Chest',14,8,False)
|
||||||
|
for s,side in [(-1,'L'),(1,'R')]:
|
||||||
|
tube('Reactor_Conduit_'+side,[(s*.06,.24,1.20),(s*.10,.20,1.08),(s*.095,.14,.96)],
|
||||||
|
[.014,.017,.012],'gold','Spine',10,14,weight_fn=lambda t:blend('Chest','Spine',t))
|
||||||
|
|
||||||
|
# Lance rifle: forged receiver, offset shoulder stock, sliding breech and crystal cage.
|
||||||
|
x,z=.12,1.34
|
||||||
|
tube('Lance_Forged_Spine',[(x,-.13,z),(x,-.63,z),(x,-1.07,z)],
|
||||||
|
[.063,.053,.043],'facet','Weapon',10,24,groove=0,smooth=False)
|
||||||
|
tube('Shoulder_Stock',[(x,-.16,z),(.21,.015,1.39),(.27,.12,1.42)],
|
||||||
|
[.055,.048,.042],'stone','Weapon',10,14,groove=0,smooth=False)
|
||||||
|
ring('Stock_Butt',(.27,.12,1.42),(0,1,0),.071,.035,'gold','Weapon',10)
|
||||||
|
for k,y in enumerate([-.19,-.405,-.72,-1.045]):
|
||||||
|
ring('Lance_Bronze_Band_%d'%k,(x,y,z),(0,1,0),.072 if k<2 else .060,.044,'gold','Weapon',10)
|
||||||
|
ring('Lance_Band_Lip_%d'%k,(x,y-.020,z),(0,1,0),.074 if k<2 else .062,.008,'edge','Weapon',10)
|
||||||
|
for s in (-1,1):
|
||||||
|
tube('Lance_Energy_Rail_%s'%s,[(x+s*.046,-.39,z),(x+s*.046,-.73,z),(x+s*.037,-1.00,z)],
|
||||||
|
[.008,.008,.006],'ember','Weapon',8,16,groove=0)
|
||||||
|
tube('Lance_Rail_Frame_%s'%s,[(x+s*.055,-.44,z+.015),(x+s*.055,-.71,z+.015),(x+s*.044,-.96,z+.015)],
|
||||||
|
[.009,.009,.007],'gold','Weapon',8,16,groove=0)
|
||||||
|
tube('Trigger_Grip',[(x,-.27,z-.018),(x,-.255,1.24),(x,-.225,1.205)],
|
||||||
|
[.031,.035,.026],'dark','Weapon',10,10,groove=0,smooth=False)
|
||||||
|
tube('Trigger_Guard',[(x,-.23,1.25),(x,-.32,1.235),(x,-.36,1.29),(x,-.32,1.32)],
|
||||||
|
[.007,.007,.007,.007],'gold','Weapon',8,18,groove=0)
|
||||||
|
ring('Support_Socket',(x,-.585,z-.012),(0,1,0),.066,.08,'dark','Weapon',10)
|
||||||
|
tube('Sliding_Breech',[(x,-.16,z+.052),(x,-.31,z+.052),(x,-.43,z+.048)],
|
||||||
|
[.031,.032,.021],'steel','Bolt',8,9,groove=0,smooth=False)
|
||||||
|
tube('Breech_Handle',[(x+.024,-.205,z+.05),(x+.09,-.205,z+.042)],
|
||||||
|
[.013,.016],'gold','Bolt',10,4,groove=0)
|
||||||
|
ell('Charge_Chamber',(x,-.355,z),(.087,.082,.073),'gold','Chamber',12,8,False)
|
||||||
|
ell('Charge_Window',(x+.075,-.355,z),(.02,.053,.042),'ember','Chamber',12,8,False)
|
||||||
|
for s in (-1,1):
|
||||||
|
tube('Muzzle_Cage_%s'%s,[(x+s*.045,-1.02,z),(x+s*.105,-1.115,z),(x+s*.058,-1.26,z)],
|
||||||
|
[.02,.025,.011],'gold','Weapon',6,10,groove=0,smooth=False)
|
||||||
|
tube('Muzzle_Cage_V_%s'%s,[(x,-1.02,z+s*.043),(x,-1.115,z+s*.082),(x,-1.26,z+s*.048)],
|
||||||
|
[.018,.021,.009],'gold','Weapon',6,10,groove=0,smooth=False)
|
||||||
|
shard('Lance_Crystal',[(x,-1.04,z),(x,-1.205,z),(x,-1.435,z)],.062,'crystal','Crystal')
|
||||||
|
shard('Lance_Crystal_Inner',[(x,-1.055,z),(x,-1.215,z),(x,-1.427,z)],.031,'ember','Crystal')
|
||||||
|
tube('Optic_Rail',[(x,-.28,z+.062),(x,-.48,z+.072)], [.022,.022],'gold','Weapon',8,6,groove=0)
|
||||||
|
ell('Rear_Sight',(x,-.29,z+.086),(.026,.016,.024),'dark','Weapon',12,8)
|
||||||
|
ell('Rear_Sight_Glow',(x,-.279,z+.091),(.006,.004,.006),'ember','Weapon',10,6)
|
||||||
|
|
||||||
|
# Recalculate coherent outward normals and export triangulated closed surfaces.
|
||||||
|
for ob in parts:
|
||||||
|
bm=bmesh.new(); bm.from_mesh(ob.data)
|
||||||
|
bmesh.ops.remove_doubles(bm,verts=list(bm.verts),dist=.0000001)
|
||||||
|
bmesh.ops.triangulate(bm,faces=list(bm.faces))
|
||||||
|
bmesh.ops.recalc_face_normals(bm,faces=list(bm.faces))
|
||||||
|
if bm.calc_volume(signed=True)<0:bmesh.ops.reverse_faces(bm,faces=list(bm.faces))
|
||||||
|
bm.to_mesh(ob.data); bm.free(); ob.data.update()
|
||||||
|
|
||||||
|
bpy.ops.object.armature_add(enter_editmode=True)
|
||||||
|
rig=bpy.context.object; rig.name='FunkenRig'
|
||||||
|
ed=rig.data.edit_bones; ed.remove(ed[0])
|
||||||
|
def bone(name,head,tail,parent=None):
|
||||||
|
b=ed.new(name); b.head=head; b.tail=tail
|
||||||
|
if parent:b.parent=ed[parent]
|
||||||
|
defs[name]=(Vector(head),Vector(tail),parent)
|
||||||
|
bone('Root',(0,0,0),(0,0,.15))
|
||||||
|
bone('Pelvis',(0,.025,.80),(0,.025,1.01),'Root')
|
||||||
|
bone('Spine',(0,.025,1.01),(0,.025,1.28),'Pelvis')
|
||||||
|
bone('Chest',(0,.025,1.28),(0,.025,1.52),'Spine')
|
||||||
|
bone('Neck',(0,.025,1.52),(0,.015,1.64),'Chest')
|
||||||
|
bone('Head',(0,.015,1.64),(0,.015,1.88),'Neck')
|
||||||
|
for s,side in [(-1,'L'),(1,'R')]:
|
||||||
|
a,b,c=SHOULDERS[side],ELBOWS[side],WRISTS[side]
|
||||||
|
bone('Clavicle.'+side,(s*.09,.025,1.49),a,'Chest')
|
||||||
|
bone('UpperArm.'+side,a,b,'Clavicle.'+side)
|
||||||
|
bone('Forearm.'+side,b,c,'UpperArm.'+side)
|
||||||
|
bone('Hand.'+side,c,c+Vector((0,-.085,0)),'Forearm.'+side)
|
||||||
|
bone('Index.'+side,c+Vector((-.039,.009,.049)),c+Vector((-.039,-.052,.005)),'Hand.'+side)
|
||||||
|
bone('Thigh.'+side,(s*.13,.025,.83),(s*.19,-.04,.48),'Pelvis')
|
||||||
|
bone('Shin.'+side,(s*.19,-.04,.48),(s*.20,.02,.135),'Thigh.'+side)
|
||||||
|
bone('Foot.'+side,(s*.20,.02,.135),(s*.20,-.19,.066),'Shin.'+side)
|
||||||
|
bone('Weapon',WEAPON_ORIGIN,(.12,-.70,1.34),'Chest')
|
||||||
|
bone('Bolt',(.12,-.16,1.392),(.12,-.42,1.392),'Weapon')
|
||||||
|
bone('Crystal',(.12,-1.04,1.34),(.12,-1.40,1.34),'Weapon')
|
||||||
|
bone('Chamber',(.12,-.355,1.34),(.12,-.405,1.34),'Weapon')
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
for ob in parts:
|
||||||
|
mod=ob.modifiers.new('Weighted mineral deformation','ARMATURE'); mod.object=rig
|
||||||
|
ob.parent=rig
|
||||||
|
rig.show_in_front=True
|
||||||
|
|
||||||
|
clips={'idle':(3.0,True),'start':(.4,False),'walk':(1.2,True),'stop':(.4,False),
|
||||||
|
'attack_a':(1.3,False),'attack_b':(1.3,False),'hit':(.5,False),'ability':(1.3,False),'death':(2.0,False)}
|
||||||
|
|
||||||
|
|
||||||
|
def curve(t,keys):
|
||||||
|
for (a,x),(b,y) in zip(keys,keys[1:]):
|
||||||
|
if t<=b:
|
||||||
|
u=max(0,(t-a)/(b-a));u=u*u*(3-2*u)
|
||||||
|
return x*(1-u)+y*u
|
||||||
|
return keys[-1][1]
|
||||||
|
|
||||||
|
|
||||||
|
def pose_rotation(pb,r):
|
||||||
|
q=Quaternion((0,0,1),r[2])@Quaternion((0,1,0),r[1])@Quaternion((1,0,0),r[0])
|
||||||
|
basis=pb.bone.matrix_local.to_quaternion()
|
||||||
|
return basis.inverted()@q@basis
|
||||||
|
|
||||||
|
|
||||||
|
def aim_bone(name,head,tail):
|
||||||
|
pb=rig.pose.bones[name]; rest=pb.bone.matrix_local
|
||||||
|
direction=defs[name][1]-defs[name][0]
|
||||||
|
rotation=direction.rotation_difference(tail-head).to_matrix().to_4x4()
|
||||||
|
pose=rotation@rest
|
||||||
|
pose.translation=head
|
||||||
|
pb.matrix=pose
|
||||||
|
bpy.context.view_layer.update()
|
||||||
|
|
||||||
|
|
||||||
|
def animate(name,t,dur):
|
||||||
|
rs,locs={},{}
|
||||||
|
def rot(b,x=0,y=0,z=0):rs[b]=(x,y,z)
|
||||||
|
if name=='idle':
|
||||||
|
breath=sin(2*pi*t/dur)
|
||||||
|
rot('Spine',.008*breath);rot('Chest',-.010*breath,0,.005*breath)
|
||||||
|
rot('Head',.008*breath,0,-.014*breath)
|
||||||
|
rot('Weapon',-.035+.006*breath)
|
||||||
|
elif name in ('walk','start','stop'):
|
||||||
|
fade=1 if name=='walk' else sin(pi*.5*t/dur) if name=='start' else cos(pi*.5*t/dur)
|
||||||
|
phase=2*pi*t/1.2; roll=sin(phase)*fade
|
||||||
|
rot('Pelvis',.025*fade,0,.035*roll);rot('Spine',-.025*fade,0,-.032*roll)
|
||||||
|
rot('Chest',.015*fade,0,.012*roll);rot('Head',-.012*fade,0,-.01*roll)
|
||||||
|
rot('Weapon',-.04*fade+.007*sin(phase*2)*fade)
|
||||||
|
for s,side in [(-1,'L'),(1,'R')]:
|
||||||
|
q=sin(phase+(0 if s<0 else pi));lift=max(0,q)
|
||||||
|
rot('Thigh.'+side,.30*q*fade)
|
||||||
|
rot('Shin.'+side,-.48*lift*fade)
|
||||||
|
rot('Foot.'+side,(-.30*q+.48*lift)*fade)
|
||||||
|
locs['Root']=(0,0,-.011*(1-cos(phase*2))*fade)
|
||||||
|
elif name in ('attack_a','attack_b','ability'):
|
||||||
|
braced=name=='attack_b'; charged=name=='ability'
|
||||||
|
aim=curve(t,[(0,0),(.30,.82),(.60,1),(.90,1),(1.04,.8),(1.30,0)])
|
||||||
|
# Contact is exactly .90 seconds; all backward recoil starts after it.
|
||||||
|
recoil=curve(t,[(0,0),(.90,0),(.933333,1),(1.04,.24),(1.14,0),(1.30,0)])
|
||||||
|
reload=curve(t,[(0,0),(.90,0),(.98,1),(1.10,1),(1.24,0),(1.30,0)])
|
||||||
|
strength=1.35 if charged else .9 if braced else 1.0
|
||||||
|
rot('Spine',-.035*aim+.025*recoil,0,-.022*aim if braced else .01*aim)
|
||||||
|
rot('Chest',-.015*aim+.015*recoil,0,.023*aim if braced else -.012*aim)
|
||||||
|
rot('Neck',.014*aim);rot('Head',.018*aim,0,-.02*aim)
|
||||||
|
rot('Weapon',.042*aim+.055*recoil*strength,0,.009*aim if braced else 0)
|
||||||
|
locs['Weapon']=(0,.054*recoil*strength,-.006*reload)
|
||||||
|
locs['Bolt']=(0,.075*reload,0)
|
||||||
|
rot('Index.R',-.085*curve(t,[(0,0),(.82,0),(.90,1),(.99,0),(1.3,0)]))
|
||||||
|
if braced:
|
||||||
|
locs['Root']=(0,.008*aim,-.025*aim)
|
||||||
|
rot('Thigh.L',.07*aim);rot('Shin.L',-.12*aim);rot('Foot.L',.05*aim)
|
||||||
|
rot('Thigh.R',.07*aim);rot('Shin.R',-.12*aim);rot('Foot.R',.05*aim)
|
||||||
|
if charged:
|
||||||
|
charge=curve(t,[(0,0),(.35,.4),(.78,1),(.90,1),(.97,0),(1.30,0)])
|
||||||
|
locs['Chamber']=(.014*charge,0,0)
|
||||||
|
rot('Crystal',0,.45*charge,0)
|
||||||
|
rot('Head',.018*aim,0,-.035*aim)
|
||||||
|
elif name=='hit':
|
||||||
|
q=curve(t,[(0,0),(.066667,1),(.20,.4),(.5,0)])
|
||||||
|
rot('Spine',.065*q,0,-.04*q);rot('Head',.08*q,0,.05*q)
|
||||||
|
rot('Weapon',.035*q)
|
||||||
|
elif name=='death':
|
||||||
|
q=curve(t,[(0,0),(.3,.1),(.72,.56),(1.25,1),(1.5,.97),(2,1)])
|
||||||
|
rot('Root',-.58*q,0,-.30*q);locs['Root']=(0,-.10*q,-.22*q)
|
||||||
|
rot('Pelvis',-.1*q);rot('Spine',-.12*q);rot('Head',-.22*q)
|
||||||
|
rot('Weapon',-.09*q,0,.08*q)
|
||||||
|
for side in ('L','R'):
|
||||||
|
rot('Thigh.'+side,.25*q);rot('Shin.'+side,-.64*q);rot('Foot.'+side,.15*q)
|
||||||
|
for pb in rig.pose.bones:
|
||||||
|
pb.rotation_mode='QUATERNION';pb.rotation_quaternion=pose_rotation(pb,rs.get(pb.name,(0,0,0)))
|
||||||
|
pb.location=pb.bone.matrix_local.to_3x3().inverted()@Vector(locs.get(pb.name,(0,0,0)))
|
||||||
|
pb.scale=(1,1,1)
|
||||||
|
bpy.context.view_layer.update()
|
||||||
|
weapon=rig.pose.bones['Weapon'].matrix@rig.data.bones['Weapon'].matrix_local.inverted()
|
||||||
|
body=rig.pose.bones['Chest'].matrix@rig.data.bones['Chest'].matrix_local.inverted()
|
||||||
|
errors=[]
|
||||||
|
for side in ('L','R'):
|
||||||
|
a=rig.pose.bones['UpperArm.'+side].head.copy()
|
||||||
|
c=weapon@WRISTS[side]
|
||||||
|
pole=body@POLES[side]
|
||||||
|
elbow=ik_elbow(a,c,.405,.395,pole)
|
||||||
|
aim_bone('UpperArm.'+side,a,elbow)
|
||||||
|
aim_bone('Forearm.'+side,elbow,c)
|
||||||
|
hand=weapon@rig.data.bones['Hand.'+side].matrix_local
|
||||||
|
hand.translation=c
|
||||||
|
rig.pose.bones['Hand.'+side].matrix=hand
|
||||||
|
bpy.context.view_layer.update()
|
||||||
|
errors.append((rig.pose.bones['Forearm.'+side].tail-c).length)
|
||||||
|
contact_samples.append({'clip':name,'t':round(t,6),'max_wrist_error':max(errors)})
|
||||||
|
|
||||||
|
|
||||||
|
for name,(dur,loop) in clips.items():
|
||||||
|
rig.animation_data_clear(); rig.animation_data_create()
|
||||||
|
action=bpy.data.actions.new(name);action.use_fake_user=True;rig.animation_data.action=action
|
||||||
|
for frame in range(round(dur*30)+1):
|
||||||
|
animate(name,frame/30,dur)
|
||||||
|
for pb in rig.pose.bones:
|
||||||
|
pb.keyframe_insert('rotation_quaternion',frame=frame)
|
||||||
|
pb.keyframe_insert('location',frame=frame)
|
||||||
|
action['loop']=loop
|
||||||
|
action['contact_seconds']=.90 if name in ('attack_a','attack_b','ability') else -1
|
||||||
|
# Every sample is explicit; linear interpolation avoids wrist overshoot.
|
||||||
|
for layer in action.layers:
|
||||||
|
for strip in layer.strips:
|
||||||
|
for bag in strip.channelbags:
|
||||||
|
for fc in bag.fcurves:
|
||||||
|
for key in fc.keyframe_points:key.interpolation='LINEAR'
|
||||||
|
rig.animation_data_clear();rig.animation_data_create();rig.animation_data.action=bpy.data.actions['idle']
|
||||||
|
bpy.context.scene.render.fps=30;bpy.context.scene.frame_set(0)
|
||||||
|
|
||||||
|
# Save named objects, editable weights, and actions before joining the export mesh.
|
||||||
|
bpy.ops.wm.save_as_mainfile(filepath=str(SRC/'funkenlanze.blend'))
|
||||||
|
source_parts=len(parts)
|
||||||
|
bpy.ops.object.select_all(action='DESELECT')
|
||||||
|
for ob in parts:ob.select_set(True)
|
||||||
|
bpy.context.view_layer.objects.active=parts[0];bpy.ops.object.join()
|
||||||
|
body=bpy.context.object;body.name='FunkenBody'
|
||||||
|
body.select_set(True);rig.select_set(True);bpy.context.view_layer.objects.active=rig
|
||||||
|
bpy.ops.export_scene.gltf(filepath=str(OUT/'funkenlanze.glb'),export_format='GLB',use_selection=True,
|
||||||
|
export_animations=True,export_animation_mode='ACTIONS',export_force_sampling=True,
|
||||||
|
export_frame_range=False,export_anim_slide_to_zero=True,export_skins=True,
|
||||||
|
export_all_influences=False,export_def_bones=True,export_anim_single_armature=True,
|
||||||
|
export_materials='EXPORT',export_yup=True)
|
||||||
|
raw=(OUT/'funkenlanze.glb').read_bytes()
|
||||||
|
doc=json.loads(raw[20:20+struct.unpack_from('<I',raw,12)[0]])
|
||||||
|
triangles=sum(doc['accessors'][q['indices']]['count']//3 for m in doc['meshes'] for q in m['primitives'])
|
||||||
|
mins=[min(v.co[i] for v in body.data.vertices) for i in range(3)]
|
||||||
|
maxs=[max(v.co[i] for v in body.data.vertices) for i in range(3)]
|
||||||
|
record={'id':'g05','role':'ranged','name':'Funkenlanze','resource_path':'res://assets/characters/funkenlanze.glb',
|
||||||
|
'triangles':triangles,'mesh_parts':1,'parts':['FunkenBody'],
|
||||||
|
'bounds_godot':{'min':[mins[0],mins[2],-maxs[1]],'max':[maxs[0],maxs[2],-mins[1]]},
|
||||||
|
'skeletal':True,'bones':len(defs),'clips':list(clips),
|
||||||
|
'attack_cycle':['attack_a','attack_b','attack_a','ability'],
|
||||||
|
'contact_seconds':.9,'sha256':hashlib.sha256(raw).hexdigest(),
|
||||||
|
'source':'assets-source/phase18/funkenlanze.blend'}
|
||||||
|
(P/'assets/phase18_manifest.json').write_text(json.dumps({'assets':[record]},indent=2)+'\n')
|
||||||
|
audit={'record':record,'exported_clips':[a['name'] for a in doc['animations']],
|
||||||
|
'skins':len(doc.get('skins',[])),'source_parts':source_parts,
|
||||||
|
'materials':[m['name'] for m in doc['materials']],
|
||||||
|
'max_baked_wrist_contact_error':max(x['max_wrist_error'] for x in contact_samples),
|
||||||
|
'contact_sample_count':len(contact_samples),
|
||||||
|
'contact_validation':'Analytic two-bone arm IK baked at 30 Hz for all nine clips.',
|
||||||
|
'shot_seconds':.90,'recoil_peak_seconds':.933333,'breech_closes_seconds':1.24}
|
||||||
|
(SRC/'funkenlanze-audit.json').write_text(json.dumps(audit,indent=2)+'\n')
|
||||||
|
print('FUNKENLANZE_BUILD',triangles,'triangles',len(defs),'bones',len(doc['animations']),'clips',
|
||||||
|
'wrist error',audit['max_baked_wrist_contact_error'],flush=True)
|
||||||
76
assets-source/phase18/funkenlanze-audit.json
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
{
|
||||||
|
"record": {
|
||||||
|
"id": "g05",
|
||||||
|
"role": "ranged",
|
||||||
|
"name": "Funkenlanze",
|
||||||
|
"resource_path": "res://assets/characters/funkenlanze.glb",
|
||||||
|
"triangles": 24036,
|
||||||
|
"mesh_parts": 1,
|
||||||
|
"parts": [
|
||||||
|
"FunkenBody"
|
||||||
|
],
|
||||||
|
"bounds_godot": {
|
||||||
|
"min": [
|
||||||
|
-0.4300000071525574,
|
||||||
|
-0.010409347712993622,
|
||||||
|
-0.2945486903190613
|
||||||
|
],
|
||||||
|
"max": [
|
||||||
|
0.5651692152023315,
|
||||||
|
2.021195411682129,
|
||||||
|
1.4350000619888306
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"skeletal": true,
|
||||||
|
"bones": 26,
|
||||||
|
"clips": [
|
||||||
|
"idle",
|
||||||
|
"start",
|
||||||
|
"walk",
|
||||||
|
"stop",
|
||||||
|
"attack_a",
|
||||||
|
"attack_b",
|
||||||
|
"hit",
|
||||||
|
"ability",
|
||||||
|
"death"
|
||||||
|
],
|
||||||
|
"attack_cycle": [
|
||||||
|
"attack_a",
|
||||||
|
"attack_b",
|
||||||
|
"attack_a",
|
||||||
|
"ability"
|
||||||
|
],
|
||||||
|
"contact_seconds": 0.9,
|
||||||
|
"sha256": "cc075b6e69ee3d25cf19ec9da8ecde6f0bcc7e98f7e5400ff214061fd8af67fd",
|
||||||
|
"source": "assets-source/phase18/funkenlanze.blend"
|
||||||
|
},
|
||||||
|
"exported_clips": [
|
||||||
|
"ability",
|
||||||
|
"attack_a",
|
||||||
|
"attack_b",
|
||||||
|
"death",
|
||||||
|
"hit",
|
||||||
|
"idle",
|
||||||
|
"start",
|
||||||
|
"stop",
|
||||||
|
"walk"
|
||||||
|
],
|
||||||
|
"skins": 1,
|
||||||
|
"source_parts": 104,
|
||||||
|
"materials": [
|
||||||
|
"FL_Basalt",
|
||||||
|
"FL_BurnishedBronze",
|
||||||
|
"FL_Obsidian",
|
||||||
|
"FL_Ember",
|
||||||
|
"FL_Joint",
|
||||||
|
"FL_BronzeEdge",
|
||||||
|
"FL_Iron",
|
||||||
|
"FL_Crystal"
|
||||||
|
],
|
||||||
|
"max_baked_wrist_contact_error": 3.0683368864616754e-07,
|
||||||
|
"contact_sample_count": 351,
|
||||||
|
"contact_validation": "Analytic two-bone arm IK baked at 30 Hz for all nine clips.",
|
||||||
|
"shot_seconds": 0.9,
|
||||||
|
"recoil_peak_seconds": 0.933333,
|
||||||
|
"breech_closes_seconds": 1.24
|
||||||
|
}
|
||||||
BIN
assets-source/phase18/funkenlanze.blend
Normal file
788
assets-source/phase19/build_glasballiste.py
Normal file
|
|
@ -0,0 +1,788 @@
|
||||||
|
"""Glasballiste: headless basalt ballista walker with a faceted ember-crystal bow.
|
||||||
|
|
||||||
|
Run: ALSOFT_DRIVERS=null blender -b --python-exit-code 1 --python assets-source/phase19/build_glasballiste.py
|
||||||
|
Blender is Z-up, -Y-forward. Rest pose is cocked and loaded. Foot contacts are solved
|
||||||
|
analytically per sample (two-bone leg IK, planted tarsus and toes) and baked to FK:
|
||||||
|
no runtime IK or constraints. One walk cycle travels 0.52 m, matching SkeletalFigure.
|
||||||
|
"""
|
||||||
|
from pathlib import Path
|
||||||
|
import bpy, bmesh, math, json, hashlib, struct
|
||||||
|
from math import sin, cos, pi, sqrt, exp
|
||||||
|
from mathutils import Vector, Quaternion
|
||||||
|
|
||||||
|
P = Path(__file__).resolve().parents[2]
|
||||||
|
SRC = Path(__file__).resolve().parent
|
||||||
|
OUT = P / 'assets/characters'
|
||||||
|
OUT.mkdir(exist_ok=True)
|
||||||
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||||
|
bpy.context.preferences.filepaths.save_version = 0
|
||||||
|
parts, defs, contact_samples, foot_samples = [], {}, [], []
|
||||||
|
X, Y, Z = Vector((1, 0, 0)), Vector((0, 1, 0)), Vector((0, 0, 1))
|
||||||
|
|
||||||
|
|
||||||
|
def material(name, color, rough=.55, metal=0, emission=0):
|
||||||
|
m = bpy.data.materials.new('GB_' + name)
|
||||||
|
m.diffuse_color = (*color, 1)
|
||||||
|
m.use_nodes = True
|
||||||
|
n, links = m.node_tree.nodes, m.node_tree.links
|
||||||
|
bs = n.get('Principled BSDF')
|
||||||
|
bs.inputs['Base Color'].default_value = (*color, 1)
|
||||||
|
bs.inputs['Metallic'].default_value = metal
|
||||||
|
bs.inputs['Roughness'].default_value = rough
|
||||||
|
vertex = n.new('ShaderNodeVertexColor')
|
||||||
|
vertex.layer_name = 'SurfaceColor'
|
||||||
|
links.new(vertex.outputs['Color'], bs.inputs['Base Color'])
|
||||||
|
if emission:
|
||||||
|
bs.inputs['Emission Color'].default_value = (*color, 1)
|
||||||
|
bs.inputs['Emission Strength'].default_value = emission
|
||||||
|
else:
|
||||||
|
tex = n.new('ShaderNodeTexNoise')
|
||||||
|
tex.inputs['Scale'].default_value = 70 if metal > .5 else 28
|
||||||
|
tex.inputs['Detail'].default_value = 2
|
||||||
|
bump = n.new('ShaderNodeBump')
|
||||||
|
bump.inputs['Strength'].default_value = .12 if metal > .5 else .20
|
||||||
|
bump.inputs['Distance'].default_value = .006
|
||||||
|
links.new(tex.outputs['Fac'], bump.inputs['Height'])
|
||||||
|
links.new(bump.outputs['Normal'], bs.inputs['Normal'])
|
||||||
|
return m
|
||||||
|
|
||||||
|
|
||||||
|
# Concept: dark basalt chassis, brass frames, large orange crystal bow limbs.
|
||||||
|
M = {
|
||||||
|
'basalt': material('Basalt', (.085, .085, .095), .80, 0),
|
||||||
|
'plate': material('BasaltPlate', (.115, .11, .115), .70, .05),
|
||||||
|
'bronze': material('Bronze', (.60, .42, .18), .34, .82),
|
||||||
|
'edge': material('BronzeEdge', (.86, .64, .30), .26, .80),
|
||||||
|
'joint': material('Joint', (.045, .045, .05), .58, .35),
|
||||||
|
'iron': material('Iron', (.23, .24, .26), .38, .78),
|
||||||
|
'crystal': material('Crystal', (1.0, .34, .05), .14, .05, .30),
|
||||||
|
'ember': material('Ember', (1.0, .58, .14), .30, .10, 2.2),
|
||||||
|
}
|
||||||
|
CRYSTAL = ((.62, .12, .02), (1.0, .50, .10))
|
||||||
|
|
||||||
|
|
||||||
|
def hash3(p, salt=0.):
|
||||||
|
v = sin(p[0] * 12.9898 + p[1] * 78.233 + p[2] * 37.719 + salt * 11.13) * 43758.5453
|
||||||
|
return v - math.floor(v)
|
||||||
|
|
||||||
|
|
||||||
|
def mesh(name, vs, fs, kind, bone, weights=None, smooth=True, uv=None, facet=0., palette=None):
|
||||||
|
me = bpy.data.meshes.new(name)
|
||||||
|
me.from_pydata(vs, [], fs)
|
||||||
|
me.update()
|
||||||
|
ob = bpy.data.objects.new(name, me)
|
||||||
|
bpy.context.collection.objects.link(ob)
|
||||||
|
parts.append(ob)
|
||||||
|
me.materials.append(M[kind])
|
||||||
|
col = me.color_attributes.new(name='SurfaceColor', type='FLOAT_COLOR', domain='CORNER')
|
||||||
|
tex = me.uv_layers.new(name='UVMap')
|
||||||
|
base = M[kind].diffuse_color[:3]
|
||||||
|
for face in me.polygons:
|
||||||
|
face.use_smooth = smooth
|
||||||
|
# Faceted crystal and rock: one tone per face, so facets read under a moving light.
|
||||||
|
h = hash3(face.center, len(parts)) if palette or facet else 0
|
||||||
|
for li in face.loop_indices:
|
||||||
|
vi = me.loops[li].vertex_index
|
||||||
|
x, y, z = vs[vi]
|
||||||
|
if palette:
|
||||||
|
color = [a * (1 - h) + b * h for a, b in zip(*palette)]
|
||||||
|
elif facet:
|
||||||
|
color = [min(1, c * (1 - facet + 2 * facet * h)) for c in base]
|
||||||
|
else:
|
||||||
|
variation = .93 + .055 * sin(19 * x + 13 * z) + .025 * cos(22 * y - 9 * z)
|
||||||
|
color = [min(1, c * variation) for c in base]
|
||||||
|
col.data[li].color = (*color, 1)
|
||||||
|
tex.data[li].uv = uv[vi] if uv else (x * 1.7 + y * .3, z * 1.5)
|
||||||
|
groups = {}
|
||||||
|
for i in range(len(vs)):
|
||||||
|
ws = weights[i] if weights else {bone: 1.0}
|
||||||
|
total = sum(ws.values())
|
||||||
|
for key, weight in ws.items():
|
||||||
|
if weight <= 1e-8:
|
||||||
|
continue
|
||||||
|
groups.setdefault(key, ob.vertex_groups.get(key) or ob.vertex_groups.new(name=key))
|
||||||
|
groups[key].add([i], weight / total, 'REPLACE')
|
||||||
|
return ob
|
||||||
|
|
||||||
|
|
||||||
|
def blend(a, b, t):
|
||||||
|
t = max(0, min(1, t))
|
||||||
|
return {a: 1 - t, b: t} if a != b else {a: 1}
|
||||||
|
|
||||||
|
|
||||||
|
def smooth(u):
|
||||||
|
u = max(0, min(1, u))
|
||||||
|
return u * u * (3 - 2 * u)
|
||||||
|
|
||||||
|
|
||||||
|
def catmull(points, t):
|
||||||
|
seg = min(len(points) - 2, int(t * (len(points) - 1)))
|
||||||
|
u = t * (len(points) - 1) - seg
|
||||||
|
a, b, c, d = [points[max(0, min(len(points) - 1, q))] for q in (seg - 1, seg, seg + 1, seg + 2)]
|
||||||
|
return .5 * ((2 * b) + (-a + c) * u + (2 * a - 5 * b + 4 * c - d) * u * u + (-a + 3 * b - 3 * c + d) * u * u * u)
|
||||||
|
|
||||||
|
|
||||||
|
def interp(values, t):
|
||||||
|
seg = min(len(values) - 2, int(t * (len(values) - 1)))
|
||||||
|
a = t * (len(values) - 1) - seg
|
||||||
|
return tuple(x * (1 - a) + y * a for x, y in zip(values[seg], values[seg + 1]))
|
||||||
|
|
||||||
|
|
||||||
|
def frame(tangent, hint):
|
||||||
|
t = tangent.normalized()
|
||||||
|
h = Vector(hint)
|
||||||
|
s = h - t * t.dot(h)
|
||||||
|
if s.length < 1e-5:
|
||||||
|
alt = Y if abs(t.y) < .9 else X
|
||||||
|
s = alt - t * t.dot(alt)
|
||||||
|
s.normalize()
|
||||||
|
return s, t.cross(s).normalized()
|
||||||
|
|
||||||
|
|
||||||
|
def chamfer(w, d, c=.3):
|
||||||
|
k = min(w, d) * c
|
||||||
|
return [(w, d - k), (w - k, d), (k - w, d), (-w, d - k), (-w, k - d), (k - w, -d), (w - k, -d), (w, k - d)]
|
||||||
|
|
||||||
|
|
||||||
|
def circle(sides):
|
||||||
|
return lambda w, d: [(w * cos(2 * pi * k / sides), d * sin(2 * pi * k / sides)) for k in range(sides)]
|
||||||
|
|
||||||
|
|
||||||
|
def lens(w, d):
|
||||||
|
# w: half height across the hint axis, d: half thickness. Six ridged crystal faces.
|
||||||
|
return [(w, 0), (w * .38, d), (-w * .38, d), (-w, 0), (-w * .38, -d), (w * .38, -d)]
|
||||||
|
|
||||||
|
|
||||||
|
def wedge(w, d):
|
||||||
|
return [(-w, -d), (w, -d), (0, d)]
|
||||||
|
|
||||||
|
|
||||||
|
def loft(name, path, sizes, kind, bone, hint=(1, 0, 0), profile=chamfer, steps=0, weight_fn=None,
|
||||||
|
smooth=False, facet=0., palette=None, jitter=0., tri=False):
|
||||||
|
"""Closed prism along a path; profile axes are the hint direction and tangent x hint."""
|
||||||
|
pts = [Vector(p) for p in path]
|
||||||
|
count = steps + 1 if steps else len(pts)
|
||||||
|
centers = [catmull(pts, j / (count - 1)) if steps else pts[j] for j in range(count)]
|
||||||
|
vs, fs, ws, uv = [], [], [], []
|
||||||
|
n, length = 0, 0
|
||||||
|
for j, c in enumerate(centers):
|
||||||
|
t = j / (count - 1)
|
||||||
|
s, u = frame(centers[min(count - 1, j + 1)] - centers[max(0, j - 1)], hint)
|
||||||
|
ring = profile(*interp(sizes, t))
|
||||||
|
n = len(ring)
|
||||||
|
if j:
|
||||||
|
length += (c - centers[j - 1]).length
|
||||||
|
for k, (a, b) in enumerate(ring):
|
||||||
|
scale = 1 + jitter * (2 * hash3((j, k, len(parts))) - 1) if jitter and 0 < j < count - 1 else 1
|
||||||
|
vs.append(c + (s * a + u * b) * scale)
|
||||||
|
ws.append(weight_fn(t) if weight_fn else {bone: 1})
|
||||||
|
uv.append((k / n, length * 2))
|
||||||
|
for j in range(1, count):
|
||||||
|
for k in range(n):
|
||||||
|
a, b = (j - 1) * n + k, (j - 1) * n + (k + 1) % n
|
||||||
|
if not tri:
|
||||||
|
fs.append((a, b, b + n, a + n))
|
||||||
|
elif (j + k) % 2:
|
||||||
|
fs += [(a, b, a + n), (b, b + n, a + n)]
|
||||||
|
else:
|
||||||
|
fs += [(a, b, b + n), (a, b + n, a + n)]
|
||||||
|
fs += [tuple(reversed(range(n))), tuple(range((count - 1) * n, count * n))]
|
||||||
|
return mesh(name, vs, fs, kind, bone, ws, smooth, uv, facet, palette)
|
||||||
|
|
||||||
|
|
||||||
|
def tube(name, points, radii, kind, bone, sides=12, steps=16, weight_fn=None):
|
||||||
|
return loft(name, points, [(r, r) for r in radii], kind, bone, (0, 0, 1), circle(sides),
|
||||||
|
steps, weight_fn, smooth=True)
|
||||||
|
|
||||||
|
|
||||||
|
def strut(name, a, b, r, kind, bone, hint=(0, 0, 1)):
|
||||||
|
a, b = Vector(a), Vector(b)
|
||||||
|
# Overlap corners by less than the radius so neighbouring struts never share an edge.
|
||||||
|
d = (b - a).normalized() * r * .8
|
||||||
|
return loft(name, [a - d, b + d], [(r, r), (r, r)], kind, bone, hint)
|
||||||
|
|
||||||
|
|
||||||
|
def disc(name, c, axis, r, width, kind, bone, sides=20):
|
||||||
|
c, ax = Vector(c), Vector(axis).normalized()
|
||||||
|
return loft(name, [c - ax * width / 2, c + ax * width / 2], [(r, r), (r, r)], kind, bone,
|
||||||
|
(0, 0, 1) if abs(ax.z) < .9 else (1, 0, 0), circle(sides))
|
||||||
|
|
||||||
|
|
||||||
|
def annulus(name, c, axis, r_in, r_out, width, kind, bone, sides=20):
|
||||||
|
c, ax = Vector(c), Vector(axis).normalized()
|
||||||
|
s, u = frame(ax, (0, 0, 1) if abs(ax.z) < .9 else (1, 0, 0))
|
||||||
|
vs, fs = [], []
|
||||||
|
for r, off in ((r_out, -width / 2), (r_out, width / 2), (r_in, width / 2), (r_in, -width / 2)):
|
||||||
|
for k in range(sides):
|
||||||
|
a = 2 * pi * k / sides
|
||||||
|
vs.append(c + ax * off + (s * cos(a) + u * sin(a)) * r)
|
||||||
|
for ring in range(4):
|
||||||
|
nxt = (ring + 1) % 4
|
||||||
|
for k in range(sides):
|
||||||
|
q = (k + 1) % sides
|
||||||
|
fs.append((ring * sides + k, ring * sides + q, nxt * sides + q, nxt * sides + k))
|
||||||
|
return mesh(name, vs, fs, kind, bone, smooth=False)
|
||||||
|
|
||||||
|
|
||||||
|
def wheel(name, c, axis, r, width, spokes, kind, bone):
|
||||||
|
c, ax = Vector(c), Vector(axis).normalized()
|
||||||
|
s, u = frame(ax, (0, 0, 1))
|
||||||
|
annulus(name + '_Rim', c, ax, r * .80, r, width, kind, bone, 24)
|
||||||
|
annulus(name + '_Lip', c + ax * width * .55, ax, r * .84, r * .96, width * .25, 'edge', bone, 24)
|
||||||
|
disc(name + '_Hub', c, ax, r * .30, width * 1.6, 'edge', bone, 12)
|
||||||
|
for k in range(spokes):
|
||||||
|
a = 2 * pi * (k + .5) / spokes
|
||||||
|
d = s * cos(a) + u * sin(a)
|
||||||
|
loft(name + '_Spoke%d' % k, [c + d * r * .24, c + d * r * .84], [(width * .30, r * .075)] * 2,
|
||||||
|
kind, bone, tuple(ax))
|
||||||
|
for k in range(20):
|
||||||
|
a = 2 * pi * k / 20
|
||||||
|
d = s * cos(a) + u * sin(a)
|
||||||
|
loft(name + '_Tooth%d' % k, [c + d * r * .96, c + d * r * 1.10], [(width * .42, r * .075), (width * .36, r * .05)],
|
||||||
|
kind, bone, tuple(ax))
|
||||||
|
|
||||||
|
|
||||||
|
def rivet(name, c, normal, bone, r=.011):
|
||||||
|
return disc(name, c, normal, r, .012, 'edge', bone, 8)
|
||||||
|
|
||||||
|
|
||||||
|
def gem(name, c, up, side, top, bottom, r, kind, bone, sides=4, palette=CRYSTAL):
|
||||||
|
c, up = Vector(c), Vector(up).normalized()
|
||||||
|
s, u = frame(up, side)
|
||||||
|
vs = [c + up * top]
|
||||||
|
for k in range(sides):
|
||||||
|
a = 2 * pi * k / sides
|
||||||
|
vs.append(c + (s * cos(a) + u * sin(a)) * r)
|
||||||
|
vs.append(c - up * bottom)
|
||||||
|
fs = []
|
||||||
|
for k in range(sides):
|
||||||
|
q = (k + 1) % sides
|
||||||
|
fs += [(0, 1 + k, 1 + q), (sides + 1, 1 + q, 1 + k)]
|
||||||
|
return mesh(name, vs, fs, kind, bone, smooth=False, palette=palette)
|
||||||
|
|
||||||
|
|
||||||
|
def on_segment(a, b, f, side_offset, front_offset, hint=(1, 0, 0)):
|
||||||
|
"""Point on a limb segment, offset along the loft frame (hint side, tangent x hint front)."""
|
||||||
|
a, b = Vector(a), Vector(b)
|
||||||
|
s, u = frame(b - a, hint)
|
||||||
|
return a.lerp(b, f) + s * side_offset + u * front_offset
|
||||||
|
|
||||||
|
|
||||||
|
def ik_knee(a, c, l1, l2, pole):
|
||||||
|
delta = c - a
|
||||||
|
d = delta.length
|
||||||
|
if not abs(l1 - l2) + .00001 < d < l1 + l2 - .00001:
|
||||||
|
raise RuntimeError('Unreachable foot contact: %.6f for %.6f' % (d, l1 + l2))
|
||||||
|
line = delta / d
|
||||||
|
v = Vector(pole) - a
|
||||||
|
perpendicular = (v - line * v.dot(line)).normalized()
|
||||||
|
along = (l1 * l1 - l2 * l2 + d * d) / (2 * d)
|
||||||
|
return a + line * along + perpendicular * sqrt(max(0, l1 * l1 - along * along))
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- chassis (Pelvis)
|
||||||
|
loft('Hull', [(0, .24, .84), (0, .12, .84), (0, -.10, .84), (0, -.22, .84)],
|
||||||
|
[(.16, .11), (.20, .14), (.20, .14), (.15, .10)], 'basalt', 'Pelvis', facet=.10)
|
||||||
|
loft('Hull_Belt', [(0, .035, .84), (0, -.035, .84)], [(.212, .152)] * 2, 'bronze', 'Pelvis')
|
||||||
|
loft('Hull_Top_Plate', [(0, .19, .985), (0, -.17, .985)], [(.15, .022), (.14, .022)], 'plate', 'Pelvis', facet=.08)
|
||||||
|
loft('Hull_Nose', [(0, -.20, .84), (0, -.245, .835)], [(.13, .085), (.11, .07)], 'bronze', 'Pelvis')
|
||||||
|
loft('Hull_Keel', [(0, .12, .705), (0, -.14, .705)], [(.10, .035)] * 2, 'joint', 'Pelvis')
|
||||||
|
gem('Hull_Vent', (0, -.25, .82), (0, -1, 0), (1, 0, 0), .045, .01, .04, 'crystal', 'Pelvis', 4)
|
||||||
|
loft('Hull_Tail', [(0, .30, .86), (0, .23, .86)], [(.12, .075), (.14, .09)], 'bronze', 'Pelvis')
|
||||||
|
for s, side in [(-1, 'L'), (1, 'R')]:
|
||||||
|
hip = Vector((s * .24, .06, .80))
|
||||||
|
disc('Hip_Housing_' + side, (s * .215, .06, .80), X, .115, .10, 'joint', 'Pelvis', 28)
|
||||||
|
annulus('Hip_Rim_' + side, (s * .268, .06, .80), X, .07, .12, .026, 'bronze', 'Pelvis', 32)
|
||||||
|
disc('Hip_Cap_' + side, (s * .284, .06, .80), X, .05, .03, 'edge', 'Pelvis', 12)
|
||||||
|
for k in range(6):
|
||||||
|
a = 2 * pi * k / 6
|
||||||
|
disc('Hip_Bolt_%s%d' % (side, k), (s * .283, .06 + .095 * cos(a), .80 + .095 * sin(a)), X,
|
||||||
|
.011, .012, 'edge', 'Pelvis', 8)
|
||||||
|
loft('Hull_Side_Plate_' + side, [(s * .205, .17, .86), (s * .205, -.13, .86)],
|
||||||
|
[(.012, .085), (.012, .075)], 'plate', 'Pelvis', (1, 0, 0), facet=.08)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- turret neck (Spine)
|
||||||
|
disc('Turret_Ring', (0, .04, 1.00), Z, .15, .05, 'bronze', 'Spine', 32)
|
||||||
|
disc('Turret_Core', (0, .04, 1.06), Z, .115, .10, 'joint', 'Spine', 20)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- ballista (Weapon)
|
||||||
|
loft('Stock', [(0, .42, 1.17), (0, .20, 1.17), (0, -.40, 1.17), (0, -.66, 1.17)],
|
||||||
|
[(.085, .065), (.09, .07), (.085, .065), (.07, .055)], 'basalt', 'Weapon', facet=.10)
|
||||||
|
loft('Ember_Channel', [(0, .30, 1.245), (0, -.62, 1.245)], [(.03, .016)] * 2, 'ember', 'Weapon')
|
||||||
|
for s, side in [(-1, 'L'), (1, 'R')]:
|
||||||
|
loft('Rail_' + side, [(s * .058, .36, 1.25), (s * .058, -.64, 1.25)], [(.016, .018)] * 2, 'edge', 'Weapon')
|
||||||
|
for k, y in enumerate((-.52, -.16, .14)):
|
||||||
|
loft('Stock_Band_%d' % k, [(0, y + .03, 1.17), (0, y - .03, 1.17)], [(.098, .078)] * 2, 'bronze', 'Weapon')
|
||||||
|
for s in (-1, 1):
|
||||||
|
for z in (1.14, 1.20):
|
||||||
|
rivet('Band_Rivet_%d_%d_%d' % (k, s, round(z * 100)), (s * .099, y, z), X, 'Weapon')
|
||||||
|
for s in (-1, 1):
|
||||||
|
for z in (1.12, 1.28):
|
||||||
|
rivet('Rear_Rivet_%d_%d' % (s, round(z * 100)), (s * .08, .576, z), Y, 'Weapon')
|
||||||
|
for z in (.79, .89):
|
||||||
|
rivet('Belt_Rivet_%d_%d' % (s, round(z * 100)), (s * .213, 0, z), X, 'Pelvis')
|
||||||
|
loft('Muzzle', [(0, -.64, 1.17), (0, -.72, 1.17)], [(.08, .062), (.074, .056)], 'bronze', 'Weapon')
|
||||||
|
annulus('Muzzle_Ring', (0, -.725, 1.20), Y, .026, .05, .02, 'edge', 'Weapon', 16)
|
||||||
|
loft('Bow_Mount', [(0, -.24, 1.16), (0, -.38, 1.16)], [(.17, .06), (.16, .055)], 'bronze', 'Weapon')
|
||||||
|
loft('Bow_Mount_Core', [(0, -.235, 1.16), (0, -.385, 1.16)], [(.12, .045)] * 2, 'joint', 'Weapon')
|
||||||
|
loft('Trigger_Box', [(0, .20, 1.07), (0, -.04, 1.07)], [(.07, .05)] * 2, 'joint', 'Weapon')
|
||||||
|
loft('Rear_Housing', [(0, .54, 1.20), (0, .28, 1.20)], [(.16, .15), (.15, .14)], 'basalt', 'Weapon', facet=.10)
|
||||||
|
loft('Rear_Top_Plate', [(0, .55, 1.36), (0, .27, 1.36)], [(.15, .018)] * 2, 'bronze', 'Weapon')
|
||||||
|
loft('Rear_Fin', [(0, .50, 1.395), (0, .34, 1.395)], [(.10, .04), (.06, .04)], 'bronze', 'Weapon', (1, 0, 0), wedge)
|
||||||
|
loft('Rear_Back_Plate', [(0, .555, 1.20), (0, .575, 1.20)], [(.11, .10)] * 2, 'plate', 'Weapon')
|
||||||
|
gem('Rear_Sight', (0, .58, 1.26), (0, 1, 0), (1, 0, 0), .035, .005, .03, 'crystal', 'Weapon', 4)
|
||||||
|
for s, side in [(-1, 'L'), (1, 'R')]:
|
||||||
|
x = s * .167
|
||||||
|
corners = [(x, .30, 1.08), (x, .52, 1.08), (x, .52, 1.32), (x, .30, 1.32)]
|
||||||
|
for k in range(4):
|
||||||
|
strut('Truss_%s%d' % (side, k), corners[k], corners[(k + 1) % 4], .013, 'bronze', 'Weapon', (1, 0, 0))
|
||||||
|
strut('Truss_Diagonal_' + side, (x, .30, 1.08), (x, .52, 1.32), .012, 'bronze', 'Weapon', (1, 0, 0))
|
||||||
|
disc('Truss_Rivet_' + side, (x, .41, 1.20), X, .022, .02, 'edge', 'Weapon', 10)
|
||||||
|
|
||||||
|
# Crank wheels on both stock sides share one Gear bone (reload winds them).
|
||||||
|
GEAR = Vector((0, -.02, 1.17))
|
||||||
|
disc('Gear_Axle', GEAR, X, .026, .27, 'iron', 'Gear', 12)
|
||||||
|
for s, side in [(-1, 'L'), (1, 'R')]:
|
||||||
|
wheel('Crank_Wheel_' + side, GEAR + X * s * .118, X, .13, .03, 5, 'bronze', 'Gear')
|
||||||
|
|
||||||
|
# Bow limbs: faceted crystal, cocked backwards; each bends gradually from root to tip.
|
||||||
|
LIMB = [(.14, -.31, 1.165), (.35, -.38, 1.172), (.60, -.33, 1.176), (.80, -.16, 1.168), (.89, .03, 1.155)]
|
||||||
|
|
||||||
|
|
||||||
|
def limb_point(s, t):
|
||||||
|
return catmull([Vector((s * p[0], p[1], p[2])) for p in LIMB], t)
|
||||||
|
|
||||||
|
|
||||||
|
for s, side in [(-1, 'L'), (1, 'R')]:
|
||||||
|
limb, tip = 'Limb.' + side, 'LimbTip.' + side
|
||||||
|
|
||||||
|
def limb_weights(t, limb=limb, tip=tip):
|
||||||
|
return blend(limb, tip, (t - .16) / .66)
|
||||||
|
|
||||||
|
path = [(s * p[0], p[1], p[2]) for p in LIMB]
|
||||||
|
loft('Crystal_Limb_' + side, path, [(.092, .046), (.086, .042), (.070, .034), (.050, .025), (.028, .013)],
|
||||||
|
'crystal', limb, (0, 0, 1), lens, 18, limb_weights, palette=CRYSTAL, jitter=.045, tri=True)
|
||||||
|
loft('Limb_Clamp_' + side, [limb_point(s, 0), limb_point(s, .11)], [(.10, .054), (.095, .05)],
|
||||||
|
'bronze', limb, (0, 0, 1))
|
||||||
|
loft('Limb_Clamp_Edge_' + side, [limb_point(s, .105), limb_point(s, .13)], [(.104, .058)] * 2,
|
||||||
|
'edge', limb, (0, 0, 1))
|
||||||
|
loft('Limb_Band_' + side, [limb_point(s, .30), limb_point(s, .345)], [(.086, .044)] * 2,
|
||||||
|
'bronze', limb, (0, 0, 1))
|
||||||
|
loft('Limb_Tip_Cap_' + side, [limb_point(s, .87), limb_point(s, 1.0) + Vector((s * .012, .03, 0))],
|
||||||
|
[(.046, .030), (.040, .026)], 'bronze', tip, (0, 0, 1))
|
||||||
|
disc('Limb_Tip_Eye_' + side, limb_point(s, 1.0) + Vector((0, .035, 0)), Z, .024, .07, 'edge', tip, 10)
|
||||||
|
|
||||||
|
def cable_weights(t, tip=tip):
|
||||||
|
return blend(tip, 'Draw', (t - .05) / .90)
|
||||||
|
|
||||||
|
tube('Draw_Cable_' + side, [limb_point(s, 1.0) + Vector((0, .035, .002)), (s * .012, .262, 1.286)],
|
||||||
|
[.0085, .0085], 'iron', tip, 8, 18, cable_weights)
|
||||||
|
|
||||||
|
loft('Nock_Claw', [(0, .31, 1.286), (0, .225, 1.286)], [(.036, .03), (.03, .026)], 'bronze', 'Draw')
|
||||||
|
for s in (-1, 1):
|
||||||
|
loft('Nock_Prong_%d' % s, [(s * .022, .235, 1.30), (s * .022, .205, 1.31)], [(.008, .008)] * 2, 'edge', 'Draw')
|
||||||
|
|
||||||
|
# Bolt (Bolt) with a long crystal quarrel head (Quarrel).
|
||||||
|
loft('Bolt_Shaft', [(0, .25, 1.29), (0, -.70, 1.29)], [(.017, .017)] * 2, 'iron', 'Bolt')
|
||||||
|
for s in (-1, 1):
|
||||||
|
loft('Bolt_Vane_%d' % s, [(s * .03, .22, 1.29), (s * .03, .08, 1.29)], [(.02, .004), (.012, .004)], 'edge', 'Bolt')
|
||||||
|
loft('Bolt_Vane_Top', [(0, .22, 1.322), (0, .08, 1.322)], [(.004, .018), (.004, .011)], 'edge', 'Bolt')
|
||||||
|
disc('Quarrel_Collar', (0, -.69, 1.29), Y, .028, .03, 'edge', 'Quarrel', 10)
|
||||||
|
loft('Quarrel_Head', [(0, -.685, 1.29), (0, -.75, 1.29), (0, -.87, 1.29), (0, -1.0, 1.29)],
|
||||||
|
[(.022, .022), (.056, .056), (.040, .040), (.004, .004)], 'crystal', 'Quarrel', (0, 0, 1), circle(6),
|
||||||
|
palette=CRYSTAL, jitter=.06, tri=True)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- legs
|
||||||
|
LEGS = {}
|
||||||
|
for s, side in [(-1, 'L'), (1, 'R')]:
|
||||||
|
H = Vector((s * .24, .06, .80))
|
||||||
|
K = Vector((s * .27, -.12, .47))
|
||||||
|
A = Vector((s * .28, .10, .17))
|
||||||
|
B = Vector((s * .28, -.03, .05))
|
||||||
|
T = Vector((s * .28, -.21, .02))
|
||||||
|
LEGS[side] = {'H': H, 'K': K, 'A': A, 'B': B, 'T': T,
|
||||||
|
'pole': K + (K - (H + A) / 2).normalized() * .6}
|
||||||
|
thigh, shin, tarsus, foot = 'Thigh.' + side, 'Shin.' + side, 'Tarsus.' + side, 'Foot.' + side
|
||||||
|
# Rigid armor over continuous flexible sleeves at hip, knee, ankle and toe joints.
|
||||||
|
loft('Thigh_Armor_' + side, [H.lerp(K, .10), H.lerp(K, .50), H.lerp(K, .90)],
|
||||||
|
[(.075, .09), (.092, .115), (.07, .085)], 'basalt', thigh, facet=.10)
|
||||||
|
loft('Thigh_Trim_' + side, [on_segment(H, K, f, s * .078, .094) for f in (.16, .5, .84)],
|
||||||
|
[(.018, .016)] * 3, 'edge', thigh)
|
||||||
|
loft('Thigh_Plate_' + side, [on_segment(H, K, f, s * .092, 0) for f in (.2, .5, .8)],
|
||||||
|
[(.012, .075), (.012, .09), (.012, .065)], 'plate', thigh, facet=.08)
|
||||||
|
gem('Thigh_Crystal_' + side, on_segment(H, K, .64, 0, .105), K - H, (1, 0, 0), .085, .055, .048, 'crystal', thigh)
|
||||||
|
disc('Knee_Joint_' + side, K, X, .085, .20, 'joint', shin, 28)
|
||||||
|
annulus('Knee_Rim_' + side, K + X * s * .102, X, .045, .092, .03, 'bronze', shin, 32)
|
||||||
|
disc('Knee_Cap_' + side, K + X * s * .118, X, .042, .026, 'edge', shin, 12)
|
||||||
|
loft('Shin_Armor_' + side, [K.lerp(A, .12), K.lerp(A, .50), K.lerp(A, .88)],
|
||||||
|
[(.07, .08), (.082, .10), (.062, .07)], 'basalt', shin, facet=.10)
|
||||||
|
loft('Shin_Band_' + side, [K.lerp(A, .76), K.lerp(A, .86)], [(.078, .082)] * 2, 'bronze', shin)
|
||||||
|
loft('Shin_Plate_' + side, [on_segment(K, A, f, s * .082, 0) for f in (.2, .45, .7)],
|
||||||
|
[(.012, .06), (.012, .08), (.012, .06)], 'plate', shin, facet=.08)
|
||||||
|
gem('Shin_Crystal_' + side, on_segment(K, A, .38, 0, .09), A - K, (1, 0, 0), .07, .05, .04, 'crystal', shin)
|
||||||
|
disc('Ankle_Joint_' + side, A, X, .065, .16, 'joint', tarsus, 24)
|
||||||
|
annulus('Ankle_Rim_' + side, A + X * s * .082, X, .034, .07, .026, 'bronze', tarsus, 28)
|
||||||
|
loft('Tarsus_Armor_' + side, [A.lerp(B, .12), A.lerp(B, .88)], [(.058, .055), (.052, .048)],
|
||||||
|
'basalt', tarsus, facet=.10)
|
||||||
|
loft('Spur_' + side, [A + Vector((0, .03, -.05)), Vector((s * .28, .20, .02))], [(.03, .028), (.010, .009)],
|
||||||
|
'joint', tarsus, facet=.10)
|
||||||
|
loft('Spur_Cap_' + side, [Vector((s * .28, .175, .026)), Vector((s * .28, .215, .013))],
|
||||||
|
[(.022, .02), (.006, .005)], 'edge', tarsus)
|
||||||
|
loft('Foot_Base_' + side, [B + Vector((0, .05, 0)), B + Vector((0, -.05, -.005))], [(.085, .045), (.08, .04)],
|
||||||
|
'basalt', foot, facet=.10)
|
||||||
|
loft('Foot_Band_' + side, [B + Vector((0, .012, .002)), B + Vector((0, -.012, .001))], [(.09, .05)] * 2,
|
||||||
|
'bronze', foot)
|
||||||
|
for k in (-1, 0, 1):
|
||||||
|
root = Vector((B.x + k * .052, B.y - .03, .038))
|
||||||
|
tip = Vector((B.x + k * .078, B.y - .19 + .025 * abs(k), .014))
|
||||||
|
loft('Toe_%s%d' % (side, k + 1), [root, root.lerp(tip, .5) + Vector((0, 0, .006)), tip],
|
||||||
|
[(.029, .030), (.025, .023), (.012, .008)], 'joint', foot, facet=.10)
|
||||||
|
loft('Toe_Claw_%s%d' % (side, k + 1), [tip.lerp(root, .30), tip + Vector((k * .004, -.028, -.004))],
|
||||||
|
[(.026, .018), (.006, .004)], 'edge', foot)
|
||||||
|
|
||||||
|
def sleeve(parent, child, a, b):
|
||||||
|
return lambda t: blend(parent, child, (t - a) / (b - a))
|
||||||
|
|
||||||
|
for f in (.28, .72):
|
||||||
|
for off in (-.045, .045):
|
||||||
|
rivet('Thigh_Rivet_%s_%d_%d' % (side, f * 100, off * 1000), on_segment(H, K, f, s * .105, off), X, thigh, .009)
|
||||||
|
for off in (-.035, .035):
|
||||||
|
rivet('Shin_Rivet_%s_%d' % (side, off * 1000), on_segment(K, A, .45, s * .095, off), X, shin, .008)
|
||||||
|
hose_a = [Vector((s * .17, .21, .79)), Vector((s * .21, .20, .70)), on_segment(H, K, .42, s * .02, -.105)]
|
||||||
|
tube('Hip_Hose_' + side, hose_a, [.017, .018, .017], 'joint', 'Pelvis', 10, 22, sleeve('Pelvis', thigh, .25, .80))
|
||||||
|
disc('Hip_Hose_Ferrule_' + side, hose_a[0], hose_a[0] - hose_a[1], .024, .03, 'bronze', 'Pelvis', 10)
|
||||||
|
disc('Hip_Hose_Clamp_' + side, hose_a[2], K - H, .023, .03, 'bronze', thigh, 10)
|
||||||
|
hose_b = [on_segment(H, K, .70, s * .03, -.092), K + Vector((s * .03, .09, .01)), on_segment(K, A, .34, s * .03, -.082)]
|
||||||
|
tube('Knee_Hose_' + side, hose_b, [.015, .016, .015], 'joint', thigh, 10, 22, sleeve(thigh, shin, .30, .70))
|
||||||
|
disc('Knee_Hose_Clamp_T_' + side, hose_b[0], K - H, .021, .026, 'bronze', thigh, 10)
|
||||||
|
disc('Knee_Hose_Clamp_S_' + side, hose_b[2], A - K, .021, .026, 'bronze', shin, 10)
|
||||||
|
|
||||||
|
tube('Hip_Sleeve_' + side, [Vector((s * .15, .06, .86)), H, H.lerp(K, .24)], [.064, .062, .058],
|
||||||
|
'joint', 'Pelvis', 14, 18, sleeve('Pelvis', thigh, .30, .78))
|
||||||
|
tube('Knee_Sleeve_' + side, [H.lerp(K, .70), K, K.lerp(A, .30)], [.056, .06, .052],
|
||||||
|
'joint', thigh, 14, 18, sleeve(thigh, shin, .28, .72))
|
||||||
|
tube('Ankle_Sleeve_' + side, [K.lerp(A, .74), A, A.lerp(B, .36)], [.046, .05, .044],
|
||||||
|
'joint', shin, 12, 16, sleeve(shin, tarsus, .28, .72))
|
||||||
|
tube('Toe_Sleeve_' + side, [A.lerp(B, .66), B, B + Vector((0, -.05, -.004))], [.036, .038, .034],
|
||||||
|
'joint', tarsus, 12, 14, sleeve(tarsus, foot, .30, .70))
|
||||||
|
|
||||||
|
# Recalculate coherent outward normals and export triangulated closed surfaces.
|
||||||
|
for ob in parts:
|
||||||
|
bm = bmesh.new()
|
||||||
|
bm.from_mesh(ob.data)
|
||||||
|
bmesh.ops.remove_doubles(bm, verts=list(bm.verts), dist=.0000001)
|
||||||
|
bmesh.ops.triangulate(bm, faces=list(bm.faces))
|
||||||
|
bmesh.ops.recalc_face_normals(bm, faces=list(bm.faces))
|
||||||
|
if bm.calc_volume(signed=True) < 0:
|
||||||
|
bmesh.ops.reverse_faces(bm, faces=list(bm.faces))
|
||||||
|
bm.to_mesh(ob.data)
|
||||||
|
bm.free()
|
||||||
|
ob.data.update()
|
||||||
|
|
||||||
|
bpy.ops.object.armature_add(enter_editmode=True)
|
||||||
|
rig = bpy.context.object
|
||||||
|
rig.name = 'GlassRig'
|
||||||
|
ed = rig.data.edit_bones
|
||||||
|
ed.remove(ed[0])
|
||||||
|
|
||||||
|
|
||||||
|
def bone(name, head, tail, parent=None):
|
||||||
|
b = ed.new(name)
|
||||||
|
b.head = head
|
||||||
|
b.tail = tail
|
||||||
|
if parent:
|
||||||
|
b.parent = ed[parent]
|
||||||
|
defs[name] = (Vector(head), Vector(tail), parent)
|
||||||
|
|
||||||
|
|
||||||
|
bone('Root', (0, 0, 0), (0, 0, .15))
|
||||||
|
bone('Pelvis', (0, .06, .80), (0, .06, .98), 'Root')
|
||||||
|
bone('Spine', (0, .04, .98), (0, .04, 1.10), 'Pelvis')
|
||||||
|
bone('Weapon', (0, .02, 1.17), (0, -.60, 1.17), 'Spine')
|
||||||
|
bone('Gear', GEAR, GEAR + Vector((.12, 0, 0)), 'Weapon')
|
||||||
|
bone('Draw', (0, .262, 1.286), (0, .36, 1.286), 'Weapon')
|
||||||
|
bone('Bolt', (0, .25, 1.29), (0, -.685, 1.29), 'Weapon')
|
||||||
|
bone('Quarrel', (0, -.685, 1.29), (0, -1.0, 1.29), 'Bolt')
|
||||||
|
for s, side in [(-1, 'L'), (1, 'R')]:
|
||||||
|
bone('Limb.' + side, limb_point(s, 0), limb_point(s, .5), 'Weapon')
|
||||||
|
bone('LimbTip.' + side, limb_point(s, .5), limb_point(s, 1.0), 'Limb.' + side)
|
||||||
|
leg = LEGS[side]
|
||||||
|
bone('Thigh.' + side, leg['H'], leg['K'], 'Pelvis')
|
||||||
|
bone('Shin.' + side, leg['K'], leg['A'], 'Thigh.' + side)
|
||||||
|
bone('Tarsus.' + side, leg['A'], leg['B'], 'Shin.' + side)
|
||||||
|
bone('Foot.' + side, leg['B'], leg['T'], 'Tarsus.' + side)
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
for ob in parts:
|
||||||
|
mod = ob.modifiers.new('Weighted ballista deformation', 'ARMATURE')
|
||||||
|
mod.object = rig
|
||||||
|
ob.parent = rig
|
||||||
|
rig.show_in_front = True
|
||||||
|
|
||||||
|
clips = {'idle': (3.0, True), 'start': (.4, False), 'walk': (1.2, True), 'stop': (.4, False),
|
||||||
|
'attack_a': (1.3, False), 'attack_b': (1.3, False), 'hit': (.5, False),
|
||||||
|
'ability': (1.3, False), 'death': (2.0, False)}
|
||||||
|
WALK, STRIDE, DUTY = 1.2, .52, .6
|
||||||
|
STANCE = STRIDE * DUTY # planted foot travels backwards with the body's 0.52 m per cycle
|
||||||
|
L1 = {side: (LEGS[side]['K'] - LEGS[side]['H']).length for side in LEGS}
|
||||||
|
L2 = {side: (LEGS[side]['A'] - LEGS[side]['K']).length for side in LEGS}
|
||||||
|
|
||||||
|
|
||||||
|
def curve(t, keys):
|
||||||
|
for (a, x), (b, y) in zip(keys, keys[1:]):
|
||||||
|
if t <= b:
|
||||||
|
u = max(0, (t - a) / (b - a))
|
||||||
|
u = u * u * (3 - 2 * u)
|
||||||
|
return x * (1 - u) + y * u
|
||||||
|
return keys[-1][1]
|
||||||
|
|
||||||
|
|
||||||
|
def pose_rotation(pb, r):
|
||||||
|
q = Quaternion((0, 0, 1), r[2]) @ Quaternion((0, 1, 0), r[1]) @ Quaternion((1, 0, 0), r[0])
|
||||||
|
basis = pb.bone.matrix_local.to_quaternion()
|
||||||
|
return basis.inverted() @ q @ basis
|
||||||
|
|
||||||
|
|
||||||
|
def aim_bone(name, head, tail):
|
||||||
|
pb = rig.pose.bones[name]
|
||||||
|
rest = pb.bone.matrix_local
|
||||||
|
direction = defs[name][1] - defs[name][0]
|
||||||
|
rotation = direction.rotation_difference(tail - head).to_matrix().to_4x4()
|
||||||
|
pose = rotation @ rest
|
||||||
|
pose.translation = head
|
||||||
|
pb.matrix = pose
|
||||||
|
bpy.context.view_layer.update()
|
||||||
|
|
||||||
|
|
||||||
|
def foot_plan(tau, offset, amp):
|
||||||
|
"""Ball offset along Y, lift, heel raise and toe droop for one foot at walk time tau."""
|
||||||
|
u = (tau / WALK + offset) % 1.0
|
||||||
|
if u < DUTY:
|
||||||
|
w = u / DUTY
|
||||||
|
return STANCE * (w - .5) * amp, 0, .42 * smooth((w - .72) / .28) * amp, 0, True
|
||||||
|
w = (u - DUTY) / (1 - DUTY)
|
||||||
|
return (STANCE * (.5 - smooth(w)) * amp, .10 * sin(pi * w) * amp,
|
||||||
|
.42 * (1 - smooth(w / .45)) * amp, .28 * sin(pi * w) * amp, False)
|
||||||
|
|
||||||
|
|
||||||
|
def place_leg(side, dy, lift, heel, toe):
|
||||||
|
leg = LEGS[side]
|
||||||
|
ball = leg['B'] + Vector((0, dy, lift))
|
||||||
|
ankle = ball + Quaternion(X, heel) @ (leg['A'] - leg['B'])
|
||||||
|
toes = ball + Quaternion(X, toe) @ (leg['T'] - leg['B'])
|
||||||
|
hip = rig.pose.bones['Thigh.' + side].head.copy()
|
||||||
|
pelvis = rig.pose.bones['Pelvis'].matrix @ rig.data.bones['Pelvis'].matrix_local.inverted()
|
||||||
|
knee = ik_knee(hip, ankle, L1[side], L2[side], pelvis @ leg['pole'])
|
||||||
|
aim_bone('Thigh.' + side, hip, knee)
|
||||||
|
aim_bone('Shin.' + side, knee, ankle)
|
||||||
|
aim_bone('Tarsus.' + side, ankle, ball)
|
||||||
|
aim_bone('Foot.' + side, ball, toes)
|
||||||
|
foot = rig.pose.bones['Foot.' + side]
|
||||||
|
return max((foot.head - ball).length, (foot.tail - toes).length,
|
||||||
|
(rig.pose.bones['Shin.' + side].tail - ankle).length), foot.head.copy(), foot.tail.copy()
|
||||||
|
|
||||||
|
|
||||||
|
def animate(name, t, dur):
|
||||||
|
rs, locs, scales = {}, {}, {}
|
||||||
|
legs = {'L': (0, 0, 0, 0, True), 'R': (0, 0, 0, 0, True)}
|
||||||
|
|
||||||
|
def rot(b, x=0, y=0, z=0):
|
||||||
|
rs[b] = (x, y, z)
|
||||||
|
|
||||||
|
if name == 'idle':
|
||||||
|
ph = 2 * pi * t / dur
|
||||||
|
breath = sin(ph)
|
||||||
|
locs['Pelvis'] = (0, 0, -.006 + .006 * cos(ph))
|
||||||
|
rot('Pelvis', .006 * breath)
|
||||||
|
rot('Spine', 0, 0, .045 * sin(ph + .4))
|
||||||
|
rot('Weapon', -.012 - .008 * breath)
|
||||||
|
for s, side in [(-1, 'L'), (1, 'R')]:
|
||||||
|
rot('Limb.' + side, 0, 0, s * .006 * breath)
|
||||||
|
rot('LimbTip.' + side, 0, 0, s * .008 * breath)
|
||||||
|
elif name in ('walk', 'start', 'stop'):
|
||||||
|
amp = 1 if name == 'walk' else smooth(t / dur) if name == 'start' else 1 - smooth(t / dur)
|
||||||
|
ph = 2 * pi * t / WALK
|
||||||
|
roll = -.03 * sin(ph) * amp
|
||||||
|
yaw = .025 * sin(ph) * amp
|
||||||
|
locs['Pelvis'] = (0, 0, -.024 * (.5 + .5 * cos(2 * pi * (t - .07) / (WALK / 2))) * amp)
|
||||||
|
rot('Pelvis', .03 * amp, roll, yaw)
|
||||||
|
# The turret counters the gait so the bolt stays on target.
|
||||||
|
rot('Spine', -.024 * amp, -.8 * roll, -.9 * yaw)
|
||||||
|
rot('Weapon', -.012 + .01 * sin(2 * ph) * amp)
|
||||||
|
for s, side in [(-1, 'L'), (1, 'R')]:
|
||||||
|
rot('LimbTip.' + side, 0, 0, s * .02 * sin(2 * ph - 1.0) * amp)
|
||||||
|
legs = {'L': foot_plan(t, 0, amp), 'R': foot_plan(t, .5, amp)}
|
||||||
|
elif name in ('attack_a', 'attack_b', 'ability'):
|
||||||
|
braced = name == 'attack_b'
|
||||||
|
charged = name == 'ability'
|
||||||
|
aim = curve(t, [(0, 0), (.30, .8), (.60, 1), (.90, 1), (1.04, .8), (1.30, 0)])
|
||||||
|
release = curve(t, [(0, 0), (.90, 0), (.933333, 1), (.98, 1), (1.24, 0), (1.30, 0)])
|
||||||
|
recoil = curve(t, [(0, 0), (.90, 0), (.933333, 1), (1.04, .22), (1.14, 0), (1.30, 0)])
|
||||||
|
charge = curve(t, [(0, 0), (.15, 0), (.85, 1), (.90, 1), (.92, 0), (1.30, 0)]) if charged else 0
|
||||||
|
k = 1.45 if charged else .75 if braced else 1.0
|
||||||
|
crouch = (.10 if charged else .07 if braced else .018) * aim
|
||||||
|
locs['Pelvis'] = (0, .045 * recoil * k, -crouch)
|
||||||
|
rot('Pelvis', -.05 * recoil * k + (.02 * aim if braced else 0))
|
||||||
|
rot('Spine', 0, 0, .04 * sin(pi * min(1, t / .9)) * aim if braced else 0)
|
||||||
|
rot('Weapon', (.012 if braced else -.035) * aim - .09 * recoil * k - .02 * charge)
|
||||||
|
wobble = .05 * sin(38 * (t - .933333)) * exp(-9 * (t - .933333)) if t > .933333 else 0
|
||||||
|
for s, side in [(-1, 'L'), (1, 'R')]:
|
||||||
|
# Limbs spring forward on release, then the crank draws them back to cocked.
|
||||||
|
rot('Limb.' + side, 0, 0, s * (-.10 * release + .05 * charge))
|
||||||
|
rot('LimbTip.' + side, 0, 0, s * (-.16 * release + .06 * charge + wobble))
|
||||||
|
locs['Draw'] = (0, -.34 * release + .05 * charge, 0)
|
||||||
|
if t < .90:
|
||||||
|
bolt, size = 0, 1
|
||||||
|
elif t < .95:
|
||||||
|
u = (t - .90) / .05
|
||||||
|
bolt, size = -1.2 * u * u, 1 - .999 * smooth(u)
|
||||||
|
elif t < 1.02:
|
||||||
|
bolt, size = -1.2, .001
|
||||||
|
else:
|
||||||
|
u = smooth((t - 1.02) / .22)
|
||||||
|
bolt, size = .30 * (1 - u), max(.001, u)
|
||||||
|
locs['Bolt'] = (0, bolt, 0)
|
||||||
|
scales['Bolt'] = size
|
||||||
|
wind = -2 * pi * smooth((t - .96) / .28)
|
||||||
|
if charged:
|
||||||
|
wind -= 2 * pi * smooth((t - .12) / .73)
|
||||||
|
spin = smooth((t - .15) / .75)
|
||||||
|
rot('Quarrel', 0, 4 * pi * spin, 0)
|
||||||
|
scales['Quarrel'] = 1 + .22 * charge * (.6 + .4 * sin(26 * t))
|
||||||
|
rot('Gear', wind)
|
||||||
|
elif name == 'hit':
|
||||||
|
q = curve(t, [(0, 0), (.066667, 1), (.20, .4), (.5, 0)])
|
||||||
|
wob = sin(30 * t) * exp(-7 * t)
|
||||||
|
locs['Pelvis'] = (0, .035 * q, -.015 * q)
|
||||||
|
rot('Pelvis', -.04 * q, .05 * q, 0)
|
||||||
|
rot('Spine', .05 * q, 0, -.07 * q)
|
||||||
|
rot('Weapon', -.06 * q)
|
||||||
|
for s, side in [(-1, 'L'), (1, 'R')]:
|
||||||
|
rot('LimbTip.' + side, 0, 0, s * .05 * wob)
|
||||||
|
elif name == 'death':
|
||||||
|
q = curve(t, [(0, 0), (.3, .12), (.72, .58), (1.25, 1), (1.5, .96), (2, 1)])
|
||||||
|
stagger = curve(t, [(0, 0), (.18, 1), (.5, .25), (.8, 0)])
|
||||||
|
snap = curve(t, [(0, 0), (.24, 0), (.30, 1), (2, 1)])
|
||||||
|
locs['Pelvis'] = (0, .05 * stagger - .04 * q, -.33 * q)
|
||||||
|
rot('Pelvis', .20 * q - .05 * stagger, -.12 * q, .08 * q)
|
||||||
|
rot('Spine', .06 * q, 0, -.10 * q)
|
||||||
|
rot('Weapon', .16 * q)
|
||||||
|
rot('Gear', -.8 * q)
|
||||||
|
# Destroyed: the bow tension lets go and the limbs sag.
|
||||||
|
locs['Draw'] = (0, -.30 * snap, -.02 * q)
|
||||||
|
locs['Bolt'] = (0, -.06 * q, -.005 * q)
|
||||||
|
for s, side in [(-1, 'L'), (1, 'R')]:
|
||||||
|
rot('Limb.' + side, 0, s * .06 * q, -s * .09 * snap)
|
||||||
|
rot('LimbTip.' + side, 0, s * .22 * q, -s * .14 * snap + s * .05 * q)
|
||||||
|
for pb in rig.pose.bones:
|
||||||
|
pb.rotation_mode = 'QUATERNION'
|
||||||
|
pb.rotation_quaternion = pose_rotation(pb, rs.get(pb.name, (0, 0, 0)))
|
||||||
|
pb.location = pb.bone.matrix_local.to_3x3().inverted() @ Vector(locs.get(pb.name, (0, 0, 0)))
|
||||||
|
size = scales.get(pb.name, 1.0)
|
||||||
|
pb.scale = (size, size, size)
|
||||||
|
bpy.context.view_layer.update()
|
||||||
|
errors = []
|
||||||
|
for side in ('L', 'R'):
|
||||||
|
dy, lift, heel, toe, planted = legs[side]
|
||||||
|
error, ball, toes = place_leg(side, dy, lift, heel, toe)
|
||||||
|
errors.append(error)
|
||||||
|
foot_samples.append({'clip': name, 't': t, 'side': side, 'planted': planted and lift == 0,
|
||||||
|
'ball': tuple(ball), 'toe': tuple(toes)})
|
||||||
|
contact_samples.append({'clip': name, 't': round(t, 6), 'max_leg_error': max(errors)})
|
||||||
|
|
||||||
|
|
||||||
|
for name, (dur, loop) in clips.items():
|
||||||
|
rig.animation_data_clear()
|
||||||
|
rig.animation_data_create()
|
||||||
|
action = bpy.data.actions.new(name)
|
||||||
|
action.use_fake_user = True
|
||||||
|
rig.animation_data.action = action
|
||||||
|
for frame in range(round(dur * 30) + 1):
|
||||||
|
animate(name, frame / 30, dur)
|
||||||
|
for pb in rig.pose.bones:
|
||||||
|
pb.keyframe_insert('rotation_quaternion', frame=frame)
|
||||||
|
pb.keyframe_insert('location', frame=frame)
|
||||||
|
pb.keyframe_insert('scale', frame=frame)
|
||||||
|
action['loop'] = loop
|
||||||
|
action['contact_seconds'] = .90 if name in ('attack_a', 'attack_b', 'ability') else -1
|
||||||
|
for layer in action.layers:
|
||||||
|
for strip in layer.strips:
|
||||||
|
for bag in strip.channelbags:
|
||||||
|
for fc in bag.fcurves:
|
||||||
|
for key in fc.keyframe_points:
|
||||||
|
key.interpolation = 'LINEAR'
|
||||||
|
rig.animation_data_clear()
|
||||||
|
rig.animation_data_create()
|
||||||
|
rig.animation_data.action = bpy.data.actions['idle']
|
||||||
|
bpy.context.scene.render.fps = 30
|
||||||
|
bpy.context.scene.frame_set(0)
|
||||||
|
|
||||||
|
bpy.ops.wm.save_as_mainfile(filepath=str(SRC / 'glasballiste.blend'))
|
||||||
|
source_parts = len(parts)
|
||||||
|
bpy.ops.object.select_all(action='DESELECT')
|
||||||
|
for ob in parts:
|
||||||
|
ob.select_set(True)
|
||||||
|
bpy.context.view_layer.objects.active = parts[0]
|
||||||
|
bpy.ops.object.join()
|
||||||
|
body = bpy.context.object
|
||||||
|
body.name = 'GlassBody'
|
||||||
|
body.select_set(True)
|
||||||
|
rig.select_set(True)
|
||||||
|
bpy.context.view_layer.objects.active = rig
|
||||||
|
bpy.ops.export_scene.gltf(filepath=str(OUT / 'glasballiste.glb'), export_format='GLB', use_selection=True,
|
||||||
|
export_animations=True, export_animation_mode='ACTIONS', export_force_sampling=True,
|
||||||
|
export_frame_range=False, export_anim_slide_to_zero=True, export_skins=True,
|
||||||
|
export_all_influences=False, export_def_bones=True, export_anim_single_armature=True,
|
||||||
|
export_materials='EXPORT', export_yup=True)
|
||||||
|
raw = (OUT / 'glasballiste.glb').read_bytes()
|
||||||
|
doc = json.loads(raw[20:20 + struct.unpack_from('<I', raw, 12)[0]])
|
||||||
|
triangles = sum(doc['accessors'][q['indices']]['count'] // 3 for m in doc['meshes'] for q in m['primitives'])
|
||||||
|
mins = [min(v.co[i] for v in body.data.vertices) for i in range(3)]
|
||||||
|
maxs = [max(v.co[i] for v in body.data.vertices) for i in range(3)]
|
||||||
|
|
||||||
|
# Ground check on the deformed mesh: nothing sinks below the floor in any clip.
|
||||||
|
lowest = {}
|
||||||
|
depsgraph = bpy.context.evaluated_depsgraph_get()
|
||||||
|
for name, (dur, loop) in clips.items():
|
||||||
|
rig.animation_data.action = bpy.data.actions[name]
|
||||||
|
low = 1e9
|
||||||
|
for frame in range(0, round(dur * 30) + 1, 2):
|
||||||
|
bpy.context.scene.frame_set(frame)
|
||||||
|
evaluated = body.evaluated_get(depsgraph)
|
||||||
|
deformed = evaluated.to_mesh()
|
||||||
|
low = min(low, min(v.co.z for v in deformed.vertices))
|
||||||
|
evaluated.to_mesh_clear()
|
||||||
|
lowest[name] = round(low, 5)
|
||||||
|
rig.animation_data.action = bpy.data.actions['idle']
|
||||||
|
bpy.context.scene.frame_set(0)
|
||||||
|
|
||||||
|
# Planted feet: ball and toe stay on their ground height and travel with the stride.
|
||||||
|
slip, height = 0, 0
|
||||||
|
for side in ('L', 'R'):
|
||||||
|
walk = [x for x in foot_samples if x['clip'] == 'walk' and x['side'] == side]
|
||||||
|
for a, b in zip(walk, walk[1:]):
|
||||||
|
if a['planted'] and b['planted']:
|
||||||
|
slip = max(slip, abs((b['ball'][1] - a['ball'][1]) - STRIDE / WALK / 30))
|
||||||
|
height = max(height, abs(b['ball'][2] - LEGS[side]['B'].z), abs(b['toe'][2] - LEGS[side]['T'].z))
|
||||||
|
record = {'id': 'g06', 'role': 'ranged', 'name': 'Glasballiste',
|
||||||
|
'resource_path': 'res://assets/characters/glasballiste.glb',
|
||||||
|
'triangles': triangles, 'mesh_parts': 1, 'parts': ['GlassBody'],
|
||||||
|
'bounds_godot': {'min': [mins[0], mins[2], -maxs[1]], 'max': [maxs[0], maxs[2], -mins[1]]},
|
||||||
|
'skeletal': True, 'bones': len(defs), 'clips': list(clips),
|
||||||
|
'attack_cycle': ['attack_a', 'attack_b', 'attack_a', 'ability'],
|
||||||
|
'contact_seconds': .9, 'sha256': hashlib.sha256(raw).hexdigest(),
|
||||||
|
'source': 'assets-source/phase19/glasballiste.blend'}
|
||||||
|
(P / 'assets/phase19_manifest.json').write_text(json.dumps({'assets': [record]}, indent=2) + '\n')
|
||||||
|
audit = {'record': record, 'exported_clips': [a['name'] for a in doc['animations']],
|
||||||
|
'skins': len(doc.get('skins', [])), 'source_parts': source_parts,
|
||||||
|
'materials': [m['name'] for m in doc['materials']],
|
||||||
|
'max_baked_leg_contact_error': max(x['max_leg_error'] for x in contact_samples),
|
||||||
|
'contact_sample_count': len(contact_samples),
|
||||||
|
'contact_validation': 'Analytic two-bone leg IK with planted tarsus/toes, baked at 30 Hz for all nine clips.',
|
||||||
|
'walk_stride_m': STRIDE, 'walk_duty_factor': DUTY,
|
||||||
|
'max_planted_slip_m_per_frame': slip, 'max_planted_height_error_m': height,
|
||||||
|
'lowest_vertex_z_by_clip': lowest,
|
||||||
|
'shot_seconds': .90, 'recoil_peak_seconds': .933333, 'bolt_hidden_seconds': [.95, 1.02],
|
||||||
|
'reload_complete_seconds': 1.24}
|
||||||
|
(SRC / 'glasballiste-audit.json').write_text(json.dumps(audit, indent=2) + '\n')
|
||||||
|
print('GLASBALLISTE_BUILD', triangles, 'triangles', len(defs), 'bones', len(doc['animations']), 'clips',
|
||||||
|
'leg error', audit['max_baked_leg_contact_error'], 'slip', slip, 'height', height, 'lowest', lowest, flush=True)
|
||||||
95
assets-source/phase19/glasballiste-audit.json
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
{
|
||||||
|
"record": {
|
||||||
|
"id": "g06",
|
||||||
|
"role": "ranged",
|
||||||
|
"name": "Glasballiste",
|
||||||
|
"resource_path": "res://assets/characters/glasballiste.glb",
|
||||||
|
"triangles": 15784,
|
||||||
|
"mesh_parts": 1,
|
||||||
|
"parts": [
|
||||||
|
"GlassBody"
|
||||||
|
],
|
||||||
|
"bounds_godot": {
|
||||||
|
"min": [
|
||||||
|
-0.9268030524253845,
|
||||||
|
0.0010433457791805267,
|
||||||
|
-0.6150000095367432
|
||||||
|
],
|
||||||
|
"max": [
|
||||||
|
0.9268030524253845,
|
||||||
|
1.434999942779541,
|
||||||
|
1.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"skeletal": true,
|
||||||
|
"bones": 20,
|
||||||
|
"clips": [
|
||||||
|
"idle",
|
||||||
|
"start",
|
||||||
|
"walk",
|
||||||
|
"stop",
|
||||||
|
"attack_a",
|
||||||
|
"attack_b",
|
||||||
|
"hit",
|
||||||
|
"ability",
|
||||||
|
"death"
|
||||||
|
],
|
||||||
|
"attack_cycle": [
|
||||||
|
"attack_a",
|
||||||
|
"attack_b",
|
||||||
|
"attack_a",
|
||||||
|
"ability"
|
||||||
|
],
|
||||||
|
"contact_seconds": 0.9,
|
||||||
|
"sha256": "c2f8a526b0b7006b549342561346acd5478406c33c5a79678f63f7c76f583dd8",
|
||||||
|
"source": "assets-source/phase19/glasballiste.blend"
|
||||||
|
},
|
||||||
|
"exported_clips": [
|
||||||
|
"ability",
|
||||||
|
"attack_a",
|
||||||
|
"attack_b",
|
||||||
|
"death",
|
||||||
|
"hit",
|
||||||
|
"idle",
|
||||||
|
"start",
|
||||||
|
"stop",
|
||||||
|
"walk"
|
||||||
|
],
|
||||||
|
"skins": 1,
|
||||||
|
"source_parts": 238,
|
||||||
|
"materials": [
|
||||||
|
"GB_Basalt",
|
||||||
|
"GB_Bronze",
|
||||||
|
"GB_BasaltPlate",
|
||||||
|
"GB_Joint",
|
||||||
|
"GB_Crystal",
|
||||||
|
"GB_BronzeEdge",
|
||||||
|
"GB_Ember",
|
||||||
|
"GB_Iron"
|
||||||
|
],
|
||||||
|
"max_baked_leg_contact_error": 4.6289636394408544e-07,
|
||||||
|
"contact_sample_count": 351,
|
||||||
|
"contact_validation": "Analytic two-bone leg IK with planted tarsus/toes, baked at 30 Hz for all nine clips.",
|
||||||
|
"walk_stride_m": 0.52,
|
||||||
|
"walk_duty_factor": 0.6,
|
||||||
|
"max_planted_slip_m_per_frame": 1.8742349413777837e-08,
|
||||||
|
"max_planted_height_error_m": 5.960464477539063e-08,
|
||||||
|
"lowest_vertex_z_by_clip": {
|
||||||
|
"idle": 0.00104,
|
||||||
|
"start": 0.00104,
|
||||||
|
"walk": 0.00104,
|
||||||
|
"stop": 0.00104,
|
||||||
|
"attack_a": 0.00104,
|
||||||
|
"attack_b": 0.00104,
|
||||||
|
"hit": 0.00104,
|
||||||
|
"ability": 0.00104,
|
||||||
|
"death": 0.00104
|
||||||
|
},
|
||||||
|
"shot_seconds": 0.9,
|
||||||
|
"recoil_peak_seconds": 0.933333,
|
||||||
|
"bolt_hidden_seconds": [
|
||||||
|
0.95,
|
||||||
|
1.02
|
||||||
|
],
|
||||||
|
"reload_complete_seconds": 1.24
|
||||||
|
}
|
||||||
BIN
assets-source/phase19/glasballiste.blend
Normal file
611
assets-source/phase19/v1-humanoid/build_glasballiste.py
Normal file
|
|
@ -0,0 +1,611 @@
|
||||||
|
"""Glasballiste: headless weighted glass ballista walker with crystal quarrel.
|
||||||
|
|
||||||
|
Run: blender --background --python assets-source/phase19/build_glasballiste.py
|
||||||
|
Blender is Z-up, -Y-forward. Loader-arm contact is solved analytically per sample,
|
||||||
|
then baked to FK animation: no runtime IK or constraints.
|
||||||
|
"""
|
||||||
|
from pathlib import Path
|
||||||
|
import bpy, bmesh, math, json, hashlib, struct
|
||||||
|
from math import sin, cos, pi, sqrt
|
||||||
|
from mathutils import Vector, Quaternion
|
||||||
|
|
||||||
|
P = Path(__file__).resolve().parents[2]
|
||||||
|
SRC = Path(__file__).resolve().parent
|
||||||
|
OUT = P / 'assets/characters'
|
||||||
|
OUT.mkdir(exist_ok=True)
|
||||||
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||||
|
bpy.context.preferences.filepaths.save_version = 0
|
||||||
|
parts, defs, contact_samples = [], {}, []
|
||||||
|
|
||||||
|
|
||||||
|
def material(name, color, rough=.55, metal=0, emission=0, glass=False):
|
||||||
|
m = bpy.data.materials.new('GB_' + name)
|
||||||
|
m.diffuse_color = (*color, 1)
|
||||||
|
m.use_nodes = True
|
||||||
|
n, links = m.node_tree.nodes, m.node_tree.links
|
||||||
|
bs = n.get('Principled BSDF')
|
||||||
|
bs.inputs['Base Color'].default_value = (*color, 1)
|
||||||
|
bs.inputs['Metallic'].default_value = metal
|
||||||
|
bs.inputs['Roughness'].default_value = rough
|
||||||
|
vertex = n.new('ShaderNodeVertexColor')
|
||||||
|
vertex.layer_name = 'SurfaceColor'
|
||||||
|
links.new(vertex.outputs['Color'], bs.inputs['Base Color'])
|
||||||
|
if glass:
|
||||||
|
bs.inputs['Transmission Weight'].default_value = .55
|
||||||
|
bs.inputs['IOR'].default_value = 1.45
|
||||||
|
bs.inputs['Alpha'].default_value = .78
|
||||||
|
m.blend_method = 'BLEND'
|
||||||
|
if emission:
|
||||||
|
bs.inputs['Emission Color'].default_value = (*color, 1)
|
||||||
|
bs.inputs['Emission Strength'].default_value = emission
|
||||||
|
else:
|
||||||
|
tex = n.new('ShaderNodeTexNoise')
|
||||||
|
tex.inputs['Scale'].default_value = 70 if metal else 28
|
||||||
|
tex.inputs['Detail'].default_value = 2
|
||||||
|
bump = n.new('ShaderNodeBump')
|
||||||
|
bump.inputs['Strength'].default_value = .12 if metal else .20
|
||||||
|
bump.inputs['Distance'].default_value = .006
|
||||||
|
links.new(tex.outputs['Fac'], bump.inputs['Height'])
|
||||||
|
links.new(bump.outputs['Normal'], bs.inputs['Normal'])
|
||||||
|
return m
|
||||||
|
|
||||||
|
|
||||||
|
M = {
|
||||||
|
'glass': material('Glass', (.92, .78, .42), .18, .05, 0.12, glass=True),
|
||||||
|
'glass2': material('GlassPale', (.78, .88, .92), .12, 0, 0.08, glass=True),
|
||||||
|
'frame': material('Frame', (.70, .44, .16), .30, .72),
|
||||||
|
'edge': material('FrameEdge', (.93, .70, .30), .24, .68),
|
||||||
|
'dark': material('Joint', (.06, .065, .075), .72, .22),
|
||||||
|
'steel': material('Steel', (.32, .34, .38), .32, .80),
|
||||||
|
'ember': material('Ember', (1.0, .42, .08), .30, .10, 1.10),
|
||||||
|
'quarrel': material('Quarrel', (1.0, .55, .12), .20, .15, .75),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def mesh(name, vs, fs, kind, bone, weights=None, smooth=True, uv=None):
|
||||||
|
me = bpy.data.meshes.new(name)
|
||||||
|
me.from_pydata(vs, [], fs)
|
||||||
|
me.update()
|
||||||
|
ob = bpy.data.objects.new(name, me)
|
||||||
|
bpy.context.collection.objects.link(ob)
|
||||||
|
parts.append(ob)
|
||||||
|
me.materials.append(M[kind])
|
||||||
|
col = me.color_attributes.new(name='SurfaceColor', type='FLOAT_COLOR', domain='CORNER')
|
||||||
|
tex = me.uv_layers.new(name='UVMap')
|
||||||
|
base = M[kind].diffuse_color[:3]
|
||||||
|
for face in me.polygons:
|
||||||
|
face.use_smooth = smooth
|
||||||
|
for li in face.loop_indices:
|
||||||
|
vi = me.loops[li].vertex_index
|
||||||
|
x, y, z = vs[vi]
|
||||||
|
variation = .93 + .055 * sin(19 * x + 13 * z) + .025 * cos(22 * y - 9 * z)
|
||||||
|
col.data[li].color = (*(min(1, c * variation) for c in base), 1)
|
||||||
|
tex.data[li].uv = uv[vi] if uv else (x * 1.7 + y * .3, z * 1.5)
|
||||||
|
groups = {}
|
||||||
|
for i in range(len(vs)):
|
||||||
|
ws = weights[i] if weights else {bone: 1.0}
|
||||||
|
total = sum(ws.values())
|
||||||
|
for key, weight in ws.items():
|
||||||
|
if weight <= 1e-8:
|
||||||
|
continue
|
||||||
|
groups.setdefault(key, ob.vertex_groups.get(key) or ob.vertex_groups.new(name=key))
|
||||||
|
groups[key].add([i], weight / total, 'REPLACE')
|
||||||
|
return ob
|
||||||
|
|
||||||
|
|
||||||
|
def blend(a, b, t):
|
||||||
|
t = max(0, min(1, t))
|
||||||
|
return {a: 1 - t, b: t} if a != b else {a: 1}
|
||||||
|
|
||||||
|
|
||||||
|
def catmull(points, t):
|
||||||
|
seg = min(len(points) - 2, int(t * (len(points) - 1)))
|
||||||
|
u = t * (len(points) - 1) - seg
|
||||||
|
a, b, c, d = [points[max(0, min(len(points) - 1, q))] for q in (seg - 1, seg, seg + 1, seg + 2)]
|
||||||
|
return .5 * ((2 * b) + (-a + c) * u + (2 * a - 5 * b + 4 * c - d) * u * u + (-a + 3 * b - 3 * c + d) * u * u * u)
|
||||||
|
|
||||||
|
|
||||||
|
def tube(name, points, radii, kind, bone, sides=14, steps=18,
|
||||||
|
flatten=1, weight_fn=None, groove=.012, smooth=True):
|
||||||
|
pts = [Vector(x) for x in points]
|
||||||
|
vs, fs, ws, uv = [], [], [], []
|
||||||
|
for j in range(steps + 1):
|
||||||
|
t = j / steps
|
||||||
|
p = catmull(pts, t)
|
||||||
|
seg = min(len(radii) - 2, int(t * (len(radii) - 1)))
|
||||||
|
a = t * (len(radii) - 1) - seg
|
||||||
|
radius = radii[seg] * (1 - a) + radii[seg + 1] * a
|
||||||
|
tangent = (catmull(pts, min(1, t + .002)) - catmull(pts, max(0, t - .002))).normalized()
|
||||||
|
ref = Vector((0, 1, 0)) if abs(tangent.y) < .9 else Vector((1, 0, 0))
|
||||||
|
u = tangent.cross(ref).normalized()
|
||||||
|
v = tangent.cross(u)
|
||||||
|
for k in range(sides):
|
||||||
|
angle = 2 * pi * k / sides
|
||||||
|
detail = 1 + groove * cos(5 * angle + t * 4)
|
||||||
|
vs.append(p + radius * detail * (u * cos(angle) + v * sin(angle) * flatten))
|
||||||
|
ws.append(weight_fn(t) if weight_fn else {bone: 1})
|
||||||
|
uv.append((k / sides, t))
|
||||||
|
if j:
|
||||||
|
for k in range(sides):
|
||||||
|
a, b = (j - 1) * sides + k, (j - 1) * sides + (k + 1) % sides
|
||||||
|
fs.append((a, b, b + sides, a + sides))
|
||||||
|
fs += [tuple(reversed(range(sides))), tuple(range(steps * sides, (steps + 1) * sides))]
|
||||||
|
return mesh(name, vs, fs, kind, bone, ws, smooth, uv)
|
||||||
|
|
||||||
|
|
||||||
|
def ell(name, c, r, kind, bone, segments=18, rings=11, smooth=True):
|
||||||
|
vs, fs = [(c[0], c[1], c[2] + r[2])], []
|
||||||
|
for j in range(1, rings):
|
||||||
|
th = pi * j / rings
|
||||||
|
for k in range(segments):
|
||||||
|
a = 2 * pi * k / segments
|
||||||
|
vs.append((c[0] + r[0] * sin(th) * cos(a), c[1] + r[1] * sin(th) * sin(a), c[2] + r[2] * cos(th)))
|
||||||
|
vs.append((c[0], c[1], c[2] - r[2]))
|
||||||
|
for k in range(segments):
|
||||||
|
fs.append((0, 1 + k, 1 + (k + 1) % segments))
|
||||||
|
for j in range(rings - 2):
|
||||||
|
for k in range(segments):
|
||||||
|
a = 1 + j * segments + k
|
||||||
|
b = 1 + j * segments + (k + 1) % segments
|
||||||
|
fs.append((a, a + segments, b + segments, b))
|
||||||
|
for k in range(segments):
|
||||||
|
fs.append((len(vs) - 1, len(vs) - 2 - k, len(vs) - 2 - (k + 1) % segments))
|
||||||
|
return mesh(name, vs, fs, kind, bone, smooth=smooth)
|
||||||
|
|
||||||
|
|
||||||
|
def shield(name, outline, center, depth, kind, bone):
|
||||||
|
c = Vector(center)
|
||||||
|
front = [Vector(p) for p in outline]
|
||||||
|
vs = front + [p.lerp(c, .13) + Vector((0, -depth * .32, 0)) for p in front]
|
||||||
|
vs += [c + Vector((0, -depth, 0))]
|
||||||
|
vs += [p + Vector((0, depth * .22, 0)) for p in front]
|
||||||
|
n = len(front)
|
||||||
|
fs = []
|
||||||
|
for i in range(n):
|
||||||
|
q = (i + 1) % n
|
||||||
|
fs += [(i, q, n + q, n + i), (n + i, n + q, 2 * n), (i, 2 * n + 1 + i, 2 * n + 1 + q, q)]
|
||||||
|
fs.append(tuple(reversed(range(2 * n + 1, 3 * n + 1))))
|
||||||
|
return mesh(name, vs, fs, kind, bone, smooth=False)
|
||||||
|
|
||||||
|
|
||||||
|
def ring(name, p, axis, radius, width, kind, bone, sides=14):
|
||||||
|
axis = Vector(axis).normalized()
|
||||||
|
p = Vector(p)
|
||||||
|
return tube(name, [p - axis * width / 2, p + axis * width / 2], [radius, radius],
|
||||||
|
kind, bone, sides, 3, groove=0, smooth=False)
|
||||||
|
|
||||||
|
|
||||||
|
def shard(name, points, radius, kind, bone):
|
||||||
|
return tube(name, points, [radius, .74 * radius, .002], kind, bone, 6, 5, groove=0, smooth=False)
|
||||||
|
|
||||||
|
|
||||||
|
def box(name, center, size, kind, bone):
|
||||||
|
cx, cy, cz = center
|
||||||
|
sx, sy, sz = size[0] / 2, size[1] / 2, size[2] / 2
|
||||||
|
vs = [(cx + dx * sx, cy + dy * sy, cz + dz * sz)
|
||||||
|
for dz in (-1, 1) for dy in (-1, 1) for dx in (-1, 1)]
|
||||||
|
fs = [(0, 1, 3, 2), (4, 6, 7, 5), (0, 4, 5, 1), (2, 3, 7, 6), (0, 2, 6, 4), (1, 5, 7, 3)]
|
||||||
|
return mesh(name, vs, fs, kind, bone, smooth=False)
|
||||||
|
|
||||||
|
|
||||||
|
def ik_elbow(a, c, l1, l2, pole):
|
||||||
|
delta = c - a
|
||||||
|
d = delta.length
|
||||||
|
if not abs(l1 - l2) + .00001 < d < l1 + l2 - .00001:
|
||||||
|
raise RuntimeError('Unreachable arm contact: %.6f for %.6f' % (d, l1 + l2))
|
||||||
|
line = delta / d
|
||||||
|
v = Vector(pole) - a
|
||||||
|
perpendicular = (v - line * v.dot(line)).normalized()
|
||||||
|
along = (l1 * l1 - l2 * l2 + d * d) / (2 * d)
|
||||||
|
return a + line * along + perpendicular * sqrt(max(0, l1 * l1 - along * along))
|
||||||
|
|
||||||
|
|
||||||
|
# Loader arms work the crank and release lever; contacts land on those sockets.
|
||||||
|
SHOULDERS = {'L': Vector((-.34, .02, 1.18)), 'R': Vector((.34, .02, 1.18))}
|
||||||
|
WRISTS = {'L': Vector((-.42, -.28, 1.02)), 'R': Vector((.18, -.38, 1.08))}
|
||||||
|
POLES = {'L': Vector((-.78, -.10, .95)), 'R': Vector((.72, -.05, .98))}
|
||||||
|
ELBOWS = {side: ik_elbow(SHOULDERS[side], WRISTS[side], .36, .35, POLES[side]) for side in ('L', 'R')}
|
||||||
|
WEAPON_ORIGIN = Vector((0, -.05, 1.22))
|
||||||
|
|
||||||
|
# Headless glass torso: broad chest housing, tapered waist, gearbox belly.
|
||||||
|
sections = [(.52, .16, .13), (.62, .22, .16), (.78, .24, .17), (.94, .26, .18),
|
||||||
|
(1.08, .30, .19), (1.20, .34, .18), (1.32, .30, .15), (1.40, .18, .10)]
|
||||||
|
vs, fs, ws = [], [], []
|
||||||
|
for j in range(43):
|
||||||
|
t = j / 42
|
||||||
|
f = t * (len(sections) - 1)
|
||||||
|
idx = min(len(sections) - 2, int(f))
|
||||||
|
u = f - idx
|
||||||
|
z, rx, ry = [sections[idx][k] * (1 - u) + sections[idx + 1][k] * u for k in range(3)]
|
||||||
|
for k in range(22):
|
||||||
|
a = 2 * pi * k / 22
|
||||||
|
mod = 1 + .03 * cos(a * 4)
|
||||||
|
vs.append((rx * cos(a) * mod, .02 + ry * sin(a), z))
|
||||||
|
ws.append(blend('Pelvis', 'Spine', (z - .60) / .32) if z < .92 else blend('Spine', 'Chest', (z - .92) / .36))
|
||||||
|
if j:
|
||||||
|
for k in range(22):
|
||||||
|
a = (j - 1) * 22 + k
|
||||||
|
b = (j - 1) * 22 + (k + 1) % 22
|
||||||
|
fs.append((a, b, b + 22, a + 22))
|
||||||
|
fs += [tuple(reversed(range(22))), tuple(range(42 * 22, 43 * 22))]
|
||||||
|
mesh('Continuous_Glass_Torso', vs, fs, 'glass2', 'Spine', ws)
|
||||||
|
|
||||||
|
# Gold frame ribs and dark gearbox instead of a head.
|
||||||
|
for s, side in [(-1, 'L'), (1, 'R')]:
|
||||||
|
outline = [(s * .02, -.16, 1.28), (s * .24, -.14, 1.34), (s * .30, -.10, 1.18),
|
||||||
|
(s * .22, -.18, 1.02), (s * .04, -.20, .92)]
|
||||||
|
shield('Chest_Frame_Rib_' + side, outline, (s * .14, -.20, 1.14), .05, 'frame', 'Chest')
|
||||||
|
outline2 = [(s * .03, -.12, .88), (s * .22, -.10, .94), (s * .26, -.10, .78),
|
||||||
|
(s * .16, -.15, .62)]
|
||||||
|
shield('Hip_Frame_' + side, outline2, (s * .14, -.14, .78), .04, 'edge', 'Pelvis')
|
||||||
|
tube('Waist_Conduit_' + side, [(s * .16, -.12, 1.02), (s * .12, -.13, .88), (s * .10, -.11, .74)],
|
||||||
|
[.016, .020, .011], 'ember', 'Spine', 8, 12, weight_fn=lambda t: blend('Chest', 'Spine', t))
|
||||||
|
|
||||||
|
box('Gearbox_Belly', (0, .04, .78), (.34, .28, .26), 'dark', 'Pelvis')
|
||||||
|
ring('Gearbox_Rim', (0, -.10, .78), (0, 1, 0), .15, .03, 'edge', 'Pelvis')
|
||||||
|
ell('Chest_Emblem', (0, -.20, 1.16), (.10, .03, .12), 'ember', 'Chest', 14, 8, False)
|
||||||
|
# Headless crown: crystal aim cluster, no neck or face.
|
||||||
|
ell('Aim_Crystal_Housing', (0, .0, 1.42), (.14, .12, .10), 'frame', 'Chest', 16, 9, False)
|
||||||
|
shard('Aim_Crystal', [(0, -.02, 1.48), (0, -.08, 1.58), (0, -.04, 1.72)], .05, 'quarrel', 'Chest')
|
||||||
|
for s in (-1, 1):
|
||||||
|
tube('Crown_Rail_%s' % s, [(s * .10, .0, 1.42), (s * .14, .02, 1.52), (s * .10, .04, 1.58)],
|
||||||
|
[.012, .014, .008], 'edge', 'Chest', 6, 8, groove=0, smooth=False)
|
||||||
|
|
||||||
|
# Loader arms: continuous cores, clamp hands on crank and release.
|
||||||
|
for s, side in [(-1, 'L'), (1, 'R')]:
|
||||||
|
a, b, c = SHOULDERS[side], ELBOWS[side], WRISTS[side]
|
||||||
|
upper, fore, hand = 'UpperArm.' + side, 'Forearm.' + side, 'Hand.' + side
|
||||||
|
points = [a, a.lerp(b, .42), b, b.lerp(c, .50), c]
|
||||||
|
|
||||||
|
def arm_weights(t, upper=upper, fore=fore, hand=hand):
|
||||||
|
if t < .12:
|
||||||
|
return blend('Chest', upper, t / .12)
|
||||||
|
if t < .43:
|
||||||
|
return {upper: 1}
|
||||||
|
if t < .60:
|
||||||
|
return blend(upper, fore, (t - .43) / .17)
|
||||||
|
if t < .91:
|
||||||
|
return {fore: 1}
|
||||||
|
return blend(fore, hand, (t - .91) / .09)
|
||||||
|
|
||||||
|
tube('Continuous_Loader_Arm_' + side, points, [.062, .058, .044, .055, .040],
|
||||||
|
'steel', upper, 16, 36, weight_fn=arm_weights)
|
||||||
|
ell('Elbow_Pivot_' + side, b, (.052, .052, .052), 'dark', fore, 12, 8)
|
||||||
|
ring('Elbow_Rim_' + side, b + Vector((s * .018, 0, 0)), (s, 0, 0), .058, .014, 'frame', fore)
|
||||||
|
shoulder = [(s * .22, -.04, 1.24), (s * .34, -.06, 1.30), (s * .44, -.02, 1.18),
|
||||||
|
(s * .38, -.08, 1.10), (s * .24, -.08, 1.12)]
|
||||||
|
shield('Shoulder_Clamp_' + side, shoulder, (s * .34, -.10, 1.18), .04, 'frame', 'Clavicle.' + side)
|
||||||
|
tube('Tapered_Armor_' + upper, [a.lerp(b, .18), a.lerp(b, .45), a.lerp(b, .74)],
|
||||||
|
[.055, .062, .048], 'glass', upper, 8, 8, groove=0, smooth=False)
|
||||||
|
ring('Wrist_Cuff_' + side, c.lerp(b, .12), c - b, .050, .028, 'edge', fore)
|
||||||
|
ell('Palm_' + side, c + Vector((0, -.015, .02)), (.048, .055, .036), 'steel', hand, 12, 8)
|
||||||
|
for k in range(3):
|
||||||
|
x = c.x + (k - 1) * .028
|
||||||
|
tube('Clamp_Finger_%s%d' % (side, k),
|
||||||
|
[(x, c.y + .008, c.z + .038), (x, c.y - .045, c.z + .052), (x, c.y - .062, c.z + .022)],
|
||||||
|
[.011, .013, .010], 'dark', hand, 8, 8, groove=0)
|
||||||
|
tube('Clamp_Thumb_' + side,
|
||||||
|
[c + Vector((s * .04, .01, .012)), c + Vector((s * .055, -.02, .028)), c + Vector((s * .03, -.045, .04))],
|
||||||
|
[.013, .015, .010], 'frame', hand, 8, 8, groove=0)
|
||||||
|
|
||||||
|
# Legs with articulated three-toe feet.
|
||||||
|
for s, side in [(-1, 'L'), (1, 'R')]:
|
||||||
|
hip = Vector((s * .16, .02, .62))
|
||||||
|
knee = Vector((s * .20, -.04, .34))
|
||||||
|
ankle = Vector((s * .21, .02, .10))
|
||||||
|
thigh, shin, foot = 'Thigh.' + side, 'Shin.' + side, 'Foot.' + side
|
||||||
|
|
||||||
|
def leg_weights(t, thigh=thigh, shin=shin, foot=foot):
|
||||||
|
if t < .42:
|
||||||
|
return {thigh: 1}
|
||||||
|
if t < .61:
|
||||||
|
return blend(thigh, shin, (t - .42) / .19)
|
||||||
|
if t < .92:
|
||||||
|
return {shin: 1}
|
||||||
|
return blend(shin, foot, (t - .92) / .08)
|
||||||
|
|
||||||
|
tube('Continuous_Leg_' + side,
|
||||||
|
[hip, hip.lerp(knee, .46), knee, knee.lerp(ankle, .55), ankle],
|
||||||
|
[.095, .098, .060, .072, .042], 'steel', thigh, 18, 38, weight_fn=leg_weights)
|
||||||
|
tube('Thigh_Glass_' + side, [hip.lerp(knee, .15), hip.lerp(knee, .50), hip.lerp(knee, .85)],
|
||||||
|
[.070, .082, .058], 'glass', thigh, 8, 8, groove=0, smooth=False)
|
||||||
|
tube('Shin_Glass_' + side, [knee.lerp(ankle, .15), knee.lerp(ankle, .55), knee.lerp(ankle, .90)],
|
||||||
|
[.058, .070, .042], 'glass', shin, 8, 8, groove=0, smooth=False)
|
||||||
|
ring('Knee_Pivot_' + side, knee + Vector((s * .012, 0, 0)), (1, 0, 0), .068, .018, 'frame', shin)
|
||||||
|
ell('Knee_Glow_' + side, knee + Vector((0, -.055, 0)), (.022, .014, .024), 'ember', shin, 10, 7)
|
||||||
|
ring('Ankle_Cuff_' + side, ankle + Vector((0, 0, .02)), (0, 0, 1), .050, .03, 'edge', foot)
|
||||||
|
for k in range(3):
|
||||||
|
ang = (k - 1) * .35
|
||||||
|
toe = Vector((s * .21 + s * .04 * ang, -.16 - .02 * abs(ang), .035))
|
||||||
|
tube('Toe_%s%d' % (side, k),
|
||||||
|
[(s * .21, .0, .06), (s * .21 + s * .03 * ang, -.08, .04), toe],
|
||||||
|
[.028, .032, .018], 'dark', foot, 8, 8, flatten=.7, groove=0, smooth=False)
|
||||||
|
tube('Toe_Cap_%s%d' % (side, k),
|
||||||
|
[(toe + Vector((0, .02, .02))), (toe + Vector((0, -.01, .01)))],
|
||||||
|
[.022, .014], 'frame', foot, 8, 4, groove=0, smooth=False)
|
||||||
|
|
||||||
|
# Ballista: twin glass limbs, draw cable, rails, rear housing and crystal quarrel.
|
||||||
|
box('Rail_L', (-.16, -.35, 1.28), (.05, 1.15, .07), 'steel', 'Weapon')
|
||||||
|
box('Rail_R', (.16, -.35, 1.28), (.05, 1.15, .07), 'steel', 'Weapon')
|
||||||
|
box('Rail_Trim_L', (-.16, -.35, 1.34), (.03, 1.10, .025), 'edge', 'Weapon')
|
||||||
|
box('Rail_Trim_R', (.16, -.35, 1.34), (.03, 1.10, .025), 'edge', 'Weapon')
|
||||||
|
box('Rear_Housing', (0, .12, 1.24), (.36, .28, .28), 'dark', 'Weapon')
|
||||||
|
box('Rear_Trim', (0, .12, 1.40), (.32, .24, .04), 'frame', 'Weapon')
|
||||||
|
ring('Crank_Drum', (0, .18, 1.24), (0, 1, 0), .11, .08, 'frame', 'Weapon', 12)
|
||||||
|
box('Gearbox_Top', (0, .02, 1.24), (.28, .22, .16), 'dark', 'Weapon')
|
||||||
|
|
||||||
|
# Glass bow limbs curve forward and outward.
|
||||||
|
for s, side, bone in [(-1, 'L', 'Limb.L'), (1, 'R', 'Limb.R')]:
|
||||||
|
tube('Glass_Limb_' + side,
|
||||||
|
[(s * .14, -.20, 1.28), (s * .42, -.42, 1.30), (s * .58, -.55, 1.26)],
|
||||||
|
[.035, .048, .022], 'glass', bone, 10, 14, flatten=.55, groove=0)
|
||||||
|
tube('Limb_Edge_' + side,
|
||||||
|
[(s * .15, -.20, 1.34), (s * .43, -.42, 1.36), (s * .58, -.55, 1.32)],
|
||||||
|
[.010, .012, .006], 'edge', bone, 6, 12, groove=0, smooth=False)
|
||||||
|
ring('Limb_Clamp_' + side, (s * .14, -.20, 1.28), (s, -.4, .1), .048, .05, 'frame', bone, 8)
|
||||||
|
tube('Draw_Cable_' + side,
|
||||||
|
[(s * .56, -.54, 1.26), (s * .20, -.55, 1.24), (0, -.52, 1.23)],
|
||||||
|
[.008, .008, .008], 'steel', 'Draw', 6, 10, groove=0, smooth=False)
|
||||||
|
|
||||||
|
box('Bolt_Shaft', (0, -.45, 1.26), (.045, .70, .045), 'steel', 'Bolt')
|
||||||
|
shard('Quarrel_Head', [(0, -.80, 1.26), (0, -.95, 1.26), (0, -1.12, 1.26)], .048, 'quarrel', 'Quarrel')
|
||||||
|
shard('Quarrel_Core', [(0, -.82, 1.26), (0, -.96, 1.26), (0, -1.10, 1.26)], .022, 'ember', 'Quarrel')
|
||||||
|
ring('Bolt_Nock', (0, -.28, 1.26), (0, 1, 0), .035, .03, 'edge', 'Bolt', 8)
|
||||||
|
# Crank handles (loader contact points).
|
||||||
|
for s, side in [(-1, 'L'), (1, 'R')]:
|
||||||
|
tube('Crank_Arm_' + side,
|
||||||
|
[(s * .20, .14, 1.18), (s * .36, .10, 1.08), (WRISTS[side].x, WRISTS[side].y + .02, WRISTS[side].z)],
|
||||||
|
[.018, .020, .016], 'frame', 'Weapon', 8, 10, groove=0, smooth=False)
|
||||||
|
ell('Crank_Knob_' + side, WRISTS[side], (.028, .028, .028), 'edge', 'Weapon', 10, 7)
|
||||||
|
box('Release_Lever', (0, -.22, 1.12), (.05, .16, .04), 'frame', 'Weapon')
|
||||||
|
|
||||||
|
# Recalculate coherent outward normals and export triangulated closed surfaces.
|
||||||
|
for ob in parts:
|
||||||
|
bm = bmesh.new()
|
||||||
|
bm.from_mesh(ob.data)
|
||||||
|
bmesh.ops.remove_doubles(bm, verts=list(bm.verts), dist=.0000001)
|
||||||
|
bmesh.ops.triangulate(bm, faces=list(bm.faces))
|
||||||
|
bmesh.ops.recalc_face_normals(bm, faces=list(bm.faces))
|
||||||
|
if bm.calc_volume(signed=True) < 0:
|
||||||
|
bmesh.ops.reverse_faces(bm, faces=list(bm.faces))
|
||||||
|
bm.to_mesh(ob.data)
|
||||||
|
bm.free()
|
||||||
|
ob.data.update()
|
||||||
|
|
||||||
|
bpy.ops.object.armature_add(enter_editmode=True)
|
||||||
|
rig = bpy.context.object
|
||||||
|
rig.name = 'GlassRig'
|
||||||
|
ed = rig.data.edit_bones
|
||||||
|
ed.remove(ed[0])
|
||||||
|
|
||||||
|
|
||||||
|
def bone(name, head, tail, parent=None):
|
||||||
|
b = ed.new(name)
|
||||||
|
b.head = head
|
||||||
|
b.tail = tail
|
||||||
|
if parent:
|
||||||
|
b.parent = ed[parent]
|
||||||
|
defs[name] = (Vector(head), Vector(tail), parent)
|
||||||
|
|
||||||
|
|
||||||
|
bone('Root', (0, 0, 0), (0, 0, .15))
|
||||||
|
bone('Pelvis', (0, .02, .58), (0, .02, .78), 'Root')
|
||||||
|
bone('Spine', (0, .02, .78), (0, .02, 1.08), 'Pelvis')
|
||||||
|
bone('Chest', (0, .02, 1.08), (0, .02, 1.38), 'Spine')
|
||||||
|
for s, side in [(-1, 'L'), (1, 'R')]:
|
||||||
|
a, b, c = SHOULDERS[side], ELBOWS[side], WRISTS[side]
|
||||||
|
bone('Clavicle.' + side, (s * .12, .02, 1.18), a, 'Chest')
|
||||||
|
bone('UpperArm.' + side, a, b, 'Clavicle.' + side)
|
||||||
|
bone('Forearm.' + side, b, c, 'UpperArm.' + side)
|
||||||
|
bone('Hand.' + side, c, c + Vector((0, -.07, 0)), 'Forearm.' + side)
|
||||||
|
bone('Thigh.' + side, (s * .16, .02, .62), (s * .20, -.04, .34), 'Pelvis')
|
||||||
|
bone('Shin.' + side, (s * .20, -.04, .34), (s * .21, .02, .10), 'Thigh.' + side)
|
||||||
|
bone('Foot.' + side, (s * .21, .02, .10), (s * .21, -.16, .04), 'Shin.' + side)
|
||||||
|
bone('Weapon', WEAPON_ORIGIN, (0, -.55, 1.26), 'Chest')
|
||||||
|
bone('Bolt', (0, -.28, 1.26), (0, -.78, 1.26), 'Weapon')
|
||||||
|
bone('Quarrel', (0, -.80, 1.26), (0, -1.10, 1.26), 'Bolt')
|
||||||
|
bone('Draw', (0, -.52, 1.23), (.56, -.54, 1.26), 'Weapon')
|
||||||
|
bone('Limb.L', (-.14, -.20, 1.28), (-.58, -.55, 1.26), 'Weapon')
|
||||||
|
bone('Limb.R', (.14, -.20, 1.28), (.58, -.55, 1.26), 'Weapon')
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
for ob in parts:
|
||||||
|
mod = ob.modifiers.new('Weighted glass deformation', 'ARMATURE')
|
||||||
|
mod.object = rig
|
||||||
|
ob.parent = rig
|
||||||
|
rig.show_in_front = True
|
||||||
|
|
||||||
|
clips = {'idle': (3.0, True), 'start': (.4, False), 'walk': (1.2, True), 'stop': (.4, False),
|
||||||
|
'attack_a': (1.3, False), 'attack_b': (1.3, False), 'hit': (.5, False),
|
||||||
|
'ability': (1.3, False), 'death': (2.0, False)}
|
||||||
|
|
||||||
|
|
||||||
|
def curve(t, keys):
|
||||||
|
for (a, x), (b, y) in zip(keys, keys[1:]):
|
||||||
|
if t <= b:
|
||||||
|
u = max(0, (t - a) / (b - a))
|
||||||
|
u = u * u * (3 - 2 * u)
|
||||||
|
return x * (1 - u) + y * u
|
||||||
|
return keys[-1][1]
|
||||||
|
|
||||||
|
|
||||||
|
def pose_rotation(pb, r):
|
||||||
|
q = Quaternion((0, 0, 1), r[2]) @ Quaternion((0, 1, 0), r[1]) @ Quaternion((1, 0, 0), r[0])
|
||||||
|
basis = pb.bone.matrix_local.to_quaternion()
|
||||||
|
return basis.inverted() @ q @ basis
|
||||||
|
|
||||||
|
|
||||||
|
def aim_bone(name, head, tail):
|
||||||
|
pb = rig.pose.bones[name]
|
||||||
|
rest = pb.bone.matrix_local
|
||||||
|
direction = defs[name][1] - defs[name][0]
|
||||||
|
rotation = direction.rotation_difference(tail - head).to_matrix().to_4x4()
|
||||||
|
pose = rotation @ rest
|
||||||
|
pose.translation = head
|
||||||
|
pb.matrix = pose
|
||||||
|
bpy.context.view_layer.update()
|
||||||
|
|
||||||
|
|
||||||
|
def animate(name, t, dur):
|
||||||
|
rs, locs = {}, {}
|
||||||
|
|
||||||
|
def rot(b, x=0, y=0, z=0):
|
||||||
|
rs[b] = (x, y, z)
|
||||||
|
|
||||||
|
if name == 'idle':
|
||||||
|
breath = sin(2 * pi * t / dur)
|
||||||
|
rot('Spine', .008 * breath)
|
||||||
|
rot('Chest', -.010 * breath, 0, .006 * breath)
|
||||||
|
rot('Weapon', -.025 + .005 * breath)
|
||||||
|
rot('Limb.L', 0, 0, .01 * breath)
|
||||||
|
rot('Limb.R', 0, 0, -.01 * breath)
|
||||||
|
elif name in ('walk', 'start', 'stop'):
|
||||||
|
fade = 1 if name == 'walk' else sin(pi * .5 * t / dur) if name == 'start' else cos(pi * .5 * t / dur)
|
||||||
|
phase = 2 * pi * t / 1.2
|
||||||
|
roll = sin(phase) * fade
|
||||||
|
rot('Pelvis', .028 * fade, 0, .04 * roll)
|
||||||
|
rot('Spine', -.028 * fade, 0, -.035 * roll)
|
||||||
|
rot('Chest', .018 * fade, 0, .014 * roll)
|
||||||
|
rot('Weapon', -.03 * fade + .006 * sin(phase * 2) * fade)
|
||||||
|
for s, side in [(-1, 'L'), (1, 'R')]:
|
||||||
|
q = sin(phase + (0 if s < 0 else pi))
|
||||||
|
lift = max(0, q)
|
||||||
|
rot('Thigh.' + side, .32 * q * fade)
|
||||||
|
rot('Shin.' + side, -.50 * lift * fade)
|
||||||
|
rot('Foot.' + side, (-.32 * q + .50 * lift) * fade)
|
||||||
|
locs['Root'] = (0, 0, -.012 * (1 - cos(phase * 2)) * fade)
|
||||||
|
elif name in ('attack_a', 'attack_b', 'ability'):
|
||||||
|
braced = name == 'attack_b'
|
||||||
|
charged = name == 'ability'
|
||||||
|
aim = curve(t, [(0, 0), (.30, .78), (.60, 1), (.90, 1), (1.04, .8), (1.30, 0)])
|
||||||
|
recoil = curve(t, [(0, 0), (.90, 0), (.933333, 1), (1.04, .22), (1.14, 0), (1.30, 0)])
|
||||||
|
draw = curve(t, [(0, 0), (.90, 0), (.98, 1), (1.12, 1), (1.26, 0), (1.30, 0)])
|
||||||
|
strength = 1.40 if charged else .88 if braced else 1.0
|
||||||
|
rot('Spine', -.03 * aim + .022 * recoil, 0, -.02 * aim if braced else .008 * aim)
|
||||||
|
rot('Chest', -.012 * aim + .014 * recoil, 0, .02 * aim if braced else -.01 * aim)
|
||||||
|
rot('Weapon', .038 * aim + .06 * recoil * strength)
|
||||||
|
locs['Weapon'] = (0, .048 * recoil * strength, -.004 * draw)
|
||||||
|
# Limbs flex back on release then spring out while drawing.
|
||||||
|
flex = .22 * recoil * strength - .08 * draw
|
||||||
|
rot('Limb.L', 0, 0, flex)
|
||||||
|
rot('Limb.R', 0, 0, -flex)
|
||||||
|
locs['Draw'] = (0, .10 * recoil, .04 * draw)
|
||||||
|
locs['Bolt'] = (0, -.12 * draw, 0)
|
||||||
|
locs['Quarrel'] = (-.20 * recoil * strength, 0, 0) if not charged else (0, -.35 * recoil, 0)
|
||||||
|
rot('Quarrel', 0, .50 * curve(t, [(0, 0), (.35, .3), (.78, 1), (.90, 1), (.97, 0), (1.3, 0)]) if charged else 0, 0)
|
||||||
|
if charged:
|
||||||
|
locs['Quarrel'] = (0, -.28 * curve(t, [(0, 0), (.35, .2), (.78, .6), (.90, 1), (1.1, .2), (1.3, 0)]), 0)
|
||||||
|
if braced:
|
||||||
|
locs['Root'] = (0, .006 * aim, -.02 * aim)
|
||||||
|
rot('Thigh.L', .06 * aim)
|
||||||
|
rot('Shin.L', -.10 * aim)
|
||||||
|
rot('Thigh.R', .06 * aim)
|
||||||
|
rot('Shin.R', -.10 * aim)
|
||||||
|
elif name == 'hit':
|
||||||
|
q = curve(t, [(0, 0), (.066667, 1), (.20, .4), (.5, 0)])
|
||||||
|
rot('Spine', .06 * q, 0, -.035 * q)
|
||||||
|
rot('Chest', .04 * q, 0, .03 * q)
|
||||||
|
rot('Weapon', .03 * q)
|
||||||
|
elif name == 'death':
|
||||||
|
q = curve(t, [(0, 0), (.3, .12), (.72, .58), (1.25, 1), (1.5, .96), (2, 1)])
|
||||||
|
rot('Root', -.55 * q, 0, -.28 * q)
|
||||||
|
locs['Root'] = (0, -.08 * q, -.20 * q)
|
||||||
|
rot('Pelvis', -.08 * q)
|
||||||
|
rot('Spine', -.12 * q)
|
||||||
|
rot('Chest', -.06 * q)
|
||||||
|
rot('Weapon', -.10 * q, 0, .06 * q)
|
||||||
|
rot('Limb.L', 0, 0, .12 * q)
|
||||||
|
rot('Limb.R', 0, 0, -.12 * q)
|
||||||
|
for side in ('L', 'R'):
|
||||||
|
rot('Thigh.' + side, .22 * q)
|
||||||
|
rot('Shin.' + side, -.60 * q)
|
||||||
|
rot('Foot.' + side, .14 * q)
|
||||||
|
for pb in rig.pose.bones:
|
||||||
|
pb.rotation_mode = 'QUATERNION'
|
||||||
|
pb.rotation_quaternion = pose_rotation(pb, rs.get(pb.name, (0, 0, 0)))
|
||||||
|
pb.location = pb.bone.matrix_local.to_3x3().inverted() @ Vector(locs.get(pb.name, (0, 0, 0)))
|
||||||
|
pb.scale = (1, 1, 1)
|
||||||
|
bpy.context.view_layer.update()
|
||||||
|
weapon = rig.pose.bones['Weapon'].matrix @ rig.data.bones['Weapon'].matrix_local.inverted()
|
||||||
|
body = rig.pose.bones['Chest'].matrix @ rig.data.bones['Chest'].matrix_local.inverted()
|
||||||
|
errors = []
|
||||||
|
for side in ('L', 'R'):
|
||||||
|
a = rig.pose.bones['UpperArm.' + side].head.copy()
|
||||||
|
c = weapon @ WRISTS[side]
|
||||||
|
pole = body @ POLES[side]
|
||||||
|
elbow = ik_elbow(a, c, .36, .35, pole)
|
||||||
|
aim_bone('UpperArm.' + side, a, elbow)
|
||||||
|
aim_bone('Forearm.' + side, elbow, c)
|
||||||
|
hand = weapon @ rig.data.bones['Hand.' + side].matrix_local
|
||||||
|
hand.translation = c
|
||||||
|
rig.pose.bones['Hand.' + side].matrix = hand
|
||||||
|
bpy.context.view_layer.update()
|
||||||
|
errors.append((rig.pose.bones['Forearm.' + side].tail - c).length)
|
||||||
|
contact_samples.append({'clip': name, 't': round(t, 6), 'max_wrist_error': max(errors)})
|
||||||
|
|
||||||
|
|
||||||
|
for name, (dur, loop) in clips.items():
|
||||||
|
rig.animation_data_clear()
|
||||||
|
rig.animation_data_create()
|
||||||
|
action = bpy.data.actions.new(name)
|
||||||
|
action.use_fake_user = True
|
||||||
|
rig.animation_data.action = action
|
||||||
|
for frame in range(round(dur * 30) + 1):
|
||||||
|
animate(name, frame / 30, dur)
|
||||||
|
for pb in rig.pose.bones:
|
||||||
|
pb.keyframe_insert('rotation_quaternion', frame=frame)
|
||||||
|
pb.keyframe_insert('location', frame=frame)
|
||||||
|
action['loop'] = loop
|
||||||
|
action['contact_seconds'] = .90 if name in ('attack_a', 'attack_b', 'ability') else -1
|
||||||
|
for layer in action.layers:
|
||||||
|
for strip in layer.strips:
|
||||||
|
for bag in strip.channelbags:
|
||||||
|
for fc in bag.fcurves:
|
||||||
|
for key in fc.keyframe_points:
|
||||||
|
key.interpolation = 'LINEAR'
|
||||||
|
rig.animation_data_clear()
|
||||||
|
rig.animation_data_create()
|
||||||
|
rig.animation_data.action = bpy.data.actions['idle']
|
||||||
|
bpy.context.scene.render.fps = 30
|
||||||
|
bpy.context.scene.frame_set(0)
|
||||||
|
|
||||||
|
bpy.ops.wm.save_as_mainfile(filepath=str(SRC / 'glasballiste.blend'))
|
||||||
|
source_parts = len(parts)
|
||||||
|
bpy.ops.object.select_all(action='DESELECT')
|
||||||
|
for ob in parts:
|
||||||
|
ob.select_set(True)
|
||||||
|
bpy.context.view_layer.objects.active = parts[0]
|
||||||
|
bpy.ops.object.join()
|
||||||
|
body = bpy.context.object
|
||||||
|
body.name = 'GlassBody'
|
||||||
|
body.select_set(True)
|
||||||
|
rig.select_set(True)
|
||||||
|
bpy.context.view_layer.objects.active = rig
|
||||||
|
bpy.ops.export_scene.gltf(filepath=str(OUT / 'glasballiste.glb'), export_format='GLB', use_selection=True,
|
||||||
|
export_animations=True, export_animation_mode='ACTIONS', export_force_sampling=True,
|
||||||
|
export_frame_range=False, export_anim_slide_to_zero=True, export_skins=True,
|
||||||
|
export_all_influences=False, export_def_bones=True, export_anim_single_armature=True,
|
||||||
|
export_materials='EXPORT', export_yup=True)
|
||||||
|
raw = (OUT / 'glasballiste.glb').read_bytes()
|
||||||
|
doc = json.loads(raw[20:20 + struct.unpack_from('<I', raw, 12)[0]])
|
||||||
|
triangles = sum(doc['accessors'][q['indices']]['count'] // 3 for m in doc['meshes'] for q in m['primitives'])
|
||||||
|
mins = [min(v.co[i] for v in body.data.vertices) for i in range(3)]
|
||||||
|
maxs = [max(v.co[i] for v in body.data.vertices) for i in range(3)]
|
||||||
|
record = {'id': 'g06', 'role': 'ranged', 'name': 'Glasballiste',
|
||||||
|
'resource_path': 'res://assets/characters/glasballiste.glb',
|
||||||
|
'triangles': triangles, 'mesh_parts': 1, 'parts': ['GlassBody'],
|
||||||
|
'bounds_godot': {'min': [mins[0], mins[2], -maxs[1]], 'max': [maxs[0], maxs[2], -mins[1]]},
|
||||||
|
'skeletal': True, 'bones': len(defs), 'clips': list(clips),
|
||||||
|
'attack_cycle': ['attack_a', 'attack_b', 'attack_a', 'ability'],
|
||||||
|
'contact_seconds': .9, 'sha256': hashlib.sha256(raw).hexdigest(),
|
||||||
|
'source': 'assets-source/phase19/glasballiste.blend'}
|
||||||
|
(P / 'assets/phase19_manifest.json').write_text(json.dumps({'assets': [record]}, indent=2) + '\n')
|
||||||
|
audit = {'record': record, 'exported_clips': [a['name'] for a in doc['animations']],
|
||||||
|
'skins': len(doc.get('skins', [])), 'source_parts': source_parts,
|
||||||
|
'materials': [m['name'] for m in doc['materials']],
|
||||||
|
'max_baked_wrist_contact_error': max(x['max_wrist_error'] for x in contact_samples),
|
||||||
|
'contact_sample_count': len(contact_samples),
|
||||||
|
'contact_validation': 'Analytic two-bone loader-arm IK baked at 30 Hz for all nine clips.',
|
||||||
|
'shot_seconds': .90, 'recoil_peak_seconds': .933333, 'draw_resets_seconds': 1.26}
|
||||||
|
(SRC / 'glasballiste-audit.json').write_text(json.dumps(audit, indent=2) + '\n')
|
||||||
|
print('GLASBALLISTE_BUILD', triangles, 'triangles', len(defs), 'bones', len(doc['animations']), 'clips',
|
||||||
|
'wrist error', audit['max_baked_wrist_contact_error'], flush=True)
|
||||||
76
assets-source/phase19/v1-humanoid/glasballiste-audit.json
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
{
|
||||||
|
"record": {
|
||||||
|
"id": "g06",
|
||||||
|
"role": "ranged",
|
||||||
|
"name": "Glasballiste",
|
||||||
|
"resource_path": "res://assets/characters/glasballiste.glb",
|
||||||
|
"triangles": 15744,
|
||||||
|
"mesh_parts": 1,
|
||||||
|
"parts": [
|
||||||
|
"GlassBody"
|
||||||
|
],
|
||||||
|
"bounds_godot": {
|
||||||
|
"min": [
|
||||||
|
-0.714865505695343,
|
||||||
|
0.008352670818567276,
|
||||||
|
-0.25999999046325684
|
||||||
|
],
|
||||||
|
"max": [
|
||||||
|
0.5886231660842896,
|
||||||
|
1.720479965209961,
|
||||||
|
1.119999885559082
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"skeletal": true,
|
||||||
|
"bones": 24,
|
||||||
|
"clips": [
|
||||||
|
"idle",
|
||||||
|
"start",
|
||||||
|
"walk",
|
||||||
|
"stop",
|
||||||
|
"attack_a",
|
||||||
|
"attack_b",
|
||||||
|
"hit",
|
||||||
|
"ability",
|
||||||
|
"death"
|
||||||
|
],
|
||||||
|
"attack_cycle": [
|
||||||
|
"attack_a",
|
||||||
|
"attack_b",
|
||||||
|
"attack_a",
|
||||||
|
"ability"
|
||||||
|
],
|
||||||
|
"contact_seconds": 0.9,
|
||||||
|
"sha256": "bb080d3c22baae6594e7743c02399509cf3284d6af1b195c9e51e98b55328133",
|
||||||
|
"source": "assets-source/phase19/glasballiste.blend"
|
||||||
|
},
|
||||||
|
"exported_clips": [
|
||||||
|
"ability",
|
||||||
|
"attack_a",
|
||||||
|
"attack_b",
|
||||||
|
"death",
|
||||||
|
"hit",
|
||||||
|
"idle",
|
||||||
|
"start",
|
||||||
|
"stop",
|
||||||
|
"walk"
|
||||||
|
],
|
||||||
|
"skins": 1,
|
||||||
|
"source_parts": 85,
|
||||||
|
"materials": [
|
||||||
|
"GB_GlassPale",
|
||||||
|
"GB_Frame",
|
||||||
|
"GB_FrameEdge",
|
||||||
|
"GB_Ember",
|
||||||
|
"GB_Joint",
|
||||||
|
"GB_Quarrel",
|
||||||
|
"GB_Steel",
|
||||||
|
"GB_Glass"
|
||||||
|
],
|
||||||
|
"max_baked_wrist_contact_error": 2.1490760299348766e-07,
|
||||||
|
"contact_sample_count": 351,
|
||||||
|
"contact_validation": "Analytic two-bone loader-arm IK baked at 30 Hz for all nine clips.",
|
||||||
|
"shot_seconds": 0.9,
|
||||||
|
"recoil_peak_seconds": 0.933333,
|
||||||
|
"draw_resets_seconds": 1.26
|
||||||
|
}
|
||||||
BIN
assets-source/phase19/v1-humanoid/glasballiste.blend
Normal file
BIN
assets-source/phase19/v1-humanoid/glasballiste.glb
Normal file
49
assets-source/phase19/v1-humanoid/phase19_manifest.json
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
{
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"id": "g06",
|
||||||
|
"role": "ranged",
|
||||||
|
"name": "Glasballiste",
|
||||||
|
"resource_path": "res://assets/characters/glasballiste.glb",
|
||||||
|
"triangles": 15744,
|
||||||
|
"mesh_parts": 1,
|
||||||
|
"parts": [
|
||||||
|
"GlassBody"
|
||||||
|
],
|
||||||
|
"bounds_godot": {
|
||||||
|
"min": [
|
||||||
|
-0.714865505695343,
|
||||||
|
0.008352670818567276,
|
||||||
|
-0.25999999046325684
|
||||||
|
],
|
||||||
|
"max": [
|
||||||
|
0.5886231660842896,
|
||||||
|
1.720479965209961,
|
||||||
|
1.119999885559082
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"skeletal": true,
|
||||||
|
"bones": 24,
|
||||||
|
"clips": [
|
||||||
|
"idle",
|
||||||
|
"start",
|
||||||
|
"walk",
|
||||||
|
"stop",
|
||||||
|
"attack_a",
|
||||||
|
"attack_b",
|
||||||
|
"hit",
|
||||||
|
"ability",
|
||||||
|
"death"
|
||||||
|
],
|
||||||
|
"attack_cycle": [
|
||||||
|
"attack_a",
|
||||||
|
"attack_b",
|
||||||
|
"attack_a",
|
||||||
|
"ability"
|
||||||
|
],
|
||||||
|
"contact_seconds": 0.9,
|
||||||
|
"sha256": "bb080d3c22baae6594e7743c02399509cf3284d6af1b195c9e51e98b55328133",
|
||||||
|
"source": "assets-source/phase19/glasballiste.blend"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
assets-source/phase19/v1-humanoid/v1-front.png
Normal file
|
After Width: | Height: | Size: 371 KiB |
331
assets-source/phase20/build_menu_stage.py
Normal file
|
|
@ -0,0 +1,331 @@
|
||||||
|
"""Menu diorama: two banks of the Grüne Kluft, one Saumkern dais per Bund.
|
||||||
|
|
||||||
|
Run: ALSOFT_DRIVERS=null blender -b --python-exit-code 1 --python assets-source/phase20/build_menu_stage.py
|
||||||
|
All placements are written in Godot coordinates (x right, y up, z towards the title camera)
|
||||||
|
and converted to Blender (Z-up, -Y forward) by G(). Materials reuse the landscape names
|
||||||
|
(ENV_*), so Godot applies the same triplanar Kluft shaders as in play.
|
||||||
|
"""
|
||||||
|
from pathlib import Path
|
||||||
|
import bpy, bmesh, json, hashlib, struct, math
|
||||||
|
from math import sin, cos, pi
|
||||||
|
from mathutils import Vector, noise
|
||||||
|
|
||||||
|
P = Path(__file__).resolve().parents[2]
|
||||||
|
SRC = Path(__file__).resolve().parent
|
||||||
|
OUT = P / 'assets/environment'
|
||||||
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||||
|
bpy.context.preferences.filepaths.save_version = 0
|
||||||
|
parts = []
|
||||||
|
|
||||||
|
|
||||||
|
def G(x, y, z):
|
||||||
|
return Vector((x, -z, y))
|
||||||
|
|
||||||
|
|
||||||
|
def material(name, color, rough=.9, metal=0.0, emission=0.0):
|
||||||
|
m = bpy.data.materials.new(name)
|
||||||
|
m.diffuse_color = (*color, 1)
|
||||||
|
m.use_nodes = True
|
||||||
|
bs = m.node_tree.nodes.get('Principled BSDF')
|
||||||
|
bs.inputs['Base Color'].default_value = (*color, 1)
|
||||||
|
bs.inputs['Roughness'].default_value = rough
|
||||||
|
bs.inputs['Metallic'].default_value = metal
|
||||||
|
if emission:
|
||||||
|
bs.inputs['Emission Color'].default_value = (*color, 1)
|
||||||
|
bs.inputs['Emission Strength'].default_value = emission
|
||||||
|
return m
|
||||||
|
|
||||||
|
|
||||||
|
M = {
|
||||||
|
'cliff': material('ENV_cliff', (.133, .153, .138)),
|
||||||
|
'limestone': material('ENV_limestone', (.283, .246, .178), .89),
|
||||||
|
'paving': material('ENV_paving', (.162, .171, .136), .92),
|
||||||
|
'soil': material('ENV_soil', (.051, .078, .041), .97),
|
||||||
|
'bark': material('ENV_bark', (.087, .056, .032), .93),
|
||||||
|
'leaf_dark': material('ENV_leaf_dark', (.031, .07, .04), .89),
|
||||||
|
'leaf_mid': material('ENV_leaf_mid', (.084, .144, .054), .85),
|
||||||
|
'leaf_light': material('ENV_leaf_light', (.258, .305, .091), .79),
|
||||||
|
'basalt': material('ENV_basalt', (.052, .052, .058), .86),
|
||||||
|
'brass': material('ENV_brass', (.515, .366, .153), .53, .65),
|
||||||
|
'ember': material('ENV_ember', (1.0, .42, .10), .4, 0, 3.2),
|
||||||
|
'spring': material('ENV_spring', (.12, .62, .50), .2, 0, .9),
|
||||||
|
'teal': material('ENV_teal', (.141, .617, .485), .36, .2, .6),
|
||||||
|
'coral': material('ENV_coral', (.738, .323, .195), .36, .2, .6),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def obj(name, vs, fs, kind, smooth=False):
|
||||||
|
me = bpy.data.meshes.new(name)
|
||||||
|
me.from_pydata([tuple(v) for v in vs], [], fs)
|
||||||
|
me.update()
|
||||||
|
me.materials.append(M[kind])
|
||||||
|
for p in me.polygons:
|
||||||
|
p.use_smooth = smooth
|
||||||
|
ob = bpy.data.objects.new(name, me)
|
||||||
|
bpy.context.collection.objects.link(ob)
|
||||||
|
parts.append(ob)
|
||||||
|
return ob
|
||||||
|
|
||||||
|
|
||||||
|
def fbm(p, seed, octaves=4):
|
||||||
|
return noise.fractal(Vector(p) * 1.0 + Vector((seed * 17.3, seed * 5.1, seed * 9.7)), .6, 2.1, octaves)
|
||||||
|
|
||||||
|
|
||||||
|
def cliff(name, center, radii, top, bottom, seed, top_kind='soil', levels=30, sides=120, amp=.55, lip=.18):
|
||||||
|
"""Rock mass: irregular footprint, layered walls, flat top for daises."""
|
||||||
|
cx, cz = center
|
||||||
|
vs, fs = [], []
|
||||||
|
heights = [top - (top - bottom) * (i / (levels - 1)) ** 1.25 for i in range(levels)]
|
||||||
|
for li, y in enumerate(heights):
|
||||||
|
for k in range(sides):
|
||||||
|
a = 2 * pi * k / sides
|
||||||
|
f = a / (2 * pi) * len(radii)
|
||||||
|
i0 = int(f) % len(radii)
|
||||||
|
u = f - int(f)
|
||||||
|
base = radii[i0] * (1 - u) + radii[(i0 + 1) % len(radii)] * u
|
||||||
|
depth = (top - y) / max(.01, top - bottom)
|
||||||
|
strata = .05 * abs(sin(y * 2.3 + seed)) + .025 * sin(y * 7.3 + a * 2 + seed)
|
||||||
|
crack = .05 * max(0.0, fbm((cos(a) * 9, y * .08, sin(a) * 9), seed + 7))
|
||||||
|
n = fbm((cos(a) * 2.2, y * .45, sin(a) * 2.2), seed, 5) - crack
|
||||||
|
r = base * (1 + strata + amp * .22 * n) * (1 + .12 * depth) + (lip if li == 0 else 0)
|
||||||
|
vs.append(G(cx + cos(a) * r, y + (fbm((cos(a) * 3, 0, sin(a) * 3), seed + 3) * .08 if li == 0 else 0), cz + sin(a) * r))
|
||||||
|
for li in range(levels - 1):
|
||||||
|
for k in range(sides):
|
||||||
|
a, b = li * sides + k, li * sides + (k + 1) % sides
|
||||||
|
fs.append((a, b, b + sides, a + sides))
|
||||||
|
fs.append(tuple(range((levels - 1) * sides, levels * sides)))
|
||||||
|
wall = obj(name + '_Wall', vs, fs + [tuple(reversed(range(sides)))], 'cliff', True)
|
||||||
|
# Separate top so the plateau carries soil/stone instead of cliff tint.
|
||||||
|
ring = [G(cx + (v.x - cx) * .985, top + .012, cz + (-v.y - cz) * .985) for v in vs[:sides]]
|
||||||
|
centre = G(cx, top + .012, cz)
|
||||||
|
tvs = [centre] + ring
|
||||||
|
tfs = [(0, 1 + (k + 1) % sides, 1 + k) for k in range(sides)]
|
||||||
|
obj(name + '_Top', tvs, tfs, top_kind)
|
||||||
|
return wall
|
||||||
|
|
||||||
|
|
||||||
|
def lathe(name, profile, sides, kind, center, rotation=0.0, smooth=False, squash=1.0):
|
||||||
|
"""Revolved solid from (radius, height) pairs, closed at both ends."""
|
||||||
|
cx, cy, cz = center
|
||||||
|
vs, fs = [], []
|
||||||
|
for r, y in profile:
|
||||||
|
for k in range(sides):
|
||||||
|
a = 2 * pi * k / sides + rotation
|
||||||
|
vs.append(G(cx + cos(a) * r, cy + y, cz + sin(a) * r * squash))
|
||||||
|
n = len(profile)
|
||||||
|
for j in range(n - 1):
|
||||||
|
for k in range(sides):
|
||||||
|
a, b = j * sides + k, j * sides + (k + 1) % sides
|
||||||
|
fs.append((a, b, b + sides, a + sides))
|
||||||
|
fs += [tuple(reversed(range(sides))), tuple(range((n - 1) * sides, n * sides))]
|
||||||
|
return obj(name, vs, fs, kind, smooth)
|
||||||
|
|
||||||
|
|
||||||
|
def annulus(name, center, r_in, r_out, y0, y1, sides, kind, rotation=0.0):
|
||||||
|
cx, cy, cz = center
|
||||||
|
vs, fs = [], []
|
||||||
|
for r, y in ((r_out, y0), (r_out, y1), (r_in, y1), (r_in, y0)):
|
||||||
|
for k in range(sides):
|
||||||
|
a = 2 * pi * k / sides + rotation
|
||||||
|
vs.append(G(cx + cos(a) * r, cy + y, cz + sin(a) * r))
|
||||||
|
for ring in range(4):
|
||||||
|
nxt = (ring + 1) % 4
|
||||||
|
for k in range(sides):
|
||||||
|
q = (k + 1) % sides
|
||||||
|
fs.append((ring * sides + k, ring * sides + q, nxt * sides + q, nxt * sides + k))
|
||||||
|
return obj(name, vs, fs, kind)
|
||||||
|
|
||||||
|
|
||||||
|
def catmull(points, t):
|
||||||
|
seg = min(len(points) - 2, int(t * (len(points) - 1)))
|
||||||
|
u = t * (len(points) - 1) - seg
|
||||||
|
a, b, c, d = [points[max(0, min(len(points) - 1, q))] for q in (seg - 1, seg, seg + 1, seg + 2)]
|
||||||
|
return .5 * ((2 * b) + (-a + c) * u + (2 * a - 5 * b + 4 * c - d) * u * u + (-a + 3 * b - 3 * c + d) * u * u * u)
|
||||||
|
|
||||||
|
|
||||||
|
def tube(name, points, radii, kind, sides=10, steps=18, seed=0, gnarl=.0):
|
||||||
|
pts = [G(*p) for p in points]
|
||||||
|
vs, fs = [], []
|
||||||
|
for j in range(steps + 1):
|
||||||
|
t = j / steps
|
||||||
|
p = catmull(pts, t)
|
||||||
|
tangent = (catmull(pts, min(1, t + .01)) - catmull(pts, max(0, t - .01))).normalized()
|
||||||
|
ref = Vector((0, 0, 1)) if abs(tangent.z) < .9 else Vector((1, 0, 0))
|
||||||
|
u = tangent.cross(ref).normalized()
|
||||||
|
v = tangent.cross(u)
|
||||||
|
seg = min(len(radii) - 2, int(t * (len(radii) - 1)))
|
||||||
|
f = t * (len(radii) - 1) - seg
|
||||||
|
radius = radii[seg] * (1 - f) + radii[seg + 1] * f
|
||||||
|
for k in range(sides):
|
||||||
|
a = 2 * pi * k / sides
|
||||||
|
wobble = 1 + gnarl * fbm((cos(a), t * 6, sin(a)), seed)
|
||||||
|
vs.append(p + (u * cos(a) + v * sin(a)) * radius * wobble)
|
||||||
|
for j in range(steps):
|
||||||
|
for k in range(sides):
|
||||||
|
a, b = j * sides + k, j * sides + (k + 1) % sides
|
||||||
|
fs.append((a, b, b + sides, a + sides))
|
||||||
|
fs += [tuple(reversed(range(sides))), tuple(range(steps * sides, (steps + 1) * sides))]
|
||||||
|
return obj(name, vs, fs, kind, True)
|
||||||
|
|
||||||
|
|
||||||
|
def blob(name, center, size, kind, seed, subdiv=3, amp=.28, flatten=None, smooth=True):
|
||||||
|
bm = bmesh.new()
|
||||||
|
bmesh.ops.create_icosphere(bm, subdivisions=subdiv, radius=1.0)
|
||||||
|
c = G(*center)
|
||||||
|
sx, sy, sz = size
|
||||||
|
for v in bm.verts:
|
||||||
|
d = v.co.normalized()
|
||||||
|
n = fbm(d * 1.7, seed)
|
||||||
|
r = 1 + amp * n
|
||||||
|
gx, gy, gz = d.x * r * sx, d.z * r * sy, -d.y * r * sz
|
||||||
|
if flatten is not None and gy < flatten * sy:
|
||||||
|
gy = flatten * sy + (gy - flatten * sy) * .15
|
||||||
|
v.co = c + Vector((gx, -gz, gy))
|
||||||
|
me = bpy.data.meshes.new(name)
|
||||||
|
bm.to_mesh(me)
|
||||||
|
bm.free()
|
||||||
|
me.materials.append(M[kind])
|
||||||
|
for p in me.polygons:
|
||||||
|
p.use_smooth = smooth
|
||||||
|
ob = bpy.data.objects.new(name, me)
|
||||||
|
bpy.context.collection.objects.link(ob)
|
||||||
|
parts.append(ob)
|
||||||
|
return ob
|
||||||
|
|
||||||
|
|
||||||
|
def slab(name, center, size, yaw, kind, tilt=0.0):
|
||||||
|
"""Box in Godot space: size (x, y, z), rotated by yaw around y."""
|
||||||
|
cx, cy, cz = center
|
||||||
|
hx, hy, hz = size[0] / 2, size[1] / 2, size[2] / 2
|
||||||
|
vs = []
|
||||||
|
for dy in (-1, 1):
|
||||||
|
for dz in (-1, 1):
|
||||||
|
for dx in (-1, 1):
|
||||||
|
x, y, z = dx * hx, dy * hy + dz * tilt, dz * hz
|
||||||
|
rx = x * cos(yaw) - z * sin(yaw)
|
||||||
|
rz = x * sin(yaw) + z * cos(yaw)
|
||||||
|
vs.append(G(cx + rx, cy + y, cz + rz))
|
||||||
|
fs = [(0, 1, 3, 2), (4, 6, 7, 5), (0, 4, 5, 1), (2, 3, 7, 6), (0, 2, 6, 4), (1, 5, 7, 3)]
|
||||||
|
return obj(name, vs, fs, kind)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- layout (Godot space)
|
||||||
|
W = (4.0, 0.0, 0.3) # Wurzelbund dais, near bank
|
||||||
|
GD = (3.0, 0.9, -10.6) # Glutpakt dais, far bank
|
||||||
|
anchors = {}
|
||||||
|
|
||||||
|
cliff('Near_Bank', (4.4, 0.8), [6.0, 6.4, 5.6, 5.0, 4.6, 5.2, 6.2, 6.8], 0.0, -15.0, 1, 'soil')
|
||||||
|
cliff('Near_Spur', (-1.6, 5.2), [2.4, 2.8, 2.2, 2.6, 3.0, 2.4], -0.55, -15.0, 2, 'soil', 12, 40)
|
||||||
|
cliff('Far_Bank', (3.6, -12.4), [6.6, 7.2, 6.4, 5.6, 5.0, 6.0, 7.4, 7.6], 0.9, -15.0, 3, 'soil')
|
||||||
|
cliff('Far_Ridge', (-4.0, -18.0), [4.0, 5.0, 4.6, 3.8, 4.4], 2.6, -15.0, 4, 'soil', 14, 44)
|
||||||
|
for i, (x, z, top, r) in enumerate([(-16, -20, 4.5, 6), (-26, -8, 6.0, 7), (-24, -32, 9.0, 9), (8, -30, 7.0, 8),
|
||||||
|
(-38, -22, 11.0, 10), (20, -24, 5.0, 7), (22, -6, 3.5, 7), (30, -18, 8.0, 9),
|
||||||
|
(16, -44, 10.0, 12), (-6, -46, 12.0, 12)]):
|
||||||
|
cliff('Distant_%d' % i, (x, z), [r, r * 1.2, r * .9, r * 1.1, r * .8], top, -15.0, 10 + i, 'soil', 16, 64, .9, .3)
|
||||||
|
|
||||||
|
# Wurzelbund dais: limestone tiers, teal inlay, roots, tree and spring.
|
||||||
|
wx, wy, wz = W
|
||||||
|
lathe('W_Dais_Base', [(3.35, -.3), (3.35, .1), (3.15, .18), (3.15, .22)], 8, 'limestone', W, pi / 8)
|
||||||
|
lathe('W_Dais_Upper', [(2.7, .2), (2.7, .32), (2.55, .37)], 8, 'paving', W, pi / 8)
|
||||||
|
annulus('W_Inlay', W, 2.80, 2.86, .215, .232, 64, 'teal')
|
||||||
|
annulus('W_Rim', W, 2.55, 2.72, .35, .385, 8, 'limestone', pi / 8)
|
||||||
|
anchors['wurzelbund_top'] = wy + .37
|
||||||
|
tree = (wx + 4.3, 0.0, wz - 4.6)
|
||||||
|
tube('W_Tree_Trunk', [tree, (tree[0] - .2, 1.6, tree[2] + .1), (tree[0] + .15, 3.2, tree[2] - .2), (tree[0] - .1, 4.4, tree[2])],
|
||||||
|
[.62, .44, .34, .22], 'bark', 14, 24, 5, .12)
|
||||||
|
for i, (dx, dy, dz, r) in enumerate([(-1.1, 3.4, .5, .12), (1.0, 3.7, -.4, .1), (.2, 4.1, .9, .09), (-.4, 4.3, -.9, .08)]):
|
||||||
|
tube('W_Branch_%d' % i, [(tree[0], dy - .5, tree[2]), (tree[0] + dx * .6, dy + .2, tree[2] + dz * .6), (tree[0] + dx * 1.4, dy + .5, tree[2] + dz * 1.4)],
|
||||||
|
[r, r * .7, r * .35], 'bark', 8, 10, 20 + i, .1)
|
||||||
|
for i in range(14):
|
||||||
|
a = i * 2.39996
|
||||||
|
r = .4 + 1.25 * ((i * .618) % 1.0)
|
||||||
|
dx, dz = cos(a) * r, sin(a) * r
|
||||||
|
dy = 4.3 + .9 * ((i * .37) % 1.0) - r * .25
|
||||||
|
s = .95 - .25 * ((i * .53) % 1.0)
|
||||||
|
kind = ('leaf_dark', 'leaf_mid', 'leaf_light')[i % 3]
|
||||||
|
blob('W_Canopy_%d' % i, (tree[0] + dx, dy, tree[2] + dz), (s, s * .7, s), kind, 30 + i, 3, .30)
|
||||||
|
for i, (a0, a1, length) in enumerate([(.2, .9, 3.2), (1.5, 2.2, 2.6), (-1.0, -.4, 2.8), (2.6, 3.2, 2.2)]):
|
||||||
|
pts = []
|
||||||
|
for s in range(5):
|
||||||
|
t = s / 4
|
||||||
|
a = a0 + (a1 - a0) * t
|
||||||
|
r = .5 + length * t
|
||||||
|
pts.append((tree[0] + cos(a + pi) * r, .28 - .38 * t + .12 * sin(t * pi), tree[2] + sin(a + pi) * r * .8))
|
||||||
|
tube('W_Root_%d' % i, pts, [.26, .18, .12, .07, .03], 'bark', 9, 20, 40 + i, .18)
|
||||||
|
for i, (dx, dz, s) in enumerate([(3.6, 1.2, .55), (-3.3, 1.6, .45), (2.9, -2.6, .6), (-3.8, -1.2, .4), (1.2, 3.4, .35), (4.6, -.8, .5)]):
|
||||||
|
# Low shrubs on the bank, clear of the dais so the figures keep their silhouettes.
|
||||||
|
blob('W_Shrub_%d' % i, (wx + dx, 0.0, wz + dz), (s, s * .55, s), ('leaf_mid', 'leaf_dark')[i % 2], 50 + i, 2, .32, -.1)
|
||||||
|
spring = (wx - 2.25, 0.0, wz - 1.6)
|
||||||
|
lathe('W_Spring_Rim', [(.95, -.05), (.95, .24), (.82, .28), (.72, .18), (.72, .1)], 16, 'limestone', spring, 0, True)
|
||||||
|
lathe('W_Spring_Water', [(.73, .1), (.73, .19)], 24, 'spring', spring)
|
||||||
|
for i, (dx, dz, h) in enumerate([(-3.6, -.6, 1.9), (-2.9, -3.1, 1.4), (3.7, 1.6, 1.2)]):
|
||||||
|
lathe('W_Menhir_%d' % i, [(.36, 0), (.40, h * .4), (.30, h * .85), (.12, h)], 6, 'limestone', (wx + dx, 0, wz + dz), i * .7)
|
||||||
|
annulus('W_Menhir_Band_%d' % i, (wx + dx, 0, wz + dz), .33, .37, h * .55, h * .58, 6, 'teal', i * .7)
|
||||||
|
|
||||||
|
# Glutpakt dais: basalt hexagon, ember fissures, brass bands, obelisks and braziers.
|
||||||
|
gx, gy, gz = GD
|
||||||
|
lathe('G_Dais_Base', [(3.4, -.3), (3.4, .12), (3.2, .2), (3.2, .24)], 6, 'basalt', GD, pi / 6)
|
||||||
|
lathe('G_Dais_Upper', [(2.75, .22), (2.75, .34), (2.6, .38)], 6, 'basalt', GD, pi / 6)
|
||||||
|
annulus('G_Inlay', GD, 2.84, 2.92, .235, .252, 6, 'brass', pi / 6)
|
||||||
|
annulus('G_Rim', GD, 2.62, 2.78, .37, .40, 6, 'brass', pi / 6)
|
||||||
|
lathe('G_Dais_Inner', [(1.55, .36), (1.55, .41), (1.45, .43)], 6, 'basalt', GD, 0)
|
||||||
|
anchors['glutpakt_top'] = gy + .38
|
||||||
|
for i, (a, length) in enumerate([(.3, 1.9), (1.4, 1.5), (2.5, 2.0), (3.6, 1.6), (4.6, 1.8), (5.6, 1.4)]):
|
||||||
|
# Fissures run from the rim towards the core and stop short of the figures' feet.
|
||||||
|
x0, z0 = gx + cos(a) * 2.55, gz + sin(a) * 2.55
|
||||||
|
x1, z1 = gx + cos(a + .12) * (2.55 - length * .55), gz + sin(a + .12) * (2.55 - length * .55)
|
||||||
|
slab('G_Fissure_%d' % i, ((x0 + x1) / 2, gy + .383, (z0 + z1) / 2), (math.hypot(x1 - x0, z1 - z0), .012, .05),
|
||||||
|
-math.atan2(z1 - z0, x1 - x0), 'ember')
|
||||||
|
for s in (-1, 1):
|
||||||
|
ob = (gx + s * 2.9, gy, gz - 1.7)
|
||||||
|
lathe('G_Obelisk_%d' % s, [(.52, 0), (.46, .3), (.40, 2.6), (.30, 3.1), (.02, 3.55)], 4, 'basalt', ob, pi / 4)
|
||||||
|
for h in (.9, 2.1):
|
||||||
|
lathe('G_Obelisk_Band_%d_%d' % (s, int(h * 10)), [(.45 - h * .02, h), (.45 - h * .02, h + .12)], 4, 'brass', ob, pi / 4)
|
||||||
|
for face in range(4):
|
||||||
|
a = face * pi / 2
|
||||||
|
# One narrow glowing seam per obelisk face.
|
||||||
|
slab('G_Obelisk_Seam_%d_%d' % (s, face), (ob[0] + cos(a) * .305, ob[1] + 1.75, ob[2] + sin(a) * .305), (.022, 1.05, .02), -a + pi / 2, 'ember')
|
||||||
|
br = (gx + s * 2.35, gy + .38, gz + 1.55)
|
||||||
|
lathe('G_Brazier_Foot_%d' % s, [(.24, 0), (.24, .06), (.08, .12), (.07, .46), (.12, .5)], 6, 'basalt', br, 0)
|
||||||
|
lathe('G_Brazier_%d' % s, [(.12, .48), (.30, .56), (.34, .66), (.30, .68), (.2, .62)], 6, 'brass', br, 0)
|
||||||
|
for c in range(5):
|
||||||
|
a = c * 1.3
|
||||||
|
blob('G_Coal_%d_%d' % (s, c), (br[0] + cos(a) * .13, br[1] + .66, br[2] + sin(a) * .13), (.09, .06, .09), 'ember', 90 + c + s * 7, 1, .3, None, False)
|
||||||
|
anchors['brazier_%d' % s] = [br[0], br[1] + .9, br[2]]
|
||||||
|
for i, (dx, dz, s) in enumerate([(-3.5, .6, .8), (3.6, .2, .9), (-1.8, 3.2, .6), (2.4, -3.3, 1.0), (-3.0, -2.9, .7)]):
|
||||||
|
blob('G_Basalt_Rock_%d' % i, (gx + dx, gy, gz + dz), (s, s * .8, s * .9), 'basalt', 70 + i, 2, .35, -.2, False)
|
||||||
|
|
||||||
|
# Foreground framing rocks and scree at the near edge.
|
||||||
|
for i, (x, y, z, s) in enumerate([(-1.2, -.6, 6.6, 1.4), (.6, -.2, 5.2, .8), (8.6, -.3, 4.4, 1.1), (-2.6, -1.0, 4.0, 1.0)]):
|
||||||
|
blob('Fore_Rock_%d' % i, (x, y, z), (s * 1.2, s, s), 'cliff', 80 + i, 2, .38, -.3, False)
|
||||||
|
|
||||||
|
bpy.ops.object.select_all(action='DESELECT')
|
||||||
|
for ob in parts:
|
||||||
|
ob.select_set(True)
|
||||||
|
bpy.context.view_layer.objects.active = parts[0]
|
||||||
|
bpy.ops.object.join()
|
||||||
|
stage = bpy.context.object
|
||||||
|
stage.name = 'MenuDiorama'
|
||||||
|
bm = bmesh.new()
|
||||||
|
bm.from_mesh(stage.data)
|
||||||
|
bmesh.ops.triangulate(bm, faces=list(bm.faces))
|
||||||
|
bm.to_mesh(stage.data)
|
||||||
|
bm.free()
|
||||||
|
bpy.ops.wm.save_as_mainfile(filepath=str(SRC / 'menu-stage.blend'))
|
||||||
|
bpy.ops.export_scene.gltf(filepath=str(OUT / 'menu-stage.glb'), export_format='GLB', use_selection=True,
|
||||||
|
export_materials='EXPORT', export_yup=True, export_animations=False)
|
||||||
|
raw = (OUT / 'menu-stage.glb').read_bytes()
|
||||||
|
doc = json.loads(raw[20:20 + struct.unpack_from('<I', raw, 12)[0]])
|
||||||
|
triangles = sum(doc['accessors'][q['indices']]['count'] // 3 for m in doc['meshes'] for q in m['primitives'])
|
||||||
|
manifest = {
|
||||||
|
'resource_path': 'res://assets/environment/menu-stage.glb', 'source': 'assets-source/phase20/menu-stage.blend',
|
||||||
|
'triangles': triangles, 'sha256': hashlib.sha256(raw).hexdigest(), 'materials': [m['name'] for m in doc['materials']],
|
||||||
|
'daises': {
|
||||||
|
'wurzelbund': {'center': [W[0], anchors['wurzelbund_top'], W[2]], 'core': [W[0], anchors['wurzelbund_top'], W[2] - 1.85],
|
||||||
|
'spring': [spring[0], .3, spring[2]]},
|
||||||
|
'glutpakt': {'center': [GD[0], anchors['glutpakt_top'], GD[2]], 'core': [GD[0], anchors['glutpakt_top'], GD[2] - 1.85],
|
||||||
|
'braziers': [anchors['brazier_-1'], anchors['brazier_1']]},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
(P / 'assets/menu_stage_manifest.json').write_text(json.dumps(manifest, indent=2) + '\n')
|
||||||
|
print('MENU_STAGE_BUILD', triangles, 'triangles', len(doc['materials']), 'materials', flush=True)
|
||||||
BIN
assets-source/phase20/menu-stage.blend
Normal file
879
assets-source/phase3/all-prompts.json
Normal file
|
|
@ -0,0 +1,879 @@
|
||||||
|
{
|
||||||
|
"phase": 3,
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"id": "g01",
|
||||||
|
"display_name": "Kohlenbeißer",
|
||||||
|
"faction": "glutpakt",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"description_de": "Kompakter Basaltkörper mit glühendem Maul, kräftigen Klauen und Messingakzenten.",
|
||||||
|
"prompt": "Use case: stylized-concept. Create ONE standalone square transparent-background game unit icon for an original low-poly fantasy tower-defense autobattler called Saumwacht. Asset ID g01, Kohlenbeißer. Subject: a squat living coal creature, charcoal-black angular boulder body, broad glowing ember mouth, two powerful short clawed arms and sturdy small legs. Simple memorable triangular silhouette, no humanoid human face. Faction Glutpakt palette exactly basalt #292B35, ember orange #EF6B3B, restrained brass #D9A441 and ash #D8CEC0. Style: flat illustrated strategy-game UI icon, crisp confident edges, 3 to 4 broad matte color values per material, subtle graphic shading, NO photorealistic or glossy rendered look. Entire single character centered, 3/4 front view facing slightly right, slight overhead angle, soft neutral key light upper left. Art occupies about 78% of canvas, broad transparent safety margin around all sides, no elements clipped. Readable at 48px. True RGBA transparent background; no checkerboard baked into image, no colored rectangle, no ground, no shadow on background, no badge, no frame, no text, no lettering, no watermark. Intended final icon 256x256 pixels, square master if higher resolution is required. Save useful image output; one character only.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a095b1-2da4-7bd1-9759-74ea0f1615bf/exec-38fac772-ec12-4d2b-8925-e70601932eaa.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/glutpakt/unit_g01_kohlenbeisser_icon.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "g02",
|
||||||
|
"display_name": "Schlackenklaue",
|
||||||
|
"faction": "glutpakt",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"description_de": "Schwer gepanzerter Kohlenkörper mit breiten Basaltschultern, glühendem Maul und großen messingbesetzten Klauen.",
|
||||||
|
"prompt": "Use case: stylized-concept. ONE standalone square unit icon for the original strategy game Saumwacht. Whole single creature centered with all limbs visible, about 76% canvas coverage and transparent margins. Three-quarter front view facing slightly right, slight overhead angle, soft neutral key light upper left. Faceted illustrated game UI art with crisp edges, broad matte planes, restrained graphic shading, very clear silhouette readable at 48px. True RGBA transparent background, no checkerboard, no ground, no shadow on background, no border, no badge, no text, no watermark. Glutpakt palette basalt #292B35, ember #EF6B3B, brass #D9A441, ash #D8CEC0. Large simple shapes, no tiny surface texture, no glossy photorealism. Intended icon 256px with a high-resolution square master. Subject: Upgraded coal-claw melee creature: heavy angular basalt shoulder plates, two oversized curved brass-tipped claws, broad ember-filled jagged mouth, glowing cracks, compact powerful legs. Visibly a heavier evolved coal creature; not a human.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a095b1-2da4-7bd1-9759-74ea0f1615bf/exec-c9760a9f-f0e9-4f5d-8884-e6488c8ef2cc.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/glutpakt/unit_g02_schlackenklaue_icon.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "g03",
|
||||||
|
"display_name": "Schlackenwacht",
|
||||||
|
"faction": "glutpakt",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"description_de": "Gedrungener Basaltwächter mit breiter Schildfront, schmaler Ofenglut und kurzen, kräftigen Armen.",
|
||||||
|
"prompt": "Use case: stylized-concept. ONE standalone square unit icon for the original strategy game Saumwacht. Whole single creature centered with all limbs visible, about 76% canvas coverage and transparent margins. Three-quarter front view facing slightly right, slight overhead angle, soft neutral key light upper left. Faceted illustrated game UI art with crisp edges, broad matte planes, restrained graphic shading, very clear silhouette readable at 48px. True RGBA transparent background, no checkerboard, no ground, no shadow on background, no border, no badge, no text, no watermark. Glutpakt palette basalt #292B35, ember #EF6B3B, brass #D9A441, ash #D8CEC0. Large simple shapes, no tiny surface texture, no glossy photorealism. Intended icon 256px with a high-resolution square master. Subject: A defensive living basalt sentinel. Broad squat rectangular slab-shaped body forming a huge shield front, small recessed orange furnace slit, two short armored arms with blocklike fists, sturdy feet. Strong nearly square silhouette, restrained brass edge accents; clearly a tank rather than a clawed monster.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a095b1-2da4-7bd1-9759-74ea0f1615bf/exec-72c5994e-0c7a-4143-8818-51d35141938c.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/glutpakt/unit_g03_schlackenwacht_icon.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "g04",
|
||||||
|
"display_name": "Basaltbastion",
|
||||||
|
"faction": "glutpakt",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"description_de": "Lebende Basaltfestung mit gestuften Schultern, gepanzerter Schildfront und glühendem Ofenkern.",
|
||||||
|
"prompt": "Use case: stylized-concept. ONE standalone square unit icon for the original strategy game Saumwacht. Whole single creature centered with all limbs visible, about 76% canvas coverage and transparent margins. Three-quarter front view facing slightly right, slight overhead angle, soft neutral key light upper left. Faceted illustrated game UI art with crisp edges, broad matte planes, restrained graphic shading, very clear silhouette readable at 48px. True RGBA transparent background, no checkerboard, no ground, no shadow on background, no border, no badge, no text, no watermark. Glutpakt palette basalt #292B35, ember #EF6B3B, brass #D9A441, ash #D8CEC0. Large simple shapes, no tiny surface texture, no glossy photorealism. Intended icon 256px with a high-resolution square master. Subject: Upgraded living fortress tank, massive broad stepped basalt slabs forming a castle-like shoulder silhouette, deep orange furnace slit, a thick angular tower-shield chest and two pillar-like arms, robust feet, controlled brass buttresses. Distinct from a plain block sentinel: layered fortress tiers, wider shoulders, taller crown of stone, still one compact creature.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a095b1-2da4-7bd1-9759-74ea0f1615bf/exec-b8f50103-9bf6-4218-8525-aead4d3e51cb.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/glutpakt/unit_g04_basaltbastion_icon.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "g05",
|
||||||
|
"display_name": "Funkenlanze",
|
||||||
|
"faction": "glutpakt",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"description_de": "Schlanker Glutkonstrukt-Schütze mit langer Messinglanze und orange leuchtender Glasspitze.",
|
||||||
|
"prompt": "Use case: stylized-concept. ONE standalone square unit icon for the original strategy game Saumwacht. Whole single creature centered with all limbs visible, about 76% canvas coverage and transparent margins. Three-quarter front view facing slightly right, slight overhead angle, soft neutral key light upper left. Faceted illustrated game UI art with crisp edges, broad matte planes, restrained graphic shading, very clear silhouette readable at 48px. True RGBA transparent background, no checkerboard, no ground, no shadow on background, no border, no badge, no text, no watermark. Glutpakt palette basalt #292B35, ember #EF6B3B, brass #D9A441, ash #D8CEC0. Large simple shapes, no tiny surface texture, no glossy photorealism. Intended icon 256px with a high-resolution square master. Subject: A slender mobile ember construct with narrow charcoal stone torso, two sturdy legs, a single long brass-and-obsidian lance launcher held diagonally forward, a pointed glowing orange glass spearhead, small angular head without a human face. Clearly a light ranged marksman with an elongated weapon silhouette, not a knight with a shield.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a095b1-2da4-7bd1-9759-74ea0f1615bf/exec-060575d4-bb01-4f8b-a30e-563a72cc22b2.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/glutpakt/unit_g05_funkenlanze_icon.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "g06",
|
||||||
|
"display_name": "Glasballiste",
|
||||||
|
"faction": "glutpakt",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"description_de": "Bewegliche Glasballiste mit großen orangefarbenen Kristallarmen, langem Bolzen und stabilem Basaltunterbau.",
|
||||||
|
"prompt": "Use case: stylized-concept. ONE standalone square unit icon for the original strategy game Saumwacht. Whole single creature centered with all limbs visible, about 76% canvas coverage and transparent margins. Three-quarter front view facing slightly right, slight overhead angle, soft neutral key light upper left. Faceted illustrated game UI art with crisp edges, broad matte planes, restrained graphic shading, very clear silhouette readable at 48px. True RGBA transparent background, no checkerboard, no ground, no shadow on background, no border, no badge, no text, no watermark. Glutpakt palette basalt #292B35, ember #EF6B3B, brass #D9A441, ash #D8CEC0. Large simple shapes, no tiny surface texture, no glossy photorealism. Intended icon 256px with a high-resolution square master. Subject: An upgraded mobile glass ballista creature, a compact charcoal basalt construct on two sturdy articulated legs supporting a very large crossbow with two swept crystalline orange limbs, long glowing glass bolt, brass mechanical bands. Three-quarter angle, bow fits entirely inside square; clearly a heavy ranged siege unit and evolved spear launcher, no shield.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a095b1-2da4-7bd1-9759-74ea0f1615bf/exec-3b6b580d-dda5-4aba-92eb-5c33ef578b57.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/glutpakt/unit_g06_glasballiste_icon.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "g07",
|
||||||
|
"display_name": "Aschenwerfer",
|
||||||
|
"faction": "glutpakt",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"description_de": "Gedrungener Aschenwerfer mit breitem Mörserkelch, glühender Kohle und kurzen Basaltbeinen.",
|
||||||
|
"prompt": "Use case: stylized-concept. ONE standalone square unit icon for the original strategy game Saumwacht. Whole single creature centered with all limbs visible, about 76% canvas coverage and transparent margins. Three-quarter front view facing slightly right, slight overhead angle, soft neutral key light upper left. Faceted illustrated game UI art with crisp edges, broad matte planes, restrained graphic shading, very clear silhouette readable at 48px. True RGBA transparent background, no checkerboard, no ground, no shadow on background, no border, no badge, no text, no watermark. Glutpakt palette basalt #292B35, ember #EF6B3B, brass #D9A441, ash #D8CEC0. Large simple shapes, no tiny surface texture, no glossy photorealism. Intended icon 256px with a high-resolution square master. Subject: A squat mobile ash mortar creature, charcoal faceted stone body and two short arms cradling a wide upward-tilted round basalt mortar bowl. One contained orange ember glowing inside the bowl, tiny restrained ash puff attached to opening. Stubby legs, brass rim, broad cup silhouette; original artillery creature rather than a human soldier.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a095b1-2da4-7bd1-9759-74ea0f1615bf/exec-fe9d03d4-bdc3-438b-9083-c15f881de061.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/glutpakt/unit_g07_aschenwerfer_icon.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "g08",
|
||||||
|
"display_name": "Kraterrufer",
|
||||||
|
"faction": "glutpakt",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"description_de": "Schwerer Kraterkörper mit großem Vulkankelch, glühendem Inneren und gestuften Basaltplatten.",
|
||||||
|
"prompt": "Use case: stylized-concept. ONE standalone square unit icon for the original strategy game Saumwacht. Whole single creature centered with all limbs visible, about 76% canvas coverage and transparent margins. Three-quarter front view facing slightly right, slight overhead angle, soft neutral key light upper left. Faceted illustrated game UI art with crisp edges, broad matte planes, restrained graphic shading, very clear silhouette readable at 48px. True RGBA transparent background, no checkerboard, no ground, no shadow on background, no border, no badge, no text, no watermark. Glutpakt palette basalt #292B35, ember #EF6B3B, brass #D9A441, ash #D8CEC0. Large simple shapes, no tiny surface texture, no glossy photorealism. Intended icon 256px with a high-resolution square master. Subject: Heavy evolved ash-mortar creature: broad armored basalt body with a very large crater-shaped mortar bowl in its shoulders, layered brass-rimmed volcanic plates, contained orange molten core, one small angular ash curl, two massive arms and stout legs. Strong wide circular crater silhouette, clearly the upgraded artillery of a smaller bowl creature, no scenery.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a095b1-2da4-7bd1-9759-74ea0f1615bf/exec-ca06a766-1878-4c6d-aa15-1c8d21280ec2.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/glutpakt/unit_g08_kraterrufer_icon.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "g09",
|
||||||
|
"display_name": "Herdhüter",
|
||||||
|
"faction": "glutpakt",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"description_de": "Kleiner Herdwächter mit offenem Glutofen und einer schützenden Messingschale vor dem Körper.",
|
||||||
|
"prompt": "Use case: stylized-concept. ONE standalone square unit icon for the original strategy game Saumwacht. Whole single creature centered with all limbs visible, about 76% canvas coverage and transparent margins. Three-quarter front view facing slightly right, slight overhead angle, soft neutral key light upper left. Faceted illustrated game UI art with crisp edges, broad matte planes, restrained graphic shading, very clear silhouette readable at 48px. True RGBA transparent background, no checkerboard, no ground, no shadow on background, no border, no badge, no text, no watermark. Glutpakt palette basalt #292B35, ember #EF6B3B, brass #D9A441, ash #D8CEC0. Large simple shapes, no tiny surface texture, no glossy photorealism. Intended icon 256px with a high-resolution square master. Subject: Small gentle living hearth guardian, upright compact basalt furnace body on short legs, open orange hearth at chest, two small arms holding a modest warm brass brazier before the body. Rounded arch opening, ash-colored upper stone hood, a few restrained ember shapes. Support unit with protective welcoming silhouette, no human face, no weapons.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a095b1-2da4-7bd1-9759-74ea0f1615bf/exec-9dc97472-ad0e-41ae-9e22-dd2ad50d284a.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/glutpakt/unit_g09_herdhueter_icon.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "g10",
|
||||||
|
"display_name": "Sonnenesse",
|
||||||
|
"faction": "glutpakt",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"description_de": "Große Sonnenesse mit strahlender Ofenöffnung, Messingkrone und breiter wärmender Glutschale.",
|
||||||
|
"prompt": "Use case: stylized-concept. ONE standalone square unit icon for the original strategy game Saumwacht. Whole single creature centered with all limbs visible, about 76% canvas coverage and transparent margins. Three-quarter front view facing slightly right, slight overhead angle, soft neutral key light upper left. Faceted illustrated game UI art with crisp edges, broad matte planes, restrained graphic shading, very clear silhouette readable at 48px. True RGBA transparent background, no checkerboard, no ground, no shadow on background, no border, no badge, no text, no watermark. Glutpakt palette basalt #292B35, ember #EF6B3B, brass #D9A441, ash #D8CEC0. Large simple shapes, no tiny surface texture, no glossy photorealism. Subject: Grand evolved living solar forge support creature: broad upright basalt furnace body, large radiant orange sunlike forge opening framed by a simple brass sun crown, ash stone mantle, sturdy feet, arms holding a wide healing ember basin. Warm controlled gold-orange illumination, majestic open silhouette; clearly stronger than small hearth guardian, still one creature without scenic halo background.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a095b1-2da4-7bd1-9759-74ea0f1615bf/exec-78c6d895-0631-491f-956a-28a74e7e9049.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/glutpakt/unit_g10_sonnenesse_icon.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "m01",
|
||||||
|
"display_name": "Kesselwicht",
|
||||||
|
"description_de": "Gedrungener Kesseltank mit rundem Petrolkessel, Keramikgesicht und kurzen Stempelbeinen.",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: original strategy-game UNIT ICON for Saumwacht, a single individual PNG cutout. Create a brand-new image. Style: crisp faceted illustrated low-poly fantasy machine, strong chunky planar shapes and simple matte shading, premium coherent game icon, not photorealistic. Composition: square 1:1 canvas, whole single subject centered, all parts visible with comfortable transparent margin, occupy about 75–80 percent of the canvas. Three-quarter front view looking toward viewer's right. Lighting: soft key light from upper left, restrained ambient fill. Palette: petrol #247A80 primary, ocher #C79A48 accents, steel #657481, cream ceramic #E6DCCA; dark slate shadows. Keep broad masses and silhouette readable at 48px. Scene: genuinely transparent background with preserved alpha, no checkerboard pattern drawn into image. No ground, no shadow plane, no frame, no text, no lettering, no logo, no border, no watermark, no unrelated props, no other characters, no realistic texture or excessive microdetail. Subject: a squat friendly formidable boiler tank automaton, round low petrol boiler body, small cream ceramic face inset into the upper front, ocher pressure gauge, short thick piston legs, two low blocky steel fists, one short chimney; simple round compact silhouette.",
|
||||||
|
"faction": "nietenkonvent",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-2a90-7821-8332-7507717ed80a/exec-c138bf79-c48f-4dc6-aff3-0968e159757a.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/nietenkonvent/unit_m01_kesselwicht_icon.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "m02",
|
||||||
|
"display_name": "Druckhüter",
|
||||||
|
"description_de": "Breiter gepanzerter Drucktank mit Schulterkesseln und massivem Schildarm.",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: original strategy-game UNIT ICON for Saumwacht, a single individual PNG cutout. Create a brand-new image. Style: crisp faceted illustrated low-poly fantasy machine, strong chunky planar shapes and simple matte shading, premium coherent game icon, not photorealistic. Composition: square 1:1 canvas, whole single subject centered, all parts visible with comfortable transparent margin, occupy about 75–80 percent of the canvas. Three-quarter front view looking toward viewer's right. Lighting: soft key light from upper left, restrained ambient fill. Palette: petrol #247A80 primary, ocher #C79A48 accents, steel #657481, cream ceramic #E6DCCA; dark slate shadows. Keep broad masses and silhouette readable at 48px. Scene: genuinely transparent background with preserved alpha, no checkerboard pattern drawn into image. No ground, no shadow plane, no frame, no text, no lettering, no logo, no border, no watermark, no unrelated props, no other characters, no realistic texture or excessive microdetail. Subject: an upgraded massive armored pressure tank automaton, very broad squared shoulders supporting two tall ocher-capped pressure cylinders, cream ceramic brow above steel face, thick petrol chest armor, massive block shield forearm and heavy piston fist, sturdy short legs; trapezoid broad powerful silhouette distinct from the small round boiler base unit.",
|
||||||
|
"faction": "nietenkonvent",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-2a90-7821-8332-7507717ed80a/exec-5b0544b5-1d30-495c-b66a-2945478ecde8.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/nietenkonvent/unit_m02_druckhueter_icon.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "m03",
|
||||||
|
"display_name": "Scherschreiter",
|
||||||
|
"description_de": "Schmaler Schreitautomat mit zwei großen Scherenarmen.",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: original strategy-game UNIT ICON for Saumwacht, a single individual PNG cutout. Create a brand-new image. Style: crisp faceted illustrated low-poly fantasy machine, strong chunky planar shapes and simple matte shading, premium coherent game icon, not photorealistic. Composition: square 1:1 canvas, whole single subject centered, all parts visible with comfortable transparent margin, occupy about 75–80 percent of the canvas. Three-quarter front view looking toward viewer's right. Lighting: soft key light from upper left, restrained ambient fill. Palette: petrol #247A80 primary, ocher #C79A48 accents, steel #657481, cream ceramic #E6DCCA; dark slate shadows. Keep broad masses and silhouette readable at 48px. Scene: genuinely transparent background with preserved alpha, no checkerboard pattern drawn into image. No ground, no shadow plane, no frame, no text, no lettering, no logo, no border, no watermark, no unrelated props, no other characters, no realistic texture or excessive microdetail. Subject: a nimble walking twin-shear automaton with a narrow upright petrol spindle body, two very large open steel scissors replacing its arms, each with two clearly defined long blade halves, jointed ocher knees and long light legs, a small pointed cream ceramic head; light angular insect-like silhouette with exactly two shear arms.",
|
||||||
|
"faction": "nietenkonvent",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-2a90-7821-8332-7507717ed80a/exec-2ce66046-595f-4163-8996-ff00d8aa83d1.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/nietenkonvent/unit_m03_scherschreiter_icon.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "m04",
|
||||||
|
"display_name": "Sechsklingler",
|
||||||
|
"description_de": "Breiter Schreitautomat mit sechs deutlich getrennten Klingen.",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: original strategy-game UNIT ICON for Saumwacht, a single individual PNG cutout. Create a brand-new image. Style: crisp faceted illustrated low-poly fantasy machine, strong chunky planar shapes and simple matte shading, premium coherent game icon, not photorealistic. Composition: square 1:1 canvas, whole single subject centered, all parts visible with comfortable transparent margin, occupy about 75–80 percent of the canvas. Three-quarter front view looking toward viewer's right. Lighting: soft key light from upper left, restrained ambient fill. Palette: petrol #247A80 primary, ocher #C79A48 accents, steel #657481, cream ceramic #E6DCCA; dark slate shadows. Keep broad masses and silhouette readable at 48px. Scene: genuinely transparent background with preserved alpha, no checkerboard pattern drawn into image. No ground, no shadow plane, no frame, no text, no lettering, no logo, no border, no watermark, no unrelated props, no other characters, no realistic texture or excessive microdetail. Subject: an upgraded six-bladed walking automaton, broad petrol hub torso with a cream ceramic mask, exactly six large steel swordlike blades on six distinct jointed arms fanning out three per side, ocher hubs, tall strong jointed legs; dramatic radial blade silhouette distinctly larger and more elaborate than a two-shear machine.",
|
||||||
|
"faction": "nietenkonvent",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-2a90-7821-8332-7507717ed80a/exec-d02ba6ad-4b15-4157-a093-c6ac7593c037.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/nietenkonvent/unit_m04_sechsklingler_icon.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "m05",
|
||||||
|
"display_name": "Bolzenknecht",
|
||||||
|
"description_de": "Schlanker Bolzenautomat mit einem deutlich lesbaren Armwerfer.",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: original strategy-game UNIT ICON for Saumwacht, a single individual PNG cutout. Create a brand-new image. Style: crisp faceted illustrated low-poly fantasy machine, strong chunky planar shapes and simple matte shading, premium coherent game icon, not photorealistic. Composition: square 1:1 canvas, whole single subject centered, all parts visible with comfortable transparent margin, occupy about 75–80 percent of the canvas. Three-quarter front view looking toward viewer's right. Lighting: soft key light from upper left, restrained ambient fill. Palette: petrol #247A80 primary, ocher #C79A48 accents, steel #657481, cream ceramic #E6DCCA; dark slate shadows. Keep broad masses and silhouette readable at 48px. Scene: genuinely transparent background with preserved alpha, no checkerboard pattern drawn into image. No ground, no shadow plane, no frame, no text, no lettering, no logo, no border, no watermark, no unrelated props, no other characters, no realistic texture or excessive microdetail. Subject: a slim bolt-launcher automaton, narrow petrol body on two slender steel legs, small cream ceramic face and ocher monocular sight, one large compact crossbow bolter replacing the right forearm and a small bolt magazine on its back, simple upright posture; one strongly readable crossbow weapon and restrained silhouette.",
|
||||||
|
"faction": "nietenkonvent",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-2a90-7821-8332-7507717ed80a/exec-f587da38-e177-4167-8f3b-d2848ee3a991.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/nietenkonvent/unit_m05_bolzenknecht_icon.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "m06",
|
||||||
|
"display_name": "Linienmeister",
|
||||||
|
"description_de": "Hoher Präzisionsautomat mit einer langen integrierten Balliste.",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: original strategy-game UNIT ICON for Saumwacht, a single individual PNG cutout. Create a brand-new image. Style: crisp faceted illustrated low-poly fantasy machine, strong chunky planar shapes and simple matte shading, premium coherent game icon, not photorealistic. Composition: square 1:1 canvas, whole single subject centered, all parts visible with comfortable transparent margin, occupy about 75–80 percent of the canvas. Three-quarter front view looking toward viewer's right. Lighting: soft key light from upper left, restrained ambient fill. Palette: petrol #247A80 primary, ocher #C79A48 accents, steel #657481, cream ceramic #E6DCCA; dark slate shadows. Keep broad masses and silhouette readable at 48px. Scene: genuinely transparent background with preserved alpha, no checkerboard pattern drawn into image. No ground, no shadow plane, no frame, no text, no lettering, no logo, no border, no watermark, no unrelated props, no other characters, no realistic texture or excessive microdetail. Subject: an upgraded tall precision ballista automaton, long petrol armored torso, cream ceramic pointed visor with ocher targeting lens, a long oversize precision ballista integrated across its upper torso with broad steel bow limbs and a long forward rail, stabilizer heel struts and a thin vertical targeting mast; tall triangular precision silhouette distinctly larger than a slim bolt-launcher.",
|
||||||
|
"faction": "nietenkonvent",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-2a90-7821-8332-7507717ed80a/exec-f5bde69e-3ed6-4c2a-a716-9ccade0d64ca.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/nietenkonvent/unit_m06_linienmeister_icon.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "m07",
|
||||||
|
"display_name": "Klangkolben",
|
||||||
|
"description_de": "Kompakter Artillerieautomat mit einem großen Glockenrohr und Kolben.",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: original strategy-game UNIT ICON for Saumwacht, a single individual PNG cutout. Create a brand-new image. Style: crisp faceted illustrated low-poly fantasy machine, strong chunky planar shapes and simple matte shading, premium coherent game icon, not photorealistic. Composition: square 1:1 canvas, whole single subject centered, all parts visible with comfortable transparent margin, occupy about 75–80 percent of the canvas. Three-quarter front view looking toward viewer's right. Lighting: soft key light from upper left, restrained ambient fill. Palette: petrol #247A80 primary, ocher #C79A48 accents, steel #657481, cream ceramic #E6DCCA; dark slate shadows. Keep broad masses and silhouette readable at 48px. Scene: genuinely transparent background with preserved alpha, no checkerboard pattern drawn into image. No ground, no shadow plane, no frame, no text, no lettering, no logo, no border, no watermark, no unrelated props, no other characters, no realistic texture or excessive microdetail. Subject: a compact piston-bell artillery automaton on two stout mechanical feet, one large ocher bell-shaped acoustic cannon angled slightly upward to the right, petrol barrel body, thick central steel striking piston visible at bell mouth, cream ceramic inset on the side, small short operator head; single horizontal bell silhouette.",
|
||||||
|
"faction": "nietenkonvent",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-2a90-7821-8332-7507717ed80a/exec-305c0b2d-78b6-44b5-9521-e2d12a900c7e.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/nietenkonvent/unit_m07_klangkolben_icon.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "m08",
|
||||||
|
"display_name": "Resonanzramme",
|
||||||
|
"description_de": "Breiter Artillerieautomat mit zwei großen Resonanzkesseln.",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: original strategy-game UNIT ICON for Saumwacht, a single individual PNG cutout. Create a brand-new image. Style: crisp faceted illustrated low-poly fantasy machine, strong chunky planar shapes and simple matte shading, premium coherent game icon, not photorealistic. Composition: square 1:1 canvas, whole single subject centered, all parts visible with comfortable transparent margin, occupy about 75–80 percent of the canvas. Three-quarter front view looking toward viewer's right. Lighting: soft key light from upper left, restrained ambient fill. Palette: petrol #247A80 primary, ocher #C79A48 accents, steel #657481, cream ceramic #E6DCCA; dark slate shadows. Keep broad masses and silhouette readable at 48px. Scene: genuinely transparent background with preserved alpha, no checkerboard pattern drawn into image. No ground, no shadow plane, no frame, no text, no lettering, no logo, no border, no watermark, no unrelated props, no other characters, no realistic texture or excessive microdetail. Subject: an upgraded broad resonance-ram artillery automaton, two enormous side-by-side forward-facing ocher resonance drums, dark steel piston centers and petrol mounting armor, small cream ceramic central command mask between the drums, four chunky steel stabilizer legs; extremely wide twin-circle silhouette distinctly different from a single small bell cannon.",
|
||||||
|
"faction": "nietenkonvent",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-2a90-7821-8332-7507717ed80a/exec-c602457d-3e07-4ab0-b5f0-20603487de64.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/nietenkonvent/unit_m08_resonanzramme_icon.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "m09",
|
||||||
|
"display_name": "Taktspindel",
|
||||||
|
"description_de": "Kleiner Taktgeber auf Spindelbeinen mit einem Dirigentenstab.",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: original strategy-game UNIT ICON for Saumwacht, a single individual PNG cutout. Create a brand-new image. Style: crisp faceted illustrated low-poly fantasy machine, strong chunky planar shapes and simple matte shading, premium coherent game icon, not photorealistic. Composition: square 1:1 canvas, whole single subject centered, all parts visible with comfortable transparent margin, occupy about 75–80 percent of the canvas. Three-quarter front view looking toward viewer's right. Lighting: soft key light from upper left, restrained ambient fill. Palette: petrol #247A80 primary, ocher #C79A48 accents, steel #657481, cream ceramic #E6DCCA; dark slate shadows. Keep broad masses and silhouette readable at 48px. Scene: genuinely transparent background with preserved alpha, no checkerboard pattern drawn into image. No ground, no shadow plane, no frame, no text, no lettering, no logo, no border, no watermark, no unrelated props, no other characters, no realistic texture or excessive microdetail. Subject: a small elegant clockwork conductor automaton, slim upright petrol spindle body widening at the middle, tiny cream ceramic face, ocher pendulum inset in the chest, two thin steel legs and narrow articulated arms, holding a single small ocher conductor baton raised diagonally; simple graceful spindle silhouette.",
|
||||||
|
"faction": "nietenkonvent",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-2a90-7821-8332-7507717ed80a/exec-7c9e483a-fd8f-4d40-af51-b18052b6ceeb.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/nietenkonvent/unit_m09_taktspindel_icon.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "m10",
|
||||||
|
"display_name": "Taktprälat",
|
||||||
|
"description_de": "Großer Uhrwerkdirigent mit kronenförmigem Metronom und breitem Mantelkörper.",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: original strategy-game UNIT ICON for Saumwacht, a single individual PNG cutout. Create a brand-new image. Style: crisp faceted illustrated low-poly fantasy machine, strong chunky planar shapes and simple matte shading, premium coherent game icon, not photorealistic. Composition: square 1:1 canvas, whole single subject centered, all parts visible with comfortable transparent margin, occupy about 75–80 percent of the canvas. Three-quarter front view looking toward viewer's right. Lighting: soft key light from upper left, restrained ambient fill. Palette: petrol #247A80 primary, ocher #C79A48 accents, steel #657481, cream ceramic #E6DCCA; dark slate shadows. Keep broad masses and silhouette readable at 48px. Scene: genuinely transparent background with preserved alpha, no checkerboard pattern drawn into image. No ground, no shadow plane, no frame, no text, no lettering, no logo, no border, no watermark, no unrelated props, no other characters, no realistic texture or excessive microdetail. Subject: an upgraded grand clockwork conductor automaton, tall imposing petrol bell-shaped ceremonial mantle made of broad mechanical armor plates, cream ceramic mask, large ocher metronome crown with three clearly readable prongs, visible ocher pendulum in its torso, two steel articulated arms conducting with a long baton, small sturdy feet; regal wide triangular silhouette distinctly different from the small thin spindle conductor.",
|
||||||
|
"faction": "nietenkonvent",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-2a90-7821-8332-7507717ed80a/exec-4af73e05-9b9a-4456-b743-cc89d9c3b138.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/nietenkonvent/unit_m10_taktpraelat_icon.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "n01",
|
||||||
|
"display_name": "Saummaske",
|
||||||
|
"faction": "nebelchor",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"description_de": "Kompakter gefalteter Nebelkörper mit heller Ovalmaske, Sichelarmen und zwei Standranken.",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: original fantasy strategy game unit icon, square transparent PNG. Subject: SAUMMASKE, a small defensive nonhuman mist spirit made of folded indigo and violet mist planes, standing on two short folded tendril feet. One large free-standing smooth oval pale mist mask with two narrow mint eye slits, a compact oval shield-like front body and small crescent arms. Its silhouette is compact, simple and beginner-tier. No human face, no human wizard, no hood. Style: polished faceted illustrated strategy-game icon, broad matte low-poly-like faces, very few large clean shapes, readable at 48px. Composition: entire single creature centered, occupies 75-80 percent of a square canvas, ample transparent margin on all sides, three-quarter front view facing slightly right. Lighting: soft upper-left key light with restrained mint luminous accents. Palette: indigo #45456F, violet #9578CF, pale mist #C4D6E5, mint #82DBCE only. Background must be actual transparent alpha; no painted checkerboard, no scenery, no ground, no shadow plane, no text, no numbers, no border, no frame, no watermark. Keep all feet and arms inside frame.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-969b-74f3-a893-5fea0b3fac02/exec-4c52cdcd-2068-4ba0-a57e-34e3d6435011.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/nebelchor/unit_n01_saummaske_icon.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "n02",
|
||||||
|
"display_name": "Grenzmaske",
|
||||||
|
"faction": "nebelchor",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"description_de": "Breiter Nebeltank mit heller Ovalmaske und mehreren geschichteten Sichelschilden.",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: original fantasy strategy-game unit icon, square transparent PNG. Subject: GRENZMASKE, the upgraded heavy shield tank of a nonhuman mist faction. A broad folded-indigo mist body supported by two sturdy short tendril feet, a free-standing large pale oval mask with narrow mint eyes at the top center, thick layered crescent shield plates wrapping the front and shoulders into a wide defensive shell. Violet and pale mist shield layers are the main identifying shape; visibly much wider and tougher than a basic compact mask spirit. No weapon, no human face, no hood, no human wizard. Style: polished faceted illustration, broad matte low-poly-like faces, few bold clean shapes, readable at 48px. Composition: single entire unit centered in square canvas with transparent margins, figure occupies 75-80 percent, three-quarter front view facing slightly right. Lighting soft from upper left, restrained mint luminous accents. Palette only indigo #45456F, violet #9578CF, mist white-blue #C4D6E5 and mint #82DBCE. Actual transparent alpha background required; no fake checkerboard, scenery, ground, shadow plane, letters, numbers, frame, border or watermark. All body parts inside canvas.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-969b-74f3-a893-5fea0b3fac02/exec-567573da-430b-4279-b9b9-a92c9ddefe48.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/nebelchor/unit_n02_grenzmaske_icon.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "n03",
|
||||||
|
"display_name": "Fadentänzer",
|
||||||
|
"faction": "nebelchor",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"description_de": "Schlanker maskierter Nebeltänzer mit zwei versetzt geführten Fadenklingen und langen Standranken.",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: original fantasy strategy-game unit icon, square transparent PNG. Subject: FADENTÄNZER, a slim agile nonhuman mist-spirit melee dancer. A small pale free-standing oval mask with mint eyes floats above a narrow body formed of folded indigo and violet ribbon-like mist. Two graceful long crescent forearms each carry a short thin pale thread-blade, kept thick enough to read as an icon. Two long folded tendril legs in a poised dancing stance, one arm diagonally high and the other low; clear balanced silhouette, no floor. No human face, no hood, no wizard, no clothes. Style: polished faceted illustration, broad matte low-poly-like planar surfaces, few large clean shapes, simplified for readability at 48px. Entire single full-body creature centered and occupying 75-80 percent square canvas, transparent margin on all sides. Three-quarter front view facing slightly right. Soft upper-left lighting; palette indigo #45456F, violet #9578CF, pale mist #C4D6E5, mint #82DBCE. Actual transparent alpha background; no checkerboard picture, no backdrop, no scenery, no ground, no shadow plane, no text, no frame, no border, no watermark. Keep entire blades and feet in frame.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-969b-74f3-a893-5fea0b3fac02/exec-d9cf6ff2-d5c7-4a8b-8e17-e0308494baba.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/nebelchor/unit_n03_fadentaenzer_icon.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "n04",
|
||||||
|
"display_name": "Saumzertrenner",
|
||||||
|
"faction": "nebelchor",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"description_de": "Hoher Nebelklingenkrieger mit Sichelkamm und großer geöffneter Sichelschere.",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: original fantasy strategy-game unit icon, square transparent PNG. Subject: SAUMZERTRENNER, an upgraded nonhuman mist-spirit melee dancer and seam cutter. Tall strong but narrow folded-indigo and violet ribbon-mist body, two planted tendril feet, free-standing pale oval mask with mint eyes and a tall simple crescent crest. The character holds ONE spectacular oversized OPEN PAIR OF SCISSORS made of two wide sweeping crescent blades with a clearly visible round mint pivot; weapon placed diagonally across the body and extending to the side so its scissor silhouette is unmistakable. A stronger more dramatic upgrade of a slim two-blade mist dancer. Nonhuman faceted mist anatomy, no human face, no hood, no wizard, no cloth robes. Style polished faceted strategy-game illustration with broad matte low-poly-like planar surfaces, simplified large shapes readable at 48px. Entire single creature and scissors centered in square canvas with generous transparent margin, occupy 75-80 percent, three-quarter front view facing slightly right. Soft upper-left light. Palette indigo #45456F, violet #9578CF, pale mist #C4D6E5, mint #82DBCE. Actual transparent alpha background; no checkerboard image, no scenery, no ground, no shadow plane, no letters, no border, frame or watermark. All blades and feet remain in canvas.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-969b-74f3-a893-5fea0b3fac02/exec-0b40c2ae-fee5-4854-813c-e3e9dd49372a.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/nebelchor/unit_n04_saumzertrenner_icon.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "n05",
|
||||||
|
"display_name": "Fernflüsterer",
|
||||||
|
"faction": "nebelchor",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"description_de": "Schmale gefaltete Maskenfigur mit langem Flüsterhorn und mintfarbener Schallöffnung.",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: original fantasy strategy-game unit icon, square transparent PNG. Subject: FERNFLÜSTERER, a nonhuman mist-spirit sharpshooter. A thin curved indigo and violet folded-mist body on two planted tendril feet, one pale narrow oval floating mask with mint slit eyes, two crescent arms aiming ONE LONG TUBULAR WHISPER HORN forward diagonally toward the right. Horn is a simple pale straight-to-gently-curved tube with a small flared round opening and mint bore, unmistakable hollow acoustic instrument; not a bow or gun. Compact base-tier design, broad readable horn and minimal details. Nonhuman mask creature, no human face, no hood, no wizard or clothes. Style: polished faceted fantasy strategy-game illustration, broad matte low-poly-like faces, large clean shapes readable at 48px. Full body including horn centered on square canvas with transparent margins, 75-80 percent occupancy. Three-quarter front view facing slightly right. Soft upper-left key light, restrained mint glow. Palette indigo #45456F, violet #9578CF, pale mist #C4D6E5, mint #82DBCE. Actual transparent alpha background required; no checkerboard drawing, backdrop, scenery, floor, ground, shadow plane, letters, numbers, frame, border or watermark. All horn and feet inside canvas.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-969b-74f3-a893-5fea0b3fac02/exec-9eb219ea-b82f-4cb3-81af-261ecda521df.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/nebelchor/unit_n05_fernfluesterer_icon.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "n06",
|
||||||
|
"display_name": "Fernrufer",
|
||||||
|
"faction": "nebelchor",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"description_de": "Aufgewerteter Nebelrufer mit zwei weit geschwungenen Resonanzhörnern und mintfarbenen Schallöffnungen.",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: original fantasy strategy-game unit icon, square transparent PNG. Subject: FERNRUFER, an upgraded nonhuman mist-spirit ranged caller. A tall folded-indigo and violet mist body, two firmly planted tendril feet, narrow free-standing pale oval mask with mint eye slits, broad upward crescent shoulders supporting EXACTLY TWO large sweeping curved resonance horns. One horn rises on each side of the body and curves forward, both visibly hollow with large flared pale openings and mint interiors, separate clear horn silhouettes. Two crescent arms gesture forward between the horns. Stronger fuller silhouette than a thin spirit with a single whisper tube. No human face, no human clothing, no hood, no wizard, no bow or gun. Style polished faceted fantasy strategy-game illustration with broad matte low-poly-like planes, bold simple shapes, readable at 48px. Single full-body creature and both horns fully inside square canvas, centered with generous transparent margins, occupy 75-80 percent. Three-quarter front view facing slightly right. Soft light from upper left, restrained luminous mint accents. Palette indigo #45456F, violet #9578CF, pale mist #C4D6E5, mint #82DBCE. Actual transparent alpha background; no checkerboard image, scenery, ground, floor, shadow plane, text, numbers, border, frame or watermark.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-969b-74f3-a893-5fea0b3fac02/exec-44a7e03a-f575-4c8b-b57b-2bca7484addf.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/nebelchor/unit_n06_fernrufer_icon.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "n07",
|
||||||
|
"display_name": "Glockenschlepper",
|
||||||
|
"faction": "nebelchor",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"description_de": "Gedrungener Nebelträger mit großer heller Glocke und deutlich sichtbarem mintfarbenem Klöppel.",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: original fantasy strategy-game unit icon, square transparent PNG. Subject: GLOCKENSCHLEPPER, a squat nonhuman mist spirit carrying one oversized bell. Short broad folded-indigo and violet mist body, two stubby planted tendril feet, small free-standing pale oval mask with mint eye slits above the bell, two short crescent arms firmly carrying a HUGE SINGLE PALE VIOLET BELL in front of its belly. Bell has a broad flared rim and a clearly visible dark hollow mouth and mint clapper; plain broad facets without ornate engravings. Its heavy burden and squat silhouette identify a base-tier area-damage unit. No human face, no hood, no wizard, no robes. Style polished faceted fantasy strategy-game illustration, broad matte low-poly-like faces, few simple bold shapes readable at 48px. Entire one creature and bell centered on square canvas, 75-80 percent canvas with generous transparent margins, three-quarter front view facing slightly right. Soft upper-left key light, restrained mint accents. Palette indigo #45456F, violet #9578CF, pale mist #C4D6E5, mint #82DBCE. Actual transparent alpha background; no checkerboard image, scenery, ground, floor, shadow plane, text, numbers, border, frame or watermark. Keep all feet and bell in frame.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-969b-74f3-a893-5fea0b3fac02/exec-70846764-a373-4cc6-a9e8-1c4d98908508.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/nebelchor/unit_n07_glockenschlepper_icon.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "n08",
|
||||||
|
"display_name": "Hohlgeläut",
|
||||||
|
"faction": "nebelchor",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"description_de": "Hoher lebender Kathedralenglockenkörper mit drei Sichelspitzen und mintfarbenem Klöppel im offenen Brustraum.",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: original fantasy strategy-game unit icon, square transparent PNG. Subject: HOHLGELÄUT, an upgraded nonhuman mist bell spirit. One large hollow cathedral-bell BODY, tall and imposing, shaped as a broad flared violet bell with three simple upward pale crescent spires and a huge dark arched hollow opening in the front. A luminous mint clapper suspended inside the hollow chest. Small pale oval mask with mint eyes hovering at the top of the arch; heavy indigo folded mist tendril legs planted below the bell, two short crescent arms at the sides. This must read as ONE CREATURE whose body is a cathedral bell, not a building or a spirit carrying a bell; bold broad base and taller upgraded silhouette. No human anatomy, no hood, no wizard, no robes, no architectural scenery. Style polished faceted fantasy strategy-game illustration, broad matte low-poly-like planar surfaces, sparse large clean shapes readable at 48px. Entire single creature centered in square canvas, 75-80 percent occupancy with generous transparent margin, three-quarter front view facing slightly right. Soft upper-left light. Palette indigo #45456F, violet #9578CF, pale mist #C4D6E5, mint #82DBCE. Actual transparent alpha background required; no checkerboard picture, scenery, ground, floor, shadow plane, text, numbers, border, frame or watermark. All spires and feet inside canvas.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-969b-74f3-a893-5fea0b3fac02/exec-eac712a9-9b56-4963-9bca-6f9cc8a6f9b5.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/nebelchor/unit_n08_hohlgelaeut_icon.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "n09",
|
||||||
|
"display_name": "Maskenkantor",
|
||||||
|
"faction": "nebelchor",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"description_de": "Kompakter maskierter Nebelsänger mit einer frei schwebenden Begleitmaske über dem erhobenen Sichelarm.",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: original fantasy strategy-game unit icon, square transparent PNG. Subject: MASKENKANTOR, a small nonhuman mist-spirit support singer. Compact folded-indigo and violet mist body supported by two short planted tendril feet, one pale oval face-mask with narrow mint eyes and an open small singing mouth; crescent arms gently conducting. EXACTLY ONE additional smaller pale mask floats separately above the raised hand on the right with visible empty transparent separation. Two masks total, only one whole creature. A simple pale crescent collar and broad folded body imply light ceremonial armor, never human clothing or hood. The companion mask shows the support role without particle clutter. Base-tier, simple compact silhouette. No human face, no hooded wizard, no microphone, no music notes. Style: polished faceted fantasy strategy-game illustration, broad matte low-poly-like surfaces, bold simple shapes readable at 48px. Entire figure and companion mask centered in square canvas, occupy 75-80 percent with ample transparent margins. Three-quarter front view facing slightly right; soft upper-left light. Palette indigo #45456F, violet #9578CF, pale mist #C4D6E5, mint #82DBCE. Actual transparent alpha background; no checkerboard drawing, scenery, ground, floor, shadow plane, text, numbers, border, frame or watermark. Keep every part inside canvas.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-969b-74f3-a893-5fea0b3fac02/exec-43662c7b-86d0-4167-95ee-9ec39d2eb8ac.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/nebelchor/unit_n09_maskenkantor_icon.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "n10",
|
||||||
|
"display_name": "Vielstimmenkantor",
|
||||||
|
"faction": "nebelchor",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"description_de": "Gekrönter Nebelchor-Kantor mit breitem Sichelkragen und drei getrennt schwebenden Begleitmasken.",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: original fantasy strategy-game unit icon, square transparent PNG. Subject: VIELSTIMMENKANTOR, an upgraded nonhuman choir spirit. ONE central tall folded-indigo and violet mist body standing on two broad planted tendril feet, one large pale oval singing mask with mint slit eyes and open mouth, topped by a bold three-point crescent crown. Two wide crescent conducting arms. EXACTLY THREE smaller pale singing masks float around the central upper body: one at left shoulder height, one at right shoulder height, one above-right; each separated from body and other masks by transparent space, no extra bodies. Four masks total, but one single game unit. Layered pale crescent collar and full broad folds make a stronger imposing support upgrade of a small singer with one companion mask. No human face, no hood or wizard, no robes, no musical notation. Style polished faceted fantasy strategy-game illustration, broad matte low-poly-like faces, large clean shapes readable at 48px, avoid tiny decoration and particle clutter. Entire single creature and all three floating masks centered in square canvas with generous margins, occupy 75-80 percent. Three-quarter front view facing slightly right. Soft upper-left light, restrained mint glow. Palette indigo #45456F, violet #9578CF, pale mist #C4D6E5, mint #82DBCE. Actual transparent alpha background; no checkerboard drawing, scene, ground, floor, shadow plane, text, numbers, border, frame or watermark. Keep all masks and feet inside canvas.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-969b-74f3-a893-5fea0b3fac02/exec-9fa0cc3d-c29c-4daf-8d30-4ee5bbc9650c.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/nebelchor/unit_n10_vielstimmenkantor_icon.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "w01",
|
||||||
|
"display_name": "Dornhopser",
|
||||||
|
"faction": "wurzelbund",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"description_de": "Kleine hüpfende Dornkapsel auf breiten Wurzelfüßen.",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: original fantasy strategy-game unit UI icon for Saumwacht, Wurzelbund faction. Style: faceted illustrated low-poly game art, broad flat matte planes, crisp silhouette and edges, chunky simple forms, high readability at 256px, not photorealistic, not a painting. Composition: ONE whole single creature centered, square canvas, complete feet and appendages visible, creature occupies 75-80% of canvas, generous transparent margins. 3/4 front view facing slightly right. Lighting: soft upper-left light, restrained shadows within subject. Palette: bark #685441, moss #56845A, leaf #A7BA6C, sparse blossom #D996AF accents. Scene: truly transparent alpha background, no checkerboard pixels, no ground plane, no scenery, no frame, no text, no logo, no watermark, no cast shadow. Wurzelbund creatures are mobile wood, root, leaf and clay organisms, never humanoid wood elves.\nSubject: Dornhopser: a small hopping round thorn-pod creature with stout root feet, compact brown seed-shell body, moss green triangular thorn blades, a tiny slit face, small paired leaf sprouts; inexpensive melee attacker. Root feet and thorn-pod silhouette are defining. No weapons, no human proportions.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-ea61-7671-8a5d-a14abb62467d/exec-6fef6f8a-ddb9-4d61-8837-7fb775e26c14.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/wurzelbund/unit_w01_dornhopser_icon.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "w02",
|
||||||
|
"display_name": "Rankenschnapper",
|
||||||
|
"faction": "wurzelbund",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"description_de": "Breiter Rankenräuber mit großem Dornenmaul und Wurzelfüßen.",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: original fantasy strategy-game unit UI icon for Saumwacht, Wurzelbund faction. Style: faceted illustrated low-poly game art, broad flat matte planes, crisp silhouette and edges, chunky simple forms, high readability at 256px, not photorealistic, not a painting. Composition: ONE whole single creature centered, square canvas, complete feet and appendages visible, creature occupies 75-80% of canvas, generous transparent margins. 3/4 front view facing slightly right. Lighting: soft upper-left light, restrained shadows within subject. Palette: bark #685441, moss #56845A, leaf #A7BA6C, sparse blossom #D996AF accents. Scene: truly transparent alpha background, no checkerboard pixels, no ground plane, no scenery, no frame, no text, no logo, no watermark, no cast shadow. Wurzelbund creatures are mobile wood, root, leaf and clay organisms, never humanoid wood elves.\nSubject: Rankenschnapper: an upgraded robust vine-jaw predator, broad hinged plant maw with a few oversized sharp thorn fangs, thick coiled bark vines forming stocky mobile torso, four chunky root feet, broad green leaf cheek plates. Controlled fierce expression; silhouette dominated by the huge horizontal snapping jaw. No humanoid anatomy, no weapons.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-ea61-7671-8a5d-a14abb62467d/exec-87dbb099-b6ea-4aef-8c18-844f0db03e04.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/wurzelbund/unit_w02_rankenschnapper_icon.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "w03",
|
||||||
|
"display_name": "Korkrücken",
|
||||||
|
"faction": "wurzelbund",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"description_de": "Gedrungener Tank mit breitem Korkpanzer, Moos und Wurzelfüßen.",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: original fantasy strategy-game unit UI icon for Saumwacht, Wurzelbund faction. Style: faceted illustrated low-poly game art, broad flat matte planes, crisp silhouette and edges, chunky simple forms, high readability at 256px, not photorealistic, not a painting. Composition: ONE whole single creature centered, square canvas, complete feet and appendages visible, creature occupies 75-80% of canvas, generous transparent margins. 3/4 front view facing slightly right. Lighting: soft upper-left light, restrained shadows within subject. Palette: bark #685441, moss #56845A, leaf #A7BA6C, sparse blossom #D996AF accents. Scene: truly transparent alpha background, no checkerboard pixels, no ground plane, no scenery, no frame, no text, no logo, no watermark, no cast shadow. Wurzelbund creatures are mobile wood, root, leaf and clay organisms, never humanoid wood elves.\nSubject: Korkrücken: a squat nonhumanoid wooden tank creature under an oversized shield-back of thick cork bark plates, four broad stubby root feet, very low head with tiny leaf-green eyes peeking under the front shell, rounded compact defensive silhouette. Bark brown dominates with moss green seams, a few large leaf plates. Entire creature easily fits the square with 12% transparent margin on EVERY side.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-ea61-7671-8a5d-a14abb62467d/exec-63e0cc2b-e4d4-49ee-af0e-83ec417f05d9.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/wurzelbund/unit_w03_korkruecken_icon.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "w04",
|
||||||
|
"display_name": "Ringborke",
|
||||||
|
"faction": "wurzelbund",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"description_de": "Großer Wurzeltank mit konzentrischen Holzringen als massiver Panzerung.",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: original fantasy strategy-game unit UI icon for Saumwacht, Wurzelbund faction. Style: faceted illustrated low-poly game art, broad flat matte planes, crisp silhouette and edges, chunky simple forms, high readability at 256px, not photorealistic, not a painting. Composition: ONE whole single creature centered, square canvas, complete feet and appendages visible, creature occupies 75-80% of canvas, generous transparent margins. 3/4 front view facing slightly right. Lighting: soft upper-left light, restrained shadows within subject. Palette: bark #685441, moss #56845A, leaf #A7BA6C, sparse blossom #D996AF accents. Scene: truly transparent alpha background, no checkerboard pixels, no ground plane, no scenery, no frame, no text, no logo, no watermark, no cast shadow. Wurzelbund creatures are mobile wood, root, leaf and clay organisms, never humanoid wood elves.\nSubject: Ringborke: a huge upgraded nonhumanoid wooden tank, massive overlapping circular trunk cross-section armor shields showing clear bold concentric wood growth rings, thick root limbs and flat wide root feet, small recessed leaf-green eyes, heavy shoulder and back bulk, tiny moss patches and few leaves. It is a mobile root organism, not a humanoid ent. Silhouette strongly broad and heavy; dominant readable wooden concentric circles. Entire creature fits square with 12% transparent margin on EVERY side.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-ea61-7671-8a5d-a14abb62467d/exec-ce1833cd-62ce-42b7-a82f-9ef98887509e.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/wurzelbund/unit_w04_ringborke_icon.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "w05",
|
||||||
|
"display_name": "Samenschleuder",
|
||||||
|
"faction": "wurzelbund",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"description_de": "Schlanker Wurzelfernkämpfer mit gebogener Ranke und offener Samenkapsel als Wurfschale.",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: original fantasy strategy-game unit UI icon for Saumwacht, Wurzelbund faction. Style: faceted illustrated low-poly game art, broad flat matte planes, crisp silhouette and edges, chunky simple forms, high readability at 256px, not photorealistic, not a painting. Composition: ONE whole single creature centered, square canvas, complete feet and appendages visible, creature occupies 75-80% of canvas, generous transparent margins. 3/4 front view facing slightly right. Lighting: soft upper-left light, restrained shadows within subject. Palette: bark #685441, moss #56845A, leaf #A7BA6C, sparse blossom #D996AF accents. Scene: truly transparent alpha background, no checkerboard pixels, no ground plane, no scenery, no frame, no text, no logo, no watermark, no cast shadow. Wurzelbund creatures are mobile wood, root, leaf and clay organisms, never humanoid wood elves.\nSubject: Samenschleuder: a slender mobile seedpod catapult creature; three thin arched living root legs spread into broad root toes, flexible curved vine torso bent back like a spring, one large open green seedpod cup at its top holding one chunky brown seed ready to launch. Two leaf fins for balance, tiny slit eyes in the bark below the cup. Clear elegant narrow ranged-unit silhouette, entirely plant organism, no humanoid arms, no mechanical hardware. Entire creature fits square with 12% transparent margin on EVERY side.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-ea61-7671-8a5d-a14abb62467d/exec-e837c72b-54e7-407d-8bf3-dae94faa4aac.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/wurzelbund/unit_w05_samenschleuder_icon.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "w06",
|
||||||
|
"display_name": "Zapfenkrone",
|
||||||
|
"faction": "wurzelbund",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"description_de": "Hoher Samenschütze mit großer Zapfenkrone, kräftigen Ranken und Blattschalen.",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: original fantasy strategy-game unit UI icon for Saumwacht, Wurzelbund faction. Style: faceted illustrated low-poly game art, broad flat matte planes, crisp silhouette and edges, chunky simple forms, high readability at 256px, not photorealistic, not a painting. Composition: ONE whole single creature centered, square canvas, complete feet and appendages visible, creature occupies 75-80% of canvas, generous transparent margins. 3/4 front view facing slightly right. Lighting: soft upper-left light, restrained shadows within subject. Palette: bark #685441, moss #56845A, leaf #A7BA6C, sparse blossom #D996AF accents. Scene: truly transparent alpha background, no checkerboard pixels, no ground plane, no scenery, no frame, no text, no logo, no watermark, no cast shadow. Wurzelbund creatures are mobile wood, root, leaf and clay organisms, never humanoid wood elves.\nSubject: Zapfenkrone: tall upgraded seed-launching plant creature with an enormous crown-shaped pinecone launcher on top, several broad overlapping bark scales forming an upward open cone crown, one chunky seed visible within, green leaf collars, strong curved vine stem torso, three mobile root legs with broad toes. Silhouette much taller and statelier than a small seedpod catapult, all living wood and plants, tiny eyes below crown, never humanoid. Entire creature fits square with 12% transparent margin on EVERY side.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-ea61-7671-8a5d-a14abb62467d/exec-9580ae74-e7fc-429f-8422-3c1dce830b15.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/wurzelbund/unit_w06_zapfenkrone_icon.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "w07",
|
||||||
|
"display_name": "Sporenkelch",
|
||||||
|
"faction": "wurzelbund",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"description_de": "Niedriger offener Pilzkelch mit rosiger Sporenmulde und breiten Wurzelfüßen.",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: original fantasy strategy-game unit UI icon for Saumwacht, Wurzelbund faction. Style: faceted illustrated low-poly game art, broad flat matte planes, crisp silhouette and edges, chunky simple forms, high readability at 256px, not photorealistic, not a painting. Composition: ONE whole single creature centered, square canvas, complete feet and appendages visible, creature occupies 75-80% of canvas, generous transparent margins. 3/4 front view facing slightly right. Lighting: soft upper-left light, restrained shadows within subject. Palette: bark #685441, moss #56845A, leaf #A7BA6C, sparse blossom #D996AF accents. Scene: truly transparent alpha background, no checkerboard pixels, no ground plane, no scenery, no frame, no text, no logo, no watermark, no cast shadow. Wurzelbund creatures are mobile wood, root, leaf and clay organisms, never humanoid wood elves.\nSubject: Sporenkelch: squat open mushroom-chalice creature, one huge concave cup fungus tilted enough to show its open spore bowl, dusty blossom pink inner cup with a few large pale seedlike spores resting inside, moss green outer lip, thick bark-clay stem body and four chunky root feet, tiny recessed eyes on stalk. Silhouette is unmistakably a broad open cup on a short rooted base, no humanoid anatomy. No drifting particles outside silhouette. Entire creature fits square with 12% transparent margin on EVERY side.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-ea61-7671-8a5d-a14abb62467d/exec-6e164b57-339c-443c-ae29-09935cd1226e.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/wurzelbund/unit_w07_sporenkelch_icon.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "w08",
|
||||||
|
"display_name": "Pilzkathedrale",
|
||||||
|
"faction": "wurzelbund",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"description_de": "Große mobile Pilzarchitektur mit gestaffelten rosigen Pilzdächern und massivem Wurzelwerk.",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: original fantasy strategy-game unit UI icon for Saumwacht, Wurzelbund faction. Style: faceted illustrated low-poly game art, broad flat matte planes, crisp silhouette and edges, chunky simple forms, high readability at 256px, not photorealistic, not a painting. Composition: ONE whole single creature centered, square canvas, complete feet and appendages visible, creature occupies 75-80% of canvas, generous transparent margins. 3/4 front view facing slightly right. Lighting: soft upper-left light, restrained shadows within subject. Palette: bark #685441, moss #56845A, leaf #A7BA6C, sparse blossom #D996AF accents. Scene: truly transparent alpha background, no checkerboard pixels, no ground plane, no scenery, no frame, no text, no logo, no watermark, no cast shadow. Wurzelbund creatures are mobile wood, root, leaf and clay organisms, never humanoid wood elves.\nSubject: Pilzkathedrale: large upgraded mobile mushroom architecture creature, several broad stacked mushroom canopies arranged as a three-tier cathedral silhouette over one living bark and root body. Dusty blossom pink fungus caps and inner gills, moss green edges and leaf accents, heavy interwoven root buttresses and four broad mobile feet. Topmost cap forms an open spore cup; smaller side canopies step down symmetrically. Recognizable organic architectural silhouette, not an actual building, no windows or doors or stone masonry, no humans. Entire creature fits square with 12% transparent margin on EVERY side.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-ea61-7671-8a5d-a14abb62467d/exec-f7f08c31-2ef2-41e1-8e89-5c63f9e6dd65.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/wurzelbund/unit_w08_pilzkathedrale_icon.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "w09",
|
||||||
|
"display_name": "Tauhüter",
|
||||||
|
"faction": "wurzelbund",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"description_de": "Kleiner Blatthüter mit einzelnem Tautropfen in einer schützenden Blattschale.",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: original fantasy strategy-game unit UI icon for Saumwacht, Wurzelbund faction. Style: faceted illustrated low-poly game art, broad flat matte planes, crisp silhouette and edges, chunky simple forms, high readability at 256px, not photorealistic, not a painting. Composition: ONE whole single creature centered, square canvas, complete feet and appendages visible, creature occupies 75-80% of canvas, generous transparent margins. 3/4 front view facing slightly right. Lighting: soft upper-left light, restrained shadows within subject. Palette: bark #685441, moss #56845A, leaf #A7BA6C, sparse blossom #D996AF accents. Scene: truly transparent alpha background, no checkerboard pixels, no ground plane, no scenery, no frame, no text, no logo, no watermark, no cast shadow. Wurzelbund creatures are mobile wood, root, leaf and clay organisms, never humanoid wood elves.\nSubject: Tauhüter: small gentle nonhumanoid leaf healer, compact woven root body on three chunky root feet, two curved root tendrils supporting a single large cupped leaf in front of its body. The leaf holds exactly ONE oversized clear pale-mint dewdrop, the focal point. A simple three-leaf canopy on its back, tiny gentle green eyes beneath canopy. Rind brown and moss green dominate with one small pink blossom. No humanoid elf anatomy, no clothes, no staff. Entire creature fits square with 12% transparent margin on EVERY side.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-ea61-7671-8a5d-a14abb62467d/exec-a9291f97-c46f-4902-a656-1f4d2537f23c.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/wurzelbund/unit_w09_tauhueter_icon.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "w10",
|
||||||
|
"display_name": "Quellgeflecht",
|
||||||
|
"faction": "wurzelbund",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"description_de": "Großer Heiler aus verflochtenen Wurzeln mit Quellwasserschale unter einem Blüten- und Blätterdach.",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: original fantasy strategy-game unit UI icon for Saumwacht, Wurzelbund faction. Style: faceted illustrated low-poly game art, broad flat matte planes, crisp silhouette and edges, chunky simple forms, high readability at 256px, not photorealistic, not a painting. Composition: ONE whole single creature centered, square canvas, complete feet and appendages visible, creature occupies 75-80% of canvas, generous transparent margins. 3/4 front view facing slightly right. Lighting: soft upper-left light, restrained shadows within subject. Palette: bark #685441, moss #56845A, leaf #A7BA6C, sparse blossom #D996AF accents. Scene: truly transparent alpha background, no checkerboard pixels, no ground plane, no scenery, no frame, no text, no logo, no watermark, no cast shadow. Wurzelbund creatures are mobile wood, root, leaf and clay organisms, never humanoid wood elves.\nSubject: Quellgeflecht: large upgraded mobile woven-root healer creature, a wide open bowl of clear pale-mint spring water cradled in interlaced chunky living roots at its chest, a broad layered leaf-and-blossom canopy arching above bowl, thick braided root body and four sturdy root feet. Gentle recessed eyes tucked below canopy, dusty pink flowers accenting green leaves. Water bowl must be conspicuous and readable. Entirely organic mobile plant organism, not humanoid, no clothing, no architecture, no floating water particles. Entire creature fits square with 12% transparent margin on EVERY side.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-ea61-7671-8a5d-a14abb62467d/exec-120bac3c-d575-4331-a7e9-5812241f56a4.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/wurzelbund/unit_w10_quellgeflecht_icon.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "s01",
|
||||||
|
"display_name": "Nadelspäher",
|
||||||
|
"description_de": "Schlanker maskierter Söldnerspäher mit kompaktem Nadelwerfer.",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: original neutral mercenary UNIT ICON for Saumwacht, a single individual PNG cutout. Create a brand-new image. Style: crisp faceted illustrated low-poly fantasy, strong chunky planar shapes and simple matte shading, coherent premium strategy-game icon, not photorealistic. Composition: square 1:1 canvas, whole single figure centered, all parts visible with comfortable transparent margin, occupy 75–80 percent of canvas. Three-quarter front view looking toward viewer's right. Lighting: soft key from upper left and restrained ambient fill. Palette: slate #536375 and rust #A8654D with ivory #D8D2BF, restrained violet #9578CF; only the rift creature may add a small mint glow. Keep broad masses readable at 48px. Scene: genuinely transparent background with preserved alpha; no checkerboard pattern drawn into the image. No ground, no shadow plane, no frame, no text, no lettering, no logos, no border, no watermark, no unrelated props, no other characters, no realistic texture, no excessive microdetail. Subject: a thin masked scout mercenary, slim upright silhouette, pointed slate-gray hood and fitted layered dark armor, simple ivory full-face mask, compact handheld dart launcher pointing right, three clearly visible thick spare darts in a rust-red back quiver, narrow limbs and practical boots; original fantasy creature-like scout with no copied character references.",
|
||||||
|
"faction": "neutral",
|
||||||
|
"kind": "mercenary_icon",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-2a90-7821-8332-7507717ed80a/exec-acd47e5f-98cd-4047-b9cf-d04833ee91c2.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/neutral/unit_s01_nadelspaeher_icon.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "s02",
|
||||||
|
"display_name": "Rostschieber",
|
||||||
|
"description_de": "Gedrungener schwerer Söldner mit Rostpanzer und großem Rammschild.",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: original neutral mercenary UNIT ICON for Saumwacht, a single individual PNG cutout. Create a brand-new image. Style: crisp faceted illustrated low-poly fantasy, strong chunky planar shapes and simple matte shading, coherent premium strategy-game icon, not photorealistic. Composition: square 1:1 canvas, whole single figure centered, all parts visible with comfortable transparent margin, occupy 75–80 percent of canvas. Three-quarter front view looking toward viewer's right. Lighting: soft key from upper left and restrained ambient fill. Palette: slate #536375 and rust #A8654D with ivory #D8D2BF, restrained violet #9578CF; only the rift creature may add a small mint glow. Keep broad masses readable at 48px. Scene: genuinely transparent background with preserved alpha; no checkerboard pattern drawn into the image. No ground, no shadow plane, no frame, no text, no lettering, no logos, no border, no watermark, no unrelated props, no other characters, no realistic texture, no excessive microdetail. Subject: a squat heavy shield-ram mercenary, wide rust-red layered armor and dark slate body, thick ivory brow guard and tiny inset eyes, huge broad rectangular shield held ahead as a battering ram with a single steel wedge along its center, compact heavy legs and a short hammerlike free fist; low rectangular silhouette emphasizing a defensive shield.",
|
||||||
|
"faction": "neutral",
|
||||||
|
"kind": "mercenary_icon",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-2a90-7821-8332-7507717ed80a/exec-119c1a0e-881e-4696-a764-be3d802eab15.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/neutral/unit_s02_rostschieber_icon.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "s03",
|
||||||
|
"display_name": "Dunstsänger",
|
||||||
|
"description_de": "Verhüllter violetter Dunstsänger mit großem Resonanzhorn.",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: original neutral mercenary UNIT ICON for Saumwacht, a single individual PNG cutout. Create a brand-new image. Style: crisp faceted illustrated low-poly fantasy, strong chunky planar shapes and simple matte shading, coherent premium strategy-game icon, not photorealistic. Composition: square 1:1 canvas, whole single figure centered, all parts visible with comfortable transparent margin, occupy 75–80 percent of canvas. Three-quarter front view looking toward viewer's right. Lighting: soft key from upper left and restrained ambient fill. Palette: slate #536375 and rust #A8654D with ivory #D8D2BF, restrained violet #9578CF; only the rift creature may add a small mint glow. Keep broad masses readable at 48px. Scene: genuinely transparent background with preserved alpha; no checkerboard pattern drawn into the image. No ground, no shadow plane, no frame, no text, no lettering, no logos, no border, no watermark, no unrelated props, no other characters, no realistic texture, no excessive microdetail. Subject: a draped ethereal mist-singer mercenary, elongated bell-shaped dark slate and muted violet robe with broad faceted folds, small simple ivory mask inside a deep hood, two slender hands holding a large rust-bronze resonance horn with an open flared bell pointing to the right, a few contained violet wisps tucked inside the silhouette; full figure with no platform or ground.",
|
||||||
|
"faction": "neutral",
|
||||||
|
"kind": "mercenary_icon",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-2a90-7821-8332-7507717ed80a/exec-977e27d0-9bb9-44ca-8b66-bfbbf04e70ce.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/neutral/unit_s03_dunstsaenger_icon.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "s04",
|
||||||
|
"display_name": "Keilbrecher",
|
||||||
|
"description_de": "Schwerer dunkelgrauer Keilbrecher mit großem Keilhammer.",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: original neutral mercenary UNIT ICON for Saumwacht, a single individual PNG cutout. Create a brand-new image. Style: crisp faceted illustrated low-poly fantasy, strong chunky planar shapes and simple matte shading, coherent premium strategy-game icon, not photorealistic. Composition: square 1:1 canvas, whole single figure centered, all parts visible with comfortable transparent margin, occupy 75–80 percent of canvas. Three-quarter front view looking toward viewer's right. Lighting: soft key from upper left and restrained ambient fill. Palette: slate #536375 and rust #A8654D with ivory #D8D2BF, restrained violet #9578CF; only the rift creature may add a small mint glow. Keep broad masses readable at 48px. Scene: genuinely transparent background with preserved alpha; no checkerboard pattern drawn into the image. No ground, no shadow plane, no frame, no text, no lettering, no logos, no border, no watermark, no unrelated props, no other characters, no realistic texture, no excessive microdetail. Subject: a heavy dark-steel walking wedge-hammer mercenary, powerful broad-shouldered stocky warrior, massive blocky slate-gray armor with restrained rust-red accents, simple ivory faceplate, two heavy hands gripping a gigantic wedge-shaped hammer with a clear triangular chisel head held diagonally across the body, short sturdy legs and wide boots; strong triangle and block silhouette.",
|
||||||
|
"faction": "neutral",
|
||||||
|
"kind": "mercenary_icon",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-2a90-7821-8332-7507717ed80a/exec-3f1a6bd4-4a83-46c3-b479-e79b4716c1eb.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/neutral/unit_s04_keilbrecher_icon.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "s05",
|
||||||
|
"display_name": "Risszehrer",
|
||||||
|
"description_de": "Hohler Risszehrer mit mintviolettem Schlund und magischer Entzugskugel.",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: original neutral mercenary UNIT ICON for Saumwacht, a single individual PNG cutout. Create a brand-new image. Style: crisp faceted illustrated low-poly fantasy, strong chunky planar shapes and simple matte shading, coherent premium strategy-game icon, not photorealistic. Composition: square 1:1 canvas, whole single figure centered, all parts visible with comfortable transparent margin, occupy only 65 percent of canvas height; make the figure relatively small with wide empty transparent margins on ALL sides. Three-quarter front view looking toward viewer's right. Lighting: soft key from upper left and restrained ambient fill. Palette: slate #536375 and rust #A8654D with ivory #D8D2BF, restrained violet #9578CF; only the rift creature may add a small mint glow. Keep broad masses readable at 48px. Scene: genuinely transparent background with preserved alpha; no checkerboard pattern drawn into the image. No ground, no shadow plane, no frame, no text, no lettering, no logos, no border, no watermark, no unrelated props, no other characters, no realistic texture, no excessive microdetail. Subject: a tall hollow rift-feeding creature mercenary, original alien fantasy silhouette, long angular slate-gray shell torso with a large hollow chest opening glowing faint mint and violet, short compact ivory broken-arc crown around a faceless dark head with a restrained mint-violet mouth slit, two long tapering arms, one claw cradling a single small glowing mint-violet healing-drain orb close to its chest, long digitigrade legs; thin eerie silhouette with a very readable empty chest hole, no gore. Framing is critical: the entire small creature must sit within the central 65 percent of the square, crown tip far below top edge, feet far above bottom edge. Actual transparent PNG alpha, not an illustrated checkerboard.",
|
||||||
|
"faction": "neutral",
|
||||||
|
"kind": "mercenary_icon",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-2a90-7821-8332-7507717ed80a/exec-d505eb20-988a-49d7-b821-be49d87c14ba.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/neutral/unit_s05_risszehrer_icon.png",
|
||||||
|
"status": "generated",
|
||||||
|
"iterations": [
|
||||||
|
{
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-2a90-7821-8332-7507717ed80a/exec-6384b126-c087-4d51-b9f7-43abd9b7e6b5.png",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: original neutral mercenary UNIT ICON for Saumwacht, a single individual PNG cutout. Create a brand-new image. Style: crisp faceted illustrated low-poly fantasy, strong chunky planar shapes and simple matte shading, coherent premium strategy-game icon, not photorealistic. Composition: square 1:1 canvas, whole single figure centered, all parts visible with comfortable transparent margin, occupy 75–80 percent of canvas. Three-quarter front view looking toward viewer's right. Lighting: soft key from upper left and restrained ambient fill. Palette: slate #536375 and rust #A8654D with ivory #D8D2BF, restrained violet #9578CF; only the rift creature may add a small mint glow. Keep broad masses readable at 48px. Scene: genuinely transparent background with preserved alpha; no checkerboard pattern drawn into the image. No ground, no shadow plane, no frame, no text, no lettering, no logos, no border, no watermark, no unrelated props, no other characters, no realistic texture, no excessive microdetail. Subject: a tall hollow rift-feeding creature mercenary, original alien fantasy silhouette, long angular slate-gray shell torso with a large hollow chest opening glowing faint mint and violet, ivory broken-arc crown around a faceless dark head with a restrained mint-violet mouth slit, two long tapering arms, one claw cradling a single small glowing mint-violet healing-drain orb close to its chest, long digitigrade legs; thin eerie silhouette with a very readable empty chest hole, no gore.",
|
||||||
|
"status": "rejected",
|
||||||
|
"reason_de": "Kronenspitze am oberen Bildrand angeschnitten."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-2a90-7821-8332-7507717ed80a/exec-97910932-6bdd-4856-881d-6fceea251897.png",
|
||||||
|
"prompt": "Use case: precise-object-edit. Edit target: this Risszehrer neutral mercenary icon. Correct only framing: extend the genuine transparent canvas and present the same entire creature at about 78 percent canvas height in a square 1:1 image, centered, with at least 10 percent empty transparent margin above the fully visible ivory crown tips and below the fully visible feet. Complete the tiny crown tip currently clipped by the top edge. Preserve the creature's exact identity, colors, faceted matte style, three-quarter front view looking right, upper-left lighting, hollow chest, mint-violet orb, limbs, pose and proportions. Do not crop any part. Actual transparent alpha outside subject; no background color, checkerboard, ground, shadow plane, text, frame or extra objects.",
|
||||||
|
"status": "rejected",
|
||||||
|
"reason_de": "Sichtbares Karomuster statt sauberem transparentem Hintergrund."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "res_gold",
|
||||||
|
"display_name": "Gold",
|
||||||
|
"faction": "resources",
|
||||||
|
"kind": "resource_icon",
|
||||||
|
"description_de": "Eine große facettierte Goldmünze mit zwei kleineren Münzen.",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: standalone resource UI icon for original fantasy strategy game Saumwacht. Faceted illustrated game UI art, large simple flat matte planes, crisp graphic edges, restrained shading, very clear shape readable at 48px and 256px. Single coherent centered icon on square canvas, whole object visible, occupies around 75% canvas with transparent margins all sides. Three-quarter front view facing slightly right, soft neutral upper-left key light. Palette warm brass gold #D9A441, pale mint teal #82DBCE, coral #E9967C, slate #536375. Truly transparent RGBA alpha background, no checkerboard pixels, no ground, no cast shadow, no border or frame, no badge background, no text, no letters, no numerals, no logos, no watermark. Avoid photorealism, glossy rendering and tiny texture.\nSubject: A chunky golden coin angled three-quarter toward camera with exactly two smaller golden coins nestled at its lower right, a compact grouped resource icon. Broad faceted bevels, simple blank coin faces with a subtle geometric inset but no currency signs and no letters. Warm gold dominates with darker brass edge planes.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-ea61-7671-8a5d-a14abb62467d/exec-2caa13f0-211c-4444-9f78-bf068af50ac5.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/resources/res_gold.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "res_sparks",
|
||||||
|
"display_name": "Funken",
|
||||||
|
"faction": "resources",
|
||||||
|
"kind": "resource_icon",
|
||||||
|
"description_de": "Kleines Messing-Glasgefäß mit leuchtender mintfarbener Funkenenergie.",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: standalone resource UI icon for original fantasy strategy game Saumwacht. Faceted illustrated game UI art, large simple flat matte planes, crisp graphic edges, restrained shading, very clear shape readable at 48px and 256px. Single coherent centered icon on square canvas, whole object visible, occupies around 75% canvas with transparent margins all sides. Three-quarter front view facing slightly right, soft neutral upper-left key light. Palette warm brass gold #D9A441, pale mint teal #82DBCE, coral #E9967C, slate #536375. Truly transparent RGBA alpha background, no checkerboard pixels, no ground, no cast shadow, no border or frame, no badge background, no text, no letters, no numerals, no logos, no watermark. Avoid photorealism, glossy rendering and tiny texture.\nSubject: A single small squat glass vessel framed by two broad brass bands and a chunky brass stopper, containing one vivid pale mint energy spark cluster. A simple slate base, large clear window allowing bright teal energy to be seen. Faceted stylized glass planes, not glossy photoreal glass. No glow leaking onto the transparent background, no floating particles outside vessel.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-ea61-7671-8a5d-a14abb62467d/exec-e1eacde7-ae55-41d3-82c5-ec33cbc40437.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/resources/res_sparks.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "res_income",
|
||||||
|
"display_name": "Einkommen",
|
||||||
|
"faction": "resources",
|
||||||
|
"kind": "resource_icon",
|
||||||
|
"description_de": "Messingsiegel mit drei von links nach rechts ansteigenden Münzstapeln.",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: standalone resource UI icon for original fantasy strategy game Saumwacht. Faceted illustrated game UI art, large simple flat matte planes, crisp graphic edges, restrained shading, very clear shape readable at 48px and 256px. Single coherent centered icon on square canvas, whole object visible, occupies around 75% canvas with transparent margins all sides. Three-quarter front view facing slightly right, soft neutral upper-left key light. Palette warm brass gold #D9A441, pale mint teal #82DBCE, coral #E9967C, slate #536375. Truly transparent RGBA alpha background, no checkerboard pixels, no ground, no cast shadow, no border or frame, no badge background, no text, no letters, no numerals, no logos, no watermark. Avoid photorealism, glossy rendering and tiny texture.\nSubject: A single thick faceted brass seal medallion with THREE bold raised stacks of coins on its face, stepping upward left to right from one to three to five coins, conveying income growth visually. Coin stacks should read like three ascending bar-chart columns made of coins, simplified broad surfaces, slate inset contrast behind raised gold geometry. No arrows required, absolutely no lettering, numerals or currency symbols. Seal itself is the object, no surrounding UI frame.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-ea61-7671-8a5d-a14abb62467d/exec-02bfd968-9507-46c5-8818-1e6eb87ff9e4.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/resources/res_income.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "res_core",
|
||||||
|
"display_name": "Kernleben",
|
||||||
|
"faction": "resources",
|
||||||
|
"kind": "resource_icon",
|
||||||
|
"description_de": "Korallfarbenes facettiertes Kristallherz in einer kleinen Messingfassung.",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: standalone resource UI icon for original fantasy strategy game Saumwacht. Faceted illustrated game UI art, large simple flat matte planes, crisp graphic edges, restrained shading, very clear shape readable at 48px and 256px. Single coherent centered icon on square canvas, whole object visible, occupies around 75% canvas with transparent margins all sides. Three-quarter front view facing slightly right, soft neutral upper-left key light. Palette warm brass gold #D9A441, pale mint teal #82DBCE, coral #E9967C, slate #536375. Truly transparent RGBA alpha background, no checkerboard pixels, no ground, no cast shadow, no border or frame, no badge background, no text, no letters, no numerals, no logos, no watermark. Avoid photorealism, glossy rendering and tiny texture.\nSubject: One chunky faceted CORAL crystal HEART cradled in a tiny warm brass support base with two short protective prongs. Heart shape has two broad rounded angular lobes and a clear pointed bottom, instantly readable as life resource. Coral #E9967C dominates, warm brass #D9A441 cradle, minimal slate underside. Broad matte crystal planes, tiny restrained highlight, no luminous halo.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-ea61-7671-8a5d-a14abb62467d/exec-725253b1-7fe0-4a4b-a960-3b5fa731b902.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/resources/res_core.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "worker_funkenleser",
|
||||||
|
"display_name": "Funkenleser",
|
||||||
|
"faction": "neutral",
|
||||||
|
"kind": "worker_icon",
|
||||||
|
"description_de": "Kleine Lehmgestalt im Arbeitstuch mit gegabeltem Funkensammler und Glasbehälter.",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: standalone resource UI icon for original fantasy strategy game Saumwacht. Faceted illustrated game UI art, large simple flat matte planes, crisp graphic edges, restrained shading, very clear shape readable at 48px and 256px. Single coherent centered icon on square canvas, whole object visible, occupies around 75% canvas with transparent margins all sides. Three-quarter front view facing slightly right, soft neutral upper-left key light. Palette warm brass gold #D9A441, pale mint teal #82DBCE, coral #E9967C, slate #536375. Truly transparent RGBA alpha background, no checkerboard pixels, no ground, no cast shadow, no border or frame, no badge background, no text, no letters, no numerals, no logos, no watermark. Avoid photorealism, glossy rendering and tiny texture.\nSubject: A whole single small neutral WORKER creature named Funkenleser, a squat original clay golem organism with a simple rounded faceted terracotta clay face and tiny inset eyes, short chunky clay feet, practical slate-colored cloth hood and apron with broad simple folds. It holds a forked brass spark-collector tool in one short clay hand, and a small clear glass container with one mint energy spark in its other hand. Tiny warm brass buckles, approachable industrious stance. No human skin, no pointed elf ears, no weapons, no writing. Entire worker and tool are fully visible, compact silhouette with 12% transparent margins all sides. Same matte faceted strategy-game art as resource icons.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-ea61-7671-8a5d-a14abb62467d/exec-143f6c4c-6567-4a09-b262-03535ac820e5.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/neutral/worker_funkenleser_icon.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "type_damage_blade",
|
||||||
|
"display_name": "Klinge",
|
||||||
|
"faction": "types",
|
||||||
|
"kind": "type_icon",
|
||||||
|
"description_de": "Einzelne gebogene Elfenbeinklinge mit Messingparierstange und Graphitgriff.",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: one fantasy strategy-game UI damage-type icon, square transparent PNG. Subject: BLADE DAMAGE, a SINGLE short broad curved ivory blade with a simple brass crossguard and short graphite grip. Orient diagonally lower left to upper right, point upward; one clean sweeping cutting edge. It must read unmistakably as a blade at 32px. Style: polished faceted flat UI illustration matching matte low-poly fantasy unit art, only a few broad planar shapes, crisp silhouette, no tiny engravings, no excessive texture. Slight three-quarter perspective and soft light from upper left. Palette graphite #292B35, ivory #D8CEC0, brass #D9A441. Entire object centered, about 75 percent of square canvas, generous transparent margin on all sides. Actual transparent alpha background, no checkerboard picture, no ground, no scene, no cast shadow, no badge, no circular disc, no frame, no letters, no numbers, no watermark. Exactly one isolated icon.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-969b-74f3-a893-5fea0b3fac02/exec-c7909917-44f8-4e6f-8809-3c86ff441734.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/types/type_damage_blade.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "type_damage_pierce",
|
||||||
|
"display_name": "Durchschlag",
|
||||||
|
"faction": "types",
|
||||||
|
"kind": "type_icon",
|
||||||
|
"description_de": "Lange Messingpfeilspitze durchstößt eine kleine graphitfarbene Rautenplatte.",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: ONE fantasy strategy-game UI damage-type icon, square transparent PNG. Subject: PIERCING DAMAGE, a SINGLE long sharp brass arrowhead with a short narrow graphite shaft visibly punching THROUGH a small thick dark graphite diamond-shaped plate. Composition diagonal lower left to upper right: arrowhead extends prominently above-right of plate, short shaft extends below-left, plate surrounds one tight angular puncture hole. Long brass spear-like triangular point dominates, small plate is secondary. No explosion, no extra arrows, no loose fragments. Must read unmistakably as piercing impact at 32px. Polished faceted flat UI illustration coherent with matte low-poly fantasy art, only a few large planar shapes, crisp bold silhouette, minimal detail and no engravings. Slight three-quarter perspective, soft light from upper left. Palette graphite #292B35, ivory #D8CEC0 for small highlight edge, brass #D9A441. Center entire object group in square canvas, 75 percent occupancy, generous transparent margins. Actual transparent alpha background, no checkerboard picture, ground, scene, cast shadow, badge, disc, frame, letters, numbers or watermark. Exactly one icon.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-969b-74f3-a893-5fea0b3fac02/exec-f0526b22-0c7b-4737-9a6c-f26cd416a116.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/types/type_damage_pierce.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "type_damage_pulse",
|
||||||
|
"display_name": "Impuls",
|
||||||
|
"faction": "types",
|
||||||
|
"kind": "type_icon",
|
||||||
|
"description_de": "Mintfarbener facettierter Sternkern zwischen zwei offenen graphitfarbenen Ringhälften.",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: ONE fantasy strategy-game UI damage-type icon, square transparent PNG. Subject: PULSE DAMAGE, a compact glowing MINT GEOMETRIC STAR-ORB inside one thick OPEN GRAPHITE RING. Core is a solid faceted orb with four short broad diamond-like energy points, NOT a spiky explosion, not a sun face. Dark ring surrounds the core but has two clearly open gaps on opposite sides, with transparent space between the core and ring; no outer badge or backing disc. Bold round compact silhouette with mint center unmistakable at 32px. Polished faceted flat UI illustration matching matte low-poly fantasy art, only a few big planar surfaces, crisp shapes and extremely restrained glow entirely near the core. Slight three-quarter perspective, soft upper-left lighting. Palette graphite #292B35, mint #82DBCE, tiny ivory #D8CEC0 highlights; no extra colors. Entire icon centered in square canvas, 75 percent occupancy, generous transparent margins. Actual transparent alpha background; no checkerboard image, ground, scenery, cast shadow, surrounding particles, circular badge, text, letters, numbers, border, frame or watermark. Exactly one isolated icon.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-969b-74f3-a893-5fea0b3fac02/exec-59c9c62a-5ffe-4820-bc71-556f2dc679bb.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/types/type_damage_pulse.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "type_armor_light",
|
||||||
|
"display_name": "Leichte Rüstung",
|
||||||
|
"faction": "types",
|
||||||
|
"kind": "type_icon",
|
||||||
|
"description_de": "Dünne blattförmige Lederpanzerplatte in warmem Ocker mit einfachem Graphitriemen.",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: ONE fantasy strategy-game UI armor-type icon, square transparent PNG. Subject: LIGHT ARMOR, a SINGLE thin elongated LEAF-SHAPED LEATHER ARMOR PLATE, pointed bottom, softly rounded upper shoulders, with a subtle central folded ridge and one simple narrow graphite leather strap wrapping the middle. Warm brass-ochre matte leather main face, ivory edge highlights, thin flexible edge visibly unlike a bulky metal shield. No plant leaf veins, no actual foliage, no full body armor, no person. Must read as a light leather protective plate at 32px. Polished faceted flat UI illustration matching matte low-poly fantasy art, only a few broad planes and crisp simple silhouette, no tiny stitches or detailed texture. Slight three-quarter perspective turned a little right; soft upper-left key light. Palette graphite #292B35, ivory #D8CEC0, brass #D9A441 interpreted as warm matte leather. Whole single object centered in square canvas, 75 percent occupancy, generous transparent margins. Actual transparent alpha background; no checkerboard picture, scenery, ground, cast shadow, badge, backing disc, text, numbers, frame, border or watermark.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-969b-74f3-a893-5fea0b3fac02/exec-4112c2a0-2ec6-47e2-8592-9ad3bf0ea66d.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/types/type_armor_light.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "type_armor_heavy",
|
||||||
|
"display_name": "Schwere Rüstung",
|
||||||
|
"faction": "types",
|
||||||
|
"kind": "type_icon",
|
||||||
|
"description_de": "Breite schwere Schildplatte aus drei Graphitstahllagen mit hellen Kanten und Messingverstärkung.",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: ONE fantasy strategy-game UI armor-type icon, square transparent PNG. Subject: HEAVY ARMOR, a SINGLE broad thick layered STEEL SHIELD PLATE with squared upper shoulders and a blunt pointed bottom. Three overlapping large graphite steel armor layers across the front, thick visible beveled ivory-gray steel edges, a small simple brass reinforcing band along the outer edge. Extremely stout heavy silhouette, visibly thick and rigid, no leather strap, no emblem, no symbols. It must read as heavy metal armor at 32px. Polished faceted flat UI illustration matching matte low-poly fantasy art, only a few broad planar faces, clean bold silhouette, no tiny rivets or engravings. Slight three-quarter perspective turned a little right, soft upper-left lighting. Palette graphite #292B35, ivory #D8CEC0 for steel highlights, brass #D9A441 minimal trim. Entire single object centered in square canvas, 75 percent occupancy with generous transparent margins. Actual transparent alpha background required; no checkerboard image, scene, ground, cast shadow, badge, backing disc, text, letters, numbers, frame, border or watermark.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-969b-74f3-a893-5fea0b3fac02/exec-2a33cdd9-c6e6-41ad-abe3-0848690d79bc.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/types/type_armor_heavy.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "type_armor_aether",
|
||||||
|
"display_name": "Ätherische Rüstung",
|
||||||
|
"faction": "types",
|
||||||
|
"kind": "type_icon",
|
||||||
|
"description_de": "Hohler violetter Geisterschild mit mintfarbener Innenkante und transparentem Innenraum.",
|
||||||
|
"prompt": "Use case: stylized-concept. Asset type: ONE fantasy strategy-game UI armor-type icon, square transparent PNG. Subject: AETHER ARMOR, a SINGLE HOLLOW GHOST SHIELD. Broad pointed shield silhouette formed from a thick faceted violet spectral rim with mint inner edge; center is a large genuinely EMPTY TRANSPARENT opening, no opaque face plate or backing. Top rim has two subtle upward crescent tips and bottom ends in one smooth pointed tip. Two very short broad tapered ghost wisps attach to lower rim, still one compact clear object. Clearly magical hollow defensive shell, no face, no eyes, no skull, no character. It must read distinctly from a metal shield at 32px. Polished faceted flat UI illustration matching matte low-poly fantasy art, a few broad matte planar shapes with restrained mint glow, crisp clean silhouette, no particles or small decoration. Slight three-quarter perspective turned a little right, soft upper-left lighting. Palette violet #9578CF, mint #82DBCE, graphite #292B35 for shadow facets, tiny ivory #D8CEC0 highlights. Single full icon centered in square canvas, 75 percent occupancy with generous transparent margins. Actual transparent alpha background and transparent center, no checkerboard picture, scenery, ground, cast shadow, badge, backing disc, text, letters, numbers, frame, border or watermark.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-969b-74f3-a893-5fea0b3fac02/exec-dd3d2287-ed73-4ecc-9f67-c61bb1512721.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/types/type_armor_aether.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "map_overview",
|
||||||
|
"display_name": "Doppelsteg – Kartenkonzept",
|
||||||
|
"faction": "map",
|
||||||
|
"kind": "map_concept",
|
||||||
|
"description_de": "Zwei parallele Felsstege über einer Schlucht, mit nördlichen Aufmarschflächen, mittleren Baufeldern und südlichen Kernplattformen in Türkis und Koralle.",
|
||||||
|
"prompt": "Original SAUMWACHT fantasy strategy terrain concept art. Matte hand-painted low-poly facets, broad simple polygonal planes, weathered grey stone #657481 and basalt #292B35, moss green #56845A, restrained brass #D9A441. Soft directional illumination from upper left. Orthographic oblique overhead view with camera looking down 55 degrees, no perspective convergence. Calm readable surfaces, no units, characters, text, letters, labels, legend, watermark, UI panels, logos or borrowed game designs. Create a wide landscape overview of one entire small symmetrical two-lane battlefield named Doppelsteg. Two separate long rectangular floating rock terraces running vertically north at top to south at bottom, parallel, with a narrow deep mist-filled chasm between. Both lanes equally wide and equally long; approximate width to length ratio 12:40 for each lane. Each lane consists from north/top to south/bottom of an open spawn apron, a central flat build field subdivided into subtle square stone pavers, a short plain approach, and a small southern raised core dais. Left core is a compact glowing mint crystal #82DBCE held in a brass cradle; right core a coral crystal #E9967C. A thin matching colored brass-inlaid leak stripe crosses each lane directly before its core. Keep the middle build fields empty and clear, stone edges only at outer borders, small tufts of moss confined to borders. No bridges crossing the gap and no extra lanes. Entire two-terrace silhouette visible with margin on all sides. Dark blue-grey mist and distant plain background, restrained atmospheric depth. This is environment art reference, no infographic.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a095b1-2da4-7bd1-9759-74ea0f1615bf/exec-ab60fe13-bb3f-4999-b130-f7e9864f15c9.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/map/map_overview.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "map_lane_floor",
|
||||||
|
"display_name": "Baufeld-Bodenkachel",
|
||||||
|
"faction": "map",
|
||||||
|
"kind": "tile_concept",
|
||||||
|
"description_de": "Flache quadratische Steinplatte mit großen ruhigen Flächen und schmalen Moosfugen für das Baufeld.",
|
||||||
|
"prompt": "Original SAUMWACHT fantasy strategy terrain concept art. Matte hand-painted low-poly facets, broad simple polygonal planes, weathered grey stone #657481 and basalt #292B35, moss green #56845A, restrained brass #D9A441. Soft directional illumination from upper left. Orthographic oblique overhead view with camera looking down 55 degrees, no perspective convergence. Calm readable surfaces, no units, characters, text, letters, labels, legend, watermark, UI panels, logos or borrowed game designs. One isolated square modular build-floor terrain tile representing a 2m by 2m square stone slab, shallow 20cm thickness, shown as a full diamond-shaped oblique square with both side faces visible. Four broad grey slate planes across a flat completely unobstructed top, very thin cracks and tiny moss seams near the outside edges only. No raised objects, no parapet, no crystal, no emblem. All four outer edges straight and modular. Tile centered with ample margin on a flat desaturated blue-grey backdrop; subtle small contact shadow. Entire tile clearly visible. Single tile only, no sheet or multiple views.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a095b1-2da4-7bd1-9759-74ea0f1615bf/exec-0fca82f6-36dc-477c-a226-482fd1acea62.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/map/map_lane_floor.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "map_lane_edge",
|
||||||
|
"display_name": "Lane-Randkachel",
|
||||||
|
"faction": "map",
|
||||||
|
"kind": "tile_concept",
|
||||||
|
"description_de": "Passende Randplatte mit niedriger äußerer Steinbrüstung und kleinem Messingmarker.",
|
||||||
|
"prompt": "Original SAUMWACHT fantasy strategy terrain concept art. Matte hand-painted low-poly facets, broad simple polygonal planes, weathered grey stone #657481 and basalt #292B35, moss green #56845A, restrained brass #D9A441. Soft directional illumination from upper left. Orthographic oblique overhead view with camera looking down 55 degrees, no perspective convergence. Calm readable surfaces, no units, characters, text, letters, labels, legend, watermark, UI panels, logos or borrowed game designs. One isolated square modular lane-edge terrain tile representing a 2m by 2m grey stone slab, shallow 20cm base thickness. Full diamond-shaped oblique square with side faces visible. Flat unobstructed build-floor top with a low straight 30cm retaining stone curb along ONLY the far/right edge; a narrow brass marker is inset into the middle of this curb. Other three edges flat and modular. Tiny moss clumps tucked beneath the outside of the curb. No railing around whole tile, no buildings, no glyphs, no crystal. Tile centered with ample margin on a flat desaturated blue-grey backdrop; subtle small contact shadow. Entire tile visible, single tile only.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a095b1-2da4-7bd1-9759-74ea0f1615bf/exec-16c351f6-4a68-4146-8c0c-fe9bb0f6dd83.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/map/map_lane_edge.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "map_leak_line",
|
||||||
|
"display_name": "Lecklinie-Kachel",
|
||||||
|
"faction": "map",
|
||||||
|
"kind": "tile_concept",
|
||||||
|
"description_de": "Steinplatte mit durchgehendem mintfarbenem Lichtstreifen in einer schmalen Messingschiene als Lecklinie.",
|
||||||
|
"prompt": "Original SAUMWACHT fantasy strategy terrain concept art. Matte hand-painted low-poly facets, broad simple polygonal planes, weathered grey stone #657481 and basalt #292B35, moss green #56845A, restrained brass #D9A441. Soft directional illumination from upper left. Orthographic oblique overhead view with camera looking down 55 degrees, no perspective convergence. Calm readable surfaces, no units, characters, text, letters, labels, legend, watermark, UI panels, logos or borrowed game designs. One isolated square modular leak-line terrain tile representing a 2m by 2m grey stone slab, shallow 20cm thickness. Full diamond-shaped oblique square with side faces visible. Flat stone top crossed from the midpoint of its left edge to the midpoint of its right edge by one thin continuous mint glowing strip #82DBCE inside a narrow flat brass inlay, perfectly flush with the floor, meant to connect into a straight line across adjacent tiles. Keep most top surface plain stone. No raised obstacles, no arrows, no letters, no symbols. Centered with ample margin on a flat desaturated blue-grey backdrop, subtle small contact shadow. Entire tile visible, single tile only.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a095b1-2da4-7bd1-9759-74ea0f1615bf/exec-d7b0bd9b-1ba6-4cf7-a787-60b1564fdf62.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/map/map_leak_line.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ui_panel_a",
|
||||||
|
"display_name": "Breiter UI-Rahmen",
|
||||||
|
"faction": "ui",
|
||||||
|
"kind": "ui_frame",
|
||||||
|
"description_de": "Breiter offener UI-Rahmen mit Basaltfassung, schmaler Messingkante und ruhigen, geraden Seiten.",
|
||||||
|
"prompt": "Create one production 2D UI FRAME PNG for original fantasy strategy game SAUMWACHT. Wide horizontal panel aspect ratio 3:1, frame fills 88% of canvas width. Absolutely straight-on orthographic view, no perspective. An EMPTY OPEN FRAME: both its entire inner rectangular window and the outside of the frame must be real alpha transparency. Render only the rim. Narrow matte basalt #292B35 outer frame, thin warm brass #D9A441 inner bevel, small geometric chamfered corner braces, tiny dark petrol #247A80 inlays; soft upper-left lighting in painted low-poly facets. Straight plain sides suitable for nine-patch stretching, corner decoration contained close to corners. Flat clean game asset, not photoreal. No center fill, no background, no text, no runes, no icons, no ground shadow, no watermark. Single isolated horizontal hollow panel, transparent center is essential.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a095b1-2da4-7bd1-9759-74ea0f1615bf/exec-478f79e7-781f-4b3d-857a-7d8d7f4d1a13.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/ui/ui_panel_a.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ui_panel_b",
|
||||||
|
"display_name": "Hoher UI-Rahmen",
|
||||||
|
"faction": "ui",
|
||||||
|
"kind": "ui_frame",
|
||||||
|
"description_de": "Hoher offener UI-Rahmen für Einheiteninfos, mit Basaltfassung und geometrischen Messingecken.",
|
||||||
|
"prompt": "Create one production 2D UI FRAME PNG for original fantasy strategy game SAUMWACHT. Tall vertical panel aspect ratio 2:3, frame fills 88% of canvas height. Absolutely straight-on orthographic view, no perspective. An EMPTY OPEN FRAME: both its entire inner rectangular window and the outside of the frame must be real alpha transparency. Render only the rim. Narrow matte basalt #292B35 outer frame, thin warm brass #D9A441 inner bevel, small geometric chamfered corner braces, tiny dark petrol #247A80 inlays; soft upper-left lighting in painted low-poly facets. Straight plain sides suitable for nine-patch stretching, corner decoration contained close to corners. Flat clean game asset, not photoreal. No center fill, no background, no text, no runes, no icons, no ground shadow, no watermark. Single isolated vertical hollow panel, transparent center is essential.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a095b1-2da4-7bd1-9759-74ea0f1615bf/exec-94ebaa1d-3e9c-4021-9b94-c9cd89ec09e5.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/ui/ui_panel_b.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ui_button_normal",
|
||||||
|
"display_name": "Button – Normal",
|
||||||
|
"faction": "ui",
|
||||||
|
"kind": "ui_button",
|
||||||
|
"description_de": "Unbeschrifteter petrolfarbener Button mit kantigem Messingrand, dunkler Innenfläche und kleinen geometrischen Eckakzenten.",
|
||||||
|
"prompt": "Use case: stylized-concept. Create one clean standalone reusable NORMAL STATE button graphic for original fantasy strategy game Saumwacht. Wide horizontal aspect ratio 3:1, front facing perfectly flat orthographic UI view, not tilted or isometric. One long dark petrol #1C2E36 rectangular central button surface, thin layered warm brass #D9A441 bevel border, dark basalt #292B35 outside rim, small tasteful geometric corner accents, softly chamfered corners, neutral upper-left lighting. Faceted matte illustrated game UI art with broad crisp color planes, matches hand-illustrated stone-and-brass strategy icons. HUGE plain empty uninterrupted label area in center, no text or symbols anywhere. Symmetric exact geometry. Button occupies about85%canvaswidth65%height with transparent safety margin on all sides. Genuine RGBA transparency outside the button; the button interior itself is solid dark petrol. No checkerboard, no opaque background, no cast shadow, no mockup, no multiplebuttons, no letters, no watermark. Production-ready isolated gameUI element, no flourishes extending outward.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a095b1-2da4-7bd1-9759-74ea0f1615bf/exec-4dd8ed27-251c-403e-a98f-2c48db223706.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/ui/ui_button_normal.png",
|
||||||
|
"status": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ui_button_hover",
|
||||||
|
"display_name": "Button – Hover",
|
||||||
|
"faction": "ui",
|
||||||
|
"kind": "ui_button",
|
||||||
|
"description_de": "Hover-Zustand mit hellerem Messingrand und einer schmalen mintfarbenen Innenkante.",
|
||||||
|
"prompt": "Use case: stylized-concept. Create one clean standalone reusable HOVER STATE button graphic for original fantasy strategy game Saumwacht. Wide horizontal aspect ratio 3:1, front facing perfectly flat orthographic UI view, not tilted or isometric. One long dark petrol #243D47 rectangular central button surface, thin layered bright warm brass #D9A441 bevel border, hairline mint #82DBCE INNER rim, dark basalt #292B35 outside rim, small tasteful geometric folded brass corner accents, softly chamfered corners, neutral upper-left lighting. Faceted matte illustrated game UI art with broad crisp color planes, matches hand-illustrated stone-and-brass strategy icons. HUGE plain empty uninterrupted label area in center, no text or symbols anywhere. Symmetric exact geometry. Button occupies about 85% canvas width and 65% height with transparent safety margin on all sides. Genuine RGBA transparency outside the button; the button interior itself is solid dark petrol. No checkerboard, no opaque background, no cast shadow, no mockup, no multiple buttons, no letters, no watermark. Production-ready isolated game UI element, no flourishes extending outward. Actual transparent background is required: please output a PNG with an alpha channel, not a visual depiction of transparency.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-969b-74f3-a893-5fea0b3fac02/exec-161237e3-3901-4bbc-bd11-ebb3b5fe9663.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/ui/ui_button_hover.png",
|
||||||
|
"status": "generated",
|
||||||
|
"reference_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/ui/ui_button_normal.png",
|
||||||
|
"iterations": [
|
||||||
|
{
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a095b1-2da4-7bd1-9759-74ea0f1615bf/exec-2cb1a707-bff3-49a5-a842-42ecc5578275.png",
|
||||||
|
"prompt": "Edit the supplied normal UI button for the original fantasy strategy game SAUMWACHT. Keep the exact canvas aspect ratio, silhouette, outside alpha margin, chamfered corners, all brass geometric corner details, and broad blank center identical to reference. This is a matching interaction state of the same control, not a redesign. Straight-on orthographic view; matte illustrated low-poly facets, top-left light; no text, icons, symbols, numbers, watermark, external shadow, or glow outside the silhouette. Preserve genuine transparent background and opaque center. Create the HOVER state: brass border subtly brighter warm gold #D9A441, a very thin restrained mint #82DBCE inner rim, center slightly brighter dark petrol. Keep the label area calm and empty.",
|
||||||
|
"reason": "Baked checkerboard; rejected because alpha channel absent."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a095b1-2da4-7bd1-9759-74ea0f1615bf/exec-dc64da6b-4a18-44a5-ae51-e06554e05552.png",
|
||||||
|
"prompt": "Remove the checkerboard backdrop from this UI button asset completely. It is accidentally painted into the image and must be erased to real PNG alpha transparency, not replaced with black, white, grey, or a new checker pattern. Keep every pixel of the actual button artwork, shape, size, position, center fill, metal edge and corner ornament unchanged. Preserve the source 3:1 canvas and button bounding box. No redesign or relighting. Outside the chamfered outer dark silhouette must have alpha=0. The button itself must remain opaque. Return a genuinely transparent RGBA PNG cutout of only the button, with no shadow outside, no background pattern.",
|
||||||
|
"reason": "RGB with painted checkerboard; replaced after verified RGBA generation."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-969b-74f3-a893-5fea0b3fac02/exec-0101f2f9-92df-41a6-ad5d-99a529442c12.png",
|
||||||
|
"prompt": "Use case: precise-object-edit. Input image is the finished normal-state button and is the exact reference and edit target. Create the matching HOVER state of this SAME fantasy strategy game UI button. Preserve exact 3:1 canvas ratio, position, silhouette, size, flat orthographic front view, all chamfered corners, corner facets, bevel thickness, and large blank dark petrol center. Preserve soft upper-left lighting. Change only the color treatment: brighten the brass bevel slightly to warm gold #D9A441, add a very narrow restrained mint #82DBCE line along the inner bevel, make the dark petrol center slightly brighter. Matte faceted illustrated UI style. Important output: PNG with a real alpha channel, fully transparent alpha=0 everywhere outside the physical button silhouette, while button center and metal are fully opaque. The reference background is transparency: retain genuine transparency, NEVER draw a checkerboard or solid-color backdrop. No glow or shadow outside the silhouette. No text, letters, symbols, icons, numbers or watermark. Return just one button on a truly transparent 3:1 canvas.",
|
||||||
|
"reason": "Reference edit returned RGB with baked checkerboard; rejected after PNG header check."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"generation_strategy": "fresh generation after reference edit failed transparency",
|
||||||
|
"alpha_verification": {
|
||||||
|
"mode": "RGBA",
|
||||||
|
"width": 2172,
|
||||||
|
"height": 724,
|
||||||
|
"extrema": [
|
||||||
|
0,
|
||||||
|
255
|
||||||
|
],
|
||||||
|
"corner_alpha": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"center_alpha": 254,
|
||||||
|
"note": "Native output preserved; center alpha 254 also occurs in the accepted normal template."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ui_button_pressed",
|
||||||
|
"display_name": "Button – Gedrückt",
|
||||||
|
"faction": "ui",
|
||||||
|
"kind": "ui_button",
|
||||||
|
"description_de": "Gedrückter Button mit abgedunkelter, eingesenkter Petrolfläche und umgekehrtem Innenrelief.",
|
||||||
|
"prompt": "Edit the supplied normal UI button for the original fantasy strategy game SAUMWACHT. Keep the exact canvas aspect ratio, silhouette, outside alpha margin, chamfered corners, all brass geometric corner details, and broad blank center identical to reference. This is a matching interaction state of the same control, not a redesign. Straight-on orthographic view; matte illustrated low-poly facets, top-left light; no text, icons, symbols, numbers, watermark, external shadow, or glow outside the silhouette. Preserve genuine transparent background and opaque center. Create the PRESSED state: center recessed inward and darker petrol, inverted inner bevel with dark top and left and restrained lower-right rim highlight. Brass outer edge slightly darker. No displacement of the control geometry.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a095b1-2da4-7bd1-9759-74ea0f1615bf/exec-4b498a09-0bc1-414f-ae71-05e3de6bc962.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/ui/ui_button_pressed.png",
|
||||||
|
"status": "generated",
|
||||||
|
"reference_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/ui/ui_button_normal.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ui_button_disabled",
|
||||||
|
"display_name": "Button – Deaktiviert",
|
||||||
|
"faction": "ui",
|
||||||
|
"kind": "ui_button",
|
||||||
|
"description_de": "Deaktivierter Button in entsättigtem Schiefergrau und gedämpftem Messing.",
|
||||||
|
"prompt": "Use case: stylized-concept. Create one clean standalone reusable DISABLED STATE button graphic for original fantasy strategy game Saumwacht. Wide horizontal aspect ratio 3:1, front facing perfectly flat orthographic UI view, not tilted or isometric. One long desaturated dark slate #394550 rectangular central button surface, thin layered DULL MUTED GRAY-BROWN BRASS bevel border (desaturated brass, not bright gold), dark basalt #292B35 outside rim, small tasteful geometric folded corner accents, softly chamfered corners, subtle upper-left lighting. Reduced highlights and contrast make the button visibly inactive. Faceted matte illustrated game UI art with broad crisp color planes, matches hand-illustrated stone-and-brass strategy icons. HUGE plain empty uninterrupted label area in center, no text or symbols anywhere. Symmetric exact geometry. Button occupies about 85% canvas width and 65% height with transparent safety margin on all sides. Genuine RGBA transparency outside the button; button interior itself is solid opaque dark slate. No checkerboard, no opaque background, no cast shadow, no mockup, no multiple buttons, no letters, no watermark, no colored glow. Production-ready isolated game UI element, no flourishes extending outward. Actual transparent background required: output a PNG with an alpha channel, not a visual depiction of transparency.",
|
||||||
|
"generation_mode": "built-in",
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a096fb-969b-74f3-a893-5fea0b3fac02/exec-2e37bcef-6fd2-4110-b43d-4033bdc211aa.png",
|
||||||
|
"master_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/ui/ui_button_disabled.png",
|
||||||
|
"status": "generated",
|
||||||
|
"reference_path": "/home/martin/Dokumente/Projekte/TowerDefense/saumwacht-godot/assets-source/phase3/masters/ui/ui_button_normal.png",
|
||||||
|
"iterations": [
|
||||||
|
{
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a095b1-2da4-7bd1-9759-74ea0f1615bf/exec-3577da02-efb1-4648-8f55-0b4969fa5667.png",
|
||||||
|
"prompt": "Edit the supplied normal UI button for the original fantasy strategy game SAUMWACHT. Keep the exact canvas aspect ratio, silhouette, outside alpha margin, chamfered corners, all brass geometric corner details, and broad blank center identical to reference. This is a matching interaction state of the same control, not a redesign. Straight-on orthographic view; matte illustrated low-poly facets, top-left light; no text, icons, symbols, numbers, watermark, external shadow, or glow outside the silhouette. Preserve genuine transparent background and opaque center. Create the DISABLED state: desaturate face to slate #536375 with dark values, desaturate brass to dull grey-brown, reduce contrast and highlights. Entire face and border remain fully opaque; only outside stays transparent.",
|
||||||
|
"reason": "Baked checkerboard; rejected because alpha channel absent."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source_path": "/home/martin/.codex/generated_images/01a095b1-2da4-7bd1-9759-74ea0f1615bf/exec-292a41ce-3b52-42c1-9190-7829cc3285e1.png",
|
||||||
|
"prompt": "Remove the checkerboard backdrop from this UI button asset completely. It is accidentally painted into the image and must be erased to real PNG alpha transparency, not replaced with black, white, grey, or a new checker pattern. Keep every pixel of the actual button artwork, shape, size, position, center fill, metal edge and corner ornament unchanged. Preserve the source 3:1 canvas and button bounding box. No redesign or relighting. Outside the chamfered outer dark silhouette must have alpha=0. The button itself must remain opaque. Return a genuinely transparent RGBA PNG cutout of only the button, with no shadow outside, no background pattern.",
|
||||||
|
"reason": "RGB with painted checkerboard; replaced after verified RGBA generation."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"generation_strategy": "fresh generation after reference edit failed transparency",
|
||||||
|
"alpha_verification": {
|
||||||
|
"mode": "RGBA",
|
||||||
|
"width": 2172,
|
||||||
|
"height": 724,
|
||||||
|
"extrema": [
|
||||||
|
0,
|
||||||
|
255
|
||||||
|
],
|
||||||
|
"corner_alpha": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"center_alpha": 252,
|
||||||
|
"note": "Native RGBA output preserved without pixel editing; generated center alpha is 252/255."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
533
assets-source/phase3/asset_plan.json
Normal file
|
|
@ -0,0 +1,533 @@
|
||||||
|
{
|
||||||
|
"phase": 3,
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"id": "g01",
|
||||||
|
"display_name": "Kohlenbeißer",
|
||||||
|
"faction": "glutpakt",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "g02",
|
||||||
|
"display_name": "Schlackenklaue",
|
||||||
|
"faction": "glutpakt",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "g03",
|
||||||
|
"display_name": "Schlackenwacht",
|
||||||
|
"faction": "glutpakt",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "g04",
|
||||||
|
"display_name": "Basaltbastion",
|
||||||
|
"faction": "glutpakt",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "g05",
|
||||||
|
"display_name": "Funkenlanze",
|
||||||
|
"faction": "glutpakt",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "g06",
|
||||||
|
"display_name": "Glasballiste",
|
||||||
|
"faction": "glutpakt",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "g07",
|
||||||
|
"display_name": "Aschenwerfer",
|
||||||
|
"faction": "glutpakt",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "g08",
|
||||||
|
"display_name": "Kraterrufer",
|
||||||
|
"faction": "glutpakt",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "g09",
|
||||||
|
"display_name": "Herdhüter",
|
||||||
|
"faction": "glutpakt",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "g10",
|
||||||
|
"display_name": "Sonnenesse",
|
||||||
|
"faction": "glutpakt",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "m01",
|
||||||
|
"display_name": "Kesselwicht",
|
||||||
|
"faction": "nietenkonvent",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "m02",
|
||||||
|
"display_name": "Druckhüter",
|
||||||
|
"faction": "nietenkonvent",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "m03",
|
||||||
|
"display_name": "Scherschreiter",
|
||||||
|
"faction": "nietenkonvent",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "m04",
|
||||||
|
"display_name": "Sechsklingler",
|
||||||
|
"faction": "nietenkonvent",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "m05",
|
||||||
|
"display_name": "Bolzenknecht",
|
||||||
|
"faction": "nietenkonvent",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "m06",
|
||||||
|
"display_name": "Linienmeister",
|
||||||
|
"faction": "nietenkonvent",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "m07",
|
||||||
|
"display_name": "Klangkolben",
|
||||||
|
"faction": "nietenkonvent",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "m08",
|
||||||
|
"display_name": "Resonanzramme",
|
||||||
|
"faction": "nietenkonvent",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "m09",
|
||||||
|
"display_name": "Taktspindel",
|
||||||
|
"faction": "nietenkonvent",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "m10",
|
||||||
|
"display_name": "Taktprälat",
|
||||||
|
"faction": "nietenkonvent",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "n01",
|
||||||
|
"display_name": "Saummaske",
|
||||||
|
"faction": "nebelchor",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "n02",
|
||||||
|
"display_name": "Grenzmaske",
|
||||||
|
"faction": "nebelchor",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "n03",
|
||||||
|
"display_name": "Fadentänzer",
|
||||||
|
"faction": "nebelchor",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "n04",
|
||||||
|
"display_name": "Saumzertrenner",
|
||||||
|
"faction": "nebelchor",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "n05",
|
||||||
|
"display_name": "Fernflüsterer",
|
||||||
|
"faction": "nebelchor",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "n06",
|
||||||
|
"display_name": "Fernrufer",
|
||||||
|
"faction": "nebelchor",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "n07",
|
||||||
|
"display_name": "Glockenschlepper",
|
||||||
|
"faction": "nebelchor",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "n08",
|
||||||
|
"display_name": "Hohlgeläut",
|
||||||
|
"faction": "nebelchor",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "n09",
|
||||||
|
"display_name": "Maskenkantor",
|
||||||
|
"faction": "nebelchor",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "n10",
|
||||||
|
"display_name": "Vielstimmenkantor",
|
||||||
|
"faction": "nebelchor",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "w01",
|
||||||
|
"display_name": "Dornhopser",
|
||||||
|
"faction": "wurzelbund",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "w02",
|
||||||
|
"display_name": "Rankenschnapper",
|
||||||
|
"faction": "wurzelbund",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "w03",
|
||||||
|
"display_name": "Korkrücken",
|
||||||
|
"faction": "wurzelbund",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "w04",
|
||||||
|
"display_name": "Ringborke",
|
||||||
|
"faction": "wurzelbund",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "w05",
|
||||||
|
"display_name": "Samenschleuder",
|
||||||
|
"faction": "wurzelbund",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "w06",
|
||||||
|
"display_name": "Zapfenkrone",
|
||||||
|
"faction": "wurzelbund",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "w07",
|
||||||
|
"display_name": "Sporenkelch",
|
||||||
|
"faction": "wurzelbund",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "w08",
|
||||||
|
"display_name": "Pilzkathedrale",
|
||||||
|
"faction": "wurzelbund",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "w09",
|
||||||
|
"display_name": "Tauhüter",
|
||||||
|
"faction": "wurzelbund",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "w10",
|
||||||
|
"display_name": "Quellgeflecht",
|
||||||
|
"faction": "wurzelbund",
|
||||||
|
"kind": "unit_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "s01",
|
||||||
|
"display_name": "Nadelspäher",
|
||||||
|
"faction": "neutral",
|
||||||
|
"kind": "mercenary_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "s02",
|
||||||
|
"display_name": "Rostschieber",
|
||||||
|
"faction": "neutral",
|
||||||
|
"kind": "mercenary_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "s03",
|
||||||
|
"display_name": "Dunstsänger",
|
||||||
|
"faction": "neutral",
|
||||||
|
"kind": "mercenary_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "s04",
|
||||||
|
"display_name": "Keilbrecher",
|
||||||
|
"faction": "neutral",
|
||||||
|
"kind": "mercenary_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "s05",
|
||||||
|
"display_name": "Risszehrer",
|
||||||
|
"faction": "neutral",
|
||||||
|
"kind": "mercenary_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "res_gold",
|
||||||
|
"display_name": "Gold",
|
||||||
|
"faction": "resources",
|
||||||
|
"kind": "resource_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "res_sparks",
|
||||||
|
"display_name": "Funken",
|
||||||
|
"faction": "resources",
|
||||||
|
"kind": "resource_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "res_income",
|
||||||
|
"display_name": "Einkommen",
|
||||||
|
"faction": "resources",
|
||||||
|
"kind": "resource_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "res_core",
|
||||||
|
"display_name": "Kernleben",
|
||||||
|
"faction": "resources",
|
||||||
|
"kind": "resource_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "worker_funkenleser",
|
||||||
|
"display_name": "Funkenleser",
|
||||||
|
"faction": "neutral",
|
||||||
|
"kind": "worker_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "type_damage_blade",
|
||||||
|
"display_name": "Klinge",
|
||||||
|
"faction": "types",
|
||||||
|
"kind": "type_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "type_damage_pierce",
|
||||||
|
"display_name": "Durchschlag",
|
||||||
|
"faction": "types",
|
||||||
|
"kind": "type_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "type_damage_pulse",
|
||||||
|
"display_name": "Impuls",
|
||||||
|
"faction": "types",
|
||||||
|
"kind": "type_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "type_armor_light",
|
||||||
|
"display_name": "Leichte Rüstung",
|
||||||
|
"faction": "types",
|
||||||
|
"kind": "type_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "type_armor_heavy",
|
||||||
|
"display_name": "Schwere Rüstung",
|
||||||
|
"faction": "types",
|
||||||
|
"kind": "type_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "type_armor_aether",
|
||||||
|
"display_name": "Ätherische Rüstung",
|
||||||
|
"faction": "types",
|
||||||
|
"kind": "type_icon",
|
||||||
|
"import_size_limit": 256,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "map_overview",
|
||||||
|
"display_name": "Doppelsteg – Kartenkonzept",
|
||||||
|
"faction": "map",
|
||||||
|
"kind": "map_concept",
|
||||||
|
"import_size_limit": 0,
|
||||||
|
"requires_alpha": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "map_lane_floor",
|
||||||
|
"display_name": "Baufeld-Bodenkachel",
|
||||||
|
"faction": "map",
|
||||||
|
"kind": "tile_concept",
|
||||||
|
"import_size_limit": 0,
|
||||||
|
"requires_alpha": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "map_lane_edge",
|
||||||
|
"display_name": "Lane-Randkachel",
|
||||||
|
"faction": "map",
|
||||||
|
"kind": "tile_concept",
|
||||||
|
"import_size_limit": 0,
|
||||||
|
"requires_alpha": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "map_leak_line",
|
||||||
|
"display_name": "Lecklinie-Kachel",
|
||||||
|
"faction": "map",
|
||||||
|
"kind": "tile_concept",
|
||||||
|
"import_size_limit": 0,
|
||||||
|
"requires_alpha": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ui_panel_a",
|
||||||
|
"display_name": "Breiter UI-Rahmen",
|
||||||
|
"faction": "ui",
|
||||||
|
"kind": "ui_frame",
|
||||||
|
"import_size_limit": 0,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ui_panel_b",
|
||||||
|
"display_name": "Hoher UI-Rahmen",
|
||||||
|
"faction": "ui",
|
||||||
|
"kind": "ui_frame",
|
||||||
|
"import_size_limit": 0,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ui_button_normal",
|
||||||
|
"display_name": "Button – Normal",
|
||||||
|
"faction": "ui",
|
||||||
|
"kind": "ui_button",
|
||||||
|
"import_size_limit": 0,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ui_button_hover",
|
||||||
|
"display_name": "Button – Hover",
|
||||||
|
"faction": "ui",
|
||||||
|
"kind": "ui_button",
|
||||||
|
"import_size_limit": 0,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ui_button_pressed",
|
||||||
|
"display_name": "Button – Gedrückt",
|
||||||
|
"faction": "ui",
|
||||||
|
"kind": "ui_button",
|
||||||
|
"import_size_limit": 0,
|
||||||
|
"requires_alpha": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ui_button_disabled",
|
||||||
|
"display_name": "Button – Deaktiviert",
|
||||||
|
"faction": "ui",
|
||||||
|
"kind": "ui_button",
|
||||||
|
"import_size_limit": 0,
|
||||||
|
"requires_alpha": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
74
assets-source/phase3/catalog.md
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
# Phase 3 · Assetkatalog
|
||||||
|
|
||||||
|
66 einzeln generierte Assets für Saumwacht. Alle PNG-Masters bleiben in nativer Auflösung erhalten; die 56 Icons werden in Godot als 256 × 256 Texturen importiert.
|
||||||
|
|
||||||
|
[Interaktive Galerie](gallery.html) · [Prompts und Herkunft](all-prompts.json)
|
||||||
|
|
||||||
|
| ID | Name | Gruppe | PNG | Beschreibung |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `g01` | [Kohlenbeißer](masters/glutpakt/unit_g01_kohlenbeisser_icon.png) | glutpakt | 1254 × 1254 | Kompakter Basaltkörper mit glühendem Maul, kräftigen Klauen und Messingakzenten. |
|
||||||
|
| `g02` | [Schlackenklaue](masters/glutpakt/unit_g02_schlackenklaue_icon.png) | glutpakt | 1254 × 1254 | Schwer gepanzerter Kohlenkörper mit breiten Basaltschultern, glühendem Maul und großen messingbesetzten Klauen. |
|
||||||
|
| `g03` | [Schlackenwacht](masters/glutpakt/unit_g03_schlackenwacht_icon.png) | glutpakt | 1254 × 1254 | Gedrungener Basaltwächter mit breiter Schildfront, schmaler Ofenglut und kurzen, kräftigen Armen. |
|
||||||
|
| `g04` | [Basaltbastion](masters/glutpakt/unit_g04_basaltbastion_icon.png) | glutpakt | 1254 × 1254 | Lebende Basaltfestung mit gestuften Schultern, gepanzerter Schildfront und glühendem Ofenkern. |
|
||||||
|
| `g05` | [Funkenlanze](masters/glutpakt/unit_g05_funkenlanze_icon.png) | glutpakt | 1254 × 1254 | Schlanker Glutkonstrukt-Schütze mit langer Messinglanze und orange leuchtender Glasspitze. |
|
||||||
|
| `g06` | [Glasballiste](masters/glutpakt/unit_g06_glasballiste_icon.png) | glutpakt | 1254 × 1254 | Bewegliche Glasballiste mit großen orangefarbenen Kristallarmen, langem Bolzen und stabilem Basaltunterbau. |
|
||||||
|
| `g07` | [Aschenwerfer](masters/glutpakt/unit_g07_aschenwerfer_icon.png) | glutpakt | 1254 × 1254 | Gedrungener Aschenwerfer mit breitem Mörserkelch, glühender Kohle und kurzen Basaltbeinen. |
|
||||||
|
| `g08` | [Kraterrufer](masters/glutpakt/unit_g08_kraterrufer_icon.png) | glutpakt | 1254 × 1254 | Schwerer Kraterkörper mit großem Vulkankelch, glühendem Inneren und gestuften Basaltplatten. |
|
||||||
|
| `g09` | [Herdhüter](masters/glutpakt/unit_g09_herdhueter_icon.png) | glutpakt | 1254 × 1254 | Kleiner Herdwächter mit offenem Glutofen und einer schützenden Messingschale vor dem Körper. |
|
||||||
|
| `g10` | [Sonnenesse](masters/glutpakt/unit_g10_sonnenesse_icon.png) | glutpakt | 1254 × 1254 | Große Sonnenesse mit strahlender Ofenöffnung, Messingkrone und breiter wärmender Glutschale. |
|
||||||
|
| `m01` | [Kesselwicht](masters/nietenkonvent/unit_m01_kesselwicht_icon.png) | nietenkonvent | 1254 × 1254 | Gedrungener Kesseltank mit rundem Petrolkessel, Keramikgesicht und kurzen Stempelbeinen. |
|
||||||
|
| `m02` | [Druckhüter](masters/nietenkonvent/unit_m02_druckhueter_icon.png) | nietenkonvent | 1254 × 1254 | Breiter gepanzerter Drucktank mit Schulterkesseln und massivem Schildarm. |
|
||||||
|
| `m03` | [Scherschreiter](masters/nietenkonvent/unit_m03_scherschreiter_icon.png) | nietenkonvent | 1254 × 1254 | Schmaler Schreitautomat mit zwei großen Scherenarmen. |
|
||||||
|
| `m04` | [Sechsklingler](masters/nietenkonvent/unit_m04_sechsklingler_icon.png) | nietenkonvent | 1254 × 1254 | Breiter Schreitautomat mit sechs deutlich getrennten Klingen. |
|
||||||
|
| `m05` | [Bolzenknecht](masters/nietenkonvent/unit_m05_bolzenknecht_icon.png) | nietenkonvent | 1254 × 1254 | Schlanker Bolzenautomat mit einem deutlich lesbaren Armwerfer. |
|
||||||
|
| `m06` | [Linienmeister](masters/nietenkonvent/unit_m06_linienmeister_icon.png) | nietenkonvent | 1254 × 1254 | Hoher Präzisionsautomat mit einer langen integrierten Balliste. |
|
||||||
|
| `m07` | [Klangkolben](masters/nietenkonvent/unit_m07_klangkolben_icon.png) | nietenkonvent | 1254 × 1254 | Kompakter Artillerieautomat mit einem großen Glockenrohr und Kolben. |
|
||||||
|
| `m08` | [Resonanzramme](masters/nietenkonvent/unit_m08_resonanzramme_icon.png) | nietenkonvent | 1254 × 1254 | Breiter Artillerieautomat mit zwei großen Resonanzkesseln. |
|
||||||
|
| `m09` | [Taktspindel](masters/nietenkonvent/unit_m09_taktspindel_icon.png) | nietenkonvent | 1254 × 1254 | Kleiner Taktgeber auf Spindelbeinen mit einem Dirigentenstab. |
|
||||||
|
| `m10` | [Taktprälat](masters/nietenkonvent/unit_m10_taktpraelat_icon.png) | nietenkonvent | 1254 × 1254 | Großer Uhrwerkdirigent mit kronenförmigem Metronom und breitem Mantelkörper. |
|
||||||
|
| `n01` | [Saummaske](masters/nebelchor/unit_n01_saummaske_icon.png) | nebelchor | 1254 × 1254 | Kompakter gefalteter Nebelkörper mit heller Ovalmaske, Sichelarmen und zwei Standranken. |
|
||||||
|
| `n02` | [Grenzmaske](masters/nebelchor/unit_n02_grenzmaske_icon.png) | nebelchor | 1254 × 1254 | Breiter Nebeltank mit heller Ovalmaske und mehreren geschichteten Sichelschilden. |
|
||||||
|
| `n03` | [Fadentänzer](masters/nebelchor/unit_n03_fadentaenzer_icon.png) | nebelchor | 1254 × 1254 | Schlanker maskierter Nebeltänzer mit zwei versetzt geführten Fadenklingen und langen Standranken. |
|
||||||
|
| `n04` | [Saumzertrenner](masters/nebelchor/unit_n04_saumzertrenner_icon.png) | nebelchor | 1254 × 1254 | Hoher Nebelklingenkrieger mit Sichelkamm und großer geöffneter Sichelschere. |
|
||||||
|
| `n05` | [Fernflüsterer](masters/nebelchor/unit_n05_fernfluesterer_icon.png) | nebelchor | 1254 × 1254 | Schmale gefaltete Maskenfigur mit langem Flüsterhorn und mintfarbener Schallöffnung. |
|
||||||
|
| `n06` | [Fernrufer](masters/nebelchor/unit_n06_fernrufer_icon.png) | nebelchor | 1254 × 1254 | Aufgewerteter Nebelrufer mit zwei weit geschwungenen Resonanzhörnern und mintfarbenen Schallöffnungen. |
|
||||||
|
| `n07` | [Glockenschlepper](masters/nebelchor/unit_n07_glockenschlepper_icon.png) | nebelchor | 1254 × 1254 | Gedrungener Nebelträger mit großer heller Glocke und deutlich sichtbarem mintfarbenem Klöppel. |
|
||||||
|
| `n08` | [Hohlgeläut](masters/nebelchor/unit_n08_hohlgelaeut_icon.png) | nebelchor | 1254 × 1254 | Hoher lebender Kathedralenglockenkörper mit drei Sichelspitzen und mintfarbenem Klöppel im offenen Brustraum. |
|
||||||
|
| `n09` | [Maskenkantor](masters/nebelchor/unit_n09_maskenkantor_icon.png) | nebelchor | 1254 × 1254 | Kompakter maskierter Nebelsänger mit einer frei schwebenden Begleitmaske über dem erhobenen Sichelarm. |
|
||||||
|
| `n10` | [Vielstimmenkantor](masters/nebelchor/unit_n10_vielstimmenkantor_icon.png) | nebelchor | 1254 × 1254 | Gekrönter Nebelchor-Kantor mit breitem Sichelkragen und drei getrennt schwebenden Begleitmasken. |
|
||||||
|
| `w01` | [Dornhopser](masters/wurzelbund/unit_w01_dornhopser_icon.png) | wurzelbund | 1254 × 1254 | Kleine hüpfende Dornkapsel auf breiten Wurzelfüßen. |
|
||||||
|
| `w02` | [Rankenschnapper](masters/wurzelbund/unit_w02_rankenschnapper_icon.png) | wurzelbund | 1254 × 1254 | Breiter Rankenräuber mit großem Dornenmaul und Wurzelfüßen. |
|
||||||
|
| `w03` | [Korkrücken](masters/wurzelbund/unit_w03_korkruecken_icon.png) | wurzelbund | 1254 × 1254 | Gedrungener Tank mit breitem Korkpanzer, Moos und Wurzelfüßen. |
|
||||||
|
| `w04` | [Ringborke](masters/wurzelbund/unit_w04_ringborke_icon.png) | wurzelbund | 1254 × 1254 | Großer Wurzeltank mit konzentrischen Holzringen als massiver Panzerung. |
|
||||||
|
| `w05` | [Samenschleuder](masters/wurzelbund/unit_w05_samenschleuder_icon.png) | wurzelbund | 1254 × 1254 | Schlanker Wurzelfernkämpfer mit gebogener Ranke und offener Samenkapsel als Wurfschale. |
|
||||||
|
| `w06` | [Zapfenkrone](masters/wurzelbund/unit_w06_zapfenkrone_icon.png) | wurzelbund | 1254 × 1254 | Hoher Samenschütze mit großer Zapfenkrone, kräftigen Ranken und Blattschalen. |
|
||||||
|
| `w07` | [Sporenkelch](masters/wurzelbund/unit_w07_sporenkelch_icon.png) | wurzelbund | 1254 × 1254 | Niedriger offener Pilzkelch mit rosiger Sporenmulde und breiten Wurzelfüßen. |
|
||||||
|
| `w08` | [Pilzkathedrale](masters/wurzelbund/unit_w08_pilzkathedrale_icon.png) | wurzelbund | 1254 × 1254 | Große mobile Pilzarchitektur mit gestaffelten rosigen Pilzdächern und massivem Wurzelwerk. |
|
||||||
|
| `w09` | [Tauhüter](masters/wurzelbund/unit_w09_tauhueter_icon.png) | wurzelbund | 1254 × 1254 | Kleiner Blatthüter mit einzelnem Tautropfen in einer schützenden Blattschale. |
|
||||||
|
| `w10` | [Quellgeflecht](masters/wurzelbund/unit_w10_quellgeflecht_icon.png) | wurzelbund | 1254 × 1254 | Großer Heiler aus verflochtenen Wurzeln mit Quellwasserschale unter einem Blüten- und Blätterdach. |
|
||||||
|
| `s01` | [Nadelspäher](masters/neutral/unit_s01_nadelspaeher_icon.png) | neutral | 1254 × 1254 | Schlanker maskierter Söldnerspäher mit kompaktem Nadelwerfer. |
|
||||||
|
| `s02` | [Rostschieber](masters/neutral/unit_s02_rostschieber_icon.png) | neutral | 1254 × 1254 | Gedrungener schwerer Söldner mit Rostpanzer und großem Rammschild. |
|
||||||
|
| `s03` | [Dunstsänger](masters/neutral/unit_s03_dunstsaenger_icon.png) | neutral | 1254 × 1254 | Verhüllter violetter Dunstsänger mit großem Resonanzhorn. |
|
||||||
|
| `s04` | [Keilbrecher](masters/neutral/unit_s04_keilbrecher_icon.png) | neutral | 1254 × 1254 | Schwerer dunkelgrauer Keilbrecher mit großem Keilhammer. |
|
||||||
|
| `s05` | [Risszehrer](masters/neutral/unit_s05_risszehrer_icon.png) | neutral | 1254 × 1254 | Hohler Risszehrer mit mintviolettem Schlund und magischer Entzugskugel. |
|
||||||
|
| `res_gold` | [Gold](masters/resources/res_gold.png) | resources | 1254 × 1254 | Eine große facettierte Goldmünze mit zwei kleineren Münzen. |
|
||||||
|
| `res_sparks` | [Funken](masters/resources/res_sparks.png) | resources | 1254 × 1254 | Kleines Messing-Glasgefäß mit leuchtender mintfarbener Funkenenergie. |
|
||||||
|
| `res_income` | [Einkommen](masters/resources/res_income.png) | resources | 1254 × 1254 | Messingsiegel mit drei von links nach rechts ansteigenden Münzstapeln. |
|
||||||
|
| `res_core` | [Kernleben](masters/resources/res_core.png) | resources | 1254 × 1254 | Korallfarbenes facettiertes Kristallherz in einer kleinen Messingfassung. |
|
||||||
|
| `worker_funkenleser` | [Funkenleser](masters/neutral/worker_funkenleser_icon.png) | neutral | 1254 × 1254 | Kleine Lehmgestalt im Arbeitstuch mit gegabeltem Funkensammler und Glasbehälter. |
|
||||||
|
| `type_damage_blade` | [Klinge](masters/types/type_damage_blade.png) | types | 1254 × 1254 | Einzelne gebogene Elfenbeinklinge mit Messingparierstange und Graphitgriff. |
|
||||||
|
| `type_damage_pierce` | [Durchschlag](masters/types/type_damage_pierce.png) | types | 1254 × 1254 | Lange Messingpfeilspitze durchstößt eine kleine graphitfarbene Rautenplatte. |
|
||||||
|
| `type_damage_pulse` | [Impuls](masters/types/type_damage_pulse.png) | types | 1254 × 1254 | Mintfarbener facettierter Sternkern zwischen zwei offenen graphitfarbenen Ringhälften. |
|
||||||
|
| `type_armor_light` | [Leichte Rüstung](masters/types/type_armor_light.png) | types | 1254 × 1254 | Dünne blattförmige Lederpanzerplatte in warmem Ocker mit einfachem Graphitriemen. |
|
||||||
|
| `type_armor_heavy` | [Schwere Rüstung](masters/types/type_armor_heavy.png) | types | 1254 × 1254 | Breite schwere Schildplatte aus drei Graphitstahllagen mit hellen Kanten und Messingverstärkung. |
|
||||||
|
| `type_armor_aether` | [Ätherische Rüstung](masters/types/type_armor_aether.png) | types | 1254 × 1254 | Hohler violetter Geisterschild mit mintfarbener Innenkante und transparentem Innenraum. |
|
||||||
|
| `map_overview` | [Doppelsteg – Kartenkonzept](masters/map/map_overview.png) | map | 1374 × 1145 | Zwei parallele Felsstege über einer Schlucht, mit nördlichen Aufmarschflächen, mittleren Baufeldern und südlichen Kernplattformen in Türkis und Koralle. |
|
||||||
|
| `map_lane_floor` | [Baufeld-Bodenkachel](masters/map/map_lane_floor.png) | map | 1254 × 1254 | Flache quadratische Steinplatte mit großen ruhigen Flächen und schmalen Moosfugen für das Baufeld. |
|
||||||
|
| `map_lane_edge` | [Lane-Randkachel](masters/map/map_lane_edge.png) | map | 1254 × 1254 | Passende Randplatte mit niedriger äußerer Steinbrüstung und kleinem Messingmarker. |
|
||||||
|
| `map_leak_line` | [Lecklinie-Kachel](masters/map/map_leak_line.png) | map | 1254 × 1254 | Steinplatte mit durchgehendem mintfarbenem Lichtstreifen in einer schmalen Messingschiene als Lecklinie. |
|
||||||
|
| `ui_panel_a` | [Breiter UI-Rahmen](masters/ui/ui_panel_a.png) | ui | 2172 × 724 | Breiter offener UI-Rahmen mit Basaltfassung, schmaler Messingkante und ruhigen, geraden Seiten. |
|
||||||
|
| `ui_panel_b` | [Hoher UI-Rahmen](masters/ui/ui_panel_b.png) | ui | 1024 × 1536 | Hoher offener UI-Rahmen für Einheiteninfos, mit Basaltfassung und geometrischen Messingecken. |
|
||||||
|
| `ui_button_normal` | [Button – Normal](masters/ui/ui_button_normal.png) | ui | 2172 × 724 | Unbeschrifteter petrolfarbener Button mit kantigem Messingrand, dunkler Innenfläche und kleinen geometrischen Eckakzenten. |
|
||||||
|
| `ui_button_hover` | [Button – Hover](masters/ui/ui_button_hover.png) | ui | 2172 × 724 | Hover-Zustand mit hellerem Messingrand und einer schmalen mintfarbenen Innenkante. |
|
||||||
|
| `ui_button_pressed` | [Button – Gedrückt](masters/ui/ui_button_pressed.png) | ui | 2172 × 724 | Gedrückter Button mit abgedunkelter, eingesenkter Petrolfläche und umgekehrtem Innenrelief. |
|
||||||
|
| `ui_button_disabled` | [Button – Deaktiviert](masters/ui/ui_button_disabled.png) | ui | 2172 × 724 | Deaktivierter Button in entsättigtem Schiefergrau und gedämpftem Messing. |
|
||||||
891
assets-source/phase3/gallery.html
Normal file
|
|
@ -0,0 +1,891 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="color-scheme" content="dark"><title>Saumwacht · Assetgalerie · Phase 3</title>
|
||||||
|
<style>
|
||||||
|
:root { color-scheme: dark; --page:#101a20; --panel:#18262e; --line:#30424a; --ink:#edf0e9; --muted:#a9b9b8; --gold:#d9b66b; --mint:#82dbce; --icon-size:128px; }
|
||||||
|
* { box-sizing:border-box; }
|
||||||
|
body { margin:0; background:var(--page); color:var(--ink); font:15px/1.55 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; }
|
||||||
|
button,input,select { font:inherit; }
|
||||||
|
button,select { cursor:pointer; }
|
||||||
|
a { color:var(--mint); text-underline-offset:4px; }
|
||||||
|
a:hover { color:#fff; }
|
||||||
|
:focus-visible { outline:3px solid var(--mint); outline-offset:4px; }
|
||||||
|
.skip { position:absolute; top:-100px; left:20px; padding:12px; background:var(--page); z-index:20; }
|
||||||
|
.skip:focus { top:10px; }
|
||||||
|
.shell { max-width:1580px; margin:0 auto; padding:0 36px; }
|
||||||
|
.hero { padding:48px 0 28px; display:flex; gap:36px; justify-content:space-between; align-items:flex-end; border-bottom:1px solid var(--line); }
|
||||||
|
.kicker { margin:0 0 12px; color:var(--gold); text-transform:uppercase; letter-spacing:.2em; font-size:11px; font-weight:750; }
|
||||||
|
h1 { margin:0; font-size:clamp(36px,5vw,66px); letter-spacing:-.045em; line-height:1.05; font-weight:650; }
|
||||||
|
.intro { margin:18px 0 0; color:var(--muted); max-width:700px; }
|
||||||
|
.stats { display:flex; gap:32px; margin:0 0 2px; flex-shrink:0; }
|
||||||
|
.stats div { min-width:68px; display:flex; flex-direction:column; }
|
||||||
|
.stats dt { color:var(--muted); font-size:11px; text-transform:uppercase; letter-spacing:.1em; }
|
||||||
|
.stats dd { order:-1; margin:0 0 6px; font-size:35px; line-height:1; color:var(--gold); font-variant-numeric:tabular-nums; }
|
||||||
|
.toolbar { margin:26px 0 0; padding:22px; background:var(--panel); border:1px solid var(--line); border-radius:14px; }
|
||||||
|
.filter-row { display:grid; grid-template-columns:minmax(220px,2fr) repeat(3,minmax(155px,1fr)); gap:14px; }
|
||||||
|
label { display:block; color:var(--muted); font-size:12px; font-weight:600; }
|
||||||
|
input,select { display:block; width:100%; height:44px; margin-top:7px; border:1px solid #41545c; border-radius:7px; background:#111e25; color:var(--ink); padding:0 12px; }
|
||||||
|
input::placeholder { color:#859a9e; }
|
||||||
|
.view-row { display:flex; flex-wrap:wrap; align-items:center; gap:20px 30px; border-top:1px solid var(--line); padding-top:18px; margin-top:18px; }
|
||||||
|
fieldset { display:flex; gap:4px; padding:0; border:0; margin:0; min-width:0; }
|
||||||
|
legend { padding:0; margin-bottom:8px; color:var(--muted); font-size:12px; }
|
||||||
|
.choice { min-height:36px; padding:6px 12px; color:var(--muted); border:1px solid #41545c; border-radius:6px; background:transparent; }
|
||||||
|
.choice[aria-pressed="true"] { background:#294844; color:#dafff5; border-color:#71b7a9; }
|
||||||
|
.choice:hover { border-color:var(--gold); color:var(--ink); }
|
||||||
|
.view-note { font-size:12px; color:var(--muted); margin:20px 0 0; flex:1; min-width:170px; }
|
||||||
|
.results-line { display:flex; gap:16px; align-items:center; justify-content:space-between; padding:24px 0 16px; }
|
||||||
|
#result-count { color:var(--muted); margin:0; font-size:13px; }
|
||||||
|
.reset { color:var(--mint); background:none; border:0; padding:6px 0; font-size:13px; }
|
||||||
|
.gallery { display:grid; grid-template-columns:repeat(auto-fill,minmax(280px,1fr)); gap:20px; align-items:start; }
|
||||||
|
.asset-card { min-width:0; overflow:hidden; border:1px solid var(--line); border-radius:12px; background:var(--panel); }
|
||||||
|
.asset-card:hover { border-color:#637968; }
|
||||||
|
.wide-card { grid-column:span 2; }
|
||||||
|
.card-top { padding:11px 16px; display:flex; justify-content:space-between; gap:10px; align-items:center; }
|
||||||
|
code { color:#bac9c8; font:12px/1.5 ui-monospace,SFMono-Regular,Consolas,monospace; }
|
||||||
|
.status { font-size:10px; letter-spacing:.025em; white-space:nowrap; }
|
||||||
|
.status span { font-size:8px; margin-right:3px; }
|
||||||
|
.status.ready { color:var(--mint); }
|
||||||
|
.status.pending { color:var(--gold); }
|
||||||
|
.preview-stage { display:flex; align-items:center; justify-content:center; overflow:hidden; border-top:1px solid #ffffff09; border-bottom:1px solid #ffffff09; background-color:#26343d; }
|
||||||
|
body[data-background="checker"] .preview-stage { background-color:#33434b; background-image:linear-gradient(45deg,#27353d 25%,transparent 25%),linear-gradient(-45deg,#27353d 25%,transparent 25%),linear-gradient(45deg,transparent 75%,#27353d 75%),linear-gradient(-45deg,transparent 75%,#27353d 75%); background-size:24px 24px; background-position:0 0,0 12px,12px -12px,-12px 0; }
|
||||||
|
body[data-background="light"] .preview-stage { background-color:#edf0e8; background-image:none; }
|
||||||
|
body[data-background="dark"] .preview-stage { background-color:#0a1118; background-image:none; }
|
||||||
|
.icon-stage { height:286px; padding:14px; }
|
||||||
|
.preview-link { display:flex; width:100%; height:100%; align-items:center; justify-content:center; }
|
||||||
|
.preview-link img { display:block; object-fit:contain; max-width:100%; }
|
||||||
|
.icon-stage img { width:var(--icon-size); height:auto; max-height:var(--icon-size); }
|
||||||
|
.media-stage { min-height:230px; padding:18px; }
|
||||||
|
.media-stage .preview-link { height:auto; }
|
||||||
|
.media-stage img { width:auto; height:auto; max-width:100%; max-height:390px; }
|
||||||
|
.card-content { padding:18px; }
|
||||||
|
.eyebrow { color:var(--gold); text-transform:uppercase; font-size:10px; letter-spacing:.09em; margin:0 0 8px; }
|
||||||
|
.eyebrow span { color:#64797b; margin:0 5px; }
|
||||||
|
h2 { font-size:20px; line-height:1.2; letter-spacing:-.025em; margin:0 0 11px; }
|
||||||
|
.description { font-size:13px; color:#b7c7c7; line-height:1.6; margin:0 0 16px; min-height:62px; }
|
||||||
|
.metadata { display:flex; flex-wrap:wrap; gap:6px; font-size:10px; color:var(--muted); }
|
||||||
|
.metadata span { border:1px solid #3c4d52; border-radius:4px; padding:3px 6px; }
|
||||||
|
details { margin-top:16px; padding-top:12px; border-top:1px solid var(--line); }
|
||||||
|
summary { font-size:12px; cursor:pointer; color:#d5dfd9; }
|
||||||
|
pre { white-space:pre-wrap; overflow-wrap:anywhere; font:12px/1.6 ui-monospace,SFMono-Regular,Consolas,monospace; color:#b9cbc6; margin:12px 0 0; }
|
||||||
|
.card-links { display:flex; flex-wrap:wrap; gap:16px; margin-top:15px; font-size:12px; }
|
||||||
|
.card-links a { text-decoration:none; }
|
||||||
|
.card-links a:hover { text-decoration:underline; }
|
||||||
|
.prompt-pending { font-size:12px; color:var(--muted); margin:16px 0 0; }
|
||||||
|
.placeholder { display:flex; flex-direction:column; gap:6px; align-items:center; text-align:center; color:#c6b88f; max-width:220px; padding:20px; background:#152229de; border:1px dashed #6d735f; border-radius:8px; }
|
||||||
|
.placeholder-mark { font-size:35px; line-height:1.2; }
|
||||||
|
.placeholder strong { font-size:14px; font-weight:550; }
|
||||||
|
.placeholder span:last-child { font-size:11px; }
|
||||||
|
.pending-card { border-style:dashed; }
|
||||||
|
.empty { padding:64px 24px; text-align:center; border:1px dashed var(--line); border-radius:12px; color:var(--muted); }
|
||||||
|
.empty strong { display:block; color:var(--ink); font-size:21px; margin-bottom:8px; }
|
||||||
|
footer { padding:32px 0 40px; color:#8ba3a5; font-size:12px; display:flex; justify-content:space-between; gap:20px; }
|
||||||
|
footer p { margin:0; }
|
||||||
|
noscript p { color:var(--gold); border:1px solid var(--gold); border-radius:8px; padding:12px; }
|
||||||
|
[hidden] { display:none !important; }
|
||||||
|
@media (max-width:1050px) { .hero { align-items:flex-start; flex-direction:column; gap:24px; } .stats { gap:40px; } .filter-row { grid-template-columns:repeat(2,minmax(0,1fr)); } }
|
||||||
|
@media (max-width:680px) { .shell { padding:0 18px; } .hero { padding-top:32px; } .toolbar { padding:16px; } .filter-row { grid-template-columns:1fr; } .gallery { grid-template-columns:1fr; } .wide-card { grid-column:auto; } .stats { gap:24px; } .stats dd { font-size:28px; } .results-line { align-items:flex-start; } footer { flex-direction:column; } }
|
||||||
|
@media (prefers-reduced-motion:no-preference) { .asset-card,.choice { transition:border-color .15s ease,background-color .15s ease; } }
|
||||||
|
</style></head>
|
||||||
|
<body data-background="checker"><a class="skip" href="#gallery">Zur Galerie springen</a>
|
||||||
|
<div class="shell"><header class="hero"><div><p class="kicker">Saumwacht / Werkstatt / Phase 03</p>
|
||||||
|
<h1>Die Welt nimmt Form an.</h1><p class="intro">Einheiten, Ressourcen und Oberflächen in einer gemeinsamen Bildsprache. Native PNG-Originale vergleichen, Details prüfen und Prompts nachlesen.</p></div>
|
||||||
|
<dl class="stats"><div><dt>Geplant</dt><dd>66</dd></div><div><dt>Vorhanden</dt><dd>66</dd></div><div><dt>Ausstehend</dt><dd>0</dd></div></dl></header>
|
||||||
|
<main><section class="toolbar" aria-label="Galerie filtern und Darstellung ändern"><div class="filter-row">
|
||||||
|
<label for="search">Suche<input id="search" type="search" placeholder="Name, ID oder Beschreibung …" autocomplete="off"></label>
|
||||||
|
<label for="faction">Fraktion & Bereich<select id="faction"><option value="">Alle Bereiche</option><option value="glutpakt">Glutpakt</option><option value="nietenkonvent">Nietenkonvent</option><option value="nebelchor">Nebelchor</option><option value="wurzelbund">Wurzelbund</option><option value="neutral">Neutrale</option><option value="resources">Ressourcen</option><option value="types">Schaden & Rüstung</option><option value="map">Karte</option><option value="ui">Oberfläche</option></select></label>
|
||||||
|
<label for="kind">Assettyp<select id="kind"><option value="">Alle Typen</option><option value="unit_icon">Einheitenicon</option><option value="mercenary_icon">Söldnericon</option><option value="resource_icon">Ressourcenicon</option><option value="worker_icon">Arbeitericon</option><option value="type_icon">Typicon</option><option value="map_concept">Kartenkonzept</option><option value="tile_concept">Bodenkachel</option><option value="ui_frame">UI-Rahmen</option><option value="ui_button">Button</option></select></label>
|
||||||
|
<label for="state">Status<select id="state"><option value="">Alle Assets</option><option value="ready">Vorhanden</option><option value="pending">Ausstehend</option></select></label></div>
|
||||||
|
<div class="view-row"><fieldset><legend>Hintergrund zur Alpha-Prüfung</legend><button class="choice" data-background-choice="checker" aria-pressed="true">Schachbrett</button><button class="choice" data-background-choice="light" aria-pressed="false">Hell</button><button class="choice" data-background-choice="dark" aria-pressed="false">Dunkel</button></fieldset>
|
||||||
|
<fieldset><legend>Icongröße</legend><button class="choice" data-size="48" aria-pressed="false">48 px</button><button class="choice" data-size="64" aria-pressed="false">64 px</button><button class="choice" data-size="128" aria-pressed="true">128 px</button><button class="choice" data-size="256" aria-pressed="false">256 px</button></fieldset>
|
||||||
|
<p class="view-note">Karten und UI behalten ihr Seitenverhältnis. Ein Klick aufs Bild öffnet das Original.</p></div></section>
|
||||||
|
<noscript><p>Alle Assets sind sichtbar. Suche, Filter und Darstellungswechsel benötigen JavaScript.</p></noscript>
|
||||||
|
<div class="results-line"><p id="result-count" role="status" aria-live="polite">66 Assets · 66 vorhanden · 0 ausstehend</p><button class="reset" id="reset">Filter zurücksetzen</button></div>
|
||||||
|
<section class="gallery" id="gallery" aria-label="Assetgalerie"><article class="asset-card" data-id="g01" data-faction="glutpakt" data-kind="unit_icon" data-state="ready" data-search="g01 Kohlenbeißer Glutpakt Einheitenicon Kompakter Basaltkörper mit glühendem Maul, kräftigen Klauen und Messingakzenten.">
|
||||||
|
<div class="card-top"><code>g01</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/glutpakt/unit_g01_kohlenbeisser_icon.png" target="_blank" rel="noopener" aria-label="Kohlenbeißer: PNG-Original öffnen">
|
||||||
|
<img src="masters/glutpakt/unit_g01_kohlenbeisser_icon.png" alt="Kohlenbeißer" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Glutpakt <span aria-hidden="true">/</span> Einheitenicon</p>
|
||||||
|
<h2>Kohlenbeißer</h2><p class="description">Kompakter Basaltkörper mit glühendem Maul, kräftigen Klauen und Messingakzenten.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Create ONE standalone square transparent-background game unit icon for an original low-poly fantasy tower-defense autobattler called Saumwacht. Asset ID g01, Kohlenbeißer. Subject: a squat living coal creature, charcoal-black angular boulder body, broad glowing ember mouth, two powerful short clawed arms and sturdy small legs. Simple memorable triangular silhouette, no humanoid human face. Faction Glutpakt palette exactly basalt #292B35, ember orange #EF6B3B, restrained brass #D9A441 and ash #D8CEC0. Style: flat illustrated strategy-game UI icon, crisp confident edges, 3 to 4 broad matte color values per material, subtle graphic shading, NO photorealistic or glossy rendered look. Entire single character centered, 3/4 front view facing slightly right, slight overhead angle, soft neutral key light upper left. Art occupies about 78% of canvas, broad transparent safety margin around all sides, no elements clipped. Readable at 48px. True RGBA transparent background; no checkerboard baked into image, no colored rectangle, no ground, no shadow on background, no badge, no frame, no text, no lettering, no watermark. Intended final icon 256x256 pixels, square master if higher resolution is required. Save useful image output; one character only.</pre></details><div class="card-links"><a href="masters/glutpakt/unit_g01_kohlenbeisser_icon.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/g01.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="g02" data-faction="glutpakt" data-kind="unit_icon" data-state="ready" data-search="g02 Schlackenklaue Glutpakt Einheitenicon Schwer gepanzerter Kohlenkörper mit breiten Basaltschultern, glühendem Maul und großen messingbesetzten Klauen.">
|
||||||
|
<div class="card-top"><code>g02</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/glutpakt/unit_g02_schlackenklaue_icon.png" target="_blank" rel="noopener" aria-label="Schlackenklaue: PNG-Original öffnen">
|
||||||
|
<img src="masters/glutpakt/unit_g02_schlackenklaue_icon.png" alt="Schlackenklaue" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Glutpakt <span aria-hidden="true">/</span> Einheitenicon</p>
|
||||||
|
<h2>Schlackenklaue</h2><p class="description">Schwer gepanzerter Kohlenkörper mit breiten Basaltschultern, glühendem Maul und großen messingbesetzten Klauen.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. ONE standalone square unit icon for the original strategy game Saumwacht. Whole single creature centered with all limbs visible, about 76% canvas coverage and transparent margins. Three-quarter front view facing slightly right, slight overhead angle, soft neutral key light upper left. Faceted illustrated game UI art with crisp edges, broad matte planes, restrained graphic shading, very clear silhouette readable at 48px. True RGBA transparent background, no checkerboard, no ground, no shadow on background, no border, no badge, no text, no watermark. Glutpakt palette basalt #292B35, ember #EF6B3B, brass #D9A441, ash #D8CEC0. Large simple shapes, no tiny surface texture, no glossy photorealism. Intended icon 256px with a high-resolution square master. Subject: Upgraded coal-claw melee creature: heavy angular basalt shoulder plates, two oversized curved brass-tipped claws, broad ember-filled jagged mouth, glowing cracks, compact powerful legs. Visibly a heavier evolved coal creature; not a human.</pre></details><div class="card-links"><a href="masters/glutpakt/unit_g02_schlackenklaue_icon.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/g02.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="g03" data-faction="glutpakt" data-kind="unit_icon" data-state="ready" data-search="g03 Schlackenwacht Glutpakt Einheitenicon Gedrungener Basaltwächter mit breiter Schildfront, schmaler Ofenglut und kurzen, kräftigen Armen.">
|
||||||
|
<div class="card-top"><code>g03</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/glutpakt/unit_g03_schlackenwacht_icon.png" target="_blank" rel="noopener" aria-label="Schlackenwacht: PNG-Original öffnen">
|
||||||
|
<img src="masters/glutpakt/unit_g03_schlackenwacht_icon.png" alt="Schlackenwacht" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Glutpakt <span aria-hidden="true">/</span> Einheitenicon</p>
|
||||||
|
<h2>Schlackenwacht</h2><p class="description">Gedrungener Basaltwächter mit breiter Schildfront, schmaler Ofenglut und kurzen, kräftigen Armen.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. ONE standalone square unit icon for the original strategy game Saumwacht. Whole single creature centered with all limbs visible, about 76% canvas coverage and transparent margins. Three-quarter front view facing slightly right, slight overhead angle, soft neutral key light upper left. Faceted illustrated game UI art with crisp edges, broad matte planes, restrained graphic shading, very clear silhouette readable at 48px. True RGBA transparent background, no checkerboard, no ground, no shadow on background, no border, no badge, no text, no watermark. Glutpakt palette basalt #292B35, ember #EF6B3B, brass #D9A441, ash #D8CEC0. Large simple shapes, no tiny surface texture, no glossy photorealism. Intended icon 256px with a high-resolution square master. Subject: A defensive living basalt sentinel. Broad squat rectangular slab-shaped body forming a huge shield front, small recessed orange furnace slit, two short armored arms with blocklike fists, sturdy feet. Strong nearly square silhouette, restrained brass edge accents; clearly a tank rather than a clawed monster.</pre></details><div class="card-links"><a href="masters/glutpakt/unit_g03_schlackenwacht_icon.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/g03.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="g04" data-faction="glutpakt" data-kind="unit_icon" data-state="ready" data-search="g04 Basaltbastion Glutpakt Einheitenicon Lebende Basaltfestung mit gestuften Schultern, gepanzerter Schildfront und glühendem Ofenkern.">
|
||||||
|
<div class="card-top"><code>g04</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/glutpakt/unit_g04_basaltbastion_icon.png" target="_blank" rel="noopener" aria-label="Basaltbastion: PNG-Original öffnen">
|
||||||
|
<img src="masters/glutpakt/unit_g04_basaltbastion_icon.png" alt="Basaltbastion" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Glutpakt <span aria-hidden="true">/</span> Einheitenicon</p>
|
||||||
|
<h2>Basaltbastion</h2><p class="description">Lebende Basaltfestung mit gestuften Schultern, gepanzerter Schildfront und glühendem Ofenkern.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. ONE standalone square unit icon for the original strategy game Saumwacht. Whole single creature centered with all limbs visible, about 76% canvas coverage and transparent margins. Three-quarter front view facing slightly right, slight overhead angle, soft neutral key light upper left. Faceted illustrated game UI art with crisp edges, broad matte planes, restrained graphic shading, very clear silhouette readable at 48px. True RGBA transparent background, no checkerboard, no ground, no shadow on background, no border, no badge, no text, no watermark. Glutpakt palette basalt #292B35, ember #EF6B3B, brass #D9A441, ash #D8CEC0. Large simple shapes, no tiny surface texture, no glossy photorealism. Intended icon 256px with a high-resolution square master. Subject: Upgraded living fortress tank, massive broad stepped basalt slabs forming a castle-like shoulder silhouette, deep orange furnace slit, a thick angular tower-shield chest and two pillar-like arms, robust feet, controlled brass buttresses. Distinct from a plain block sentinel: layered fortress tiers, wider shoulders, taller crown of stone, still one compact creature.</pre></details><div class="card-links"><a href="masters/glutpakt/unit_g04_basaltbastion_icon.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/g04.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="g05" data-faction="glutpakt" data-kind="unit_icon" data-state="ready" data-search="g05 Funkenlanze Glutpakt Einheitenicon Schlanker Glutkonstrukt-Schütze mit langer Messinglanze und orange leuchtender Glasspitze.">
|
||||||
|
<div class="card-top"><code>g05</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/glutpakt/unit_g05_funkenlanze_icon.png" target="_blank" rel="noopener" aria-label="Funkenlanze: PNG-Original öffnen">
|
||||||
|
<img src="masters/glutpakt/unit_g05_funkenlanze_icon.png" alt="Funkenlanze" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Glutpakt <span aria-hidden="true">/</span> Einheitenicon</p>
|
||||||
|
<h2>Funkenlanze</h2><p class="description">Schlanker Glutkonstrukt-Schütze mit langer Messinglanze und orange leuchtender Glasspitze.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. ONE standalone square unit icon for the original strategy game Saumwacht. Whole single creature centered with all limbs visible, about 76% canvas coverage and transparent margins. Three-quarter front view facing slightly right, slight overhead angle, soft neutral key light upper left. Faceted illustrated game UI art with crisp edges, broad matte planes, restrained graphic shading, very clear silhouette readable at 48px. True RGBA transparent background, no checkerboard, no ground, no shadow on background, no border, no badge, no text, no watermark. Glutpakt palette basalt #292B35, ember #EF6B3B, brass #D9A441, ash #D8CEC0. Large simple shapes, no tiny surface texture, no glossy photorealism. Intended icon 256px with a high-resolution square master. Subject: A slender mobile ember construct with narrow charcoal stone torso, two sturdy legs, a single long brass-and-obsidian lance launcher held diagonally forward, a pointed glowing orange glass spearhead, small angular head without a human face. Clearly a light ranged marksman with an elongated weapon silhouette, not a knight with a shield.</pre></details><div class="card-links"><a href="masters/glutpakt/unit_g05_funkenlanze_icon.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/g05.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="g06" data-faction="glutpakt" data-kind="unit_icon" data-state="ready" data-search="g06 Glasballiste Glutpakt Einheitenicon Bewegliche Glasballiste mit großen orangefarbenen Kristallarmen, langem Bolzen und stabilem Basaltunterbau.">
|
||||||
|
<div class="card-top"><code>g06</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/glutpakt/unit_g06_glasballiste_icon.png" target="_blank" rel="noopener" aria-label="Glasballiste: PNG-Original öffnen">
|
||||||
|
<img src="masters/glutpakt/unit_g06_glasballiste_icon.png" alt="Glasballiste" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Glutpakt <span aria-hidden="true">/</span> Einheitenicon</p>
|
||||||
|
<h2>Glasballiste</h2><p class="description">Bewegliche Glasballiste mit großen orangefarbenen Kristallarmen, langem Bolzen und stabilem Basaltunterbau.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. ONE standalone square unit icon for the original strategy game Saumwacht. Whole single creature centered with all limbs visible, about 76% canvas coverage and transparent margins. Three-quarter front view facing slightly right, slight overhead angle, soft neutral key light upper left. Faceted illustrated game UI art with crisp edges, broad matte planes, restrained graphic shading, very clear silhouette readable at 48px. True RGBA transparent background, no checkerboard, no ground, no shadow on background, no border, no badge, no text, no watermark. Glutpakt palette basalt #292B35, ember #EF6B3B, brass #D9A441, ash #D8CEC0. Large simple shapes, no tiny surface texture, no glossy photorealism. Intended icon 256px with a high-resolution square master. Subject: An upgraded mobile glass ballista creature, a compact charcoal basalt construct on two sturdy articulated legs supporting a very large crossbow with two swept crystalline orange limbs, long glowing glass bolt, brass mechanical bands. Three-quarter angle, bow fits entirely inside square; clearly a heavy ranged siege unit and evolved spear launcher, no shield.</pre></details><div class="card-links"><a href="masters/glutpakt/unit_g06_glasballiste_icon.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/g06.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="g07" data-faction="glutpakt" data-kind="unit_icon" data-state="ready" data-search="g07 Aschenwerfer Glutpakt Einheitenicon Gedrungener Aschenwerfer mit breitem Mörserkelch, glühender Kohle und kurzen Basaltbeinen.">
|
||||||
|
<div class="card-top"><code>g07</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/glutpakt/unit_g07_aschenwerfer_icon.png" target="_blank" rel="noopener" aria-label="Aschenwerfer: PNG-Original öffnen">
|
||||||
|
<img src="masters/glutpakt/unit_g07_aschenwerfer_icon.png" alt="Aschenwerfer" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Glutpakt <span aria-hidden="true">/</span> Einheitenicon</p>
|
||||||
|
<h2>Aschenwerfer</h2><p class="description">Gedrungener Aschenwerfer mit breitem Mörserkelch, glühender Kohle und kurzen Basaltbeinen.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. ONE standalone square unit icon for the original strategy game Saumwacht. Whole single creature centered with all limbs visible, about 76% canvas coverage and transparent margins. Three-quarter front view facing slightly right, slight overhead angle, soft neutral key light upper left. Faceted illustrated game UI art with crisp edges, broad matte planes, restrained graphic shading, very clear silhouette readable at 48px. True RGBA transparent background, no checkerboard, no ground, no shadow on background, no border, no badge, no text, no watermark. Glutpakt palette basalt #292B35, ember #EF6B3B, brass #D9A441, ash #D8CEC0. Large simple shapes, no tiny surface texture, no glossy photorealism. Intended icon 256px with a high-resolution square master. Subject: A squat mobile ash mortar creature, charcoal faceted stone body and two short arms cradling a wide upward-tilted round basalt mortar bowl. One contained orange ember glowing inside the bowl, tiny restrained ash puff attached to opening. Stubby legs, brass rim, broad cup silhouette; original artillery creature rather than a human soldier.</pre></details><div class="card-links"><a href="masters/glutpakt/unit_g07_aschenwerfer_icon.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/g07.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="g08" data-faction="glutpakt" data-kind="unit_icon" data-state="ready" data-search="g08 Kraterrufer Glutpakt Einheitenicon Schwerer Kraterkörper mit großem Vulkankelch, glühendem Inneren und gestuften Basaltplatten.">
|
||||||
|
<div class="card-top"><code>g08</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/glutpakt/unit_g08_kraterrufer_icon.png" target="_blank" rel="noopener" aria-label="Kraterrufer: PNG-Original öffnen">
|
||||||
|
<img src="masters/glutpakt/unit_g08_kraterrufer_icon.png" alt="Kraterrufer" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Glutpakt <span aria-hidden="true">/</span> Einheitenicon</p>
|
||||||
|
<h2>Kraterrufer</h2><p class="description">Schwerer Kraterkörper mit großem Vulkankelch, glühendem Inneren und gestuften Basaltplatten.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. ONE standalone square unit icon for the original strategy game Saumwacht. Whole single creature centered with all limbs visible, about 76% canvas coverage and transparent margins. Three-quarter front view facing slightly right, slight overhead angle, soft neutral key light upper left. Faceted illustrated game UI art with crisp edges, broad matte planes, restrained graphic shading, very clear silhouette readable at 48px. True RGBA transparent background, no checkerboard, no ground, no shadow on background, no border, no badge, no text, no watermark. Glutpakt palette basalt #292B35, ember #EF6B3B, brass #D9A441, ash #D8CEC0. Large simple shapes, no tiny surface texture, no glossy photorealism. Intended icon 256px with a high-resolution square master. Subject: Heavy evolved ash-mortar creature: broad armored basalt body with a very large crater-shaped mortar bowl in its shoulders, layered brass-rimmed volcanic plates, contained orange molten core, one small angular ash curl, two massive arms and stout legs. Strong wide circular crater silhouette, clearly the upgraded artillery of a smaller bowl creature, no scenery.</pre></details><div class="card-links"><a href="masters/glutpakt/unit_g08_kraterrufer_icon.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/g08.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="g09" data-faction="glutpakt" data-kind="unit_icon" data-state="ready" data-search="g09 Herdhüter Glutpakt Einheitenicon Kleiner Herdwächter mit offenem Glutofen und einer schützenden Messingschale vor dem Körper.">
|
||||||
|
<div class="card-top"><code>g09</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/glutpakt/unit_g09_herdhueter_icon.png" target="_blank" rel="noopener" aria-label="Herdhüter: PNG-Original öffnen">
|
||||||
|
<img src="masters/glutpakt/unit_g09_herdhueter_icon.png" alt="Herdhüter" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Glutpakt <span aria-hidden="true">/</span> Einheitenicon</p>
|
||||||
|
<h2>Herdhüter</h2><p class="description">Kleiner Herdwächter mit offenem Glutofen und einer schützenden Messingschale vor dem Körper.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. ONE standalone square unit icon for the original strategy game Saumwacht. Whole single creature centered with all limbs visible, about 76% canvas coverage and transparent margins. Three-quarter front view facing slightly right, slight overhead angle, soft neutral key light upper left. Faceted illustrated game UI art with crisp edges, broad matte planes, restrained graphic shading, very clear silhouette readable at 48px. True RGBA transparent background, no checkerboard, no ground, no shadow on background, no border, no badge, no text, no watermark. Glutpakt palette basalt #292B35, ember #EF6B3B, brass #D9A441, ash #D8CEC0. Large simple shapes, no tiny surface texture, no glossy photorealism. Intended icon 256px with a high-resolution square master. Subject: Small gentle living hearth guardian, upright compact basalt furnace body on short legs, open orange hearth at chest, two small arms holding a modest warm brass brazier before the body. Rounded arch opening, ash-colored upper stone hood, a few restrained ember shapes. Support unit with protective welcoming silhouette, no human face, no weapons.</pre></details><div class="card-links"><a href="masters/glutpakt/unit_g09_herdhueter_icon.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/g09.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="g10" data-faction="glutpakt" data-kind="unit_icon" data-state="ready" data-search="g10 Sonnenesse Glutpakt Einheitenicon Große Sonnenesse mit strahlender Ofenöffnung, Messingkrone und breiter wärmender Glutschale.">
|
||||||
|
<div class="card-top"><code>g10</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/glutpakt/unit_g10_sonnenesse_icon.png" target="_blank" rel="noopener" aria-label="Sonnenesse: PNG-Original öffnen">
|
||||||
|
<img src="masters/glutpakt/unit_g10_sonnenesse_icon.png" alt="Sonnenesse" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Glutpakt <span aria-hidden="true">/</span> Einheitenicon</p>
|
||||||
|
<h2>Sonnenesse</h2><p class="description">Große Sonnenesse mit strahlender Ofenöffnung, Messingkrone und breiter wärmender Glutschale.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. ONE standalone square unit icon for the original strategy game Saumwacht. Whole single creature centered with all limbs visible, about 76% canvas coverage and transparent margins. Three-quarter front view facing slightly right, slight overhead angle, soft neutral key light upper left. Faceted illustrated game UI art with crisp edges, broad matte planes, restrained graphic shading, very clear silhouette readable at 48px. True RGBA transparent background, no checkerboard, no ground, no shadow on background, no border, no badge, no text, no watermark. Glutpakt palette basalt #292B35, ember #EF6B3B, brass #D9A441, ash #D8CEC0. Large simple shapes, no tiny surface texture, no glossy photorealism. Subject: Grand evolved living solar forge support creature: broad upright basalt furnace body, large radiant orange sunlike forge opening framed by a simple brass sun crown, ash stone mantle, sturdy feet, arms holding a wide healing ember basin. Warm controlled gold-orange illumination, majestic open silhouette; clearly stronger than small hearth guardian, still one creature without scenic halo background.</pre></details><div class="card-links"><a href="masters/glutpakt/unit_g10_sonnenesse_icon.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/g10.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="m01" data-faction="nietenkonvent" data-kind="unit_icon" data-state="ready" data-search="m01 Kesselwicht Nietenkonvent Einheitenicon Gedrungener Kesseltank mit rundem Petrolkessel, Keramikgesicht und kurzen Stempelbeinen.">
|
||||||
|
<div class="card-top"><code>m01</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/nietenkonvent/unit_m01_kesselwicht_icon.png" target="_blank" rel="noopener" aria-label="Kesselwicht: PNG-Original öffnen">
|
||||||
|
<img src="masters/nietenkonvent/unit_m01_kesselwicht_icon.png" alt="Kesselwicht" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Nietenkonvent <span aria-hidden="true">/</span> Einheitenicon</p>
|
||||||
|
<h2>Kesselwicht</h2><p class="description">Gedrungener Kesseltank mit rundem Petrolkessel, Keramikgesicht und kurzen Stempelbeinen.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Asset type: original strategy-game UNIT ICON for Saumwacht, a single individual PNG cutout. Create a brand-new image. Style: crisp faceted illustrated low-poly fantasy machine, strong chunky planar shapes and simple matte shading, premium coherent game icon, not photorealistic. Composition: square 1:1 canvas, whole single subject centered, all parts visible with comfortable transparent margin, occupy about 75–80 percent of the canvas. Three-quarter front view looking toward viewer's right. Lighting: soft key light from upper left, restrained ambient fill. Palette: petrol #247A80 primary, ocher #C79A48 accents, steel #657481, cream ceramic #E6DCCA; dark slate shadows. Keep broad masses and silhouette readable at 48px. Scene: genuinely transparent background with preserved alpha, no checkerboard pattern drawn into image. No ground, no shadow plane, no frame, no text, no lettering, no logo, no border, no watermark, no unrelated props, no other characters, no realistic texture or excessive microdetail. Subject: a squat friendly formidable boiler tank automaton, round low petrol boiler body, small cream ceramic face inset into the upper front, ocher pressure gauge, short thick piston legs, two low blocky steel fists, one short chimney; simple round compact silhouette.</pre></details><div class="card-links"><a href="masters/nietenkonvent/unit_m01_kesselwicht_icon.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/m01.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="m02" data-faction="nietenkonvent" data-kind="unit_icon" data-state="ready" data-search="m02 Druckhüter Nietenkonvent Einheitenicon Breiter gepanzerter Drucktank mit Schulterkesseln und massivem Schildarm.">
|
||||||
|
<div class="card-top"><code>m02</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/nietenkonvent/unit_m02_druckhueter_icon.png" target="_blank" rel="noopener" aria-label="Druckhüter: PNG-Original öffnen">
|
||||||
|
<img src="masters/nietenkonvent/unit_m02_druckhueter_icon.png" alt="Druckhüter" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Nietenkonvent <span aria-hidden="true">/</span> Einheitenicon</p>
|
||||||
|
<h2>Druckhüter</h2><p class="description">Breiter gepanzerter Drucktank mit Schulterkesseln und massivem Schildarm.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Asset type: original strategy-game UNIT ICON for Saumwacht, a single individual PNG cutout. Create a brand-new image. Style: crisp faceted illustrated low-poly fantasy machine, strong chunky planar shapes and simple matte shading, premium coherent game icon, not photorealistic. Composition: square 1:1 canvas, whole single subject centered, all parts visible with comfortable transparent margin, occupy about 75–80 percent of the canvas. Three-quarter front view looking toward viewer's right. Lighting: soft key light from upper left, restrained ambient fill. Palette: petrol #247A80 primary, ocher #C79A48 accents, steel #657481, cream ceramic #E6DCCA; dark slate shadows. Keep broad masses and silhouette readable at 48px. Scene: genuinely transparent background with preserved alpha, no checkerboard pattern drawn into image. No ground, no shadow plane, no frame, no text, no lettering, no logo, no border, no watermark, no unrelated props, no other characters, no realistic texture or excessive microdetail. Subject: an upgraded massive armored pressure tank automaton, very broad squared shoulders supporting two tall ocher-capped pressure cylinders, cream ceramic brow above steel face, thick petrol chest armor, massive block shield forearm and heavy piston fist, sturdy short legs; trapezoid broad powerful silhouette distinct from the small round boiler base unit.</pre></details><div class="card-links"><a href="masters/nietenkonvent/unit_m02_druckhueter_icon.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/m02.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="m03" data-faction="nietenkonvent" data-kind="unit_icon" data-state="ready" data-search="m03 Scherschreiter Nietenkonvent Einheitenicon Schmaler Schreitautomat mit zwei großen Scherenarmen.">
|
||||||
|
<div class="card-top"><code>m03</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/nietenkonvent/unit_m03_scherschreiter_icon.png" target="_blank" rel="noopener" aria-label="Scherschreiter: PNG-Original öffnen">
|
||||||
|
<img src="masters/nietenkonvent/unit_m03_scherschreiter_icon.png" alt="Scherschreiter" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Nietenkonvent <span aria-hidden="true">/</span> Einheitenicon</p>
|
||||||
|
<h2>Scherschreiter</h2><p class="description">Schmaler Schreitautomat mit zwei großen Scherenarmen.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Asset type: original strategy-game UNIT ICON for Saumwacht, a single individual PNG cutout. Create a brand-new image. Style: crisp faceted illustrated low-poly fantasy machine, strong chunky planar shapes and simple matte shading, premium coherent game icon, not photorealistic. Composition: square 1:1 canvas, whole single subject centered, all parts visible with comfortable transparent margin, occupy about 75–80 percent of the canvas. Three-quarter front view looking toward viewer's right. Lighting: soft key light from upper left, restrained ambient fill. Palette: petrol #247A80 primary, ocher #C79A48 accents, steel #657481, cream ceramic #E6DCCA; dark slate shadows. Keep broad masses and silhouette readable at 48px. Scene: genuinely transparent background with preserved alpha, no checkerboard pattern drawn into image. No ground, no shadow plane, no frame, no text, no lettering, no logo, no border, no watermark, no unrelated props, no other characters, no realistic texture or excessive microdetail. Subject: a nimble walking twin-shear automaton with a narrow upright petrol spindle body, two very large open steel scissors replacing its arms, each with two clearly defined long blade halves, jointed ocher knees and long light legs, a small pointed cream ceramic head; light angular insect-like silhouette with exactly two shear arms.</pre></details><div class="card-links"><a href="masters/nietenkonvent/unit_m03_scherschreiter_icon.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/m03.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="m04" data-faction="nietenkonvent" data-kind="unit_icon" data-state="ready" data-search="m04 Sechsklingler Nietenkonvent Einheitenicon Breiter Schreitautomat mit sechs deutlich getrennten Klingen.">
|
||||||
|
<div class="card-top"><code>m04</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/nietenkonvent/unit_m04_sechsklingler_icon.png" target="_blank" rel="noopener" aria-label="Sechsklingler: PNG-Original öffnen">
|
||||||
|
<img src="masters/nietenkonvent/unit_m04_sechsklingler_icon.png" alt="Sechsklingler" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Nietenkonvent <span aria-hidden="true">/</span> Einheitenicon</p>
|
||||||
|
<h2>Sechsklingler</h2><p class="description">Breiter Schreitautomat mit sechs deutlich getrennten Klingen.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Asset type: original strategy-game UNIT ICON for Saumwacht, a single individual PNG cutout. Create a brand-new image. Style: crisp faceted illustrated low-poly fantasy machine, strong chunky planar shapes and simple matte shading, premium coherent game icon, not photorealistic. Composition: square 1:1 canvas, whole single subject centered, all parts visible with comfortable transparent margin, occupy about 75–80 percent of the canvas. Three-quarter front view looking toward viewer's right. Lighting: soft key light from upper left, restrained ambient fill. Palette: petrol #247A80 primary, ocher #C79A48 accents, steel #657481, cream ceramic #E6DCCA; dark slate shadows. Keep broad masses and silhouette readable at 48px. Scene: genuinely transparent background with preserved alpha, no checkerboard pattern drawn into image. No ground, no shadow plane, no frame, no text, no lettering, no logo, no border, no watermark, no unrelated props, no other characters, no realistic texture or excessive microdetail. Subject: an upgraded six-bladed walking automaton, broad petrol hub torso with a cream ceramic mask, exactly six large steel swordlike blades on six distinct jointed arms fanning out three per side, ocher hubs, tall strong jointed legs; dramatic radial blade silhouette distinctly larger and more elaborate than a two-shear machine.</pre></details><div class="card-links"><a href="masters/nietenkonvent/unit_m04_sechsklingler_icon.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/m04.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="m05" data-faction="nietenkonvent" data-kind="unit_icon" data-state="ready" data-search="m05 Bolzenknecht Nietenkonvent Einheitenicon Schlanker Bolzenautomat mit einem deutlich lesbaren Armwerfer.">
|
||||||
|
<div class="card-top"><code>m05</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/nietenkonvent/unit_m05_bolzenknecht_icon.png" target="_blank" rel="noopener" aria-label="Bolzenknecht: PNG-Original öffnen">
|
||||||
|
<img src="masters/nietenkonvent/unit_m05_bolzenknecht_icon.png" alt="Bolzenknecht" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Nietenkonvent <span aria-hidden="true">/</span> Einheitenicon</p>
|
||||||
|
<h2>Bolzenknecht</h2><p class="description">Schlanker Bolzenautomat mit einem deutlich lesbaren Armwerfer.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Asset type: original strategy-game UNIT ICON for Saumwacht, a single individual PNG cutout. Create a brand-new image. Style: crisp faceted illustrated low-poly fantasy machine, strong chunky planar shapes and simple matte shading, premium coherent game icon, not photorealistic. Composition: square 1:1 canvas, whole single subject centered, all parts visible with comfortable transparent margin, occupy about 75–80 percent of the canvas. Three-quarter front view looking toward viewer's right. Lighting: soft key light from upper left, restrained ambient fill. Palette: petrol #247A80 primary, ocher #C79A48 accents, steel #657481, cream ceramic #E6DCCA; dark slate shadows. Keep broad masses and silhouette readable at 48px. Scene: genuinely transparent background with preserved alpha, no checkerboard pattern drawn into image. No ground, no shadow plane, no frame, no text, no lettering, no logo, no border, no watermark, no unrelated props, no other characters, no realistic texture or excessive microdetail. Subject: a slim bolt-launcher automaton, narrow petrol body on two slender steel legs, small cream ceramic face and ocher monocular sight, one large compact crossbow bolter replacing the right forearm and a small bolt magazine on its back, simple upright posture; one strongly readable crossbow weapon and restrained silhouette.</pre></details><div class="card-links"><a href="masters/nietenkonvent/unit_m05_bolzenknecht_icon.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/m05.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="m06" data-faction="nietenkonvent" data-kind="unit_icon" data-state="ready" data-search="m06 Linienmeister Nietenkonvent Einheitenicon Hoher Präzisionsautomat mit einer langen integrierten Balliste.">
|
||||||
|
<div class="card-top"><code>m06</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/nietenkonvent/unit_m06_linienmeister_icon.png" target="_blank" rel="noopener" aria-label="Linienmeister: PNG-Original öffnen">
|
||||||
|
<img src="masters/nietenkonvent/unit_m06_linienmeister_icon.png" alt="Linienmeister" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Nietenkonvent <span aria-hidden="true">/</span> Einheitenicon</p>
|
||||||
|
<h2>Linienmeister</h2><p class="description">Hoher Präzisionsautomat mit einer langen integrierten Balliste.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Asset type: original strategy-game UNIT ICON for Saumwacht, a single individual PNG cutout. Create a brand-new image. Style: crisp faceted illustrated low-poly fantasy machine, strong chunky planar shapes and simple matte shading, premium coherent game icon, not photorealistic. Composition: square 1:1 canvas, whole single subject centered, all parts visible with comfortable transparent margin, occupy about 75–80 percent of the canvas. Three-quarter front view looking toward viewer's right. Lighting: soft key light from upper left, restrained ambient fill. Palette: petrol #247A80 primary, ocher #C79A48 accents, steel #657481, cream ceramic #E6DCCA; dark slate shadows. Keep broad masses and silhouette readable at 48px. Scene: genuinely transparent background with preserved alpha, no checkerboard pattern drawn into image. No ground, no shadow plane, no frame, no text, no lettering, no logo, no border, no watermark, no unrelated props, no other characters, no realistic texture or excessive microdetail. Subject: an upgraded tall precision ballista automaton, long petrol armored torso, cream ceramic pointed visor with ocher targeting lens, a long oversize precision ballista integrated across its upper torso with broad steel bow limbs and a long forward rail, stabilizer heel struts and a thin vertical targeting mast; tall triangular precision silhouette distinctly larger than a slim bolt-launcher.</pre></details><div class="card-links"><a href="masters/nietenkonvent/unit_m06_linienmeister_icon.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/m06.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="m07" data-faction="nietenkonvent" data-kind="unit_icon" data-state="ready" data-search="m07 Klangkolben Nietenkonvent Einheitenicon Kompakter Artillerieautomat mit einem großen Glockenrohr und Kolben.">
|
||||||
|
<div class="card-top"><code>m07</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/nietenkonvent/unit_m07_klangkolben_icon.png" target="_blank" rel="noopener" aria-label="Klangkolben: PNG-Original öffnen">
|
||||||
|
<img src="masters/nietenkonvent/unit_m07_klangkolben_icon.png" alt="Klangkolben" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Nietenkonvent <span aria-hidden="true">/</span> Einheitenicon</p>
|
||||||
|
<h2>Klangkolben</h2><p class="description">Kompakter Artillerieautomat mit einem großen Glockenrohr und Kolben.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Asset type: original strategy-game UNIT ICON for Saumwacht, a single individual PNG cutout. Create a brand-new image. Style: crisp faceted illustrated low-poly fantasy machine, strong chunky planar shapes and simple matte shading, premium coherent game icon, not photorealistic. Composition: square 1:1 canvas, whole single subject centered, all parts visible with comfortable transparent margin, occupy about 75–80 percent of the canvas. Three-quarter front view looking toward viewer's right. Lighting: soft key light from upper left, restrained ambient fill. Palette: petrol #247A80 primary, ocher #C79A48 accents, steel #657481, cream ceramic #E6DCCA; dark slate shadows. Keep broad masses and silhouette readable at 48px. Scene: genuinely transparent background with preserved alpha, no checkerboard pattern drawn into image. No ground, no shadow plane, no frame, no text, no lettering, no logo, no border, no watermark, no unrelated props, no other characters, no realistic texture or excessive microdetail. Subject: a compact piston-bell artillery automaton on two stout mechanical feet, one large ocher bell-shaped acoustic cannon angled slightly upward to the right, petrol barrel body, thick central steel striking piston visible at bell mouth, cream ceramic inset on the side, small short operator head; single horizontal bell silhouette.</pre></details><div class="card-links"><a href="masters/nietenkonvent/unit_m07_klangkolben_icon.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/m07.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="m08" data-faction="nietenkonvent" data-kind="unit_icon" data-state="ready" data-search="m08 Resonanzramme Nietenkonvent Einheitenicon Breiter Artillerieautomat mit zwei großen Resonanzkesseln.">
|
||||||
|
<div class="card-top"><code>m08</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/nietenkonvent/unit_m08_resonanzramme_icon.png" target="_blank" rel="noopener" aria-label="Resonanzramme: PNG-Original öffnen">
|
||||||
|
<img src="masters/nietenkonvent/unit_m08_resonanzramme_icon.png" alt="Resonanzramme" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Nietenkonvent <span aria-hidden="true">/</span> Einheitenicon</p>
|
||||||
|
<h2>Resonanzramme</h2><p class="description">Breiter Artillerieautomat mit zwei großen Resonanzkesseln.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Asset type: original strategy-game UNIT ICON for Saumwacht, a single individual PNG cutout. Create a brand-new image. Style: crisp faceted illustrated low-poly fantasy machine, strong chunky planar shapes and simple matte shading, premium coherent game icon, not photorealistic. Composition: square 1:1 canvas, whole single subject centered, all parts visible with comfortable transparent margin, occupy about 75–80 percent of the canvas. Three-quarter front view looking toward viewer's right. Lighting: soft key light from upper left, restrained ambient fill. Palette: petrol #247A80 primary, ocher #C79A48 accents, steel #657481, cream ceramic #E6DCCA; dark slate shadows. Keep broad masses and silhouette readable at 48px. Scene: genuinely transparent background with preserved alpha, no checkerboard pattern drawn into image. No ground, no shadow plane, no frame, no text, no lettering, no logo, no border, no watermark, no unrelated props, no other characters, no realistic texture or excessive microdetail. Subject: an upgraded broad resonance-ram artillery automaton, two enormous side-by-side forward-facing ocher resonance drums, dark steel piston centers and petrol mounting armor, small cream ceramic central command mask between the drums, four chunky steel stabilizer legs; extremely wide twin-circle silhouette distinctly different from a single small bell cannon.</pre></details><div class="card-links"><a href="masters/nietenkonvent/unit_m08_resonanzramme_icon.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/m08.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="m09" data-faction="nietenkonvent" data-kind="unit_icon" data-state="ready" data-search="m09 Taktspindel Nietenkonvent Einheitenicon Kleiner Taktgeber auf Spindelbeinen mit einem Dirigentenstab.">
|
||||||
|
<div class="card-top"><code>m09</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/nietenkonvent/unit_m09_taktspindel_icon.png" target="_blank" rel="noopener" aria-label="Taktspindel: PNG-Original öffnen">
|
||||||
|
<img src="masters/nietenkonvent/unit_m09_taktspindel_icon.png" alt="Taktspindel" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Nietenkonvent <span aria-hidden="true">/</span> Einheitenicon</p>
|
||||||
|
<h2>Taktspindel</h2><p class="description">Kleiner Taktgeber auf Spindelbeinen mit einem Dirigentenstab.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Asset type: original strategy-game UNIT ICON for Saumwacht, a single individual PNG cutout. Create a brand-new image. Style: crisp faceted illustrated low-poly fantasy machine, strong chunky planar shapes and simple matte shading, premium coherent game icon, not photorealistic. Composition: square 1:1 canvas, whole single subject centered, all parts visible with comfortable transparent margin, occupy about 75–80 percent of the canvas. Three-quarter front view looking toward viewer's right. Lighting: soft key light from upper left, restrained ambient fill. Palette: petrol #247A80 primary, ocher #C79A48 accents, steel #657481, cream ceramic #E6DCCA; dark slate shadows. Keep broad masses and silhouette readable at 48px. Scene: genuinely transparent background with preserved alpha, no checkerboard pattern drawn into image. No ground, no shadow plane, no frame, no text, no lettering, no logo, no border, no watermark, no unrelated props, no other characters, no realistic texture or excessive microdetail. Subject: a small elegant clockwork conductor automaton, slim upright petrol spindle body widening at the middle, tiny cream ceramic face, ocher pendulum inset in the chest, two thin steel legs and narrow articulated arms, holding a single small ocher conductor baton raised diagonally; simple graceful spindle silhouette.</pre></details><div class="card-links"><a href="masters/nietenkonvent/unit_m09_taktspindel_icon.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/m09.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="m10" data-faction="nietenkonvent" data-kind="unit_icon" data-state="ready" data-search="m10 Taktprälat Nietenkonvent Einheitenicon Großer Uhrwerkdirigent mit kronenförmigem Metronom und breitem Mantelkörper.">
|
||||||
|
<div class="card-top"><code>m10</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/nietenkonvent/unit_m10_taktpraelat_icon.png" target="_blank" rel="noopener" aria-label="Taktprälat: PNG-Original öffnen">
|
||||||
|
<img src="masters/nietenkonvent/unit_m10_taktpraelat_icon.png" alt="Taktprälat" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Nietenkonvent <span aria-hidden="true">/</span> Einheitenicon</p>
|
||||||
|
<h2>Taktprälat</h2><p class="description">Großer Uhrwerkdirigent mit kronenförmigem Metronom und breitem Mantelkörper.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Asset type: original strategy-game UNIT ICON for Saumwacht, a single individual PNG cutout. Create a brand-new image. Style: crisp faceted illustrated low-poly fantasy machine, strong chunky planar shapes and simple matte shading, premium coherent game icon, not photorealistic. Composition: square 1:1 canvas, whole single subject centered, all parts visible with comfortable transparent margin, occupy about 75–80 percent of the canvas. Three-quarter front view looking toward viewer's right. Lighting: soft key light from upper left, restrained ambient fill. Palette: petrol #247A80 primary, ocher #C79A48 accents, steel #657481, cream ceramic #E6DCCA; dark slate shadows. Keep broad masses and silhouette readable at 48px. Scene: genuinely transparent background with preserved alpha, no checkerboard pattern drawn into image. No ground, no shadow plane, no frame, no text, no lettering, no logo, no border, no watermark, no unrelated props, no other characters, no realistic texture or excessive microdetail. Subject: an upgraded grand clockwork conductor automaton, tall imposing petrol bell-shaped ceremonial mantle made of broad mechanical armor plates, cream ceramic mask, large ocher metronome crown with three clearly readable prongs, visible ocher pendulum in its torso, two steel articulated arms conducting with a long baton, small sturdy feet; regal wide triangular silhouette distinctly different from the small thin spindle conductor.</pre></details><div class="card-links"><a href="masters/nietenkonvent/unit_m10_taktpraelat_icon.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/m10.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="n01" data-faction="nebelchor" data-kind="unit_icon" data-state="ready" data-search="n01 Saummaske Nebelchor Einheitenicon Kompakter gefalteter Nebelkörper mit heller Ovalmaske, Sichelarmen und zwei Standranken.">
|
||||||
|
<div class="card-top"><code>n01</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/nebelchor/unit_n01_saummaske_icon.png" target="_blank" rel="noopener" aria-label="Saummaske: PNG-Original öffnen">
|
||||||
|
<img src="masters/nebelchor/unit_n01_saummaske_icon.png" alt="Saummaske" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Nebelchor <span aria-hidden="true">/</span> Einheitenicon</p>
|
||||||
|
<h2>Saummaske</h2><p class="description">Kompakter gefalteter Nebelkörper mit heller Ovalmaske, Sichelarmen und zwei Standranken.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Asset type: original fantasy strategy game unit icon, square transparent PNG. Subject: SAUMMASKE, a small defensive nonhuman mist spirit made of folded indigo and violet mist planes, standing on two short folded tendril feet. One large free-standing smooth oval pale mist mask with two narrow mint eye slits, a compact oval shield-like front body and small crescent arms. Its silhouette is compact, simple and beginner-tier. No human face, no human wizard, no hood. Style: polished faceted illustrated strategy-game icon, broad matte low-poly-like faces, very few large clean shapes, readable at 48px. Composition: entire single creature centered, occupies 75-80 percent of a square canvas, ample transparent margin on all sides, three-quarter front view facing slightly right. Lighting: soft upper-left key light with restrained mint luminous accents. Palette: indigo #45456F, violet #9578CF, pale mist #C4D6E5, mint #82DBCE only. Background must be actual transparent alpha; no painted checkerboard, no scenery, no ground, no shadow plane, no text, no numbers, no border, no frame, no watermark. Keep all feet and arms inside frame.</pre></details><div class="card-links"><a href="masters/nebelchor/unit_n01_saummaske_icon.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/n01.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="n02" data-faction="nebelchor" data-kind="unit_icon" data-state="ready" data-search="n02 Grenzmaske Nebelchor Einheitenicon Breiter Nebeltank mit heller Ovalmaske und mehreren geschichteten Sichelschilden.">
|
||||||
|
<div class="card-top"><code>n02</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/nebelchor/unit_n02_grenzmaske_icon.png" target="_blank" rel="noopener" aria-label="Grenzmaske: PNG-Original öffnen">
|
||||||
|
<img src="masters/nebelchor/unit_n02_grenzmaske_icon.png" alt="Grenzmaske" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Nebelchor <span aria-hidden="true">/</span> Einheitenicon</p>
|
||||||
|
<h2>Grenzmaske</h2><p class="description">Breiter Nebeltank mit heller Ovalmaske und mehreren geschichteten Sichelschilden.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Asset type: original fantasy strategy-game unit icon, square transparent PNG. Subject: GRENZMASKE, the upgraded heavy shield tank of a nonhuman mist faction. A broad folded-indigo mist body supported by two sturdy short tendril feet, a free-standing large pale oval mask with narrow mint eyes at the top center, thick layered crescent shield plates wrapping the front and shoulders into a wide defensive shell. Violet and pale mist shield layers are the main identifying shape; visibly much wider and tougher than a basic compact mask spirit. No weapon, no human face, no hood, no human wizard. Style: polished faceted illustration, broad matte low-poly-like faces, few bold clean shapes, readable at 48px. Composition: single entire unit centered in square canvas with transparent margins, figure occupies 75-80 percent, three-quarter front view facing slightly right. Lighting soft from upper left, restrained mint luminous accents. Palette only indigo #45456F, violet #9578CF, mist white-blue #C4D6E5 and mint #82DBCE. Actual transparent alpha background required; no fake checkerboard, scenery, ground, shadow plane, letters, numbers, frame, border or watermark. All body parts inside canvas.</pre></details><div class="card-links"><a href="masters/nebelchor/unit_n02_grenzmaske_icon.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/n02.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="n03" data-faction="nebelchor" data-kind="unit_icon" data-state="ready" data-search="n03 Fadentänzer Nebelchor Einheitenicon Schlanker maskierter Nebeltänzer mit zwei versetzt geführten Fadenklingen und langen Standranken.">
|
||||||
|
<div class="card-top"><code>n03</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/nebelchor/unit_n03_fadentaenzer_icon.png" target="_blank" rel="noopener" aria-label="Fadentänzer: PNG-Original öffnen">
|
||||||
|
<img src="masters/nebelchor/unit_n03_fadentaenzer_icon.png" alt="Fadentänzer" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Nebelchor <span aria-hidden="true">/</span> Einheitenicon</p>
|
||||||
|
<h2>Fadentänzer</h2><p class="description">Schlanker maskierter Nebeltänzer mit zwei versetzt geführten Fadenklingen und langen Standranken.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Asset type: original fantasy strategy-game unit icon, square transparent PNG. Subject: FADENTÄNZER, a slim agile nonhuman mist-spirit melee dancer. A small pale free-standing oval mask with mint eyes floats above a narrow body formed of folded indigo and violet ribbon-like mist. Two graceful long crescent forearms each carry a short thin pale thread-blade, kept thick enough to read as an icon. Two long folded tendril legs in a poised dancing stance, one arm diagonally high and the other low; clear balanced silhouette, no floor. No human face, no hood, no wizard, no clothes. Style: polished faceted illustration, broad matte low-poly-like planar surfaces, few large clean shapes, simplified for readability at 48px. Entire single full-body creature centered and occupying 75-80 percent square canvas, transparent margin on all sides. Three-quarter front view facing slightly right. Soft upper-left lighting; palette indigo #45456F, violet #9578CF, pale mist #C4D6E5, mint #82DBCE. Actual transparent alpha background; no checkerboard picture, no backdrop, no scenery, no ground, no shadow plane, no text, no frame, no border, no watermark. Keep entire blades and feet in frame.</pre></details><div class="card-links"><a href="masters/nebelchor/unit_n03_fadentaenzer_icon.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/n03.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="n04" data-faction="nebelchor" data-kind="unit_icon" data-state="ready" data-search="n04 Saumzertrenner Nebelchor Einheitenicon Hoher Nebelklingenkrieger mit Sichelkamm und großer geöffneter Sichelschere.">
|
||||||
|
<div class="card-top"><code>n04</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/nebelchor/unit_n04_saumzertrenner_icon.png" target="_blank" rel="noopener" aria-label="Saumzertrenner: PNG-Original öffnen">
|
||||||
|
<img src="masters/nebelchor/unit_n04_saumzertrenner_icon.png" alt="Saumzertrenner" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Nebelchor <span aria-hidden="true">/</span> Einheitenicon</p>
|
||||||
|
<h2>Saumzertrenner</h2><p class="description">Hoher Nebelklingenkrieger mit Sichelkamm und großer geöffneter Sichelschere.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Asset type: original fantasy strategy-game unit icon, square transparent PNG. Subject: SAUMZERTRENNER, an upgraded nonhuman mist-spirit melee dancer and seam cutter. Tall strong but narrow folded-indigo and violet ribbon-mist body, two planted tendril feet, free-standing pale oval mask with mint eyes and a tall simple crescent crest. The character holds ONE spectacular oversized OPEN PAIR OF SCISSORS made of two wide sweeping crescent blades with a clearly visible round mint pivot; weapon placed diagonally across the body and extending to the side so its scissor silhouette is unmistakable. A stronger more dramatic upgrade of a slim two-blade mist dancer. Nonhuman faceted mist anatomy, no human face, no hood, no wizard, no cloth robes. Style polished faceted strategy-game illustration with broad matte low-poly-like planar surfaces, simplified large shapes readable at 48px. Entire single creature and scissors centered in square canvas with generous transparent margin, occupy 75-80 percent, three-quarter front view facing slightly right. Soft upper-left light. Palette indigo #45456F, violet #9578CF, pale mist #C4D6E5, mint #82DBCE. Actual transparent alpha background; no checkerboard image, no scenery, no ground, no shadow plane, no letters, no border, frame or watermark. All blades and feet remain in canvas.</pre></details><div class="card-links"><a href="masters/nebelchor/unit_n04_saumzertrenner_icon.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/n04.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="n05" data-faction="nebelchor" data-kind="unit_icon" data-state="ready" data-search="n05 Fernflüsterer Nebelchor Einheitenicon Schmale gefaltete Maskenfigur mit langem Flüsterhorn und mintfarbener Schallöffnung.">
|
||||||
|
<div class="card-top"><code>n05</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/nebelchor/unit_n05_fernfluesterer_icon.png" target="_blank" rel="noopener" aria-label="Fernflüsterer: PNG-Original öffnen">
|
||||||
|
<img src="masters/nebelchor/unit_n05_fernfluesterer_icon.png" alt="Fernflüsterer" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Nebelchor <span aria-hidden="true">/</span> Einheitenicon</p>
|
||||||
|
<h2>Fernflüsterer</h2><p class="description">Schmale gefaltete Maskenfigur mit langem Flüsterhorn und mintfarbener Schallöffnung.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Asset type: original fantasy strategy-game unit icon, square transparent PNG. Subject: FERNFLÜSTERER, a nonhuman mist-spirit sharpshooter. A thin curved indigo and violet folded-mist body on two planted tendril feet, one pale narrow oval floating mask with mint slit eyes, two crescent arms aiming ONE LONG TUBULAR WHISPER HORN forward diagonally toward the right. Horn is a simple pale straight-to-gently-curved tube with a small flared round opening and mint bore, unmistakable hollow acoustic instrument; not a bow or gun. Compact base-tier design, broad readable horn and minimal details. Nonhuman mask creature, no human face, no hood, no wizard or clothes. Style: polished faceted fantasy strategy-game illustration, broad matte low-poly-like faces, large clean shapes readable at 48px. Full body including horn centered on square canvas with transparent margins, 75-80 percent occupancy. Three-quarter front view facing slightly right. Soft upper-left key light, restrained mint glow. Palette indigo #45456F, violet #9578CF, pale mist #C4D6E5, mint #82DBCE. Actual transparent alpha background required; no checkerboard drawing, backdrop, scenery, floor, ground, shadow plane, letters, numbers, frame, border or watermark. All horn and feet inside canvas.</pre></details><div class="card-links"><a href="masters/nebelchor/unit_n05_fernfluesterer_icon.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/n05.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="n06" data-faction="nebelchor" data-kind="unit_icon" data-state="ready" data-search="n06 Fernrufer Nebelchor Einheitenicon Aufgewerteter Nebelrufer mit zwei weit geschwungenen Resonanzhörnern und mintfarbenen Schallöffnungen.">
|
||||||
|
<div class="card-top"><code>n06</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/nebelchor/unit_n06_fernrufer_icon.png" target="_blank" rel="noopener" aria-label="Fernrufer: PNG-Original öffnen">
|
||||||
|
<img src="masters/nebelchor/unit_n06_fernrufer_icon.png" alt="Fernrufer" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Nebelchor <span aria-hidden="true">/</span> Einheitenicon</p>
|
||||||
|
<h2>Fernrufer</h2><p class="description">Aufgewerteter Nebelrufer mit zwei weit geschwungenen Resonanzhörnern und mintfarbenen Schallöffnungen.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Asset type: original fantasy strategy-game unit icon, square transparent PNG. Subject: FERNRUFER, an upgraded nonhuman mist-spirit ranged caller. A tall folded-indigo and violet mist body, two firmly planted tendril feet, narrow free-standing pale oval mask with mint eye slits, broad upward crescent shoulders supporting EXACTLY TWO large sweeping curved resonance horns. One horn rises on each side of the body and curves forward, both visibly hollow with large flared pale openings and mint interiors, separate clear horn silhouettes. Two crescent arms gesture forward between the horns. Stronger fuller silhouette than a thin spirit with a single whisper tube. No human face, no human clothing, no hood, no wizard, no bow or gun. Style polished faceted fantasy strategy-game illustration with broad matte low-poly-like planes, bold simple shapes, readable at 48px. Single full-body creature and both horns fully inside square canvas, centered with generous transparent margins, occupy 75-80 percent. Three-quarter front view facing slightly right. Soft light from upper left, restrained luminous mint accents. Palette indigo #45456F, violet #9578CF, pale mist #C4D6E5, mint #82DBCE. Actual transparent alpha background; no checkerboard image, scenery, ground, floor, shadow plane, text, numbers, border, frame or watermark.</pre></details><div class="card-links"><a href="masters/nebelchor/unit_n06_fernrufer_icon.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/n06.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="n07" data-faction="nebelchor" data-kind="unit_icon" data-state="ready" data-search="n07 Glockenschlepper Nebelchor Einheitenicon Gedrungener Nebelträger mit großer heller Glocke und deutlich sichtbarem mintfarbenem Klöppel.">
|
||||||
|
<div class="card-top"><code>n07</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/nebelchor/unit_n07_glockenschlepper_icon.png" target="_blank" rel="noopener" aria-label="Glockenschlepper: PNG-Original öffnen">
|
||||||
|
<img src="masters/nebelchor/unit_n07_glockenschlepper_icon.png" alt="Glockenschlepper" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Nebelchor <span aria-hidden="true">/</span> Einheitenicon</p>
|
||||||
|
<h2>Glockenschlepper</h2><p class="description">Gedrungener Nebelträger mit großer heller Glocke und deutlich sichtbarem mintfarbenem Klöppel.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Asset type: original fantasy strategy-game unit icon, square transparent PNG. Subject: GLOCKENSCHLEPPER, a squat nonhuman mist spirit carrying one oversized bell. Short broad folded-indigo and violet mist body, two stubby planted tendril feet, small free-standing pale oval mask with mint eye slits above the bell, two short crescent arms firmly carrying a HUGE SINGLE PALE VIOLET BELL in front of its belly. Bell has a broad flared rim and a clearly visible dark hollow mouth and mint clapper; plain broad facets without ornate engravings. Its heavy burden and squat silhouette identify a base-tier area-damage unit. No human face, no hood, no wizard, no robes. Style polished faceted fantasy strategy-game illustration, broad matte low-poly-like faces, few simple bold shapes readable at 48px. Entire one creature and bell centered on square canvas, 75-80 percent canvas with generous transparent margins, three-quarter front view facing slightly right. Soft upper-left key light, restrained mint accents. Palette indigo #45456F, violet #9578CF, pale mist #C4D6E5, mint #82DBCE. Actual transparent alpha background; no checkerboard image, scenery, ground, floor, shadow plane, text, numbers, border, frame or watermark. Keep all feet and bell in frame.</pre></details><div class="card-links"><a href="masters/nebelchor/unit_n07_glockenschlepper_icon.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/n07.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="n08" data-faction="nebelchor" data-kind="unit_icon" data-state="ready" data-search="n08 Hohlgeläut Nebelchor Einheitenicon Hoher lebender Kathedralenglockenkörper mit drei Sichelspitzen und mintfarbenem Klöppel im offenen Brustraum.">
|
||||||
|
<div class="card-top"><code>n08</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/nebelchor/unit_n08_hohlgelaeut_icon.png" target="_blank" rel="noopener" aria-label="Hohlgeläut: PNG-Original öffnen">
|
||||||
|
<img src="masters/nebelchor/unit_n08_hohlgelaeut_icon.png" alt="Hohlgeläut" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Nebelchor <span aria-hidden="true">/</span> Einheitenicon</p>
|
||||||
|
<h2>Hohlgeläut</h2><p class="description">Hoher lebender Kathedralenglockenkörper mit drei Sichelspitzen und mintfarbenem Klöppel im offenen Brustraum.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Asset type: original fantasy strategy-game unit icon, square transparent PNG. Subject: HOHLGELÄUT, an upgraded nonhuman mist bell spirit. One large hollow cathedral-bell BODY, tall and imposing, shaped as a broad flared violet bell with three simple upward pale crescent spires and a huge dark arched hollow opening in the front. A luminous mint clapper suspended inside the hollow chest. Small pale oval mask with mint eyes hovering at the top of the arch; heavy indigo folded mist tendril legs planted below the bell, two short crescent arms at the sides. This must read as ONE CREATURE whose body is a cathedral bell, not a building or a spirit carrying a bell; bold broad base and taller upgraded silhouette. No human anatomy, no hood, no wizard, no robes, no architectural scenery. Style polished faceted fantasy strategy-game illustration, broad matte low-poly-like planar surfaces, sparse large clean shapes readable at 48px. Entire single creature centered in square canvas, 75-80 percent occupancy with generous transparent margin, three-quarter front view facing slightly right. Soft upper-left light. Palette indigo #45456F, violet #9578CF, pale mist #C4D6E5, mint #82DBCE. Actual transparent alpha background required; no checkerboard picture, scenery, ground, floor, shadow plane, text, numbers, border, frame or watermark. All spires and feet inside canvas.</pre></details><div class="card-links"><a href="masters/nebelchor/unit_n08_hohlgelaeut_icon.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/n08.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="n09" data-faction="nebelchor" data-kind="unit_icon" data-state="ready" data-search="n09 Maskenkantor Nebelchor Einheitenicon Kompakter maskierter Nebelsänger mit einer frei schwebenden Begleitmaske über dem erhobenen Sichelarm.">
|
||||||
|
<div class="card-top"><code>n09</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/nebelchor/unit_n09_maskenkantor_icon.png" target="_blank" rel="noopener" aria-label="Maskenkantor: PNG-Original öffnen">
|
||||||
|
<img src="masters/nebelchor/unit_n09_maskenkantor_icon.png" alt="Maskenkantor" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Nebelchor <span aria-hidden="true">/</span> Einheitenicon</p>
|
||||||
|
<h2>Maskenkantor</h2><p class="description">Kompakter maskierter Nebelsänger mit einer frei schwebenden Begleitmaske über dem erhobenen Sichelarm.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Asset type: original fantasy strategy-game unit icon, square transparent PNG. Subject: MASKENKANTOR, a small nonhuman mist-spirit support singer. Compact folded-indigo and violet mist body supported by two short planted tendril feet, one pale oval face-mask with narrow mint eyes and an open small singing mouth; crescent arms gently conducting. EXACTLY ONE additional smaller pale mask floats separately above the raised hand on the right with visible empty transparent separation. Two masks total, only one whole creature. A simple pale crescent collar and broad folded body imply light ceremonial armor, never human clothing or hood. The companion mask shows the support role without particle clutter. Base-tier, simple compact silhouette. No human face, no hooded wizard, no microphone, no music notes. Style: polished faceted fantasy strategy-game illustration, broad matte low-poly-like surfaces, bold simple shapes readable at 48px. Entire figure and companion mask centered in square canvas, occupy 75-80 percent with ample transparent margins. Three-quarter front view facing slightly right; soft upper-left light. Palette indigo #45456F, violet #9578CF, pale mist #C4D6E5, mint #82DBCE. Actual transparent alpha background; no checkerboard drawing, scenery, ground, floor, shadow plane, text, numbers, border, frame or watermark. Keep every part inside canvas.</pre></details><div class="card-links"><a href="masters/nebelchor/unit_n09_maskenkantor_icon.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/n09.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="n10" data-faction="nebelchor" data-kind="unit_icon" data-state="ready" data-search="n10 Vielstimmenkantor Nebelchor Einheitenicon Gekrönter Nebelchor-Kantor mit breitem Sichelkragen und drei getrennt schwebenden Begleitmasken.">
|
||||||
|
<div class="card-top"><code>n10</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/nebelchor/unit_n10_vielstimmenkantor_icon.png" target="_blank" rel="noopener" aria-label="Vielstimmenkantor: PNG-Original öffnen">
|
||||||
|
<img src="masters/nebelchor/unit_n10_vielstimmenkantor_icon.png" alt="Vielstimmenkantor" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Nebelchor <span aria-hidden="true">/</span> Einheitenicon</p>
|
||||||
|
<h2>Vielstimmenkantor</h2><p class="description">Gekrönter Nebelchor-Kantor mit breitem Sichelkragen und drei getrennt schwebenden Begleitmasken.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Asset type: original fantasy strategy-game unit icon, square transparent PNG. Subject: VIELSTIMMENKANTOR, an upgraded nonhuman choir spirit. ONE central tall folded-indigo and violet mist body standing on two broad planted tendril feet, one large pale oval singing mask with mint slit eyes and open mouth, topped by a bold three-point crescent crown. Two wide crescent conducting arms. EXACTLY THREE smaller pale singing masks float around the central upper body: one at left shoulder height, one at right shoulder height, one above-right; each separated from body and other masks by transparent space, no extra bodies. Four masks total, but one single game unit. Layered pale crescent collar and full broad folds make a stronger imposing support upgrade of a small singer with one companion mask. No human face, no hood or wizard, no robes, no musical notation. Style polished faceted fantasy strategy-game illustration, broad matte low-poly-like faces, large clean shapes readable at 48px, avoid tiny decoration and particle clutter. Entire single creature and all three floating masks centered in square canvas with generous margins, occupy 75-80 percent. Three-quarter front view facing slightly right. Soft upper-left light, restrained mint glow. Palette indigo #45456F, violet #9578CF, pale mist #C4D6E5, mint #82DBCE. Actual transparent alpha background; no checkerboard drawing, scene, ground, floor, shadow plane, text, numbers, border, frame or watermark. Keep all masks and feet inside canvas.</pre></details><div class="card-links"><a href="masters/nebelchor/unit_n10_vielstimmenkantor_icon.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/n10.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="w01" data-faction="wurzelbund" data-kind="unit_icon" data-state="ready" data-search="w01 Dornhopser Wurzelbund Einheitenicon Kleine hüpfende Dornkapsel auf breiten Wurzelfüßen.">
|
||||||
|
<div class="card-top"><code>w01</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/wurzelbund/unit_w01_dornhopser_icon.png" target="_blank" rel="noopener" aria-label="Dornhopser: PNG-Original öffnen">
|
||||||
|
<img src="masters/wurzelbund/unit_w01_dornhopser_icon.png" alt="Dornhopser" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Wurzelbund <span aria-hidden="true">/</span> Einheitenicon</p>
|
||||||
|
<h2>Dornhopser</h2><p class="description">Kleine hüpfende Dornkapsel auf breiten Wurzelfüßen.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Asset type: original fantasy strategy-game unit UI icon for Saumwacht, Wurzelbund faction. Style: faceted illustrated low-poly game art, broad flat matte planes, crisp silhouette and edges, chunky simple forms, high readability at 256px, not photorealistic, not a painting. Composition: ONE whole single creature centered, square canvas, complete feet and appendages visible, creature occupies 75-80% of canvas, generous transparent margins. 3/4 front view facing slightly right. Lighting: soft upper-left light, restrained shadows within subject. Palette: bark #685441, moss #56845A, leaf #A7BA6C, sparse blossom #D996AF accents. Scene: truly transparent alpha background, no checkerboard pixels, no ground plane, no scenery, no frame, no text, no logo, no watermark, no cast shadow. Wurzelbund creatures are mobile wood, root, leaf and clay organisms, never humanoid wood elves.
|
||||||
|
Subject: Dornhopser: a small hopping round thorn-pod creature with stout root feet, compact brown seed-shell body, moss green triangular thorn blades, a tiny slit face, small paired leaf sprouts; inexpensive melee attacker. Root feet and thorn-pod silhouette are defining. No weapons, no human proportions.</pre></details><div class="card-links"><a href="masters/wurzelbund/unit_w01_dornhopser_icon.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/w01.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="w02" data-faction="wurzelbund" data-kind="unit_icon" data-state="ready" data-search="w02 Rankenschnapper Wurzelbund Einheitenicon Breiter Rankenräuber mit großem Dornenmaul und Wurzelfüßen.">
|
||||||
|
<div class="card-top"><code>w02</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/wurzelbund/unit_w02_rankenschnapper_icon.png" target="_blank" rel="noopener" aria-label="Rankenschnapper: PNG-Original öffnen">
|
||||||
|
<img src="masters/wurzelbund/unit_w02_rankenschnapper_icon.png" alt="Rankenschnapper" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Wurzelbund <span aria-hidden="true">/</span> Einheitenicon</p>
|
||||||
|
<h2>Rankenschnapper</h2><p class="description">Breiter Rankenräuber mit großem Dornenmaul und Wurzelfüßen.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Asset type: original fantasy strategy-game unit UI icon for Saumwacht, Wurzelbund faction. Style: faceted illustrated low-poly game art, broad flat matte planes, crisp silhouette and edges, chunky simple forms, high readability at 256px, not photorealistic, not a painting. Composition: ONE whole single creature centered, square canvas, complete feet and appendages visible, creature occupies 75-80% of canvas, generous transparent margins. 3/4 front view facing slightly right. Lighting: soft upper-left light, restrained shadows within subject. Palette: bark #685441, moss #56845A, leaf #A7BA6C, sparse blossom #D996AF accents. Scene: truly transparent alpha background, no checkerboard pixels, no ground plane, no scenery, no frame, no text, no logo, no watermark, no cast shadow. Wurzelbund creatures are mobile wood, root, leaf and clay organisms, never humanoid wood elves.
|
||||||
|
Subject: Rankenschnapper: an upgraded robust vine-jaw predator, broad hinged plant maw with a few oversized sharp thorn fangs, thick coiled bark vines forming stocky mobile torso, four chunky root feet, broad green leaf cheek plates. Controlled fierce expression; silhouette dominated by the huge horizontal snapping jaw. No humanoid anatomy, no weapons.</pre></details><div class="card-links"><a href="masters/wurzelbund/unit_w02_rankenschnapper_icon.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/w02.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="w03" data-faction="wurzelbund" data-kind="unit_icon" data-state="ready" data-search="w03 Korkrücken Wurzelbund Einheitenicon Gedrungener Tank mit breitem Korkpanzer, Moos und Wurzelfüßen.">
|
||||||
|
<div class="card-top"><code>w03</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/wurzelbund/unit_w03_korkruecken_icon.png" target="_blank" rel="noopener" aria-label="Korkrücken: PNG-Original öffnen">
|
||||||
|
<img src="masters/wurzelbund/unit_w03_korkruecken_icon.png" alt="Korkrücken" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Wurzelbund <span aria-hidden="true">/</span> Einheitenicon</p>
|
||||||
|
<h2>Korkrücken</h2><p class="description">Gedrungener Tank mit breitem Korkpanzer, Moos und Wurzelfüßen.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Asset type: original fantasy strategy-game unit UI icon for Saumwacht, Wurzelbund faction. Style: faceted illustrated low-poly game art, broad flat matte planes, crisp silhouette and edges, chunky simple forms, high readability at 256px, not photorealistic, not a painting. Composition: ONE whole single creature centered, square canvas, complete feet and appendages visible, creature occupies 75-80% of canvas, generous transparent margins. 3/4 front view facing slightly right. Lighting: soft upper-left light, restrained shadows within subject. Palette: bark #685441, moss #56845A, leaf #A7BA6C, sparse blossom #D996AF accents. Scene: truly transparent alpha background, no checkerboard pixels, no ground plane, no scenery, no frame, no text, no logo, no watermark, no cast shadow. Wurzelbund creatures are mobile wood, root, leaf and clay organisms, never humanoid wood elves.
|
||||||
|
Subject: Korkrücken: a squat nonhumanoid wooden tank creature under an oversized shield-back of thick cork bark plates, four broad stubby root feet, very low head with tiny leaf-green eyes peeking under the front shell, rounded compact defensive silhouette. Bark brown dominates with moss green seams, a few large leaf plates. Entire creature easily fits the square with 12% transparent margin on EVERY side.</pre></details><div class="card-links"><a href="masters/wurzelbund/unit_w03_korkruecken_icon.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/w03.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="w04" data-faction="wurzelbund" data-kind="unit_icon" data-state="ready" data-search="w04 Ringborke Wurzelbund Einheitenicon Großer Wurzeltank mit konzentrischen Holzringen als massiver Panzerung.">
|
||||||
|
<div class="card-top"><code>w04</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/wurzelbund/unit_w04_ringborke_icon.png" target="_blank" rel="noopener" aria-label="Ringborke: PNG-Original öffnen">
|
||||||
|
<img src="masters/wurzelbund/unit_w04_ringborke_icon.png" alt="Ringborke" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Wurzelbund <span aria-hidden="true">/</span> Einheitenicon</p>
|
||||||
|
<h2>Ringborke</h2><p class="description">Großer Wurzeltank mit konzentrischen Holzringen als massiver Panzerung.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Asset type: original fantasy strategy-game unit UI icon for Saumwacht, Wurzelbund faction. Style: faceted illustrated low-poly game art, broad flat matte planes, crisp silhouette and edges, chunky simple forms, high readability at 256px, not photorealistic, not a painting. Composition: ONE whole single creature centered, square canvas, complete feet and appendages visible, creature occupies 75-80% of canvas, generous transparent margins. 3/4 front view facing slightly right. Lighting: soft upper-left light, restrained shadows within subject. Palette: bark #685441, moss #56845A, leaf #A7BA6C, sparse blossom #D996AF accents. Scene: truly transparent alpha background, no checkerboard pixels, no ground plane, no scenery, no frame, no text, no logo, no watermark, no cast shadow. Wurzelbund creatures are mobile wood, root, leaf and clay organisms, never humanoid wood elves.
|
||||||
|
Subject: Ringborke: a huge upgraded nonhumanoid wooden tank, massive overlapping circular trunk cross-section armor shields showing clear bold concentric wood growth rings, thick root limbs and flat wide root feet, small recessed leaf-green eyes, heavy shoulder and back bulk, tiny moss patches and few leaves. It is a mobile root organism, not a humanoid ent. Silhouette strongly broad and heavy; dominant readable wooden concentric circles. Entire creature fits square with 12% transparent margin on EVERY side.</pre></details><div class="card-links"><a href="masters/wurzelbund/unit_w04_ringborke_icon.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/w04.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="w05" data-faction="wurzelbund" data-kind="unit_icon" data-state="ready" data-search="w05 Samenschleuder Wurzelbund Einheitenicon Schlanker Wurzelfernkämpfer mit gebogener Ranke und offener Samenkapsel als Wurfschale.">
|
||||||
|
<div class="card-top"><code>w05</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/wurzelbund/unit_w05_samenschleuder_icon.png" target="_blank" rel="noopener" aria-label="Samenschleuder: PNG-Original öffnen">
|
||||||
|
<img src="masters/wurzelbund/unit_w05_samenschleuder_icon.png" alt="Samenschleuder" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Wurzelbund <span aria-hidden="true">/</span> Einheitenicon</p>
|
||||||
|
<h2>Samenschleuder</h2><p class="description">Schlanker Wurzelfernkämpfer mit gebogener Ranke und offener Samenkapsel als Wurfschale.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Asset type: original fantasy strategy-game unit UI icon for Saumwacht, Wurzelbund faction. Style: faceted illustrated low-poly game art, broad flat matte planes, crisp silhouette and edges, chunky simple forms, high readability at 256px, not photorealistic, not a painting. Composition: ONE whole single creature centered, square canvas, complete feet and appendages visible, creature occupies 75-80% of canvas, generous transparent margins. 3/4 front view facing slightly right. Lighting: soft upper-left light, restrained shadows within subject. Palette: bark #685441, moss #56845A, leaf #A7BA6C, sparse blossom #D996AF accents. Scene: truly transparent alpha background, no checkerboard pixels, no ground plane, no scenery, no frame, no text, no logo, no watermark, no cast shadow. Wurzelbund creatures are mobile wood, root, leaf and clay organisms, never humanoid wood elves.
|
||||||
|
Subject: Samenschleuder: a slender mobile seedpod catapult creature; three thin arched living root legs spread into broad root toes, flexible curved vine torso bent back like a spring, one large open green seedpod cup at its top holding one chunky brown seed ready to launch. Two leaf fins for balance, tiny slit eyes in the bark below the cup. Clear elegant narrow ranged-unit silhouette, entirely plant organism, no humanoid arms, no mechanical hardware. Entire creature fits square with 12% transparent margin on EVERY side.</pre></details><div class="card-links"><a href="masters/wurzelbund/unit_w05_samenschleuder_icon.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/w05.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="w06" data-faction="wurzelbund" data-kind="unit_icon" data-state="ready" data-search="w06 Zapfenkrone Wurzelbund Einheitenicon Hoher Samenschütze mit großer Zapfenkrone, kräftigen Ranken und Blattschalen.">
|
||||||
|
<div class="card-top"><code>w06</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/wurzelbund/unit_w06_zapfenkrone_icon.png" target="_blank" rel="noopener" aria-label="Zapfenkrone: PNG-Original öffnen">
|
||||||
|
<img src="masters/wurzelbund/unit_w06_zapfenkrone_icon.png" alt="Zapfenkrone" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Wurzelbund <span aria-hidden="true">/</span> Einheitenicon</p>
|
||||||
|
<h2>Zapfenkrone</h2><p class="description">Hoher Samenschütze mit großer Zapfenkrone, kräftigen Ranken und Blattschalen.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Asset type: original fantasy strategy-game unit UI icon for Saumwacht, Wurzelbund faction. Style: faceted illustrated low-poly game art, broad flat matte planes, crisp silhouette and edges, chunky simple forms, high readability at 256px, not photorealistic, not a painting. Composition: ONE whole single creature centered, square canvas, complete feet and appendages visible, creature occupies 75-80% of canvas, generous transparent margins. 3/4 front view facing slightly right. Lighting: soft upper-left light, restrained shadows within subject. Palette: bark #685441, moss #56845A, leaf #A7BA6C, sparse blossom #D996AF accents. Scene: truly transparent alpha background, no checkerboard pixels, no ground plane, no scenery, no frame, no text, no logo, no watermark, no cast shadow. Wurzelbund creatures are mobile wood, root, leaf and clay organisms, never humanoid wood elves.
|
||||||
|
Subject: Zapfenkrone: tall upgraded seed-launching plant creature with an enormous crown-shaped pinecone launcher on top, several broad overlapping bark scales forming an upward open cone crown, one chunky seed visible within, green leaf collars, strong curved vine stem torso, three mobile root legs with broad toes. Silhouette much taller and statelier than a small seedpod catapult, all living wood and plants, tiny eyes below crown, never humanoid. Entire creature fits square with 12% transparent margin on EVERY side.</pre></details><div class="card-links"><a href="masters/wurzelbund/unit_w06_zapfenkrone_icon.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/w06.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="w07" data-faction="wurzelbund" data-kind="unit_icon" data-state="ready" data-search="w07 Sporenkelch Wurzelbund Einheitenicon Niedriger offener Pilzkelch mit rosiger Sporenmulde und breiten Wurzelfüßen.">
|
||||||
|
<div class="card-top"><code>w07</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/wurzelbund/unit_w07_sporenkelch_icon.png" target="_blank" rel="noopener" aria-label="Sporenkelch: PNG-Original öffnen">
|
||||||
|
<img src="masters/wurzelbund/unit_w07_sporenkelch_icon.png" alt="Sporenkelch" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Wurzelbund <span aria-hidden="true">/</span> Einheitenicon</p>
|
||||||
|
<h2>Sporenkelch</h2><p class="description">Niedriger offener Pilzkelch mit rosiger Sporenmulde und breiten Wurzelfüßen.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Asset type: original fantasy strategy-game unit UI icon for Saumwacht, Wurzelbund faction. Style: faceted illustrated low-poly game art, broad flat matte planes, crisp silhouette and edges, chunky simple forms, high readability at 256px, not photorealistic, not a painting. Composition: ONE whole single creature centered, square canvas, complete feet and appendages visible, creature occupies 75-80% of canvas, generous transparent margins. 3/4 front view facing slightly right. Lighting: soft upper-left light, restrained shadows within subject. Palette: bark #685441, moss #56845A, leaf #A7BA6C, sparse blossom #D996AF accents. Scene: truly transparent alpha background, no checkerboard pixels, no ground plane, no scenery, no frame, no text, no logo, no watermark, no cast shadow. Wurzelbund creatures are mobile wood, root, leaf and clay organisms, never humanoid wood elves.
|
||||||
|
Subject: Sporenkelch: squat open mushroom-chalice creature, one huge concave cup fungus tilted enough to show its open spore bowl, dusty blossom pink inner cup with a few large pale seedlike spores resting inside, moss green outer lip, thick bark-clay stem body and four chunky root feet, tiny recessed eyes on stalk. Silhouette is unmistakably a broad open cup on a short rooted base, no humanoid anatomy. No drifting particles outside silhouette. Entire creature fits square with 12% transparent margin on EVERY side.</pre></details><div class="card-links"><a href="masters/wurzelbund/unit_w07_sporenkelch_icon.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/w07.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="w08" data-faction="wurzelbund" data-kind="unit_icon" data-state="ready" data-search="w08 Pilzkathedrale Wurzelbund Einheitenicon Große mobile Pilzarchitektur mit gestaffelten rosigen Pilzdächern und massivem Wurzelwerk.">
|
||||||
|
<div class="card-top"><code>w08</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/wurzelbund/unit_w08_pilzkathedrale_icon.png" target="_blank" rel="noopener" aria-label="Pilzkathedrale: PNG-Original öffnen">
|
||||||
|
<img src="masters/wurzelbund/unit_w08_pilzkathedrale_icon.png" alt="Pilzkathedrale" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Wurzelbund <span aria-hidden="true">/</span> Einheitenicon</p>
|
||||||
|
<h2>Pilzkathedrale</h2><p class="description">Große mobile Pilzarchitektur mit gestaffelten rosigen Pilzdächern und massivem Wurzelwerk.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Asset type: original fantasy strategy-game unit UI icon for Saumwacht, Wurzelbund faction. Style: faceted illustrated low-poly game art, broad flat matte planes, crisp silhouette and edges, chunky simple forms, high readability at 256px, not photorealistic, not a painting. Composition: ONE whole single creature centered, square canvas, complete feet and appendages visible, creature occupies 75-80% of canvas, generous transparent margins. 3/4 front view facing slightly right. Lighting: soft upper-left light, restrained shadows within subject. Palette: bark #685441, moss #56845A, leaf #A7BA6C, sparse blossom #D996AF accents. Scene: truly transparent alpha background, no checkerboard pixels, no ground plane, no scenery, no frame, no text, no logo, no watermark, no cast shadow. Wurzelbund creatures are mobile wood, root, leaf and clay organisms, never humanoid wood elves.
|
||||||
|
Subject: Pilzkathedrale: large upgraded mobile mushroom architecture creature, several broad stacked mushroom canopies arranged as a three-tier cathedral silhouette over one living bark and root body. Dusty blossom pink fungus caps and inner gills, moss green edges and leaf accents, heavy interwoven root buttresses and four broad mobile feet. Topmost cap forms an open spore cup; smaller side canopies step down symmetrically. Recognizable organic architectural silhouette, not an actual building, no windows or doors or stone masonry, no humans. Entire creature fits square with 12% transparent margin on EVERY side.</pre></details><div class="card-links"><a href="masters/wurzelbund/unit_w08_pilzkathedrale_icon.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/w08.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="w09" data-faction="wurzelbund" data-kind="unit_icon" data-state="ready" data-search="w09 Tauhüter Wurzelbund Einheitenicon Kleiner Blatthüter mit einzelnem Tautropfen in einer schützenden Blattschale.">
|
||||||
|
<div class="card-top"><code>w09</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/wurzelbund/unit_w09_tauhueter_icon.png" target="_blank" rel="noopener" aria-label="Tauhüter: PNG-Original öffnen">
|
||||||
|
<img src="masters/wurzelbund/unit_w09_tauhueter_icon.png" alt="Tauhüter" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Wurzelbund <span aria-hidden="true">/</span> Einheitenicon</p>
|
||||||
|
<h2>Tauhüter</h2><p class="description">Kleiner Blatthüter mit einzelnem Tautropfen in einer schützenden Blattschale.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Asset type: original fantasy strategy-game unit UI icon for Saumwacht, Wurzelbund faction. Style: faceted illustrated low-poly game art, broad flat matte planes, crisp silhouette and edges, chunky simple forms, high readability at 256px, not photorealistic, not a painting. Composition: ONE whole single creature centered, square canvas, complete feet and appendages visible, creature occupies 75-80% of canvas, generous transparent margins. 3/4 front view facing slightly right. Lighting: soft upper-left light, restrained shadows within subject. Palette: bark #685441, moss #56845A, leaf #A7BA6C, sparse blossom #D996AF accents. Scene: truly transparent alpha background, no checkerboard pixels, no ground plane, no scenery, no frame, no text, no logo, no watermark, no cast shadow. Wurzelbund creatures are mobile wood, root, leaf and clay organisms, never humanoid wood elves.
|
||||||
|
Subject: Tauhüter: small gentle nonhumanoid leaf healer, compact woven root body on three chunky root feet, two curved root tendrils supporting a single large cupped leaf in front of its body. The leaf holds exactly ONE oversized clear pale-mint dewdrop, the focal point. A simple three-leaf canopy on its back, tiny gentle green eyes beneath canopy. Rind brown and moss green dominate with one small pink blossom. No humanoid elf anatomy, no clothes, no staff. Entire creature fits square with 12% transparent margin on EVERY side.</pre></details><div class="card-links"><a href="masters/wurzelbund/unit_w09_tauhueter_icon.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/w09.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="w10" data-faction="wurzelbund" data-kind="unit_icon" data-state="ready" data-search="w10 Quellgeflecht Wurzelbund Einheitenicon Großer Heiler aus verflochtenen Wurzeln mit Quellwasserschale unter einem Blüten- und Blätterdach.">
|
||||||
|
<div class="card-top"><code>w10</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/wurzelbund/unit_w10_quellgeflecht_icon.png" target="_blank" rel="noopener" aria-label="Quellgeflecht: PNG-Original öffnen">
|
||||||
|
<img src="masters/wurzelbund/unit_w10_quellgeflecht_icon.png" alt="Quellgeflecht" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Wurzelbund <span aria-hidden="true">/</span> Einheitenicon</p>
|
||||||
|
<h2>Quellgeflecht</h2><p class="description">Großer Heiler aus verflochtenen Wurzeln mit Quellwasserschale unter einem Blüten- und Blätterdach.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Asset type: original fantasy strategy-game unit UI icon for Saumwacht, Wurzelbund faction. Style: faceted illustrated low-poly game art, broad flat matte planes, crisp silhouette and edges, chunky simple forms, high readability at 256px, not photorealistic, not a painting. Composition: ONE whole single creature centered, square canvas, complete feet and appendages visible, creature occupies 75-80% of canvas, generous transparent margins. 3/4 front view facing slightly right. Lighting: soft upper-left light, restrained shadows within subject. Palette: bark #685441, moss #56845A, leaf #A7BA6C, sparse blossom #D996AF accents. Scene: truly transparent alpha background, no checkerboard pixels, no ground plane, no scenery, no frame, no text, no logo, no watermark, no cast shadow. Wurzelbund creatures are mobile wood, root, leaf and clay organisms, never humanoid wood elves.
|
||||||
|
Subject: Quellgeflecht: large upgraded mobile woven-root healer creature, a wide open bowl of clear pale-mint spring water cradled in interlaced chunky living roots at its chest, a broad layered leaf-and-blossom canopy arching above bowl, thick braided root body and four sturdy root feet. Gentle recessed eyes tucked below canopy, dusty pink flowers accenting green leaves. Water bowl must be conspicuous and readable. Entirely organic mobile plant organism, not humanoid, no clothing, no architecture, no floating water particles. Entire creature fits square with 12% transparent margin on EVERY side.</pre></details><div class="card-links"><a href="masters/wurzelbund/unit_w10_quellgeflecht_icon.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/w10.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="s01" data-faction="neutral" data-kind="mercenary_icon" data-state="ready" data-search="s01 Nadelspäher Neutrale Söldnericon Schlanker maskierter Söldnerspäher mit kompaktem Nadelwerfer.">
|
||||||
|
<div class="card-top"><code>s01</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/neutral/unit_s01_nadelspaeher_icon.png" target="_blank" rel="noopener" aria-label="Nadelspäher: PNG-Original öffnen">
|
||||||
|
<img src="masters/neutral/unit_s01_nadelspaeher_icon.png" alt="Nadelspäher" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Neutrale <span aria-hidden="true">/</span> Söldnericon</p>
|
||||||
|
<h2>Nadelspäher</h2><p class="description">Schlanker maskierter Söldnerspäher mit kompaktem Nadelwerfer.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Asset type: original neutral mercenary UNIT ICON for Saumwacht, a single individual PNG cutout. Create a brand-new image. Style: crisp faceted illustrated low-poly fantasy, strong chunky planar shapes and simple matte shading, coherent premium strategy-game icon, not photorealistic. Composition: square 1:1 canvas, whole single figure centered, all parts visible with comfortable transparent margin, occupy 75–80 percent of canvas. Three-quarter front view looking toward viewer's right. Lighting: soft key from upper left and restrained ambient fill. Palette: slate #536375 and rust #A8654D with ivory #D8D2BF, restrained violet #9578CF; only the rift creature may add a small mint glow. Keep broad masses readable at 48px. Scene: genuinely transparent background with preserved alpha; no checkerboard pattern drawn into the image. No ground, no shadow plane, no frame, no text, no lettering, no logos, no border, no watermark, no unrelated props, no other characters, no realistic texture, no excessive microdetail. Subject: a thin masked scout mercenary, slim upright silhouette, pointed slate-gray hood and fitted layered dark armor, simple ivory full-face mask, compact handheld dart launcher pointing right, three clearly visible thick spare darts in a rust-red back quiver, narrow limbs and practical boots; original fantasy creature-like scout with no copied character references.</pre></details><div class="card-links"><a href="masters/neutral/unit_s01_nadelspaeher_icon.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/s01.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="s02" data-faction="neutral" data-kind="mercenary_icon" data-state="ready" data-search="s02 Rostschieber Neutrale Söldnericon Gedrungener schwerer Söldner mit Rostpanzer und großem Rammschild.">
|
||||||
|
<div class="card-top"><code>s02</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/neutral/unit_s02_rostschieber_icon.png" target="_blank" rel="noopener" aria-label="Rostschieber: PNG-Original öffnen">
|
||||||
|
<img src="masters/neutral/unit_s02_rostschieber_icon.png" alt="Rostschieber" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Neutrale <span aria-hidden="true">/</span> Söldnericon</p>
|
||||||
|
<h2>Rostschieber</h2><p class="description">Gedrungener schwerer Söldner mit Rostpanzer und großem Rammschild.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Asset type: original neutral mercenary UNIT ICON for Saumwacht, a single individual PNG cutout. Create a brand-new image. Style: crisp faceted illustrated low-poly fantasy, strong chunky planar shapes and simple matte shading, coherent premium strategy-game icon, not photorealistic. Composition: square 1:1 canvas, whole single figure centered, all parts visible with comfortable transparent margin, occupy 75–80 percent of canvas. Three-quarter front view looking toward viewer's right. Lighting: soft key from upper left and restrained ambient fill. Palette: slate #536375 and rust #A8654D with ivory #D8D2BF, restrained violet #9578CF; only the rift creature may add a small mint glow. Keep broad masses readable at 48px. Scene: genuinely transparent background with preserved alpha; no checkerboard pattern drawn into the image. No ground, no shadow plane, no frame, no text, no lettering, no logos, no border, no watermark, no unrelated props, no other characters, no realistic texture, no excessive microdetail. Subject: a squat heavy shield-ram mercenary, wide rust-red layered armor and dark slate body, thick ivory brow guard and tiny inset eyes, huge broad rectangular shield held ahead as a battering ram with a single steel wedge along its center, compact heavy legs and a short hammerlike free fist; low rectangular silhouette emphasizing a defensive shield.</pre></details><div class="card-links"><a href="masters/neutral/unit_s02_rostschieber_icon.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/s02.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="s03" data-faction="neutral" data-kind="mercenary_icon" data-state="ready" data-search="s03 Dunstsänger Neutrale Söldnericon Verhüllter violetter Dunstsänger mit großem Resonanzhorn.">
|
||||||
|
<div class="card-top"><code>s03</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/neutral/unit_s03_dunstsaenger_icon.png" target="_blank" rel="noopener" aria-label="Dunstsänger: PNG-Original öffnen">
|
||||||
|
<img src="masters/neutral/unit_s03_dunstsaenger_icon.png" alt="Dunstsänger" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Neutrale <span aria-hidden="true">/</span> Söldnericon</p>
|
||||||
|
<h2>Dunstsänger</h2><p class="description">Verhüllter violetter Dunstsänger mit großem Resonanzhorn.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Asset type: original neutral mercenary UNIT ICON for Saumwacht, a single individual PNG cutout. Create a brand-new image. Style: crisp faceted illustrated low-poly fantasy, strong chunky planar shapes and simple matte shading, coherent premium strategy-game icon, not photorealistic. Composition: square 1:1 canvas, whole single figure centered, all parts visible with comfortable transparent margin, occupy 75–80 percent of canvas. Three-quarter front view looking toward viewer's right. Lighting: soft key from upper left and restrained ambient fill. Palette: slate #536375 and rust #A8654D with ivory #D8D2BF, restrained violet #9578CF; only the rift creature may add a small mint glow. Keep broad masses readable at 48px. Scene: genuinely transparent background with preserved alpha; no checkerboard pattern drawn into the image. No ground, no shadow plane, no frame, no text, no lettering, no logos, no border, no watermark, no unrelated props, no other characters, no realistic texture, no excessive microdetail. Subject: a draped ethereal mist-singer mercenary, elongated bell-shaped dark slate and muted violet robe with broad faceted folds, small simple ivory mask inside a deep hood, two slender hands holding a large rust-bronze resonance horn with an open flared bell pointing to the right, a few contained violet wisps tucked inside the silhouette; full figure with no platform or ground.</pre></details><div class="card-links"><a href="masters/neutral/unit_s03_dunstsaenger_icon.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/s03.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="s04" data-faction="neutral" data-kind="mercenary_icon" data-state="ready" data-search="s04 Keilbrecher Neutrale Söldnericon Schwerer dunkelgrauer Keilbrecher mit großem Keilhammer.">
|
||||||
|
<div class="card-top"><code>s04</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/neutral/unit_s04_keilbrecher_icon.png" target="_blank" rel="noopener" aria-label="Keilbrecher: PNG-Original öffnen">
|
||||||
|
<img src="masters/neutral/unit_s04_keilbrecher_icon.png" alt="Keilbrecher" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Neutrale <span aria-hidden="true">/</span> Söldnericon</p>
|
||||||
|
<h2>Keilbrecher</h2><p class="description">Schwerer dunkelgrauer Keilbrecher mit großem Keilhammer.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Asset type: original neutral mercenary UNIT ICON for Saumwacht, a single individual PNG cutout. Create a brand-new image. Style: crisp faceted illustrated low-poly fantasy, strong chunky planar shapes and simple matte shading, coherent premium strategy-game icon, not photorealistic. Composition: square 1:1 canvas, whole single figure centered, all parts visible with comfortable transparent margin, occupy 75–80 percent of canvas. Three-quarter front view looking toward viewer's right. Lighting: soft key from upper left and restrained ambient fill. Palette: slate #536375 and rust #A8654D with ivory #D8D2BF, restrained violet #9578CF; only the rift creature may add a small mint glow. Keep broad masses readable at 48px. Scene: genuinely transparent background with preserved alpha; no checkerboard pattern drawn into the image. No ground, no shadow plane, no frame, no text, no lettering, no logos, no border, no watermark, no unrelated props, no other characters, no realistic texture, no excessive microdetail. Subject: a heavy dark-steel walking wedge-hammer mercenary, powerful broad-shouldered stocky warrior, massive blocky slate-gray armor with restrained rust-red accents, simple ivory faceplate, two heavy hands gripping a gigantic wedge-shaped hammer with a clear triangular chisel head held diagonally across the body, short sturdy legs and wide boots; strong triangle and block silhouette.</pre></details><div class="card-links"><a href="masters/neutral/unit_s04_keilbrecher_icon.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/s04.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="s05" data-faction="neutral" data-kind="mercenary_icon" data-state="ready" data-search="s05 Risszehrer Neutrale Söldnericon Hohler Risszehrer mit mintviolettem Schlund und magischer Entzugskugel.">
|
||||||
|
<div class="card-top"><code>s05</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/neutral/unit_s05_risszehrer_icon.png" target="_blank" rel="noopener" aria-label="Risszehrer: PNG-Original öffnen">
|
||||||
|
<img src="masters/neutral/unit_s05_risszehrer_icon.png" alt="Risszehrer" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Neutrale <span aria-hidden="true">/</span> Söldnericon</p>
|
||||||
|
<h2>Risszehrer</h2><p class="description">Hohler Risszehrer mit mintviolettem Schlund und magischer Entzugskugel.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Asset type: original neutral mercenary UNIT ICON for Saumwacht, a single individual PNG cutout. Create a brand-new image. Style: crisp faceted illustrated low-poly fantasy, strong chunky planar shapes and simple matte shading, coherent premium strategy-game icon, not photorealistic. Composition: square 1:1 canvas, whole single figure centered, all parts visible with comfortable transparent margin, occupy only 65 percent of canvas height; make the figure relatively small with wide empty transparent margins on ALL sides. Three-quarter front view looking toward viewer's right. Lighting: soft key from upper left and restrained ambient fill. Palette: slate #536375 and rust #A8654D with ivory #D8D2BF, restrained violet #9578CF; only the rift creature may add a small mint glow. Keep broad masses readable at 48px. Scene: genuinely transparent background with preserved alpha; no checkerboard pattern drawn into the image. No ground, no shadow plane, no frame, no text, no lettering, no logos, no border, no watermark, no unrelated props, no other characters, no realistic texture, no excessive microdetail. Subject: a tall hollow rift-feeding creature mercenary, original alien fantasy silhouette, long angular slate-gray shell torso with a large hollow chest opening glowing faint mint and violet, short compact ivory broken-arc crown around a faceless dark head with a restrained mint-violet mouth slit, two long tapering arms, one claw cradling a single small glowing mint-violet healing-drain orb close to its chest, long digitigrade legs; thin eerie silhouette with a very readable empty chest hole, no gore. Framing is critical: the entire small creature must sit within the central 65 percent of the square, crown tip far below top edge, feet far above bottom edge. Actual transparent PNG alpha, not an illustrated checkerboard.</pre></details><div class="card-links"><a href="masters/neutral/unit_s05_risszehrer_icon.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/s05.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="res_gold" data-faction="resources" data-kind="resource_icon" data-state="ready" data-search="res_gold Gold Ressourcen Ressourcenicon Eine große facettierte Goldmünze mit zwei kleineren Münzen.">
|
||||||
|
<div class="card-top"><code>res_gold</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/resources/res_gold.png" target="_blank" rel="noopener" aria-label="Gold: PNG-Original öffnen">
|
||||||
|
<img src="masters/resources/res_gold.png" alt="Gold" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Ressourcen <span aria-hidden="true">/</span> Ressourcenicon</p>
|
||||||
|
<h2>Gold</h2><p class="description">Eine große facettierte Goldmünze mit zwei kleineren Münzen.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Asset type: standalone resource UI icon for original fantasy strategy game Saumwacht. Faceted illustrated game UI art, large simple flat matte planes, crisp graphic edges, restrained shading, very clear shape readable at 48px and 256px. Single coherent centered icon on square canvas, whole object visible, occupies around 75% canvas with transparent margins all sides. Three-quarter front view facing slightly right, soft neutral upper-left key light. Palette warm brass gold #D9A441, pale mint teal #82DBCE, coral #E9967C, slate #536375. Truly transparent RGBA alpha background, no checkerboard pixels, no ground, no cast shadow, no border or frame, no badge background, no text, no letters, no numerals, no logos, no watermark. Avoid photorealism, glossy rendering and tiny texture.
|
||||||
|
Subject: A chunky golden coin angled three-quarter toward camera with exactly two smaller golden coins nestled at its lower right, a compact grouped resource icon. Broad faceted bevels, simple blank coin faces with a subtle geometric inset but no currency signs and no letters. Warm gold dominates with darker brass edge planes.</pre></details><div class="card-links"><a href="masters/resources/res_gold.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/res_gold.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="res_sparks" data-faction="resources" data-kind="resource_icon" data-state="ready" data-search="res_sparks Funken Ressourcen Ressourcenicon Kleines Messing-Glasgefäß mit leuchtender mintfarbener Funkenenergie.">
|
||||||
|
<div class="card-top"><code>res_sparks</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/resources/res_sparks.png" target="_blank" rel="noopener" aria-label="Funken: PNG-Original öffnen">
|
||||||
|
<img src="masters/resources/res_sparks.png" alt="Funken" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Ressourcen <span aria-hidden="true">/</span> Ressourcenicon</p>
|
||||||
|
<h2>Funken</h2><p class="description">Kleines Messing-Glasgefäß mit leuchtender mintfarbener Funkenenergie.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Asset type: standalone resource UI icon for original fantasy strategy game Saumwacht. Faceted illustrated game UI art, large simple flat matte planes, crisp graphic edges, restrained shading, very clear shape readable at 48px and 256px. Single coherent centered icon on square canvas, whole object visible, occupies around 75% canvas with transparent margins all sides. Three-quarter front view facing slightly right, soft neutral upper-left key light. Palette warm brass gold #D9A441, pale mint teal #82DBCE, coral #E9967C, slate #536375. Truly transparent RGBA alpha background, no checkerboard pixels, no ground, no cast shadow, no border or frame, no badge background, no text, no letters, no numerals, no logos, no watermark. Avoid photorealism, glossy rendering and tiny texture.
|
||||||
|
Subject: A single small squat glass vessel framed by two broad brass bands and a chunky brass stopper, containing one vivid pale mint energy spark cluster. A simple slate base, large clear window allowing bright teal energy to be seen. Faceted stylized glass planes, not glossy photoreal glass. No glow leaking onto the transparent background, no floating particles outside vessel.</pre></details><div class="card-links"><a href="masters/resources/res_sparks.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/res_sparks.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="res_income" data-faction="resources" data-kind="resource_icon" data-state="ready" data-search="res_income Einkommen Ressourcen Ressourcenicon Messingsiegel mit drei von links nach rechts ansteigenden Münzstapeln.">
|
||||||
|
<div class="card-top"><code>res_income</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/resources/res_income.png" target="_blank" rel="noopener" aria-label="Einkommen: PNG-Original öffnen">
|
||||||
|
<img src="masters/resources/res_income.png" alt="Einkommen" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Ressourcen <span aria-hidden="true">/</span> Ressourcenicon</p>
|
||||||
|
<h2>Einkommen</h2><p class="description">Messingsiegel mit drei von links nach rechts ansteigenden Münzstapeln.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Asset type: standalone resource UI icon for original fantasy strategy game Saumwacht. Faceted illustrated game UI art, large simple flat matte planes, crisp graphic edges, restrained shading, very clear shape readable at 48px and 256px. Single coherent centered icon on square canvas, whole object visible, occupies around 75% canvas with transparent margins all sides. Three-quarter front view facing slightly right, soft neutral upper-left key light. Palette warm brass gold #D9A441, pale mint teal #82DBCE, coral #E9967C, slate #536375. Truly transparent RGBA alpha background, no checkerboard pixels, no ground, no cast shadow, no border or frame, no badge background, no text, no letters, no numerals, no logos, no watermark. Avoid photorealism, glossy rendering and tiny texture.
|
||||||
|
Subject: A single thick faceted brass seal medallion with THREE bold raised stacks of coins on its face, stepping upward left to right from one to three to five coins, conveying income growth visually. Coin stacks should read like three ascending bar-chart columns made of coins, simplified broad surfaces, slate inset contrast behind raised gold geometry. No arrows required, absolutely no lettering, numerals or currency symbols. Seal itself is the object, no surrounding UI frame.</pre></details><div class="card-links"><a href="masters/resources/res_income.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/res_income.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="res_core" data-faction="resources" data-kind="resource_icon" data-state="ready" data-search="res_core Kernleben Ressourcen Ressourcenicon Korallfarbenes facettiertes Kristallherz in einer kleinen Messingfassung.">
|
||||||
|
<div class="card-top"><code>res_core</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/resources/res_core.png" target="_blank" rel="noopener" aria-label="Kernleben: PNG-Original öffnen">
|
||||||
|
<img src="masters/resources/res_core.png" alt="Kernleben" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Ressourcen <span aria-hidden="true">/</span> Ressourcenicon</p>
|
||||||
|
<h2>Kernleben</h2><p class="description">Korallfarbenes facettiertes Kristallherz in einer kleinen Messingfassung.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Asset type: standalone resource UI icon for original fantasy strategy game Saumwacht. Faceted illustrated game UI art, large simple flat matte planes, crisp graphic edges, restrained shading, very clear shape readable at 48px and 256px. Single coherent centered icon on square canvas, whole object visible, occupies around 75% canvas with transparent margins all sides. Three-quarter front view facing slightly right, soft neutral upper-left key light. Palette warm brass gold #D9A441, pale mint teal #82DBCE, coral #E9967C, slate #536375. Truly transparent RGBA alpha background, no checkerboard pixels, no ground, no cast shadow, no border or frame, no badge background, no text, no letters, no numerals, no logos, no watermark. Avoid photorealism, glossy rendering and tiny texture.
|
||||||
|
Subject: One chunky faceted CORAL crystal HEART cradled in a tiny warm brass support base with two short protective prongs. Heart shape has two broad rounded angular lobes and a clear pointed bottom, instantly readable as life resource. Coral #E9967C dominates, warm brass #D9A441 cradle, minimal slate underside. Broad matte crystal planes, tiny restrained highlight, no luminous halo.</pre></details><div class="card-links"><a href="masters/resources/res_core.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/res_core.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="worker_funkenleser" data-faction="neutral" data-kind="worker_icon" data-state="ready" data-search="worker_funkenleser Funkenleser Neutrale Arbeitericon Kleine Lehmgestalt im Arbeitstuch mit gegabeltem Funkensammler und Glasbehälter.">
|
||||||
|
<div class="card-top"><code>worker_funkenleser</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/neutral/worker_funkenleser_icon.png" target="_blank" rel="noopener" aria-label="Funkenleser: PNG-Original öffnen">
|
||||||
|
<img src="masters/neutral/worker_funkenleser_icon.png" alt="Funkenleser" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Neutrale <span aria-hidden="true">/</span> Arbeitericon</p>
|
||||||
|
<h2>Funkenleser</h2><p class="description">Kleine Lehmgestalt im Arbeitstuch mit gegabeltem Funkensammler und Glasbehälter.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Asset type: standalone resource UI icon for original fantasy strategy game Saumwacht. Faceted illustrated game UI art, large simple flat matte planes, crisp graphic edges, restrained shading, very clear shape readable at 48px and 256px. Single coherent centered icon on square canvas, whole object visible, occupies around 75% canvas with transparent margins all sides. Three-quarter front view facing slightly right, soft neutral upper-left key light. Palette warm brass gold #D9A441, pale mint teal #82DBCE, coral #E9967C, slate #536375. Truly transparent RGBA alpha background, no checkerboard pixels, no ground, no cast shadow, no border or frame, no badge background, no text, no letters, no numerals, no logos, no watermark. Avoid photorealism, glossy rendering and tiny texture.
|
||||||
|
Subject: A whole single small neutral WORKER creature named Funkenleser, a squat original clay golem organism with a simple rounded faceted terracotta clay face and tiny inset eyes, short chunky clay feet, practical slate-colored cloth hood and apron with broad simple folds. It holds a forked brass spark-collector tool in one short clay hand, and a small clear glass container with one mint energy spark in its other hand. Tiny warm brass buckles, approachable industrious stance. No human skin, no pointed elf ears, no weapons, no writing. Entire worker and tool are fully visible, compact silhouette with 12% transparent margins all sides. Same matte faceted strategy-game art as resource icons.</pre></details><div class="card-links"><a href="masters/neutral/worker_funkenleser_icon.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/worker_funkenleser.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="type_damage_blade" data-faction="types" data-kind="type_icon" data-state="ready" data-search="type_damage_blade Klinge Schaden & Rüstung Typicon Einzelne gebogene Elfenbeinklinge mit Messingparierstange und Graphitgriff.">
|
||||||
|
<div class="card-top"><code>type_damage_blade</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/types/type_damage_blade.png" target="_blank" rel="noopener" aria-label="Klinge: PNG-Original öffnen">
|
||||||
|
<img src="masters/types/type_damage_blade.png" alt="Klinge" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Schaden & Rüstung <span aria-hidden="true">/</span> Typicon</p>
|
||||||
|
<h2>Klinge</h2><p class="description">Einzelne gebogene Elfenbeinklinge mit Messingparierstange und Graphitgriff.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Asset type: one fantasy strategy-game UI damage-type icon, square transparent PNG. Subject: BLADE DAMAGE, a SINGLE short broad curved ivory blade with a simple brass crossguard and short graphite grip. Orient diagonally lower left to upper right, point upward; one clean sweeping cutting edge. It must read unmistakably as a blade at 32px. Style: polished faceted flat UI illustration matching matte low-poly fantasy unit art, only a few broad planar shapes, crisp silhouette, no tiny engravings, no excessive texture. Slight three-quarter perspective and soft light from upper left. Palette graphite #292B35, ivory #D8CEC0, brass #D9A441. Entire object centered, about 75 percent of square canvas, generous transparent margin on all sides. Actual transparent alpha background, no checkerboard picture, no ground, no scene, no cast shadow, no badge, no circular disc, no frame, no letters, no numbers, no watermark. Exactly one isolated icon.</pre></details><div class="card-links"><a href="masters/types/type_damage_blade.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/type_damage_blade.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="type_damage_pierce" data-faction="types" data-kind="type_icon" data-state="ready" data-search="type_damage_pierce Durchschlag Schaden & Rüstung Typicon Lange Messingpfeilspitze durchstößt eine kleine graphitfarbene Rautenplatte.">
|
||||||
|
<div class="card-top"><code>type_damage_pierce</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/types/type_damage_pierce.png" target="_blank" rel="noopener" aria-label="Durchschlag: PNG-Original öffnen">
|
||||||
|
<img src="masters/types/type_damage_pierce.png" alt="Durchschlag" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Schaden & Rüstung <span aria-hidden="true">/</span> Typicon</p>
|
||||||
|
<h2>Durchschlag</h2><p class="description">Lange Messingpfeilspitze durchstößt eine kleine graphitfarbene Rautenplatte.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Asset type: ONE fantasy strategy-game UI damage-type icon, square transparent PNG. Subject: PIERCING DAMAGE, a SINGLE long sharp brass arrowhead with a short narrow graphite shaft visibly punching THROUGH a small thick dark graphite diamond-shaped plate. Composition diagonal lower left to upper right: arrowhead extends prominently above-right of plate, short shaft extends below-left, plate surrounds one tight angular puncture hole. Long brass spear-like triangular point dominates, small plate is secondary. No explosion, no extra arrows, no loose fragments. Must read unmistakably as piercing impact at 32px. Polished faceted flat UI illustration coherent with matte low-poly fantasy art, only a few large planar shapes, crisp bold silhouette, minimal detail and no engravings. Slight three-quarter perspective, soft light from upper left. Palette graphite #292B35, ivory #D8CEC0 for small highlight edge, brass #D9A441. Center entire object group in square canvas, 75 percent occupancy, generous transparent margins. Actual transparent alpha background, no checkerboard picture, ground, scene, cast shadow, badge, disc, frame, letters, numbers or watermark. Exactly one icon.</pre></details><div class="card-links"><a href="masters/types/type_damage_pierce.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/type_damage_pierce.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="type_damage_pulse" data-faction="types" data-kind="type_icon" data-state="ready" data-search="type_damage_pulse Impuls Schaden & Rüstung Typicon Mintfarbener facettierter Sternkern zwischen zwei offenen graphitfarbenen Ringhälften.">
|
||||||
|
<div class="card-top"><code>type_damage_pulse</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/types/type_damage_pulse.png" target="_blank" rel="noopener" aria-label="Impuls: PNG-Original öffnen">
|
||||||
|
<img src="masters/types/type_damage_pulse.png" alt="Impuls" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Schaden & Rüstung <span aria-hidden="true">/</span> Typicon</p>
|
||||||
|
<h2>Impuls</h2><p class="description">Mintfarbener facettierter Sternkern zwischen zwei offenen graphitfarbenen Ringhälften.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Asset type: ONE fantasy strategy-game UI damage-type icon, square transparent PNG. Subject: PULSE DAMAGE, a compact glowing MINT GEOMETRIC STAR-ORB inside one thick OPEN GRAPHITE RING. Core is a solid faceted orb with four short broad diamond-like energy points, NOT a spiky explosion, not a sun face. Dark ring surrounds the core but has two clearly open gaps on opposite sides, with transparent space between the core and ring; no outer badge or backing disc. Bold round compact silhouette with mint center unmistakable at 32px. Polished faceted flat UI illustration matching matte low-poly fantasy art, only a few big planar surfaces, crisp shapes and extremely restrained glow entirely near the core. Slight three-quarter perspective, soft upper-left lighting. Palette graphite #292B35, mint #82DBCE, tiny ivory #D8CEC0 highlights; no extra colors. Entire icon centered in square canvas, 75 percent occupancy, generous transparent margins. Actual transparent alpha background; no checkerboard image, ground, scenery, cast shadow, surrounding particles, circular badge, text, letters, numbers, border, frame or watermark. Exactly one isolated icon.</pre></details><div class="card-links"><a href="masters/types/type_damage_pulse.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/type_damage_pulse.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="type_armor_light" data-faction="types" data-kind="type_icon" data-state="ready" data-search="type_armor_light Leichte Rüstung Schaden & Rüstung Typicon Dünne blattförmige Lederpanzerplatte in warmem Ocker mit einfachem Graphitriemen.">
|
||||||
|
<div class="card-top"><code>type_armor_light</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/types/type_armor_light.png" target="_blank" rel="noopener" aria-label="Leichte Rüstung: PNG-Original öffnen">
|
||||||
|
<img src="masters/types/type_armor_light.png" alt="Leichte Rüstung" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Schaden & Rüstung <span aria-hidden="true">/</span> Typicon</p>
|
||||||
|
<h2>Leichte Rüstung</h2><p class="description">Dünne blattförmige Lederpanzerplatte in warmem Ocker mit einfachem Graphitriemen.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Asset type: ONE fantasy strategy-game UI armor-type icon, square transparent PNG. Subject: LIGHT ARMOR, a SINGLE thin elongated LEAF-SHAPED LEATHER ARMOR PLATE, pointed bottom, softly rounded upper shoulders, with a subtle central folded ridge and one simple narrow graphite leather strap wrapping the middle. Warm brass-ochre matte leather main face, ivory edge highlights, thin flexible edge visibly unlike a bulky metal shield. No plant leaf veins, no actual foliage, no full body armor, no person. Must read as a light leather protective plate at 32px. Polished faceted flat UI illustration matching matte low-poly fantasy art, only a few broad planes and crisp simple silhouette, no tiny stitches or detailed texture. Slight three-quarter perspective turned a little right; soft upper-left key light. Palette graphite #292B35, ivory #D8CEC0, brass #D9A441 interpreted as warm matte leather. Whole single object centered in square canvas, 75 percent occupancy, generous transparent margins. Actual transparent alpha background; no checkerboard picture, scenery, ground, cast shadow, badge, backing disc, text, numbers, frame, border or watermark.</pre></details><div class="card-links"><a href="masters/types/type_armor_light.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/type_armor_light.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="type_armor_heavy" data-faction="types" data-kind="type_icon" data-state="ready" data-search="type_armor_heavy Schwere Rüstung Schaden & Rüstung Typicon Breite schwere Schildplatte aus drei Graphitstahllagen mit hellen Kanten und Messingverstärkung.">
|
||||||
|
<div class="card-top"><code>type_armor_heavy</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/types/type_armor_heavy.png" target="_blank" rel="noopener" aria-label="Schwere Rüstung: PNG-Original öffnen">
|
||||||
|
<img src="masters/types/type_armor_heavy.png" alt="Schwere Rüstung" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Schaden & Rüstung <span aria-hidden="true">/</span> Typicon</p>
|
||||||
|
<h2>Schwere Rüstung</h2><p class="description">Breite schwere Schildplatte aus drei Graphitstahllagen mit hellen Kanten und Messingverstärkung.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Asset type: ONE fantasy strategy-game UI armor-type icon, square transparent PNG. Subject: HEAVY ARMOR, a SINGLE broad thick layered STEEL SHIELD PLATE with squared upper shoulders and a blunt pointed bottom. Three overlapping large graphite steel armor layers across the front, thick visible beveled ivory-gray steel edges, a small simple brass reinforcing band along the outer edge. Extremely stout heavy silhouette, visibly thick and rigid, no leather strap, no emblem, no symbols. It must read as heavy metal armor at 32px. Polished faceted flat UI illustration matching matte low-poly fantasy art, only a few broad planar faces, clean bold silhouette, no tiny rivets or engravings. Slight three-quarter perspective turned a little right, soft upper-left lighting. Palette graphite #292B35, ivory #D8CEC0 for steel highlights, brass #D9A441 minimal trim. Entire single object centered in square canvas, 75 percent occupancy with generous transparent margins. Actual transparent alpha background required; no checkerboard image, scene, ground, cast shadow, badge, backing disc, text, letters, numbers, frame, border or watermark.</pre></details><div class="card-links"><a href="masters/types/type_armor_heavy.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/type_armor_heavy.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="type_armor_aether" data-faction="types" data-kind="type_icon" data-state="ready" data-search="type_armor_aether Ätherische Rüstung Schaden & Rüstung Typicon Hohler violetter Geisterschild mit mintfarbener Innenkante und transparentem Innenraum.">
|
||||||
|
<div class="card-top"><code>type_armor_aether</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage icon-stage"><a class="preview-link" href="masters/types/type_armor_aether.png" target="_blank" rel="noopener" aria-label="Ätherische Rüstung: PNG-Original öffnen">
|
||||||
|
<img src="masters/types/type_armor_aether.png" alt="Ätherische Rüstung" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Schaden & Rüstung <span aria-hidden="true">/</span> Typicon</p>
|
||||||
|
<h2>Ätherische Rüstung</h2><p class="description">Hohler violetter Geisterschild mit mintfarbener Innenkante und transparentem Innenraum.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Asset type: ONE fantasy strategy-game UI armor-type icon, square transparent PNG. Subject: AETHER ARMOR, a SINGLE HOLLOW GHOST SHIELD. Broad pointed shield silhouette formed from a thick faceted violet spectral rim with mint inner edge; center is a large genuinely EMPTY TRANSPARENT opening, no opaque face plate or backing. Top rim has two subtle upward crescent tips and bottom ends in one smooth pointed tip. Two very short broad tapered ghost wisps attach to lower rim, still one compact clear object. Clearly magical hollow defensive shell, no face, no eyes, no skull, no character. It must read distinctly from a metal shield at 32px. Polished faceted flat UI illustration matching matte low-poly fantasy art, a few broad matte planar shapes with restrained mint glow, crisp clean silhouette, no particles or small decoration. Slight three-quarter perspective turned a little right, soft upper-left lighting. Palette violet #9578CF, mint #82DBCE, graphite #292B35 for shadow facets, tiny ivory #D8CEC0 highlights. Single full icon centered in square canvas, 75 percent occupancy with generous transparent margins. Actual transparent alpha background and transparent center, no checkerboard picture, scenery, ground, cast shadow, badge, backing disc, text, letters, numbers, frame, border or watermark.</pre></details><div class="card-links"><a href="masters/types/type_armor_aether.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/type_armor_aether.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card wide-card" data-id="map_overview" data-faction="map" data-kind="map_concept" data-state="ready" data-search="map_overview Doppelsteg – Kartenkonzept Karte Kartenkonzept Zwei parallele Felsstege über einer Schlucht, mit nördlichen Aufmarschflächen, mittleren Baufeldern und südlichen Kernplattformen in Türkis und Koralle.">
|
||||||
|
<div class="card-top"><code>map_overview</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage media-stage"><a class="preview-link" href="masters/map/map_overview.png" target="_blank" rel="noopener" aria-label="Doppelsteg – Kartenkonzept: PNG-Original öffnen">
|
||||||
|
<img src="masters/map/map_overview.png" alt="Doppelsteg – Kartenkonzept" width="1374" height="1145" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Karte <span aria-hidden="true">/</span> Kartenkonzept</p>
|
||||||
|
<h2>Doppelsteg – Kartenkonzept</h2><p class="description">Zwei parallele Felsstege über einer Schlucht, mit nördlichen Aufmarschflächen, mittleren Baufeldern und südlichen Kernplattformen in Türkis und Koralle.</p>
|
||||||
|
<div class="metadata"><span>1374 × 1145 px</span><span>Flächiges Motiv</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Original SAUMWACHT fantasy strategy terrain concept art. Matte hand-painted low-poly facets, broad simple polygonal planes, weathered grey stone #657481 and basalt #292B35, moss green #56845A, restrained brass #D9A441. Soft directional illumination from upper left. Orthographic oblique overhead view with camera looking down 55 degrees, no perspective convergence. Calm readable surfaces, no units, characters, text, letters, labels, legend, watermark, UI panels, logos or borrowed game designs. Create a wide landscape overview of one entire small symmetrical two-lane battlefield named Doppelsteg. Two separate long rectangular floating rock terraces running vertically north at top to south at bottom, parallel, with a narrow deep mist-filled chasm between. Both lanes equally wide and equally long; approximate width to length ratio 12:40 for each lane. Each lane consists from north/top to south/bottom of an open spawn apron, a central flat build field subdivided into subtle square stone pavers, a short plain approach, and a small southern raised core dais. Left core is a compact glowing mint crystal #82DBCE held in a brass cradle; right core a coral crystal #E9967C. A thin matching colored brass-inlaid leak stripe crosses each lane directly before its core. Keep the middle build fields empty and clear, stone edges only at outer borders, small tufts of moss confined to borders. No bridges crossing the gap and no extra lanes. Entire two-terrace silhouette visible with margin on all sides. Dark blue-grey mist and distant plain background, restrained atmospheric depth. This is environment art reference, no infographic.</pre></details><div class="card-links"><a href="masters/map/map_overview.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/map_overview.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="map_lane_floor" data-faction="map" data-kind="tile_concept" data-state="ready" data-search="map_lane_floor Baufeld-Bodenkachel Karte Bodenkachel Flache quadratische Steinplatte mit großen ruhigen Flächen und schmalen Moosfugen für das Baufeld.">
|
||||||
|
<div class="card-top"><code>map_lane_floor</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage media-stage"><a class="preview-link" href="masters/map/map_lane_floor.png" target="_blank" rel="noopener" aria-label="Baufeld-Bodenkachel: PNG-Original öffnen">
|
||||||
|
<img src="masters/map/map_lane_floor.png" alt="Baufeld-Bodenkachel" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Karte <span aria-hidden="true">/</span> Bodenkachel</p>
|
||||||
|
<h2>Baufeld-Bodenkachel</h2><p class="description">Flache quadratische Steinplatte mit großen ruhigen Flächen und schmalen Moosfugen für das Baufeld.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Flächiges Motiv</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Original SAUMWACHT fantasy strategy terrain concept art. Matte hand-painted low-poly facets, broad simple polygonal planes, weathered grey stone #657481 and basalt #292B35, moss green #56845A, restrained brass #D9A441. Soft directional illumination from upper left. Orthographic oblique overhead view with camera looking down 55 degrees, no perspective convergence. Calm readable surfaces, no units, characters, text, letters, labels, legend, watermark, UI panels, logos or borrowed game designs. One isolated square modular build-floor terrain tile representing a 2m by 2m square stone slab, shallow 20cm thickness, shown as a full diamond-shaped oblique square with both side faces visible. Four broad grey slate planes across a flat completely unobstructed top, very thin cracks and tiny moss seams near the outside edges only. No raised objects, no parapet, no crystal, no emblem. All four outer edges straight and modular. Tile centered with ample margin on a flat desaturated blue-grey backdrop; subtle small contact shadow. Entire tile clearly visible. Single tile only, no sheet or multiple views.</pre></details><div class="card-links"><a href="masters/map/map_lane_floor.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/map_lane_floor.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="map_lane_edge" data-faction="map" data-kind="tile_concept" data-state="ready" data-search="map_lane_edge Lane-Randkachel Karte Bodenkachel Passende Randplatte mit niedriger äußerer Steinbrüstung und kleinem Messingmarker.">
|
||||||
|
<div class="card-top"><code>map_lane_edge</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage media-stage"><a class="preview-link" href="masters/map/map_lane_edge.png" target="_blank" rel="noopener" aria-label="Lane-Randkachel: PNG-Original öffnen">
|
||||||
|
<img src="masters/map/map_lane_edge.png" alt="Lane-Randkachel" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Karte <span aria-hidden="true">/</span> Bodenkachel</p>
|
||||||
|
<h2>Lane-Randkachel</h2><p class="description">Passende Randplatte mit niedriger äußerer Steinbrüstung und kleinem Messingmarker.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Flächiges Motiv</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Original SAUMWACHT fantasy strategy terrain concept art. Matte hand-painted low-poly facets, broad simple polygonal planes, weathered grey stone #657481 and basalt #292B35, moss green #56845A, restrained brass #D9A441. Soft directional illumination from upper left. Orthographic oblique overhead view with camera looking down 55 degrees, no perspective convergence. Calm readable surfaces, no units, characters, text, letters, labels, legend, watermark, UI panels, logos or borrowed game designs. One isolated square modular lane-edge terrain tile representing a 2m by 2m grey stone slab, shallow 20cm base thickness. Full diamond-shaped oblique square with side faces visible. Flat unobstructed build-floor top with a low straight 30cm retaining stone curb along ONLY the far/right edge; a narrow brass marker is inset into the middle of this curb. Other three edges flat and modular. Tiny moss clumps tucked beneath the outside of the curb. No railing around whole tile, no buildings, no glyphs, no crystal. Tile centered with ample margin on a flat desaturated blue-grey backdrop; subtle small contact shadow. Entire tile visible, single tile only.</pre></details><div class="card-links"><a href="masters/map/map_lane_edge.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/map_lane_edge.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="map_leak_line" data-faction="map" data-kind="tile_concept" data-state="ready" data-search="map_leak_line Lecklinie-Kachel Karte Bodenkachel Steinplatte mit durchgehendem mintfarbenem Lichtstreifen in einer schmalen Messingschiene als Lecklinie.">
|
||||||
|
<div class="card-top"><code>map_leak_line</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage media-stage"><a class="preview-link" href="masters/map/map_leak_line.png" target="_blank" rel="noopener" aria-label="Lecklinie-Kachel: PNG-Original öffnen">
|
||||||
|
<img src="masters/map/map_leak_line.png" alt="Lecklinie-Kachel" width="1254" height="1254" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Karte <span aria-hidden="true">/</span> Bodenkachel</p>
|
||||||
|
<h2>Lecklinie-Kachel</h2><p class="description">Steinplatte mit durchgehendem mintfarbenem Lichtstreifen in einer schmalen Messingschiene als Lecklinie.</p>
|
||||||
|
<div class="metadata"><span>1254 × 1254 px</span><span>Flächiges Motiv</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Original SAUMWACHT fantasy strategy terrain concept art. Matte hand-painted low-poly facets, broad simple polygonal planes, weathered grey stone #657481 and basalt #292B35, moss green #56845A, restrained brass #D9A441. Soft directional illumination from upper left. Orthographic oblique overhead view with camera looking down 55 degrees, no perspective convergence. Calm readable surfaces, no units, characters, text, letters, labels, legend, watermark, UI panels, logos or borrowed game designs. One isolated square modular leak-line terrain tile representing a 2m by 2m grey stone slab, shallow 20cm thickness. Full diamond-shaped oblique square with side faces visible. Flat stone top crossed from the midpoint of its left edge to the midpoint of its right edge by one thin continuous mint glowing strip #82DBCE inside a narrow flat brass inlay, perfectly flush with the floor, meant to connect into a straight line across adjacent tiles. Keep most top surface plain stone. No raised obstacles, no arrows, no letters, no symbols. Centered with ample margin on a flat desaturated blue-grey backdrop, subtle small contact shadow. Entire tile visible, single tile only.</pre></details><div class="card-links"><a href="masters/map/map_leak_line.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/map_leak_line.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card wide-card" data-id="ui_panel_a" data-faction="ui" data-kind="ui_frame" data-state="ready" data-search="ui_panel_a Breiter UI-Rahmen Oberfläche UI-Rahmen Breiter offener UI-Rahmen mit Basaltfassung, schmaler Messingkante und ruhigen, geraden Seiten.">
|
||||||
|
<div class="card-top"><code>ui_panel_a</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage media-stage"><a class="preview-link" href="masters/ui/ui_panel_a.png" target="_blank" rel="noopener" aria-label="Breiter UI-Rahmen: PNG-Original öffnen">
|
||||||
|
<img src="masters/ui/ui_panel_a.png" alt="Breiter UI-Rahmen" width="2172" height="724" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Oberfläche <span aria-hidden="true">/</span> UI-Rahmen</p>
|
||||||
|
<h2>Breiter UI-Rahmen</h2><p class="description">Breiter offener UI-Rahmen mit Basaltfassung, schmaler Messingkante und ruhigen, geraden Seiten.</p>
|
||||||
|
<div class="metadata"><span>2172 × 724 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Create one production 2D UI FRAME PNG for original fantasy strategy game SAUMWACHT. Wide horizontal panel aspect ratio 3:1, frame fills 88% of canvas width. Absolutely straight-on orthographic view, no perspective. An EMPTY OPEN FRAME: both its entire inner rectangular window and the outside of the frame must be real alpha transparency. Render only the rim. Narrow matte basalt #292B35 outer frame, thin warm brass #D9A441 inner bevel, small geometric chamfered corner braces, tiny dark petrol #247A80 inlays; soft upper-left lighting in painted low-poly facets. Straight plain sides suitable for nine-patch stretching, corner decoration contained close to corners. Flat clean game asset, not photoreal. No center fill, no background, no text, no runes, no icons, no ground shadow, no watermark. Single isolated horizontal hollow panel, transparent center is essential.</pre></details><div class="card-links"><a href="masters/ui/ui_panel_a.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/ui_panel_a.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card" data-id="ui_panel_b" data-faction="ui" data-kind="ui_frame" data-state="ready" data-search="ui_panel_b Hoher UI-Rahmen Oberfläche UI-Rahmen Hoher offener UI-Rahmen für Einheiteninfos, mit Basaltfassung und geometrischen Messingecken.">
|
||||||
|
<div class="card-top"><code>ui_panel_b</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage media-stage"><a class="preview-link" href="masters/ui/ui_panel_b.png" target="_blank" rel="noopener" aria-label="Hoher UI-Rahmen: PNG-Original öffnen">
|
||||||
|
<img src="masters/ui/ui_panel_b.png" alt="Hoher UI-Rahmen" width="1024" height="1536" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Oberfläche <span aria-hidden="true">/</span> UI-Rahmen</p>
|
||||||
|
<h2>Hoher UI-Rahmen</h2><p class="description">Hoher offener UI-Rahmen für Einheiteninfos, mit Basaltfassung und geometrischen Messingecken.</p>
|
||||||
|
<div class="metadata"><span>1024 × 1536 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Create one production 2D UI FRAME PNG for original fantasy strategy game SAUMWACHT. Tall vertical panel aspect ratio 2:3, frame fills 88% of canvas height. Absolutely straight-on orthographic view, no perspective. An EMPTY OPEN FRAME: both its entire inner rectangular window and the outside of the frame must be real alpha transparency. Render only the rim. Narrow matte basalt #292B35 outer frame, thin warm brass #D9A441 inner bevel, small geometric chamfered corner braces, tiny dark petrol #247A80 inlays; soft upper-left lighting in painted low-poly facets. Straight plain sides suitable for nine-patch stretching, corner decoration contained close to corners. Flat clean game asset, not photoreal. No center fill, no background, no text, no runes, no icons, no ground shadow, no watermark. Single isolated vertical hollow panel, transparent center is essential.</pre></details><div class="card-links"><a href="masters/ui/ui_panel_b.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/ui_panel_b.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card wide-card" data-id="ui_button_normal" data-faction="ui" data-kind="ui_button" data-state="ready" data-search="ui_button_normal Button – Normal Oberfläche Button Unbeschrifteter petrolfarbener Button mit kantigem Messingrand, dunkler Innenfläche und kleinen geometrischen Eckakzenten.">
|
||||||
|
<div class="card-top"><code>ui_button_normal</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage media-stage"><a class="preview-link" href="masters/ui/ui_button_normal.png" target="_blank" rel="noopener" aria-label="Button – Normal: PNG-Original öffnen">
|
||||||
|
<img src="masters/ui/ui_button_normal.png" alt="Button – Normal" width="2172" height="724" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Oberfläche <span aria-hidden="true">/</span> Button</p>
|
||||||
|
<h2>Button – Normal</h2><p class="description">Unbeschrifteter petrolfarbener Button mit kantigem Messingrand, dunkler Innenfläche und kleinen geometrischen Eckakzenten.</p>
|
||||||
|
<div class="metadata"><span>2172 × 724 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Create one clean standalone reusable NORMAL STATE button graphic for original fantasy strategy game Saumwacht. Wide horizontal aspect ratio 3:1, front facing perfectly flat orthographic UI view, not tilted or isometric. One long dark petrol #1C2E36 rectangular central button surface, thin layered warm brass #D9A441 bevel border, dark basalt #292B35 outside rim, small tasteful geometric corner accents, softly chamfered corners, neutral upper-left lighting. Faceted matte illustrated game UI art with broad crisp color planes, matches hand-illustrated stone-and-brass strategy icons. HUGE plain empty uninterrupted label area in center, no text or symbols anywhere. Symmetric exact geometry. Button occupies about85%canvaswidth65%height with transparent safety margin on all sides. Genuine RGBA transparency outside the button; the button interior itself is solid dark petrol. No checkerboard, no opaque background, no cast shadow, no mockup, no multiplebuttons, no letters, no watermark. Production-ready isolated gameUI element, no flourishes extending outward.</pre></details><div class="card-links"><a href="masters/ui/ui_button_normal.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/ui_button_normal.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card wide-card" data-id="ui_button_hover" data-faction="ui" data-kind="ui_button" data-state="ready" data-search="ui_button_hover Button – Hover Oberfläche Button Hover-Zustand mit hellerem Messingrand und einer schmalen mintfarbenen Innenkante.">
|
||||||
|
<div class="card-top"><code>ui_button_hover</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage media-stage"><a class="preview-link" href="masters/ui/ui_button_hover.png" target="_blank" rel="noopener" aria-label="Button – Hover: PNG-Original öffnen">
|
||||||
|
<img src="masters/ui/ui_button_hover.png" alt="Button – Hover" width="2172" height="724" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Oberfläche <span aria-hidden="true">/</span> Button</p>
|
||||||
|
<h2>Button – Hover</h2><p class="description">Hover-Zustand mit hellerem Messingrand und einer schmalen mintfarbenen Innenkante.</p>
|
||||||
|
<div class="metadata"><span>2172 × 724 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Create one clean standalone reusable HOVER STATE button graphic for original fantasy strategy game Saumwacht. Wide horizontal aspect ratio 3:1, front facing perfectly flat orthographic UI view, not tilted or isometric. One long dark petrol #243D47 rectangular central button surface, thin layered bright warm brass #D9A441 bevel border, hairline mint #82DBCE INNER rim, dark basalt #292B35 outside rim, small tasteful geometric folded brass corner accents, softly chamfered corners, neutral upper-left lighting. Faceted matte illustrated game UI art with broad crisp color planes, matches hand-illustrated stone-and-brass strategy icons. HUGE plain empty uninterrupted label area in center, no text or symbols anywhere. Symmetric exact geometry. Button occupies about 85% canvas width and 65% height with transparent safety margin on all sides. Genuine RGBA transparency outside the button; the button interior itself is solid dark petrol. No checkerboard, no opaque background, no cast shadow, no mockup, no multiple buttons, no letters, no watermark. Production-ready isolated game UI element, no flourishes extending outward. Actual transparent background is required: please output a PNG with an alpha channel, not a visual depiction of transparency.</pre></details><div class="card-links"><a href="masters/ui/ui_button_hover.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/ui_button_hover.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card wide-card" data-id="ui_button_pressed" data-faction="ui" data-kind="ui_button" data-state="ready" data-search="ui_button_pressed Button – Gedrückt Oberfläche Button Gedrückter Button mit abgedunkelter, eingesenkter Petrolfläche und umgekehrtem Innenrelief.">
|
||||||
|
<div class="card-top"><code>ui_button_pressed</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage media-stage"><a class="preview-link" href="masters/ui/ui_button_pressed.png" target="_blank" rel="noopener" aria-label="Button – Gedrückt: PNG-Original öffnen">
|
||||||
|
<img src="masters/ui/ui_button_pressed.png" alt="Button – Gedrückt" width="2172" height="724" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Oberfläche <span aria-hidden="true">/</span> Button</p>
|
||||||
|
<h2>Button – Gedrückt</h2><p class="description">Gedrückter Button mit abgedunkelter, eingesenkter Petrolfläche und umgekehrtem Innenrelief.</p>
|
||||||
|
<div class="metadata"><span>2172 × 724 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Edit the supplied normal UI button for the original fantasy strategy game SAUMWACHT. Keep the exact canvas aspect ratio, silhouette, outside alpha margin, chamfered corners, all brass geometric corner details, and broad blank center identical to reference. This is a matching interaction state of the same control, not a redesign. Straight-on orthographic view; matte illustrated low-poly facets, top-left light; no text, icons, symbols, numbers, watermark, external shadow, or glow outside the silhouette. Preserve genuine transparent background and opaque center. Create the PRESSED state: center recessed inward and darker petrol, inverted inner bevel with dark top and left and restrained lower-right rim highlight. Brass outer edge slightly darker. No displacement of the control geometry.</pre></details><div class="card-links"><a href="masters/ui/ui_button_pressed.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/ui_button_pressed.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article><article class="asset-card wide-card" data-id="ui_button_disabled" data-faction="ui" data-kind="ui_button" data-state="ready" data-search="ui_button_disabled Button – Deaktiviert Oberfläche Button Deaktivierter Button in entsättigtem Schiefergrau und gedämpftem Messing.">
|
||||||
|
<div class="card-top"><code>ui_button_disabled</code><span class="status ready"><span aria-hidden="true">●</span> Vorhanden</span></div>
|
||||||
|
<div class="preview-stage media-stage"><a class="preview-link" href="masters/ui/ui_button_disabled.png" target="_blank" rel="noopener" aria-label="Button – Deaktiviert: PNG-Original öffnen">
|
||||||
|
<img src="masters/ui/ui_button_disabled.png" alt="Button – Deaktiviert" width="2172" height="724" loading="lazy" decoding="async">
|
||||||
|
</a></div>
|
||||||
|
<div class="card-content"><p class="eyebrow">Oberfläche <span aria-hidden="true">/</span> Button</p>
|
||||||
|
<h2>Button – Deaktiviert</h2><p class="description">Deaktivierter Button in entsättigtem Schiefergrau und gedämpftem Messing.</p>
|
||||||
|
<div class="metadata"><span>2172 × 724 px</span><span>Alpha vorgesehen</span></div>
|
||||||
|
<details><summary>Generierungsprompt</summary><pre>Use case: stylized-concept. Create one clean standalone reusable DISABLED STATE button graphic for original fantasy strategy game Saumwacht. Wide horizontal aspect ratio 3:1, front facing perfectly flat orthographic UI view, not tilted or isometric. One long desaturated dark slate #394550 rectangular central button surface, thin layered DULL MUTED GRAY-BROWN BRASS bevel border (desaturated brass, not bright gold), dark basalt #292B35 outside rim, small tasteful geometric folded corner accents, softly chamfered corners, subtle upper-left lighting. Reduced highlights and contrast make the button visibly inactive. Faceted matte illustrated game UI art with broad crisp color planes, matches hand-illustrated stone-and-brass strategy icons. HUGE plain empty uninterrupted label area in center, no text or symbols anywhere. Symmetric exact geometry. Button occupies about 85% canvas width and 65% height with transparent safety margin on all sides. Genuine RGBA transparency outside the button; button interior itself is solid opaque dark slate. No checkerboard, no opaque background, no cast shadow, no mockup, no multiple buttons, no letters, no watermark, no colored glow. Production-ready isolated game UI element, no flourishes extending outward. Actual transparent background required: output a PNG with an alpha channel, not a visual depiction of transparency.</pre></details><div class="card-links"><a href="masters/ui/ui_button_disabled.png" target="_blank" rel="noopener">PNG-Original <span aria-hidden="true">↗</span></a>
|
||||||
|
<a href="records/ui_button_disabled.json" target="_blank" rel="noopener">Datensatz <span aria-hidden="true">↗</span></a></div>
|
||||||
|
</div>
|
||||||
|
</article></section>
|
||||||
|
<div class="empty" id="empty" hidden><strong>Keine passenden Assets.</strong>Andere Suchbegriffe wählen oder die Filter zurücksetzen.</div></main>
|
||||||
|
<footer><p>Offline-Galerie · Bilder unverändert aus den nativen PNG-Masters.</p><p>Aktualisieren: <code>python3 tools/build_asset_gallery.py</code></p></footer></div>
|
||||||
|
<script>
|
||||||
|
(() => {
|
||||||
|
const cards = [...document.querySelectorAll('.asset-card')];
|
||||||
|
const search = document.getElementById('search');
|
||||||
|
const faction = document.getElementById('faction');
|
||||||
|
const kind = document.getElementById('kind');
|
||||||
|
const state = document.getElementById('state');
|
||||||
|
const count = document.getElementById('result-count');
|
||||||
|
const normalize = value => value.toLocaleLowerCase('de').normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/ß/g, 'ss');
|
||||||
|
const texts = new Map(cards.map(card => [card, normalize(card.dataset.search)]));
|
||||||
|
function filter() {
|
||||||
|
const terms = normalize(search.value).trim().split(/\s+/).filter(Boolean);
|
||||||
|
let visible = 0;
|
||||||
|
let ready = 0;
|
||||||
|
for (const card of cards) {
|
||||||
|
const match = (!faction.value || card.dataset.faction === faction.value)
|
||||||
|
&& (!kind.value || card.dataset.kind === kind.value)
|
||||||
|
&& (!state.value || card.dataset.state === state.value)
|
||||||
|
&& terms.every(term => texts.get(card).includes(term));
|
||||||
|
card.hidden = !match;
|
||||||
|
if (match) { visible++; if (card.dataset.state === 'ready') ready++; }
|
||||||
|
}
|
||||||
|
count.textContent = `${visible} von ${cards.length} Assets · ${ready} vorhanden · ${visible - ready} ausstehend`;
|
||||||
|
document.getElementById('empty').hidden = visible !== 0;
|
||||||
|
}
|
||||||
|
search.addEventListener('input', filter);
|
||||||
|
for (const select of [faction, kind, state]) select.addEventListener('change', filter);
|
||||||
|
document.getElementById('reset').addEventListener('click', () => {
|
||||||
|
search.value = ''; faction.value = ''; kind.value = ''; state.value = ''; filter(); search.focus();
|
||||||
|
});
|
||||||
|
for (const button of document.querySelectorAll('[data-background-choice]')) {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
document.body.dataset.background = button.dataset.backgroundChoice;
|
||||||
|
document.querySelectorAll('[data-background-choice]').forEach(item => item.setAttribute('aria-pressed', String(item === button)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const button of document.querySelectorAll('[data-size]')) {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
document.documentElement.style.setProperty('--icon-size', `${button.dataset.size}px`);
|
||||||
|
document.querySelectorAll('[data-size]').forEach(item => item.setAttribute('aria-pressed', String(item === button)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
filter();
|
||||||
|
})();
|
||||||
|
</script></body></html>
|
||||||
|
After Width: | Height: | Size: 1,012 KiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 844 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
BIN
assets-source/phase3/masters/map/map_lane_edge.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
assets-source/phase3/masters/map/map_lane_floor.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
assets-source/phase3/masters/map/map_leak_line.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
assets-source/phase3/masters/map/map_overview.png
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 824 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 555 KiB |
|
After Width: | Height: | Size: 907 KiB |
|
After Width: | Height: | Size: 838 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1 MiB |
|
After Width: | Height: | Size: 972 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 807 KiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 478 KiB |
BIN
assets-source/phase3/masters/neutral/worker_funkenleser_icon.png
Normal file
|
After Width: | Height: | Size: 1 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 810 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 726 KiB |
|
After Width: | Height: | Size: 936 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 560 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
BIN
assets-source/phase3/masters/resources/res_core.png
Normal file
|
After Width: | Height: | Size: 794 KiB |
BIN
assets-source/phase3/masters/resources/res_gold.png
Normal file
|
After Width: | Height: | Size: 919 KiB |
BIN
assets-source/phase3/masters/resources/res_income.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
assets-source/phase3/masters/resources/res_sparks.png
Normal file
|
After Width: | Height: | Size: 993 KiB |
BIN
assets-source/phase3/masters/types/type_armor_aether.png
Normal file
|
After Width: | Height: | Size: 589 KiB |
BIN
assets-source/phase3/masters/types/type_armor_heavy.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
assets-source/phase3/masters/types/type_armor_light.png
Normal file
|
After Width: | Height: | Size: 671 KiB |
BIN
assets-source/phase3/masters/types/type_damage_blade.png
Normal file
|
After Width: | Height: | Size: 380 KiB |
BIN
assets-source/phase3/masters/types/type_damage_pierce.png
Normal file
|
After Width: | Height: | Size: 568 KiB |