Expose the runtime block/item registry through compact material search and single-material descriptions. Preserve private block-entity data through checked edits and durable undo without sending payloads to model context. Include the completed terrain tools, isolated world/map plugin, station lift, ACP streaming and guidance fixes, local camera auto-connect, construction scripts, and their public documentation, references and verification records. Active station decoration and private runtime data remain outside this commit. Validation: 145 Maven tests, 47 Bridge tests, successful camera Gradle build, and isolated Paper verification of all 1,196 block defaults plus 5,392 independent property cases for placement, same-material edits and restoration.
189 lines
15 KiB
Python
189 lines
15 KiB
Python
"""Design-stage contours and circulation derived from Shacraft's approved height field.
|
|
No world access or mutation. Coordinates use north=-Z; fields are planning data only.
|
|
"""
|
|
from pathlib import Path
|
|
import json, math
|
|
import numpy as np
|
|
import matplotlib
|
|
matplotlib.use('Agg')
|
|
import matplotlib.pyplot as plt
|
|
from matplotlib.colors import LightSource
|
|
from matplotlib.path import Path as MplPath
|
|
ROOT=Path(__file__).resolve().parents[2]
|
|
OUT=ROOT/'.runtime/layout-study'; OUT.mkdir(parents=True,exist_ok=True)
|
|
h=np.fromfile(ROOT/'.runtime/terrain-study/natural.f32',dtype='>f4').reshape(768,768).astype(float)
|
|
features=[];routes=[]
|
|
colors={'01':'#84d440','02':'#f4d35e','03':'#bc80ef','04':'#40c8e3','05':'#64d9a0','06':'#ef9863','07':'#f5f4e9','08':'#4ca3ff','09':'#f07eaf'}
|
|
districts=[
|
|
('01','Arrival square',[0,8],'An open hexagonal plaza; preserve the raised grassy crown. Main north axis reveals the clock tower.'),
|
|
('02','Clock station',[-18,-119],'A long station hall with projecting clock tower and two end pavilions. Its forecourt faces spawn.'),
|
|
('03','Portal concourse',[-201,-156],'A chamfered hall with six separate portal-bay footprints and a sunken-feeling garden approach.'),
|
|
('04','Airship harbor',[179,-137],'East-bank terminal with three west-facing piers above the water gorge. One future flagship uses the middle pier.'),
|
|
('05','Sky gardens',[207,88],'Three distinct rounded landmarks connected by winding garden paths: greenhouse, winter garden, observatory.'),
|
|
('06','Market quarter',[-195,225],'Six small footprints follow the slope around a compact square. Use stepped streets and individual foundations.'),
|
|
('07','Arrival viaduct',[8,284],'A long south approach follows the valley ridge; the final viaduct crosses the merging river branches.'),
|
|
('08','Lake waterworks',[-135,43],'A compact pumping house above the east lake shore and a low over-water promenade.'),
|
|
('09','Scenic overlooks',[48,-180],'Small optional lookouts frame the valley, lake and arrival route; no mountain flattening.')]
|
|
districts=[dict(id=i,name=n,label=p,color=colors[i],intent=t) for i,n,p,t in districts]
|
|
|
|
def closed(points): return points+[points[0]] if points[0]!=points[-1] else points
|
|
|
|
def add(i,d,n,p,role='building',**kw):
|
|
f=dict(id=i,district=d,name=n,type='polyline' if role in ('detail','promenade','pier','bridge','axis') else 'polygon',points=p,color=colors[d],role=role,**kw)
|
|
features.append(f);return f
|
|
|
|
def ellipse(cx,cz,rx,rz=None,n=72,angle=0):
|
|
rz=rx if rz is None else rz;a=math.radians(angle)
|
|
return closed([[round(cx+math.cos(t)*rx*math.cos(a)-math.sin(t)*rz*math.sin(a)),round(cz+math.cos(t)*rx*math.sin(a)+math.sin(t)*rz*math.cos(a))]for t in np.linspace(0,2*math.pi,n,endpoint=False)])
|
|
|
|
def rect(cx,cz,w,d,angle=0):
|
|
a=math.radians(angle)
|
|
return closed([[round(cx+x*math.cos(a)-z*math.sin(a)),round(cz+x*math.sin(a)+z*math.cos(a))]for x,z in[(-w/2,-d/2),(w/2,-d/2),(w/2,d/2),(-w/2,d/2)]])
|
|
|
|
def smooth(points):
|
|
p=np.array([points[0]]+points+[points[-1]],float);out=[]
|
|
for k in range(1,len(p)-2):
|
|
a,b,c,d=p[k-1:k+3];n=max(2,int(np.linalg.norm(c-b)))
|
|
for t in np.linspace(0,1,n,endpoint=False):
|
|
q=.5*((2*b)+(-a+c)*t+(2*a-5*b+4*c-d)*t*t+(-a+3*b-3*c+d)*t*t*t)
|
|
q=[round(float(q[0])),round(float(q[1]))]
|
|
if not out or q!=out[-1]:out.append(q)
|
|
out.append(points[-1]);return out
|
|
|
|
def route(i,name,points,width=7,role='main',**kw):
|
|
r=dict(id=i,name=name,points=smooth(points),waypoints=points,width=width,role=role,**kw);routes.append(r);return r
|
|
|
|
add('arrival-hex','01','Arrival square',closed([[0,-37],[39,-15],[43,31],[0,57],[-43,31],[-39,-15]]),'plaza')
|
|
add('spawn-medallion','01','Shacraft medallion reserve',ellipse(0,9,16,n=36),'plaza')
|
|
add('clock-station','02','Clock station and tower',closed([[-74,-139],[-51,-139],[-51,-145],[16,-145],[16,-139],[40,-139],[40,-98],[13,-98],[13,-83],[-25,-83],[-25,-98],[-74,-98]]))
|
|
add('station-clock-base','02','Clock tower base',rect(-6,-105,18,18),'detail')
|
|
for cx in[-63,29]:add('station-pavilion-'+str(cx),'02','End pavilion',rect(cx,-119,16,30),'detail')
|
|
add('station-forecourt','02','Station forecourt',ellipse(-5,-65,36,15,n=48),'plaza')
|
|
add('portal-hall','03','Portal concourse',closed([[-227,-163],[-161,-163],[-152,-154],[-152,-122],[-161,-113],[-227,-113],[-236,-122],[-236,-154]]))
|
|
for k,(cx,cz) in enumerate([(x,z)for z in[-151,-125]for x in[-218,-194,-170]],1):add('portal-bay-'+str(k),'03','Portal bay '+str(k),rect(cx,cz,13,8),'detail',number=k)
|
|
add('portal-forecourt','03','Portal garden court',ellipse(-159,-93,17,12,n=48),'plaza')
|
|
add('harbor-terminal','04','Airship terminal',closed([[169,-170],[191,-170],[199,-162],[199,-103],[191,-95],[169,-95],[161,-103],[161,-162]]))
|
|
for k,z in enumerate([-158,-132,-106],1):
|
|
add('airship-pier-'+str(k),'04','Airship pier '+str(k),closed([[161,z-4],[107,z-4],[103,z],[107,z+4],[161,z+4]]),'pier',deck_y=79,number=k)
|
|
add('flagship-reserve','04','Flagship mooring reserve',ellipse(78,-132,29,10,n=48),'detail',deck_y=92)
|
|
for i,(x,z,rx,rz,n) in enumerate([(185,23,25,25,'Palm glasshouse'),(211,94,22,18,'Winter garden'),(233,152,15,15,'Observatory')],1):
|
|
add('garden-'+str(i),'05',n,ellipse(x,z,rx,rz), 'building')
|
|
add('garden-court-'+str(i),'05',n+' surrounding walk',ellipse(x,z,rx+7,rz+7),'plaza')
|
|
add('market-square','06','Market square',ellipse(-195,225,22,25,n=48),'plaza')
|
|
for i,(x,z,w,d,a,n) in enumerate([(-219,188,24,18,-8,'Bakery'),(-177,195,24,18,7,'Crafts hall'),(-162,231,17,26,8,'Tea house'),(-178,272,22,18,-8,'Guild shop'),(-214,272,23,17,8,'Workshop'),(-233,239,22,26,-5,'Guild hall')],1):
|
|
add('market-'+str(i),'06',n,rect(x,z,w,d,a))
|
|
add('waterworks-pump','08','Pumping house',rect(-135,43,14,18,-15))
|
|
add('waterworks-lookout','08','Waterworks lookout',ellipse(-135,69,10,n=32),'plaza')
|
|
shore=[]
|
|
for z in range(-69,76,3):
|
|
wet=np.where(h[z+384,:384]<48)[0]-384
|
|
if len(wet):shore.append([int(wet[-1]-3),z])
|
|
add('lake-boardwalk','08','Lake boardwalk alignment',smooth(shore),'promenade',deck_y=51)
|
|
for i,(x,z,r,n) in enumerate([(43,-173,8,'North clock-view belvedere'),(-130,-188,7,'Portal ridge overlook'),(229,-76,8,'Harbor overlook'),(266,110,7,'Garden lookout'),(-258,258,7,'Valley approach lookout')],1):
|
|
add('overlook-'+str(i),'09',n,ellipse(x,z,r,n=32),'plaza')
|
|
# Pull the portal footprint north onto its drier shoulder; retain all six bays.
|
|
for f in features:
|
|
if f['id']=='portal-hall' or f['id'].startswith('portal-bay-'):
|
|
f['points']=[[x-7,round(-156+(z+138)*.8)] for x,z in f['points']]
|
|
# Direct spawn routes make the principal destinations easy to find.
|
|
route('station-axis','Clock avenue',[[0,-37],[-4,-48],[-5,-65],[-6,-83]],9)
|
|
route('portal-radial','Portal approach',[[-35,-14],[-67,-43],[-107,-65],[-135,-75],[-159,-81]],7)
|
|
route('portal-forecourt-link','Portal court entrance',[[-159,-105],[-154,-120],[-164,-139]],5,'secondary')
|
|
route('lake-radial','Lake walk',[[-42,18],[-77,27],[-104,38],[-126,40]],5,'secondary')
|
|
route('east-radial','Garden approach',[[42,16],[65,10],[82,8]],7)
|
|
route('south-axis','Arrival avenue',[[0,57],[3,97],[-5,143],[4,189],[8,237],[7,281],[5,321]],9)
|
|
# A district promenade closes two large loops and bypasses spawn.
|
|
route('ring-northwest','Station to portals',[[-40,-65],[-77,-76],[-116,-89],[-145,-105],[-152,-122],[-164,-139]],7)
|
|
route('ring-west','West inner promenade',[[-147,-111],[-125,-80],[-118,-41],[-110,4],[-115,47],[-113,88],[-127,126]],7)
|
|
route('ring-market-north','Market north street',[[-213,128],[-218,148],[-205,171],[-193,182],[-195,200]],7)
|
|
route('ring-market-south','Market south street',[[-195,250],[-187,253],[-170,252],[-145,242]],7)
|
|
route('ring-south','South inner promenade',[[-72,242],[-36,238],[8,237],[31,218],[46,194],[59,166]],7)
|
|
route('ring-garden-south','Garden south promenade',[[143,166],[172,179],[203,172],[218,157]],7)
|
|
route('ring-gardens','Garden promenade',[[218,137],[204,117],[186,109],[176,88],[184,65],[201,46]],7)
|
|
route('ring-harbor','Harbor garden promenade',[[185,-9],[197,-43],[192,-72],[181,-95]],7)
|
|
route('ring-northeast','Station to east bridge',[[40,-95],[60,-91],[82,-78]],7)
|
|
route('harbor-bridge-landing','Harbor bridge landing',[[155,-81],[171,-83],[180,-95]],7)
|
|
route('garden-bridge-landing','Garden bridge landing',[[151,8],[157,16],[160,23]],7)
|
|
route('market-shop-street','Market north street',[[-207,192],[-205,198],[-203,202]],5,'secondary')
|
|
route('market-south-shop-street','Market south shops',[[-187,253],[-178,259],[-178,262]],5,'secondary')
|
|
route('market-workshop-street','Workshop approach',[[-198,250],[-207,254],[-214,262]],5,'secondary')
|
|
route('market-guild-street','Guild approach',[[-216,231],[-220,234],[-222,236]],5,'secondary')
|
|
route('market-teahouse-street','Tea house approach',[[-173,229],[-171,229]],5,'secondary')
|
|
route('market-crafts-street','Crafts approach',[[-184,203],[-179,206]],5,'secondary')
|
|
route('waterworks-connector','Waterworks approach',[[-114,44],[-122,44],[-128,43]],5,'secondary')
|
|
route('waterworks-lookout-path','Waterworks viewing path',[[-135,52],[-135,59]],3,'secondary')
|
|
# Bridges are measured separately. Grade is a later construction task, not terrain flattening.
|
|
for i,n,a,b,w in [
|
|
('bridge-northeast','Clock / harbor bridge',[82,-78],[155,-81],7),
|
|
('bridge-east','Spawn / garden bridge',[82,8],[151,8],7),
|
|
('bridge-southeast','Garden / south bridge',[59,166],[143,166],7),
|
|
('bridge-market-north','Market / lake bridge',[-127,126],[-213,128],7),
|
|
('bridge-market-south','Market / arrival bridge',[-145,242],[-72,242],7),
|
|
('arrival-viaduct','Arrival viaduct',[5,321],[5,383],11)]:
|
|
pts=np.linspace(a,b,int(np.linalg.norm(np.array(b)-a))+1).round().astype(int)
|
|
heights=h[pts[:,1]+384,pts[:,0]+384]
|
|
deck=int(math.ceil(max(heights[0],heights[-1])))+2
|
|
if i=='arrival-viaduct':deck=max(deck,65)
|
|
r=route(i,n,[a,b],w,'bridge',deck_y=deck,minimum_ground=float(heights.min()),endpoint_ground=[float(heights[0]),float(heights[-1])]);
|
|
# Edge lines are appropriate for construction marking; they are not a complete bridge.
|
|
r['intent']='Mark both parapet alignments and abutments; preserve the river and banks.'
|
|
r['abutments']=[dict(point=q,ground_y=round(float(y),1),deck_y=deck,future_stairs=(deck-math.floor(y)>4),rise=deck-math.floor(y)) for q,y in zip([a,b],[heights[0],heights[-1]])]
|
|
# Scenic branches never substitute for main circulation.
|
|
for i,n,p in[
|
|
('north-overlook-path','North overlook',[[40,-128],[58,-140],[62,-161],[50,-171]]),
|
|
('portal-overlook-path','Portal ridge path',[[-159,-151],[-136,-158],[-123,-172],[-129,-181]]),
|
|
('harbor-overlook-path','Harbor lookout trail',[[196,-80],[213,-75],[221,-76]]),
|
|
('garden-overlook-path','Garden lookout trail',[[232,96],[249,99],[259,107]]),
|
|
('south-overlook-path','Valley lookout trail',[[-218,236],[-224,219],[-248,223],[-263,240],[-264,251]])]:route(i,n,p,3,'secondary')
|
|
# Geometric checks on the immutable approved field; live blocks must still be checked before writes.
|
|
zz,xx=np.mgrid[-384:384,-384:384]
|
|
for f in features:
|
|
pts=np.array(f['points']);sample=h[np.clip(pts[:,1]+384,0,767),np.clip(pts[:,0]+384,0,767)]
|
|
if f['type']=='polygon':
|
|
x0,z0=pts.min(axis=0);x1,z1=pts.max(axis=0);px,pz=np.meshgrid(np.arange(x0,x1+1),np.arange(z0,z1+1))
|
|
mask=MplPath(pts).contains_points(np.column_stack((px.flat,pz.flat)),radius=.01).reshape(px.shape)
|
|
sample=h[z0+384:z1+385,x0+384:x1+385][mask]
|
|
f['ground']={'min':round(float(sample.min()),2),'max':round(float(sample.max()),2),'water_samples':int((sample<48).sum()),'samples':len(sample)}
|
|
for r in routes:
|
|
pts=np.array(r['points']);v=h[pts[:,1]+384,pts[:,0]+384]
|
|
ds=np.linalg.norm(np.diff(pts,axis=0),axis=1);grade=np.abs(np.diff(v))/np.maximum(ds,.001)
|
|
r['analysis']={'length':round(float(ds.sum()),1),'ground_min':round(float(v.min()),1),'ground_max':round(float(v.max()),1),'p95_raw_grade':round(float(np.percentile(grade,95)),2),'water_samples':int((v<48).sum())}
|
|
plan={'schema':'shacraft-layout-study-v1','world':'shacraft_lobby_v2','bounds':{'min_x':-384,'max_x':383,'min_z':-384,'max_z':383},'water_y':48,'status':'offline design; not a world mutation or live snapshot','districts':districts,'features':features,'routes':routes,'construction_notes':[
|
|
'Colored outlines are building reservations, not instructions to level their entire bounding boxes.',
|
|
'Walks remain aligned to terrain; steep local runs need short stairs during the later building phase.',
|
|
'Main avenues 9 blocks, district ring 7, side paths 5, scenic trails 3; marker widths can be thinner than final clear widths.',
|
|
'Keep protected water intact. Bridges have a separate planned deck Y above their endpoint terrain.',
|
|
'The natural western lake bank is too steep for a main ring street. The main route uses the calm inner/east lake shoulder; the low over-water boardwalk is secondary.',
|
|
'No minigame arenas. Six portal bays, three harbor piers, one future flagship reservation.'
|
|
]}
|
|
(OUT/'layout.json').write_text(json.dumps(plan,indent=2)+'\n')
|
|
# Render a design review from data, not a Minecraft screenshot.
|
|
dz,dx=np.gradient(h);rock=np.clip((np.hypot(dx,dz)-.55)/1.5,0,1)[...,None]
|
|
rgb=np.array([.32,.40,.27])*(1-rock)+np.array([.49,.49,.46])*rock
|
|
rgb=LightSource(315,42).shade_rgb(rgb,h,vert_exag=1,blend_mode='soft');rgb=np.where((h<48)[...,None],np.array([.13,.28,.35]),rgb)
|
|
fig,ax=plt.subplots(figsize=(14,14),facecolor='#151b18');ax.set_facecolor('#151b18')
|
|
ax.imshow(rgb,extent=(-384,384,384,-384))
|
|
for r in routes:
|
|
p=np.array(r['points']);lw=2.2 if r['role']!='secondary' else 1
|
|
ax.plot(p[:,0],p[:,1],color='#f7f2de' if r['role']!='bridge' else '#ffffff',lw=lw,alpha=.92,linestyle='-' if r['role']!='bridge' else '--')
|
|
for f in features:
|
|
p=np.array(f['points']);ax.plot(p[:,0],p[:,1],color=f['color'],lw=2 if f['role'] not in ('detail','promenade') else 1.25)
|
|
for d in districts:
|
|
if d['id']=='09':continue
|
|
x,z=d['label'];ax.text(x,z,d['id'],color='#111914',ha='center',va='center',fontsize=12,fontweight='bold',bbox=dict(boxstyle='circle,pad=.35',fc=d['color'],ec='#111914',lw=1))
|
|
ax.set(xlim=(-325,310),ylim=(384,-255),xlabel='X / blocks',ylabel='Z / blocks; north up')
|
|
ax.set_title('SHACRAFT / terrain-adapted lobby layout',loc='left',color='#f5f3ea',fontsize=19,pad=22)
|
|
ax.tick_params(colors='#b5c0b8');ax.xaxis.label.set_color('#b5c0b8');ax.yaxis.label.set_color('#b5c0b8')
|
|
for i,d in enumerate(districts):
|
|
fig.text(.085+(i%3)*.302,.065-(i//3)*.021,d['id']+' '+d['name'],color=d['color'],fontsize=11)
|
|
fig.text(.085,.009,'DESIGN STUDY • Exact approved height field; outlines not yet placed. White: routes / dashed: future bridge spans.',color='#b5c0b8',fontsize=9)
|
|
fig.subplots_adjust(left=.06,right=.985,top=.945,bottom=.11)
|
|
fig.savefig(OUT/'layout-preview.png',dpi=140);plt.close(fig)
|
|
print(json.dumps({'features':len(features),'routes':len(routes),'path_length':round(sum(r['analysis']['length']for r in routes)), 'output':str(OUT)}))
|
|
print('BUILDING HEIGHT SPANS')
|
|
for f in features:
|
|
if f['role']=='building':print(f['id'],f['ground'])
|
|
print('BRIDGES')
|
|
for r in routes:
|
|
if r['role']=='bridge':print(r['id'],r['deck_y'],r['endpoint_ground'],r['analysis'])
|