import * as T from '../dist/vendor/three.core.js'; import {createNpcCharacter,NPC_MODEL_IDS} from './npc-models.mjs'; import {writeFile,mkdir} from 'node:fs/promises'; import {group, mesh as primMesh, ball, box as primBox, rod as primRod, path as primPath} from '../dist/model-primitives.js'; const out=new URL('../dist/assets/',import.meta.url);await mkdir(new URL('models/',out),{recursive:true});await mkdir(new URL('audio/',out),{recursive:true}); const mat=(color,metalness=0,roughness=.8,emissive=0)=>new T.MeshStandardMaterial({color,metalness,roughness,emissive,emissiveIntensity:1}); const iron=mat(0x657878,.78,.36),edge=mat(0x9da9a3,.8,.3),dark=mat(0x242c2d,.7,.7),leather=mat(0x322c25),cloth=mat(0x682927),bone=mat(0xc4b798),skin=mat(0x657063),rot=mat(0x565343),black=mat(0x151f20),eye=mat(0xff8c31,0,.3,0xff6519),blue=mat(0x68f4de,0,.2,0x30adbb),gold=mat(0xa69a63,.65,.4); function mesh(parent,geometry,material,x=0,y=0,z=0,sx=1,sy=1,sz=1){return primMesh(parent,geometry,material,x,y,z,sx,sy,sz,{castShadow:false,receiveShadow:false});} const orb=(p,m,x,y,z,sx,sy,sz)=>ball(p,m,x,y,z,sx,sy,sz,{castShadow:false,receiveShadow:false}); const box=(p,m,x,y,z,sx,sy,sz)=>primBox(p,m,x,y,z,sx,sy,sz,{castShadow:false,receiveShadow:false}); function boneBetween(p,m,a,b,r1,r2=r1,n=9){return primRod(p,m,a,b,r1,r2,n,{castShadow:false,receiveShadow:false});} function spike(p,m,points,r=.1){primPath(p,m,points,r,{taper:'falloff',divisor:points.length-1,sides:9,castShadow:false,receiveShadow:false});} function skull(p,x,y,z,s=.3,eyes=eye){orb(p,bone,x,y,z,s,.95*s,.82*s);orb(p,black,x,y-.08*s,z+s*.75,s*.48,s*.3,s*.2);for(const side of [-1,1]){orb(p,black,x+side*s*.48,y+s*.18,z+s*.69,s*.34,s*.25,s*.13);orb(p,eyes,x+side*s*.48,y+s*.18,z+s*.82,s*.12,s*.09,s*.055);}box(p,bone,x,y-s*.5,z+s*.55,s*.95,s*.22,s*.42);for(let i=-2;i<=2;i++)box(p,bone,x+i*s*.18,y-s*.4,z+s*.81,s*.1,s*.24,s*.1);} function cloak(p,material,length=1.45,width=.84){const vertices=[],indices=[];for(let j=0;j<=8;j++)for(let i=0;i<=10;i++){const u=i/10,v=j/8;vertices.push((u-.5)*width*(.65+v*.6),-v*length,-.08-v*.32+Math.cos(u*Math.PI*8)*.035+Math.sin(v*4)*.035);}for(let j=0;j<8;j++)for(let i=0;i<10;i++){const a=j*11+i;indices.push(a,a+11,a+1,a+1,a+11,a+12);}const g=new T.BufferGeometry();g.setAttribute('position',new T.Float32BufferAttribute(vertices,3));g.setIndex(indices);g.computeVertexNormals();const m=material.clone();m.side=T.DoubleSide;mesh(p,g,m);} function warden(){const root=new T.Group();root.name='Warden';const body=group(root,'body',0,1.18,0);orb(body,iron,0,.11,0,.34,.43,.21);box(body,dark,0,-.22,.02,.55,.15,.39);box(body,gold,0,-.23,.23,.12,.11,.04);for(let i=0;i<3;i++)box(body,iron,0,-.35-i*.1,.03,.53+i*.03,.12,.38);const cape=group(body,'cape',0,.39,-.18);cloak(cape,cloth,1.52,.85);const head=group(body,'head',0,.58,.01);orb(head,iron,0,0,0,.245,.29,.21);box(head,dark,0,.0,.197,.34,.075,.04);box(head,gold,0,.19,.19,.042,.19,.045);box(head,edge,0,-.1,.23,.05,.23,.07);for(const s of [-1,1]){orb(body,iron,s*.4,.33,0,.25,.18,.29);box(body,gold,s*.4,.4,.12,.3,.025,.025);const leg=group(root,s<0?'legL':'legR',s*.2,.89,0);boneBetween(leg,dark,[0,0,0],[0,-.5,.015],.14,.12);orb(leg,iron,0,-.34,.06,.145,.14,.16);boneBetween(leg,iron,[0,-.42,.03],[0,-.72,.02],.13,.1);box(leg,dark,0,-.78,.08,.24,.17,.39);const arm=group(body,s<0?'armL':'armR',s*.43,.19,.02);boneBetween(arm,iron,[0,0,0],[s*.09,-.32,.04],.12,.09);boneBetween(arm,leather,[s*.09,-.32,.04],[s*.06,-.53,.13],.1,.08);orb(arm,dark,s*.06,-.54,.14,.095,.12,.09);if(s===1){const sword=group(arm,'weapon',.06,-.48,.23);box(sword,gold,0,0,.08,.4,.055,.08);boneBetween(sword,leather,[0,0,-.12],[0,0,.1],.045);const blade=box(sword,edge,0,0,.63,.115,.045,1.1);spike(sword,edge,[[0,0,1.13],[0,0,1.4]],.055);}else{const shield=group(arm,'shield',-.1,-.34,.19);orb(shield,iron,0,0,0,.3,.39,.07);orb(shield,dark,0,0,.048,.245,.33,.032);box(shield,gold,0,0,.085,.045,.58,.018);box(shield,gold,0,.12,.087,.35,.035,.02);orb(shield,edge,0,0,.105,.08,.08,.045);}}return root;} function ghoul(){const root=new T.Group();root.name='Hollow';const body=group(root,'body',0,.98,0);orb(body,skin,0,.18,0,.3,.48,.22);orb(body,rot,0,.38,-.13,.35,.28,.22);for(let i=0;i<5;i++){for(const s of [-1,1])boneBetween(body,bone,[s*.05,.38-i*.115,.2],[s*(.24-i*.012),.33-i*.1,.135],.025);}for(let i=0;i<5;i++)orb(body,bone,0,.38-i*.12,-.22,.06,.07,.07);const head=group(body,'head',0,.57,.21);skull(head,0,0,0,.24);orb(head,skin,0,.13,-.05,.23,.19,.18);for(const s of [-1,1]){const leg=group(root,s<0?'legL':'legR',s*.18,.74,0);boneBetween(leg,rot,[0,0,0],[s*.08,-.34,.11],.12,.065);boneBetween(leg,skin,[s*.08,-.34,.11],[0,-.68,.04],.075,.045);orb(leg,rot,0,-.7,.16,.09,.07,.19);const arm=group(body,s<0?'armL':'armR',s*.31,.35,0);orb(arm,skin,0,0,0,.16,.18,.15);boneBetween(arm,skin,[0,0,0],[s*.13,-.38,.15],.13,.065);boneBetween(arm,rot,[s*.13,-.38,.15],[s*.12,-.74,.38],.07,.045);orb(arm,skin,s*.12,-.76,.39,.09,.12,.08);for(let i=0;i<3;i++)spike(arm,bone,[[s*.12+(i-1)*.052,-.79,.4],[s*.12+(i-1)*.075,-.91,.54],[s*.12+(i-1)*.075,-.99,.6]],.025);}return root;} function hound(){const root=new T.Group();root.name='Grave hound';const body=group(root,'body',0,.69,0);orb(body,rot,0,0,0,.29,.31,.58);orb(body,skin,0,.1,.28,.34,.39,.29);for(let i=0;i<6;i++){const z=-.38+i*.14;spike(body,bone,[[0,.25,z],[0,.44+i*.015,z-.06],[0,.5+i*.01,z-.17]],.065);for(const s of [-1,1])boneBetween(body,bone,[s*.2,.22,z],[s*.3,-.13,z+.045],.027);}const head=group(body,'head',0,.16,.57);orb(head,skin,0,0,0,.235,.23,.32);orb(head,rot,0,-.07,.25,.17,.14,.25);orb(head,black,0,-.03,.44,.12,.065,.07);for(const s of [-1,1]){orb(head,eye,s*.18,.1,.21,.045,.037,.04);spike(head,bone,[[s*.15,.17,-.02],[s*.23,.45,-.18],[s*.18,.53,-.28]],.09);for(let i=0;i<4;i++)spike(head,bone,[[s*.14,-.09,.13+i*.075],[s*.14,-.22,.16+i*.075]],.025);for(const z of [-.38,.38]){const leg=group(root,`${s<0?'legL':'legR'}${z<0?'Back':'Front'}`,s*.24,.68,z);boneBetween(leg,skin,[0,0,0],[s*.11,-.31,-.14],.095,.055);boneBetween(leg,rot,[s*.11,-.31,-.14],[s*.05,-.58,.04],.055,.035);orb(leg,skin,s*.05,-.6,.11,.1,.055,.15);for(let i=0;i<3;i++)spike(leg,bone,[[s*.05+(i-1)*.055,-.59,.19],[s*.05+(i-1)*.055,-.61,.29]],.018);}}spike(body,skin,[[0,.1,-.5],[0,.24,-.86],[.1,.15,-1.13],[.15,.38,-1.3]],.09);return root;} function revenant(boss=false){const root=new T.Group();root.name=boss?'The Bellkeeper':'Cinder revenant';const body=group(root,'body',0,1.25,0);orb(body,boss?iron:black,0,.12,0,.4,.58,.25);const robe=group(body,'robe',0,.18,.04);cloak(robe,boss?leather:black,1.31,1.0);const front=group(body,'frontRobe',0,.04,.25);front.rotation.y=Math.PI;cloak(front,boss?cloth:leather,1.2,.74);for(let i=0;i<5;i++)for(const s of [-1,1])boneBetween(body,boss?gold:bone,[s*.04,.44-i*.12,.25],[s*.3,.38-i*.12,.18],.033);const head=group(body,'head',0,.75,.03);skull(head,0,0,0,.29,boss?eye:blue);for(const s of [-1,1]){spike(head,bone,[[s*.2,.16,0],[s*.38,.47,-.06],[s*.48,.75,.02],[s*.39,1.02,.13]],.12);if(boss){spike(head,bone,[[s*.4,.53,-.04],[s*.65,.67,.03],[s*.73,.88,.06]],.07);spike(head,bone,[[s*.44,.71,.015],[s*.62,.88,-.12],[s*.7,1.03,-.18]],.06);}const leg=group(root,s<0?'legL':'legR',s*.2,.65,0);boneBetween(leg,dark,[0,0,0],[0,-.54,.03],.11,.075);box(leg,dark,0,-.59,.06,.18,.14,.29);const arm=group(body,s<0?'armL':'armR',s*.45,.42,0);orb(arm,boss?iron:rot,0,0,0,.21,.18,.21);for(let i=0;i<3;i++)spike(arm,bone,[[s*.07,.11,(i-1)*.12],[s*.2,.34,(i-1)*.16]],.07);boneBetween(arm,rot,[0,0,0],[s*.11,-.4,.08],.1,.06);boneBetween(arm,bone,[s*.11,-.4,.08],[s*.06,-.71,.25],.075,.045);for(let i=0;i<3;i++)spike(arm,bone,[[s*.06+(i-1)*.065,-.69,.27],[s*.06+(i-1)*.09,-.85,.42],[s*.06+(i-1)*.09,-.99,.4]],.032);if(s===1){const staff=group(arm,'weapon',.14,-.45,.36);boneBetween(staff,leather,[0,-.74,0],[0,1.17,0],.055,.035);orb(staff,boss?eye:blue,0,1.03,0,.13,.19,.13);for(const k of [-1,1])spike(staff,bone,[[0,.7,0],[k*.24,1,0],[k*.14,1.32,0]],.055);}}if(boss)root.scale.setScalar(1.65);return root;} function glb(root){const doc={asset:{version:'2.0',generator:'Hallowmere procedural creature workshop'},scene:0,scenes:[{nodes:[]}],nodes:[],meshes:[],materials:[],accessors:[],bufferViews:[],buffers:[]};const chunks=[];let byteOffset=0;const materialIds=new Map(),geometryIds=new Map(); function accessor(array,type,componentType,target){const padding=(4-byteOffset%4)%4;if(padding){chunks.push(Buffer.alloc(padding));byteOffset+=padding;}const b=Buffer.from(array.buffer,array.byteOffset,array.byteLength);const v=doc.bufferViews.push({buffer:0,byteOffset,byteLength:b.length,target})-1;chunks.push(b);byteOffset+=b.length;const size={SCALAR:1,VEC2:2,VEC3:3,VEC4:4}[type];const a={bufferView:v,componentType,count:array.length/size,type};if(type==='VEC3'){a.min=[Infinity,Infinity,Infinity];a.max=[-Infinity,-Infinity,-Infinity];for(let i=0;i[name,createNpcCharacter(name)]); const models=onlyNpcs?npcModels:[['warden',warden()],['hollow',ghoul()],['grave-hound',hound()],['revenant',revenant()],['bellkeeper',revenant(true)],...npcModels]; for(const [name,model] of models){await writeFile(new URL(`models/${name}.glb`,out),glb(model));console.log(`Generated ${name}.glb`);} if(!onlyNpcs)await (await import('./generate-audio.mjs')).generateAudio();