RayMelius Claude Opus 4.6 commited on
Commit
275f596
·
1 Parent(s): 5cb46c4

Enhanced building architecture: chimneys, porches, bay windows on houses; balconies and cornices on apartments; glass curtain-wall office; sawtooth roof factory with loading dock; rose window church with buttresses; clock tower school; cinema with ticket booth; hospital with helipad; museum with columns and dome; park with gazebo and flower beds; ornate town square fountain

Browse files
Files changed (1) hide show
  1. web/3d.html +793 -314
web/3d.html CHANGED
@@ -683,48 +683,101 @@ function updateBadge(badge, count) {
683
  function createHouse(id, locData) {
684
  const group = new THREE.Group();
685
  const h = hash(id);
 
686
  const w = 3 + (h % 2), d = 3 + ((h >> 2) % 2), wallH = 2.5 + (h % 3) * 0.3;
687
  const wallColor = BLDG_COLORS.house[h % BLDG_COLORS.house.length];
688
  const roofColor = BLDG_COLORS.roof[h % BLDG_COLORS.roof.length];
 
689
 
690
- // Walls
691
  const walls = new THREE.Mesh(new THREE.BoxGeometry(w, wallH, d), mat(wallColor));
692
  walls.position.y = wallH / 2;
693
- walls.castShadow = true;
694
- walls.receiveShadow = true;
695
  addEdges(walls, 0x000000, 0.08);
696
  group.add(walls);
697
 
698
- // Roof
699
- const roofH = 1.5;
700
- const roofGeo = new THREE.ConeGeometry(Math.max(w, d) * 0.75, roofH, 4);
701
- roofGeo.rotateY(Math.PI / 4);
702
- const roof = new THREE.Mesh(roofGeo, mat(roofColor));
703
- roof.position.y = wallH + roofH / 2;
704
- roof.castShadow = true;
705
- group.add(roof);
706
-
707
- // Door
708
- const door = new THREE.Mesh(
709
- new THREE.BoxGeometry(0.6, 1.2, 0.1),
710
- mat(BLDG_COLORS.door)
711
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
712
  door.position.set(0, 0.6, d / 2 + 0.05);
713
- door.userData.isDoor = true;
714
- group.add(door);
 
715
 
716
- // Windows
717
  const winMat = mat(isNight ? BLDG_COLORS.windowLit : BLDG_COLORS.window,
718
  isNight ? { emissive: BLDG_COLORS.windowLit, emissiveIntensity: 0.5 } : {});
719
  for (let side of [-1, 1]) {
720
  const win = new THREE.Mesh(new THREE.BoxGeometry(0.6, 0.5, 0.1), winMat);
721
  win.position.set(side * (w / 2 + 0.05), wallH * 0.6, 0);
722
  win.rotation.y = Math.PI / 2;
723
- win.userData.isWindow = true;
724
- group.add(win);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
725
  }
726
 
727
- // Bed
728
  const bed = new THREE.Mesh(new THREE.BoxGeometry(1.2, 0.15, 0.7), mat(0x8b6040));
729
  bed.position.set(0.6, 0.08, 0); bed.userData.isFurniture = true; group.add(bed);
730
  const mattress = new THREE.Mesh(new THREE.BoxGeometry(1.1, 0.1, 0.6), mat(0xe8e0d0));
@@ -732,8 +785,8 @@ function createHouse(id, locData) {
732
  const pillow = new THREE.Mesh(new THREE.BoxGeometry(0.25, 0.08, 0.35), mat(0xf0f0f0));
733
  pillow.position.set(1.1, 0.24, 0); pillow.userData.isFurniture = true; group.add(pillow);
734
 
735
- const label = createLabel(locData.label, group, wallH + roofH + 1);
736
- const badge = createOccupantBadge(group, wallH + roofH);
737
  group.userData = { id, type: 'house', label, badge, locData };
738
  return group;
739
  }
@@ -741,47 +794,90 @@ function createHouse(id, locData) {
741
  function createApartment(id, locData) {
742
  const group = new THREE.Group();
743
  const h = hash(id);
 
744
  const w = 5, d = 4, wallH = 7 + (h % 4);
745
  const wallColor = BLDG_COLORS.apartment[h % BLDG_COLORS.apartment.length];
 
746
 
747
  const walls = new THREE.Mesh(new THREE.BoxGeometry(w, wallH, d), mat(wallColor));
748
  walls.position.y = wallH / 2;
749
- walls.castShadow = true;
750
- walls.receiveShadow = true;
751
- addEdges(walls);
752
- group.add(walls);
753
 
754
- // Flat roof cap
755
- const roofCap = new THREE.Mesh(
756
- new THREE.BoxGeometry(w + 0.3, 0.3, d + 0.3),
757
- mat(wallColor - 0x202020)
758
- );
759
- roofCap.position.y = wallH;
760
- group.add(roofCap);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
761
 
762
- // Window grid
763
  const winMat = mat(isNight ? BLDG_COLORS.windowLit : BLDG_COLORS.window,
764
  isNight ? { emissive: BLDG_COLORS.windowLit, emissiveIntensity: 0.4 } : {});
765
  const floors = Math.floor(wallH / 2);
 
766
  for (let f = 0; f < floors; f++) {
 
767
  for (let wx = -1; wx <= 1; wx++) {
768
- const win = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.6, 0.1), winMat);
769
- win.position.set(wx * 1.4, 1.5 + f * 2, d / 2 + 0.05);
770
- win.userData.isWindow = true;
771
- group.add(win);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
772
  }
773
  }
774
 
775
- // Beds inside
776
  for (let bx = -1; bx <= 1; bx += 2) {
777
  const bed = new THREE.Mesh(new THREE.BoxGeometry(1.0, 0.12, 0.5), mat(0x8b6040));
778
  bed.position.set(bx * 1.2, 0.06, 0); bed.userData.isFurniture = true; group.add(bed);
779
- const matt = new THREE.Mesh(new THREE.BoxGeometry(0.9, 0.08, 0.45), mat(0xe0d8c8));
780
- matt.position.set(bx * 1.2, 0.14, 0); matt.userData.isFurniture = true; group.add(matt);
781
  }
782
 
783
- const label = createLabel(locData.label, group, wallH + 2);
784
- const badge = createOccupantBadge(group, wallH + 1);
 
785
  group.userData = { id, type: 'apartment', label, badge, locData };
786
  return group;
787
  }
@@ -830,28 +926,58 @@ function createShop(id, locData) {
830
 
831
  function createOffice(id, locData) {
832
  const group = new THREE.Group();
833
- const w = 5, d = 5, wallH = 5;
 
834
 
835
- const walls = new THREE.Mesh(
836
- new THREE.BoxGeometry(w, wallH, d),
837
- mat(BLDG_COLORS.office, { roughness: 0.4, metalness: 0.2 })
838
  );
839
- walls.position.y = wallH / 2;
840
- walls.castShadow = true;
841
- walls.receiveShadow = true;
842
- addEdges(walls);
843
- group.add(walls);
844
-
845
- // Glass windows
846
- const winMat = mat(BLDG_COLORS.window, { roughness: 0.2, metalness: 0.3, opacity: 0.7, transparent: true });
847
- for (let f = 0; f < 2; f++) {
848
- for (let side of [1, -1]) {
849
- const win = new THREE.Mesh(new THREE.BoxGeometry(w * 0.35, 1.2, 0.1), winMat);
850
- win.position.set(side * (w * 0.2), 1.5 + f * 2, d / 2 + 0.05);
851
- win.userData.isWindow = true;
852
- group.add(win);
 
 
 
 
 
 
 
 
853
  }
854
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
855
 
856
  const label = createLabel(locData.label, group, wallH + 2);
857
  const badge = createOccupantBadge(group, wallH + 1);
@@ -862,71 +988,138 @@ function createOffice(id, locData) {
862
  function createTower(id, locData) {
863
  const group = new THREE.Group();
864
  const w = 4.5, d = 4.5, wallH = 14;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
865
 
866
- // Base wider section
867
- const base = new THREE.Mesh(
868
- new THREE.BoxGeometry(w + 1, 3, d + 1),
869
- mat(BLDG_COLORS.tower - 0x101010)
870
- );
871
- base.position.y = 1.5;
872
- base.castShadow = true;
873
- base.receiveShadow = true;
874
- addEdges(base);
875
- group.add(base);
876
-
877
- // Tower body
878
- const body = new THREE.Mesh(
879
- new THREE.BoxGeometry(w, wallH, d),
880
- mat(BLDG_COLORS.tower, { roughness: 0.3, metalness: 0.3 })
881
- );
882
- body.position.y = wallH / 2;
883
- body.castShadow = true;
884
- body.receiveShadow = true;
885
- addEdges(body);
886
- group.add(body);
887
-
888
- // Glass strips
889
- const glassMat = mat(BLDG_COLORS.window, { roughness: 0.15, metalness: 0.4, opacity: 0.6, transparent: true });
890
- for (let f = 0; f < 6; f++) {
891
- const strip = new THREE.Mesh(new THREE.BoxGeometry(w * 0.8, 0.8, 0.1), glassMat);
892
- strip.position.set(0, 2 + f * 2, d / 2 + 0.05);
893
- strip.userData.isWindow = true;
894
- group.add(strip);
895
- }
896
-
897
- // Antenna
898
- const antenna = new THREE.Mesh(
899
- new THREE.CylinderGeometry(0.08, 0.08, 3, 6),
900
- mat(0x888888, { metalness: 0.5 })
901
- );
902
- antenna.position.y = wallH + 1.5;
903
- group.add(antenna);
904
 
905
- const label = createLabel(locData.label, group, wallH + 4);
906
- const badge = createOccupantBadge(group, wallH + 3);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
907
  group.userData = { id, type: 'tower', label, badge, locData };
908
  return group;
909
  }
910
 
911
  function createHospital(id, locData) {
912
  const group = new THREE.Group();
913
- const w = 7, d = 5, wallH = 4;
914
 
915
  const walls = new THREE.Mesh(new THREE.BoxGeometry(w, wallH, d), mat(BLDG_COLORS.hospital));
916
- walls.position.y = wallH / 2;
917
- walls.castShadow = true;
918
- walls.receiveShadow = true;
919
- addEdges(walls);
920
- group.add(walls);
 
 
 
 
 
 
 
 
 
 
 
 
921
 
922
- // Red cross
923
  const crossMat = mat(0xcc2222);
924
  const crossH = new THREE.Mesh(new THREE.BoxGeometry(1.5, 0.4, 0.1), crossMat);
925
- crossH.position.set(0, wallH * 0.7, d / 2 + 0.06);
926
- group.add(crossH);
927
  const crossV = new THREE.Mesh(new THREE.BoxGeometry(0.4, 1.5, 0.1), crossMat);
928
- crossV.position.set(0, wallH * 0.7, d / 2 + 0.06);
929
- group.add(crossV);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
930
 
931
  const label = createLabel(locData.label, group, wallH + 2);
932
  const badge = createOccupantBadge(group, wallH + 1);
@@ -936,52 +1129,80 @@ function createHospital(id, locData) {
936
 
937
  function createChurch(id, locData) {
938
  const group = new THREE.Group();
939
- const w = 4, d = 6, wallH = 4;
 
940
 
941
- const walls = new THREE.Mesh(new THREE.BoxGeometry(w, wallH, d), mat(BLDG_COLORS.church));
942
- walls.position.y = wallH / 2;
943
- walls.castShadow = true;
944
- walls.receiveShadow = true;
945
- addEdges(walls);
946
- group.add(walls);
947
 
948
- // Roof
949
  const roofGeo = new THREE.BufferGeometry();
950
- const hw = w / 2 + 0.2, hd = d / 2 + 0.2;
951
- const rh = 2;
952
- const verts = new Float32Array([
953
- -hw, wallH, -hd, hw, wallH, -hd, 0, wallH + rh, -hd,
954
- -hw, wallH, hd, hw, wallH, hd, 0, wallH + rh, hd,
955
- -hw, wallH, -hd, -hw, wallH, hd, 0, wallH + rh, -hd, 0, wallH + rh, hd,
956
- hw, wallH, -hd, hw, wallH, hd, 0, wallH + rh, -hd, 0, wallH + rh, hd,
957
  ]);
958
- const idx = [0,1,2, 3,5,4, 2,3,0, 2,5,3, 1,4,5, 1,5,2];
959
- roofGeo.setAttribute('position', new THREE.BufferAttribute(verts, 3));
960
- roofGeo.setIndex(idx);
961
  roofGeo.computeVertexNormals();
962
  const roof = new THREE.Mesh(roofGeo, mat(BLDG_COLORS.roof[0]));
963
- roof.castShadow = true;
964
- group.add(roof);
965
-
966
- // Steeple
967
- const steeple = new THREE.Mesh(
968
- new THREE.ConeGeometry(0.8, 4, 4),
969
- mat(BLDG_COLORS.roof[1])
970
- );
971
- steeple.position.set(0, wallH + rh + 2, -d / 2 + 1);
972
- steeple.castShadow = true;
973
- group.add(steeple);
974
-
975
- // Steeple base
976
- const stBase = new THREE.Mesh(
977
- new THREE.BoxGeometry(1.6, 2, 1.6),
978
- mat(BLDG_COLORS.church)
979
- );
980
- stBase.position.set(0, wallH + 1, -d / 2 + 1);
981
- group.add(stBase);
982
 
983
- const label = createLabel(locData.label, group, wallH + rh + 5);
984
- const badge = createOccupantBadge(group, wallH + rh + 4);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
985
  group.userData = { id, type: 'church', label, badge, locData };
986
  return group;
987
  }
@@ -991,32 +1212,60 @@ function createSchool(id, locData) {
991
  const w = 6, d = 5, wallH = 3.5;
992
 
993
  const walls = new THREE.Mesh(new THREE.BoxGeometry(w, wallH, d), mat(BLDG_COLORS.school));
994
- walls.position.y = wallH / 2;
995
- walls.castShadow = true;
996
- walls.receiveShadow = true;
997
- addEdges(walls);
998
- group.add(walls);
999
 
1000
- // Windows
1001
- const winMat = mat(BLDG_COLORS.window);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1002
  for (let i = -2; i <= 2; i++) {
 
1003
  const win = new THREE.Mesh(new THREE.BoxGeometry(0.6, 0.8, 0.1), winMat);
1004
  win.position.set(i * 1.1, wallH * 0.6, d / 2 + 0.05);
1005
- win.userData.isWindow = true;
1006
- group.add(win);
 
 
 
 
 
 
 
 
1007
  }
1008
 
1009
- // Flag pole
1010
- const pole = new THREE.Mesh(new THREE.CylinderGeometry(0.05, 0.05, 3, 6), mat(0x888888));
1011
- pole.position.set(w / 2 - 0.5, wallH + 1.5, -d / 2 + 0.5);
1012
- group.add(pole);
1013
-
1014
  const flag = new THREE.Mesh(new THREE.PlaneGeometry(1, 0.6), mat(0xcc3333, { side: THREE.DoubleSide }));
1015
- flag.position.set(w / 2 - 0.5 + 0.5, wallH + 2.5, -d / 2 + 0.5);
1016
- group.add(flag);
 
 
1017
 
1018
- const label = createLabel(locData.label, group, wallH + 4);
1019
- const badge = createOccupantBadge(group, wallH + 3);
1020
  group.userData = { id, type: 'school', label, badge, locData };
1021
  return group;
1022
  }
@@ -1024,42 +1273,66 @@ function createSchool(id, locData) {
1024
  function createFactory(id, locData) {
1025
  const group = new THREE.Group();
1026
  const w = 7, d = 5, wallH = 4.5;
 
 
1027
 
1028
- const walls = new THREE.Mesh(new THREE.BoxGeometry(w, wallH, d), mat(BLDG_COLORS.factory));
1029
- walls.position.y = wallH / 2;
1030
- walls.castShadow = true;
1031
- walls.receiveShadow = true;
1032
- addEdges(walls);
1033
- group.add(walls);
1034
-
1035
- // Chimney
1036
- const chimney = new THREE.Mesh(
1037
- new THREE.CylinderGeometry(0.5, 0.6, 4, 8),
1038
- mat(0x555555)
1039
- );
1040
- chimney.position.set(w / 2 - 1, wallH + 2, -d / 2 + 1);
1041
- chimney.castShadow = true;
1042
- group.add(chimney);
1043
-
1044
- // Second chimney
1045
- const chimney2 = new THREE.Mesh(
1046
- new THREE.CylinderGeometry(0.4, 0.5, 3, 8),
1047
- mat(0x555555)
1048
- );
1049
- chimney2.position.set(w / 2 - 2.5, wallH + 1.5, -d / 2 + 1);
1050
- chimney2.castShadow = true;
1051
- group.add(chimney2);
1052
-
1053
- // Sawtooth roof detail
1054
- const roofCap = new THREE.Mesh(
1055
- new THREE.BoxGeometry(w + 0.2, 0.3, d + 0.2),
1056
- mat(0x555555)
1057
- );
1058
- roofCap.position.y = wallH;
1059
- group.add(roofCap);
1060
 
1061
- const label = createLabel(locData.label, group, wallH + 5);
1062
- const badge = createOccupantBadge(group, wallH + 4);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1063
  group.userData = { id, type: 'factory', label, badge, locData };
1064
  return group;
1065
  }
@@ -1069,65 +1342,122 @@ function createCinema(id, locData) {
1069
  const w = 5.5, d = 5, wallH = 5;
1070
 
1071
  const walls = new THREE.Mesh(new THREE.BoxGeometry(w, wallH, d), mat(BLDG_COLORS.cinema));
1072
- walls.position.y = wallH / 2;
1073
- walls.castShadow = true;
1074
- walls.receiveShadow = true;
1075
- addEdges(walls);
1076
- group.add(walls);
1077
-
1078
- // Marquee
1079
- const marquee = new THREE.Mesh(
1080
- new THREE.BoxGeometry(w + 1, 0.8, 0.5),
1081
- mat(0xddcc44, { emissive: 0x887722, emissiveIntensity: 0.3 })
1082
- );
1083
- marquee.position.set(0, wallH * 0.8, d / 2 + 0.3);
1084
- group.add(marquee);
1085
 
1086
- const label = createLabel(locData.label, group, wallH + 2);
1087
- const badge = createOccupantBadge(group, wallH + 1);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1088
  group.userData = { id, type: 'cinema', label, badge, locData };
1089
  return group;
1090
  }
1091
 
1092
  function createPark(id, locData) {
1093
  const group = new THREE.Group();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1094
 
1095
- // Park ground
1096
- const parkGround = new THREE.Mesh(
1097
- new THREE.BoxGeometry(8, 0.1, 6),
1098
- mat(BLDG_COLORS.park)
1099
- );
1100
- parkGround.position.y = 0.05;
1101
- parkGround.receiveShadow = true;
1102
- group.add(parkGround);
1103
-
1104
- // Path
1105
- const path = new THREE.Mesh(
1106
- new THREE.BoxGeometry(1, 0.12, 6),
1107
- mat(0xccbbaa)
1108
- );
1109
- path.position.y = 0.06;
1110
- group.add(path);
1111
-
1112
- // Trees
1113
  for (let i = 0; i < 5; i++) {
1114
  const tx = (Math.random() - 0.5) * 6;
1115
  const tz = (Math.random() - 0.5) * 4;
1116
- if (Math.abs(tx) > 1) {
1117
  const t = createTree(0, 0, 0.35 + Math.random() * 0.25);
1118
  t.position.set(tx, 0, tz);
1119
- scene.remove(t);
1120
- group.add(t);
1121
  }
1122
  }
1123
 
1124
- // Bench
1125
- const benchSeat = new THREE.Mesh(new THREE.BoxGeometry(1.5, 0.1, 0.5), mat(0x8b6040));
1126
- benchSeat.position.set(2, 0.5, 0);
1127
- group.add(benchSeat);
1128
- const benchBack = new THREE.Mesh(new THREE.BoxGeometry(1.5, 0.6, 0.08), mat(0x8b6040));
1129
- benchBack.position.set(2, 0.7, -0.22);
1130
- group.add(benchBack);
 
 
 
 
 
 
1131
 
1132
  const label = createLabel(locData.label, group, 5);
1133
  const badge = createOccupantBadge(group, 4);
@@ -1174,19 +1504,50 @@ function createSportsField(id, locData) {
1174
  function createPolice(id, locData) {
1175
  const group = new THREE.Group();
1176
  const w = 5, d = 4, wallH = 3.5;
1177
- const walls = new THREE.Mesh(new THREE.BoxGeometry(w, wallH, d), mat(BLDG_COLORS.police));
 
 
1178
  walls.position.y = wallH / 2; walls.castShadow = true; walls.receiveShadow = true;
1179
  addEdges(walls); group.add(walls);
 
1180
  const roof = new THREE.Mesh(new THREE.BoxGeometry(w + 0.4, 0.25, d + 0.4), mat(0x2a4a6a));
1181
  roof.position.y = wallH; group.add(roof);
1182
- const stripe = new THREE.Mesh(new THREE.BoxGeometry(w + 0.1, 0.6, 0.1), mat(0x4488cc));
1183
- stripe.position.set(0, wallH * 0.85, d / 2 + 0.06); group.add(stripe);
1184
- const door = new THREE.Mesh(new THREE.BoxGeometry(1, 2, 0.1), mat(0x2a3a5a));
1185
- door.position.set(0, 1, d / 2 + 0.06); group.add(door);
1186
- for (let sx of [-1.5, 1.5]) {
1187
- const win = new THREE.Mesh(new THREE.BoxGeometry(0.7, 0.7, 0.1), mat(BLDG_COLORS.window, { roughness: 0.3 }));
 
 
 
 
 
 
 
 
 
 
 
 
 
1188
  win.position.set(sx, wallH * 0.6, d / 2 + 0.06); win.userData.isWindow = true; group.add(win);
 
 
 
 
 
 
1189
  }
 
 
 
 
 
 
 
 
 
1190
  const label = createLabel(locData.label, group, wallH + 2);
1191
  const badge = createOccupantBadge(group, wallH + 1);
1192
  group.userData = { id, type: 'police', label, badge, locData };
@@ -1216,25 +1577,73 @@ function createFireStation(id, locData) {
1216
 
1217
  function createMuseum(id, locData) {
1218
  const group = new THREE.Group();
1219
- const w = 6, d = 5, wallH = 4;
1220
- const walls = new THREE.Mesh(new THREE.BoxGeometry(w, wallH, d), mat(BLDG_COLORS.museum));
 
 
 
1221
  walls.position.y = wallH / 2; walls.castShadow = true; walls.receiveShadow = true;
1222
  addEdges(walls); group.add(walls);
1223
- const pediment = new THREE.Mesh(
1224
- new THREE.ConeGeometry(w * 0.55, 2, 3),
1225
- mat(BLDG_COLORS.museum)
1226
- );
1227
- pediment.rotation.set(0, Math.PI / 6, 0);
1228
- pediment.scale.set(1, 1, 0.4);
1229
- pediment.position.set(0, wallH + 0.8, 0); pediment.castShadow = true; group.add(pediment);
1230
- for (let cx = -2; cx <= 2; cx += 1.3) {
1231
- const col = new THREE.Mesh(new THREE.CylinderGeometry(0.2, 0.25, wallH, 8), mat(0xd0c090));
1232
- col.position.set(cx, wallH / 2, d / 2 + 0.4); col.castShadow = true; group.add(col);
1233
- }
1234
- const steps = new THREE.Mesh(new THREE.BoxGeometry(w + 1, 0.3, 1.5), mat(0xc8c0a0));
1235
- steps.position.set(0, 0.15, d / 2 + 1); steps.receiveShadow = true; group.add(steps);
1236
- const label = createLabel(locData.label, group, wallH + 4);
1237
- const badge = createOccupantBadge(group, wallH + 3);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1238
  group.userData = { id, type: 'museum', label, badge, locData };
1239
  return group;
1240
  }
@@ -1244,22 +1653,63 @@ function createMall(id, locData) {
1244
  const h = hash(id);
1245
  const w = 8, d = 6, wallH = 5;
1246
  const clr = Array.isArray(BLDG_COLORS.mall) ? BLDG_COLORS.mall[h % BLDG_COLORS.mall.length] : BLDG_COLORS.mall;
 
 
1247
  const walls = new THREE.Mesh(new THREE.BoxGeometry(w, wallH, d), mat(clr));
1248
  walls.position.y = wallH / 2; walls.castShadow = true; walls.receiveShadow = true;
1249
  addEdges(walls); group.add(walls);
1250
- const roof = new THREE.Mesh(new THREE.BoxGeometry(w + 0.5, 0.3, d + 0.5), mat(0x808080));
 
1251
  roof.position.y = wallH; group.add(roof);
1252
- const glass = new THREE.Mesh(
1253
- new THREE.BoxGeometry(w * 0.8, wallH * 0.6, 0.1),
1254
- mat(BLDG_COLORS.window, { roughness: 0.2, metalness: 0.2, transparent: true, opacity: 0.7 })
1255
- );
1256
- glass.position.set(0, wallH * 0.4, d / 2 + 0.06); glass.userData.isWindow = true; group.add(glass);
1257
- for (let sx of [-2.5, 0, 2.5]) {
1258
- const awning = new THREE.Mesh(new THREE.BoxGeometry(2, 0.08, 1.2), mat(0xcc8844, { roughness: 0.6 }));
1259
- awning.position.set(sx, wallH * 0.7, d / 2 + 0.6); awning.castShadow = true; group.add(awning);
 
 
 
 
 
1260
  }
1261
- const label = createLabel(locData.label, group, wallH + 2);
1262
- const badge = createOccupantBadge(group, wallH + 1);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1263
  group.userData = { id, type: 'mall', label, badge, locData };
1264
  return group;
1265
  }
@@ -1318,37 +1768,66 @@ function createMarket(id, locData) {
1318
 
1319
  function createSquare(id, locData) {
1320
  const group = new THREE.Group();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1321
 
1322
- const plaza = new THREE.Mesh(
1323
- new THREE.BoxGeometry(7, 0.1, 7),
1324
- mat(BLDG_COLORS.square)
1325
- );
1326
- plaza.position.y = 0.05;
1327
- plaza.receiveShadow = true;
1328
- group.add(plaza);
1329
-
1330
- // Fountain
1331
- const basin = new THREE.Mesh(
1332
- new THREE.CylinderGeometry(1.2, 1.4, 0.5, 12),
1333
- mat(0x999988)
1334
- );
1335
- basin.position.y = 0.25;
1336
- group.add(basin);
1337
- const waterTop = new THREE.Mesh(
1338
- new THREE.CylinderGeometry(1.1, 1.1, 0.05, 12),
1339
- mat(PALETTE.water, { roughness: 0.2, metalness: 0.1 })
1340
- );
1341
- waterTop.position.y = 0.48;
1342
- group.add(waterTop);
1343
- const spout = new THREE.Mesh(
1344
- new THREE.CylinderGeometry(0.1, 0.15, 1, 8),
1345
- mat(0x888888)
1346
- );
1347
- spout.position.y = 1;
1348
- group.add(spout);
1349
 
1350
- const label = createLabel(locData.label, group, 4);
1351
- const badge = createOccupantBadge(group, 3);
1352
  group.userData = { id, type: 'square', label, badge, locData };
1353
  return group;
1354
  }
 
683
  function createHouse(id, locData) {
684
  const group = new THREE.Group();
685
  const h = hash(id);
686
+ const variant = h % 4;
687
  const w = 3 + (h % 2), d = 3 + ((h >> 2) % 2), wallH = 2.5 + (h % 3) * 0.3;
688
  const wallColor = BLDG_COLORS.house[h % BLDG_COLORS.house.length];
689
  const roofColor = BLDG_COLORS.roof[h % BLDG_COLORS.roof.length];
690
+ const trimColor = ((wallColor & 0xfefefe) >> 1) + 0x404040;
691
 
 
692
  const walls = new THREE.Mesh(new THREE.BoxGeometry(w, wallH, d), mat(wallColor));
693
  walls.position.y = wallH / 2;
694
+ walls.castShadow = true; walls.receiveShadow = true;
 
695
  addEdges(walls, 0x000000, 0.08);
696
  group.add(walls);
697
 
698
+ const roofH = 1.5 + (variant === 2 ? 0.5 : 0);
699
+ if (variant === 1) {
700
+ const roofGeo = new THREE.BufferGeometry();
701
+ const hw = w / 2 + 0.25, hd = d / 2 + 0.25, rh = roofH;
702
+ const v = new Float32Array([
703
+ -hw,wallH,-hd, hw,wallH,-hd, 0,wallH+rh,-hd,
704
+ -hw,wallH,hd, hw,wallH,hd, 0,wallH+rh,hd,
705
+ -hw,wallH,-hd, -hw,wallH,hd, 0,wallH+rh,-hd, 0,wallH+rh,hd,
706
+ hw,wallH,-hd, hw,wallH,hd, 0,wallH+rh,-hd, 0,wallH+rh,hd,
707
+ ]);
708
+ roofGeo.setAttribute('position', new THREE.BufferAttribute(v, 3));
709
+ roofGeo.setIndex([0,1,2, 3,5,4, 2,3,0, 2,5,3, 1,4,5, 1,5,2]);
710
+ roofGeo.computeVertexNormals();
711
+ const roof = new THREE.Mesh(roofGeo, mat(roofColor));
712
+ roof.castShadow = true; group.add(roof);
713
+ } else {
714
+ const roofGeo = new THREE.ConeGeometry(Math.max(w, d) * 0.75, roofH, 4);
715
+ roofGeo.rotateY(Math.PI / 4);
716
+ const roof = new THREE.Mesh(roofGeo, mat(roofColor));
717
+ roof.position.y = wallH + roofH / 2;
718
+ roof.castShadow = true; group.add(roof);
719
+ }
720
+
721
+ if (variant !== 3) {
722
+ const chimW = 0.35, chimH = 1.2 + (h % 3) * 0.3;
723
+ const chimney = new THREE.Mesh(new THREE.BoxGeometry(chimW, chimH, chimW), mat(0x884422));
724
+ chimney.position.set(w * 0.25, wallH + roofH * 0.5 + chimH / 2, -d * 0.2);
725
+ chimney.castShadow = true; group.add(chimney);
726
+ const chimCap = new THREE.Mesh(new THREE.BoxGeometry(chimW + 0.1, 0.08, chimW + 0.1), mat(0x666666));
727
+ chimCap.position.set(w * 0.25, wallH + roofH * 0.5 + chimH + 0.04, -d * 0.2);
728
+ group.add(chimCap);
729
+ }
730
+
731
+ const porchW = w * 0.6, porchD = 0.8;
732
+ const porchRoof = new THREE.Mesh(new THREE.BoxGeometry(porchW + 0.3, 0.08, porchD + 0.3), mat(roofColor));
733
+ porchRoof.position.set(0, wallH * 0.78, d / 2 + porchD / 2 + 0.1);
734
+ porchRoof.castShadow = true; group.add(porchRoof);
735
+ for (let s of [-1, 1]) {
736
+ const post = new THREE.Mesh(new THREE.CylinderGeometry(0.04, 0.04, wallH * 0.78, 6), mat(0xf0f0f0));
737
+ post.position.set(s * porchW / 2, wallH * 0.39, d / 2 + porchD);
738
+ group.add(post);
739
+ }
740
+ const step = new THREE.Mesh(new THREE.BoxGeometry(porchW, 0.12, porchD), mat(0xc8b8a0));
741
+ step.position.set(0, 0.06, d / 2 + porchD / 2 + 0.1);
742
+ step.receiveShadow = true; group.add(step);
743
+
744
+ const door = new THREE.Mesh(new THREE.BoxGeometry(0.6, 1.2, 0.1), mat(BLDG_COLORS.door));
745
  door.position.set(0, 0.6, d / 2 + 0.05);
746
+ door.userData.isDoor = true; group.add(door);
747
+ const knob = new THREE.Mesh(new THREE.SphereGeometry(0.04, 6, 4), mat(0xccaa44, { metalness: 0.6 }));
748
+ knob.position.set(0.2, 0.55, d / 2 + 0.11); group.add(knob);
749
 
 
750
  const winMat = mat(isNight ? BLDG_COLORS.windowLit : BLDG_COLORS.window,
751
  isNight ? { emissive: BLDG_COLORS.windowLit, emissiveIntensity: 0.5 } : {});
752
  for (let side of [-1, 1]) {
753
  const win = new THREE.Mesh(new THREE.BoxGeometry(0.6, 0.5, 0.1), winMat);
754
  win.position.set(side * (w / 2 + 0.05), wallH * 0.6, 0);
755
  win.rotation.y = Math.PI / 2;
756
+ win.userData.isWindow = true; group.add(win);
757
+ for (let sh of [-0.35, 0.35]) {
758
+ const shutter = new THREE.Mesh(new THREE.BoxGeometry(0.12, 0.55, 0.06), mat(trimColor));
759
+ shutter.position.set(side * (w / 2 + 0.07), wallH * 0.6, sh);
760
+ group.add(shutter);
761
+ }
762
+ }
763
+ const frontWin = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.5, 0.1), winMat);
764
+ frontWin.position.set(w * 0.28, wallH * 0.6, d / 2 + 0.05);
765
+ frontWin.userData.isWindow = true; group.add(frontWin);
766
+
767
+ if (variant === 3) {
768
+ const bayW = 1.2, bayD = 0.6, bayH = wallH * 0.7;
769
+ const bay = new THREE.Mesh(new THREE.BoxGeometry(bayW, bayH, bayD), mat(wallColor));
770
+ bay.position.set(-w * 0.3, bayH / 2, d / 2 + bayD / 2);
771
+ bay.castShadow = true; group.add(bay);
772
+ const bayRoof = new THREE.Mesh(new THREE.BoxGeometry(bayW + 0.15, 0.08, bayD + 0.15), mat(roofColor));
773
+ bayRoof.position.set(-w * 0.3, bayH + 0.04, d / 2 + bayD / 2); group.add(bayRoof);
774
+ for (let bx of [-0.35, 0, 0.35]) {
775
+ const bw = new THREE.Mesh(new THREE.BoxGeometry(0.25, 0.4, 0.06), winMat);
776
+ bw.position.set(-w * 0.3 + bx, bayH * 0.6, d / 2 + bayD + 0.03);
777
+ bw.userData.isWindow = true; group.add(bw);
778
+ }
779
  }
780
 
 
781
  const bed = new THREE.Mesh(new THREE.BoxGeometry(1.2, 0.15, 0.7), mat(0x8b6040));
782
  bed.position.set(0.6, 0.08, 0); bed.userData.isFurniture = true; group.add(bed);
783
  const mattress = new THREE.Mesh(new THREE.BoxGeometry(1.1, 0.1, 0.6), mat(0xe8e0d0));
 
785
  const pillow = new THREE.Mesh(new THREE.BoxGeometry(0.25, 0.08, 0.35), mat(0xf0f0f0));
786
  pillow.position.set(1.1, 0.24, 0); pillow.userData.isFurniture = true; group.add(pillow);
787
 
788
+ const label = createLabel(locData.label, group, wallH + roofH + 1.5);
789
+ const badge = createOccupantBadge(group, wallH + roofH + 0.5);
790
  group.userData = { id, type: 'house', label, badge, locData };
791
  return group;
792
  }
 
794
  function createApartment(id, locData) {
795
  const group = new THREE.Group();
796
  const h = hash(id);
797
+ const variant = h % 3;
798
  const w = 5, d = 4, wallH = 7 + (h % 4);
799
  const wallColor = BLDG_COLORS.apartment[h % BLDG_COLORS.apartment.length];
800
+ const accentColor = wallColor - 0x181818;
801
 
802
  const walls = new THREE.Mesh(new THREE.BoxGeometry(w, wallH, d), mat(wallColor));
803
  walls.position.y = wallH / 2;
804
+ walls.castShadow = true; walls.receiveShadow = true;
805
+ addEdges(walls); group.add(walls);
 
 
806
 
807
+ if (variant === 1 && wallH >= 9) {
808
+ const setW = w - 1, setD = d - 0.5, setH = 2.5;
809
+ const upper = new THREE.Mesh(new THREE.BoxGeometry(setW, setH, setD), mat(wallColor + 0x080808));
810
+ upper.position.y = wallH + setH / 2; upper.castShadow = true; addEdges(upper); group.add(upper);
811
+ const upperCap = new THREE.Mesh(new THREE.BoxGeometry(setW + 0.2, 0.2, setD + 0.2), mat(accentColor));
812
+ upperCap.position.y = wallH + setH; group.add(upperCap);
813
+ }
814
+
815
+ const corniceMat = mat(accentColor);
816
+ const roofCap = new THREE.Mesh(new THREE.BoxGeometry(w + 0.4, 0.35, d + 0.4), corniceMat);
817
+ roofCap.position.y = wallH; group.add(roofCap);
818
+ const midCornice = new THREE.Mesh(new THREE.BoxGeometry(w + 0.15, 0.12, d + 0.15), corniceMat);
819
+ midCornice.position.y = wallH * 0.5; group.add(midCornice);
820
+ const baseCornice = new THREE.Mesh(new THREE.BoxGeometry(w + 0.2, 0.2, d + 0.2), corniceMat);
821
+ baseCornice.position.y = 0.1; group.add(baseCornice);
822
+
823
+ const entranceW = 2.5, entranceD = 1.2;
824
+ const canopy = new THREE.Mesh(new THREE.BoxGeometry(entranceW, 0.1, entranceD), mat(accentColor - 0x101010));
825
+ canopy.position.set(0, 2.8, d / 2 + entranceD / 2); canopy.castShadow = true; group.add(canopy);
826
+ const eDoor = new THREE.Mesh(new THREE.BoxGeometry(1.4, 2.2, 0.1), mat(0x3a3a5a));
827
+ eDoor.position.set(0, 1.1, d / 2 + 0.05); eDoor.userData.isDoor = true; group.add(eDoor);
828
+ const eGlass = new THREE.Mesh(new THREE.BoxGeometry(1.0, 1.5, 0.06),
829
+ mat(BLDG_COLORS.window, { transparent: true, opacity: 0.5, roughness: 0.2 }));
830
+ eGlass.position.set(0, 1.3, d / 2 + 0.08); eGlass.userData.isWindow = true; group.add(eGlass);
831
 
 
832
  const winMat = mat(isNight ? BLDG_COLORS.windowLit : BLDG_COLORS.window,
833
  isNight ? { emissive: BLDG_COLORS.windowLit, emissiveIntensity: 0.4 } : {});
834
  const floors = Math.floor(wallH / 2);
835
+ const balconyFloors = variant === 0 ? [1, 3] : variant === 2 ? [0, 2, 4] : [2];
836
  for (let f = 0; f < floors; f++) {
837
+ const fy = 1.5 + f * 2;
838
  for (let wx = -1; wx <= 1; wx++) {
839
+ const win = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.7, 0.1), winMat);
840
+ win.position.set(wx * 1.4, fy, d / 2 + 0.05);
841
+ win.userData.isWindow = true; group.add(win);
842
+ const sill = new THREE.Mesh(new THREE.BoxGeometry(0.6, 0.06, 0.12), corniceMat);
843
+ sill.position.set(wx * 1.4, fy - 0.38, d / 2 + 0.08); group.add(sill);
844
+ }
845
+ if (balconyFloors.includes(f) && f > 0) {
846
+ const balcW = w * 0.85, balcD = 0.7;
847
+ const balcFloor = new THREE.Mesh(new THREE.BoxGeometry(balcW, 0.1, balcD), mat(0xa0a0a0));
848
+ balcFloor.position.set(0, fy - 0.45, d / 2 + balcD / 2 + 0.05); group.add(balcFloor);
849
+ const railMat = mat(0x444444, { metalness: 0.3 });
850
+ const rail = new THREE.Mesh(new THREE.BoxGeometry(balcW, 0.04, 0.04), railMat);
851
+ rail.position.set(0, fy - 0.05, d / 2 + balcD + 0.05); group.add(rail);
852
+ for (let rx = -balcW / 2 + 0.15; rx <= balcW / 2; rx += 0.3) {
853
+ const bar = new THREE.Mesh(new THREE.BoxGeometry(0.02, 0.35, 0.02), railMat);
854
+ bar.position.set(rx, fy - 0.25, d / 2 + balcD + 0.05); group.add(bar);
855
+ }
856
+ }
857
+ for (let side of [-1, 1]) {
858
+ const sWin = new THREE.Mesh(new THREE.BoxGeometry(0.1, 0.6, 0.45), winMat);
859
+ sWin.position.set(side * (w / 2 + 0.05), fy, 0);
860
+ sWin.userData.isWindow = true; group.add(sWin);
861
+ }
862
+ }
863
+
864
+ if (variant === 2) {
865
+ for (let rx = -1.5; rx <= 1.5; rx += 3) {
866
+ const roofBox = new THREE.Mesh(new THREE.BoxGeometry(0.8, 0.6, 0.8), mat(0x888888));
867
+ roofBox.position.set(rx, wallH + 0.6, 0); group.add(roofBox);
868
  }
869
  }
870
 
 
871
  for (let bx = -1; bx <= 1; bx += 2) {
872
  const bed = new THREE.Mesh(new THREE.BoxGeometry(1.0, 0.12, 0.5), mat(0x8b6040));
873
  bed.position.set(bx * 1.2, 0.06, 0); bed.userData.isFurniture = true; group.add(bed);
874
+ const mattObj = new THREE.Mesh(new THREE.BoxGeometry(0.9, 0.08, 0.45), mat(0xe0d8c8));
875
+ mattObj.position.set(bx * 1.2, 0.14, 0); mattObj.userData.isFurniture = true; group.add(mattObj);
876
  }
877
 
878
+ const totalH = variant === 1 && wallH >= 9 ? wallH + 2.5 : wallH;
879
+ const label = createLabel(locData.label, group, totalH + 2);
880
+ const badge = createOccupantBadge(group, totalH + 1);
881
  group.userData = { id, type: 'apartment', label, badge, locData };
882
  return group;
883
  }
 
926
 
927
  function createOffice(id, locData) {
928
  const group = new THREE.Group();
929
+ const w = 5, d = 5, wallH = 6;
930
+ const frameMat = mat(0x555565, { metalness: 0.4, roughness: 0.3 });
931
 
932
+ const core = new THREE.Mesh(
933
+ new THREE.BoxGeometry(w - 0.3, wallH, d - 0.3),
934
+ mat(0x404050, { roughness: 0.4, metalness: 0.2 })
935
  );
936
+ core.position.y = wallH / 2; core.castShadow = true; core.receiveShadow = true;
937
+ group.add(core);
938
+
939
+ const glassMat = mat(BLDG_COLORS.window, { roughness: 0.1, metalness: 0.4, opacity: 0.55, transparent: true });
940
+ for (let face = 0; face < 4; face++) {
941
+ const fw = face % 2 === 0 ? w : d;
942
+ const fd = face % 2 === 0 ? d : w;
943
+ const faceGroup = new THREE.Group();
944
+ const floors = 3;
945
+ for (let f = 0; f < floors; f++) {
946
+ const fy = 0.8 + f * 1.8;
947
+ const band = new THREE.Mesh(new THREE.BoxGeometry(fw + 0.05, 0.08, 0.08), frameMat);
948
+ band.position.set(0, fy - 0.15, fd / 2 + 0.02); faceGroup.add(band);
949
+ for (let gx = -1; gx <= 1; gx++) {
950
+ const pane = new THREE.Mesh(new THREE.BoxGeometry(fw * 0.28, 1.4, 0.06), glassMat);
951
+ pane.position.set(gx * (fw * 0.3), fy + 0.55, fd / 2 + 0.04);
952
+ pane.userData.isWindow = true; faceGroup.add(pane);
953
+ }
954
+ for (let mx = -1.5; mx <= 1.5; mx++) {
955
+ const mullion = new THREE.Mesh(new THREE.BoxGeometry(0.06, 1.5, 0.06), frameMat);
956
+ mullion.position.set(mx * (fw * 0.2), fy + 0.55, fd / 2 + 0.05); faceGroup.add(mullion);
957
+ }
958
  }
959
+ const topBand = new THREE.Mesh(new THREE.BoxGeometry(fw + 0.05, 0.08, 0.08), frameMat);
960
+ topBand.position.set(0, wallH - 0.1, fd / 2 + 0.02); faceGroup.add(topBand);
961
+ faceGroup.rotation.y = (face * Math.PI) / 2;
962
+ group.add(faceGroup);
963
+ }
964
+
965
+ const roofCap = new THREE.Mesh(new THREE.BoxGeometry(w + 0.3, 0.25, d + 0.3), frameMat);
966
+ roofCap.position.y = wallH; group.add(roofCap);
967
+ const parapet = new THREE.Mesh(new THREE.BoxGeometry(w + 0.35, 0.5, d + 0.35),
968
+ mat(0x555565, { metalness: 0.3 }));
969
+ parapet.position.y = wallH + 0.25;
970
+ const parapetInner = new THREE.Mesh(new THREE.BoxGeometry(w - 0.1, 0.6, d - 0.1), mat(0x555565));
971
+ parapetInner.position.y = wallH + 0.25;
972
+ const parapetCSG = parapet; group.add(parapetCSG);
973
+
974
+ const lobbyW = 2, lobbyH = 2.5, lobbyD = 1;
975
+ const lobby = new THREE.Mesh(new THREE.BoxGeometry(lobbyW, lobbyH, lobbyD),
976
+ mat(0x88aacc, { transparent: true, opacity: 0.4, roughness: 0.1, metalness: 0.3 }));
977
+ lobby.position.set(0, lobbyH / 2, d / 2 + lobbyD / 2);
978
+ lobby.userData.isWindow = true; group.add(lobby);
979
+ const lobbyFrame = new THREE.Mesh(new THREE.BoxGeometry(lobbyW + 0.15, 0.1, lobbyD + 0.15), frameMat);
980
+ lobbyFrame.position.set(0, lobbyH, d / 2 + lobbyD / 2); group.add(lobbyFrame);
981
 
982
  const label = createLabel(locData.label, group, wallH + 2);
983
  const badge = createOccupantBadge(group, wallH + 1);
 
988
  function createTower(id, locData) {
989
  const group = new THREE.Group();
990
  const w = 4.5, d = 4.5, wallH = 14;
991
+ const towerMat = mat(BLDG_COLORS.tower, { roughness: 0.25, metalness: 0.35 });
992
+ const frameMat = mat(0x445566, { metalness: 0.5, roughness: 0.3 });
993
+ const glassMat = mat(BLDG_COLORS.window, { roughness: 0.1, metalness: 0.5, opacity: 0.5, transparent: true });
994
+
995
+ const base = new THREE.Mesh(new THREE.BoxGeometry(w + 1.5, 3.5, d + 1.5), mat(BLDG_COLORS.tower - 0x101010));
996
+ base.position.y = 1.75; base.castShadow = true; base.receiveShadow = true;
997
+ addEdges(base); group.add(base);
998
+ const baseLobby = new THREE.Mesh(new THREE.BoxGeometry(2.5, 2.8, 0.8),
999
+ mat(0x88bbdd, { transparent: true, opacity: 0.4, roughness: 0.1 }));
1000
+ baseLobby.position.set(0, 1.4, (d + 1.5) / 2 + 0.3);
1001
+ baseLobby.userData.isWindow = true; group.add(baseLobby);
1002
+
1003
+ const body = new THREE.Mesh(new THREE.BoxGeometry(w, wallH - 3, d), towerMat);
1004
+ body.position.y = 3.5 + (wallH - 3) / 2; body.castShadow = true; body.receiveShadow = true;
1005
+ addEdges(body); group.add(body);
1006
+
1007
+ for (let face = 0; face < 4; face++) {
1008
+ const fw = w, fd = d;
1009
+ const faceGrp = new THREE.Group();
1010
+ for (let f = 0; f < 5; f++) {
1011
+ const fy = 4.5 + f * 2;
1012
+ const strip = new THREE.Mesh(new THREE.BoxGeometry(fw * 0.85, 1.3, 0.08), glassMat);
1013
+ strip.position.set(0, fy, fd / 2 + 0.04);
1014
+ strip.userData.isWindow = true; faceGrp.add(strip);
1015
+ for (let mx = -1; mx <= 1; mx++) {
1016
+ const mull = new THREE.Mesh(new THREE.BoxGeometry(0.06, 1.4, 0.06), frameMat);
1017
+ mull.position.set(mx * (fw * 0.28), fy, fd / 2 + 0.05); faceGrp.add(mull);
1018
+ }
1019
+ const hBar = new THREE.Mesh(new THREE.BoxGeometry(fw * 0.88, 0.06, 0.06), frameMat);
1020
+ hBar.position.set(0, fy + 0.65, fd / 2 + 0.05); faceGrp.add(hBar);
1021
+ }
1022
+ faceGrp.rotation.y = (face * Math.PI) / 2;
1023
+ group.add(faceGrp);
1024
+ }
1025
 
1026
+ const deckY = wallH - 0.5;
1027
+ const deckW = w + 1.2, deckD = d + 1.2;
1028
+ const deckFloor = new THREE.Mesh(new THREE.BoxGeometry(deckW, 0.2, deckD), frameMat);
1029
+ deckFloor.position.y = deckY; group.add(deckFloor);
1030
+ const railMat = mat(0x888888, { metalness: 0.4 });
1031
+ for (let side of [-1, 1]) {
1032
+ const railH = new THREE.Mesh(new THREE.BoxGeometry(deckW, 0.06, 0.06), railMat);
1033
+ railH.position.set(0, deckY + 0.8, side * deckD / 2); group.add(railH);
1034
+ const railS = new THREE.Mesh(new THREE.BoxGeometry(0.06, 0.06, deckD), railMat);
1035
+ railS.position.set(side * deckW / 2, deckY + 0.8, 0); group.add(railS);
1036
+ for (let i = -3; i <= 3; i++) {
1037
+ const post = new THREE.Mesh(new THREE.BoxGeometry(0.04, 0.8, 0.04), railMat);
1038
+ post.position.set(i * 0.8, deckY + 0.4, side * deckD / 2); group.add(post);
1039
+ }
1040
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1041
 
1042
+ const crownH = 3;
1043
+ const crownW = w * 0.7, crownD = d * 0.7;
1044
+ const crown = new THREE.Mesh(new THREE.BoxGeometry(crownW, crownH, crownD), towerMat);
1045
+ crown.position.y = wallH + crownH / 2; crown.castShadow = true;
1046
+ addEdges(crown); group.add(crown);
1047
+ for (let face = 0; face < 4; face++) {
1048
+ const cfGrp = new THREE.Group();
1049
+ const cg = new THREE.Mesh(new THREE.BoxGeometry(crownW * 0.7, crownH * 0.7, 0.06), glassMat);
1050
+ cg.position.set(0, wallH + crownH / 2, crownD / 2 + 0.03);
1051
+ cg.userData.isWindow = true; cfGrp.add(cg);
1052
+ cfGrp.rotation.y = (face * Math.PI) / 2;
1053
+ group.add(cfGrp);
1054
+ }
1055
+
1056
+ const spireH = 4;
1057
+ const spire = new THREE.Mesh(new THREE.ConeGeometry(0.4, spireH, 8), mat(0x888899, { metalness: 0.6 }));
1058
+ spire.position.y = wallH + crownH + spireH / 2; group.add(spire);
1059
+ const beacon = new THREE.Mesh(new THREE.SphereGeometry(0.15, 8, 6),
1060
+ mat(0xff4444, { emissive: 0xff2222, emissiveIntensity: 0.6 }));
1061
+ beacon.position.y = wallH + crownH + spireH + 0.15; group.add(beacon);
1062
+
1063
+ const label = createLabel(locData.label, group, wallH + crownH + spireH + 2);
1064
+ const badge = createOccupantBadge(group, wallH + crownH + spireH + 1);
1065
  group.userData = { id, type: 'tower', label, badge, locData };
1066
  return group;
1067
  }
1068
 
1069
  function createHospital(id, locData) {
1070
  const group = new THREE.Group();
1071
+ const w = 7, d = 5, wallH = 5;
1072
 
1073
  const walls = new THREE.Mesh(new THREE.BoxGeometry(w, wallH, d), mat(BLDG_COLORS.hospital));
1074
+ walls.position.y = wallH / 2; walls.castShadow = true; walls.receiveShadow = true;
1075
+ addEdges(walls); group.add(walls);
1076
+
1077
+ const wingW = 3, wingD = 3, wingH = 3.5;
1078
+ const wing = new THREE.Mesh(new THREE.BoxGeometry(wingW, wingH, wingD), mat(0xe8e8e8));
1079
+ wing.position.set(-w / 2 + wingW / 2 - 0.5, wingH / 2, -d / 2 - wingD / 2 + 0.5);
1080
+ wing.castShadow = true; wing.receiveShadow = true; addEdges(wing); group.add(wing);
1081
+
1082
+ const winMat = mat(isNight ? BLDG_COLORS.windowLit : BLDG_COLORS.window,
1083
+ isNight ? { emissive: BLDG_COLORS.windowLit, emissiveIntensity: 0.3 } : {});
1084
+ for (let f = 0; f < 2; f++) {
1085
+ for (let wx = -2; wx <= 2; wx++) {
1086
+ const win = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.6, 0.1), winMat);
1087
+ win.position.set(wx * 1.2, 1.5 + f * 2, d / 2 + 0.05);
1088
+ win.userData.isWindow = true; group.add(win);
1089
+ }
1090
+ }
1091
 
 
1092
  const crossMat = mat(0xcc2222);
1093
  const crossH = new THREE.Mesh(new THREE.BoxGeometry(1.5, 0.4, 0.1), crossMat);
1094
+ crossH.position.set(0, wallH - 0.5, d / 2 + 0.06); group.add(crossH);
 
1095
  const crossV = new THREE.Mesh(new THREE.BoxGeometry(0.4, 1.5, 0.1), crossMat);
1096
+ crossV.position.set(0, wallH - 0.5, d / 2 + 0.06); group.add(crossV);
1097
+
1098
+ const canopyW = 4, canopyD = 2.5;
1099
+ const canopy = new THREE.Mesh(new THREE.BoxGeometry(canopyW, 0.12, canopyD), mat(0xdddddd));
1100
+ canopy.position.set(0, 3, d / 2 + canopyD / 2); canopy.castShadow = true; group.add(canopy);
1101
+ for (let s of [-1, 1]) {
1102
+ const pole = new THREE.Mesh(new THREE.CylinderGeometry(0.08, 0.08, 3, 6), mat(0x888888));
1103
+ pole.position.set(s * (canopyW / 2 - 0.3), 1.5, d / 2 + canopyD - 0.3); group.add(pole);
1104
+ }
1105
+ const emergSign = new THREE.Mesh(new THREE.BoxGeometry(2, 0.4, 0.08),
1106
+ mat(0xcc2222, { emissive: 0xaa1111, emissiveIntensity: 0.5 }));
1107
+ emergSign.position.set(0, 3.3, d / 2 + canopyD / 2); group.add(emergSign);
1108
+
1109
+ const roofCap = new THREE.Mesh(new THREE.BoxGeometry(w + 0.3, 0.25, d + 0.3), mat(0xcccccc));
1110
+ roofCap.position.y = wallH; group.add(roofCap);
1111
+ const heliR = 1.5;
1112
+ const heliPad = new THREE.Mesh(new THREE.CylinderGeometry(heliR, heliR, 0.05, 16), mat(0xaaaaaa));
1113
+ heliPad.position.set(w / 4, wallH + 0.15, 0); group.add(heliPad);
1114
+ const heliCircle = new THREE.Mesh(
1115
+ new THREE.RingGeometry(heliR - 0.15, heliR, 16),
1116
+ mat(0xeeee22, { side: THREE.DoubleSide }));
1117
+ heliCircle.rotation.x = -Math.PI / 2;
1118
+ heliCircle.position.set(w / 4, wallH + 0.2, 0); group.add(heliCircle);
1119
+ const hBar = new THREE.Mesh(new THREE.BoxGeometry(1.2, 0.04, 0.15), mat(0xeeee22));
1120
+ hBar.position.set(w / 4, wallH + 0.2, 0); group.add(hBar);
1121
+ const hBar2 = new THREE.Mesh(new THREE.BoxGeometry(0.15, 0.04, 1.2), mat(0xeeee22));
1122
+ hBar2.position.set(w / 4, wallH + 0.2, 0); group.add(hBar2);
1123
 
1124
  const label = createLabel(locData.label, group, wallH + 2);
1125
  const badge = createOccupantBadge(group, wallH + 1);
 
1129
 
1130
  function createChurch(id, locData) {
1131
  const group = new THREE.Group();
1132
+ const w = 4, d = 6, wallH = 4.5;
1133
+ const stoneMat = mat(BLDG_COLORS.church);
1134
 
1135
+ const walls = new THREE.Mesh(new THREE.BoxGeometry(w, wallH, d), stoneMat);
1136
+ walls.position.y = wallH / 2; walls.castShadow = true; walls.receiveShadow = true;
1137
+ addEdges(walls); group.add(walls);
 
 
 
1138
 
 
1139
  const roofGeo = new THREE.BufferGeometry();
1140
+ const hw = w / 2 + 0.2, hd = d / 2 + 0.2, rh = 2.2;
1141
+ const v = new Float32Array([
1142
+ -hw,wallH,-hd, hw,wallH,-hd, 0,wallH+rh,-hd,
1143
+ -hw,wallH,hd, hw,wallH,hd, 0,wallH+rh,hd,
1144
+ -hw,wallH,-hd, -hw,wallH,hd, 0,wallH+rh,-hd, 0,wallH+rh,hd,
1145
+ hw,wallH,-hd, hw,wallH,hd, 0,wallH+rh,-hd, 0,wallH+rh,hd,
 
1146
  ]);
1147
+ roofGeo.setAttribute('position', new THREE.BufferAttribute(v, 3));
1148
+ roofGeo.setIndex([0,1,2, 3,5,4, 2,3,0, 2,5,3, 1,4,5, 1,5,2]);
 
1149
  roofGeo.computeVertexNormals();
1150
  const roof = new THREE.Mesh(roofGeo, mat(BLDG_COLORS.roof[0]));
1151
+ roof.castShadow = true; group.add(roof);
1152
+
1153
+ for (let bz = -1.5; bz <= 1.5; bz += 1.5) {
1154
+ for (let side of [-1, 1]) {
1155
+ const buttress = new THREE.Mesh(new THREE.BoxGeometry(0.3, wallH * 0.8, 0.5), stoneMat);
1156
+ buttress.position.set(side * (w / 2 + 0.15), wallH * 0.4, bz);
1157
+ buttress.castShadow = true; group.add(buttress);
1158
+ const buttTop = new THREE.Mesh(new THREE.ConeGeometry(0.25, 0.6, 4), stoneMat);
1159
+ buttTop.position.set(side * (w / 2 + 0.15), wallH * 0.8 + 0.3, bz);
1160
+ buttTop.rotation.y = Math.PI / 4; group.add(buttTop);
1161
+ }
1162
+ }
 
 
 
 
 
 
 
1163
 
1164
+ const roseR = 0.7;
1165
+ const roseFrame = new THREE.Mesh(new THREE.TorusGeometry(roseR, 0.06, 8, 16),
1166
+ mat(0x888878, { metalness: 0.3 }));
1167
+ roseFrame.position.set(0, wallH * 0.7, d / 2 + 0.06); group.add(roseFrame);
1168
+ const roseGlass = new THREE.Mesh(new THREE.CircleGeometry(roseR - 0.05, 16),
1169
+ mat(0x4466aa, { transparent: true, opacity: 0.6, emissive: 0x223355, emissiveIntensity: 0.2, side: THREE.DoubleSide }));
1170
+ roseGlass.position.set(0, wallH * 0.7, d / 2 + 0.07);
1171
+ roseGlass.userData.isWindow = true; group.add(roseGlass);
1172
+ for (let i = 0; i < 8; i++) {
1173
+ const ang = (i / 8) * Math.PI * 2;
1174
+ const spoke = new THREE.Mesh(new THREE.BoxGeometry(0.03, roseR * 1.6, 0.03), mat(0x888878));
1175
+ spoke.position.set(0, wallH * 0.7, d / 2 + 0.065);
1176
+ spoke.rotation.z = ang; group.add(spoke);
1177
+ }
1178
+
1179
+ const doorW = 1.2, doorH = 2;
1180
+ const doorArch = new THREE.Mesh(new THREE.BoxGeometry(doorW, doorH, 0.15), mat(BLDG_COLORS.door));
1181
+ doorArch.position.set(0, doorH / 2, d / 2 + 0.05); doorArch.userData.isDoor = true; group.add(doorArch);
1182
+ const archTop = new THREE.Mesh(new THREE.CylinderGeometry(doorW / 2, doorW / 2, 0.15, 12, 1, false, 0, Math.PI),
1183
+ mat(BLDG_COLORS.door));
1184
+ archTop.rotation.x = Math.PI / 2; archTop.rotation.z = Math.PI;
1185
+ archTop.position.set(0, doorH, d / 2 + 0.05); group.add(archTop);
1186
+
1187
+ const stBase = new THREE.Mesh(new THREE.BoxGeometry(1.8, 2.5, 1.8), stoneMat);
1188
+ stBase.position.set(0, wallH + 1.25, -d / 2 + 1);
1189
+ stBase.castShadow = true; group.add(stBase);
1190
+ for (let corner of [[-0.7,-0.7],[0.7,-0.7],[-0.7,0.7],[0.7,0.7]]) {
1191
+ const pin = new THREE.Mesh(new THREE.ConeGeometry(0.15, 0.8, 4), mat(BLDG_COLORS.roof[1]));
1192
+ pin.position.set(corner[0], wallH + 2.9, -d / 2 + 1 + corner[1]);
1193
+ pin.rotation.y = Math.PI / 4; group.add(pin);
1194
+ }
1195
+ const steeple = new THREE.Mesh(new THREE.ConeGeometry(0.7, 4.5, 4), mat(BLDG_COLORS.roof[1]));
1196
+ steeple.position.set(0, wallH + rh + 2.5, -d / 2 + 1);
1197
+ steeple.rotation.y = Math.PI / 4;
1198
+ steeple.castShadow = true; group.add(steeple);
1199
+ const crossBar1 = new THREE.Mesh(new THREE.BoxGeometry(0.06, 0.8, 0.06), mat(0xccaa44, { metalness: 0.5 }));
1200
+ crossBar1.position.set(0, wallH + rh + 5.1, -d / 2 + 1); group.add(crossBar1);
1201
+ const crossBar2 = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.06, 0.06), mat(0xccaa44, { metalness: 0.5 }));
1202
+ crossBar2.position.set(0, wallH + rh + 5.2, -d / 2 + 1); group.add(crossBar2);
1203
+
1204
+ const label = createLabel(locData.label, group, wallH + rh + 6);
1205
+ const badge = createOccupantBadge(group, wallH + rh + 5);
1206
  group.userData = { id, type: 'church', label, badge, locData };
1207
  return group;
1208
  }
 
1212
  const w = 6, d = 5, wallH = 3.5;
1213
 
1214
  const walls = new THREE.Mesh(new THREE.BoxGeometry(w, wallH, d), mat(BLDG_COLORS.school));
1215
+ walls.position.y = wallH / 2; walls.castShadow = true; walls.receiveShadow = true;
1216
+ addEdges(walls); group.add(walls);
 
 
 
1217
 
1218
+ const entW = 2, entH = wallH + 1.5, entD = 0.6;
1219
+ const entrance = new THREE.Mesh(new THREE.BoxGeometry(entW, entH, entD), mat(BLDG_COLORS.school - 0x101010));
1220
+ entrance.position.set(0, entH / 2, d / 2 + entD / 2 - 0.1);
1221
+ entrance.castShadow = true; group.add(entrance);
1222
+ const pediment = new THREE.Mesh(new THREE.ConeGeometry(entW * 0.6, 1, 3), mat(BLDG_COLORS.school - 0x101010));
1223
+ pediment.position.set(0, entH + 0.3, d / 2 + entD / 2 - 0.1);
1224
+ pediment.scale.z = 0.3; pediment.rotation.y = Math.PI / 6; group.add(pediment);
1225
+
1226
+ const door = new THREE.Mesh(new THREE.BoxGeometry(1.2, 2, 0.1), mat(BLDG_COLORS.door));
1227
+ door.position.set(0, 1, d / 2 + entD - 0.05); door.userData.isDoor = true; group.add(door);
1228
+
1229
+ const clockR = 0.4;
1230
+ const clockFace = new THREE.Mesh(new THREE.CircleGeometry(clockR, 16),
1231
+ mat(0xf8f8f0, { side: THREE.DoubleSide }));
1232
+ clockFace.position.set(0, entH - 0.5, d / 2 + entD + 0.01); group.add(clockFace);
1233
+ const clockRim = new THREE.Mesh(new THREE.TorusGeometry(clockR, 0.04, 8, 16), mat(0x444444));
1234
+ clockRim.position.set(0, entH - 0.5, d / 2 + entD + 0.01); group.add(clockRim);
1235
+ const hourHand = new THREE.Mesh(new THREE.BoxGeometry(0.04, clockR * 0.5, 0.02), mat(0x222222));
1236
+ hourHand.position.set(0, entH - 0.5 + clockR * 0.2, d / 2 + entD + 0.02); group.add(hourHand);
1237
+ const minHand = new THREE.Mesh(new THREE.BoxGeometry(0.03, clockR * 0.7, 0.02), mat(0x222222));
1238
+ minHand.position.set(clockR * 0.15, entH - 0.5, d / 2 + entD + 0.02);
1239
+ minHand.rotation.z = -Math.PI / 3; group.add(minHand);
1240
+
1241
+ const winMat = mat(isNight ? BLDG_COLORS.windowLit : BLDG_COLORS.window,
1242
+ isNight ? { emissive: BLDG_COLORS.windowLit, emissiveIntensity: 0.3 } : {});
1243
  for (let i = -2; i <= 2; i++) {
1244
+ if (Math.abs(i) < 1) continue;
1245
  const win = new THREE.Mesh(new THREE.BoxGeometry(0.6, 0.8, 0.1), winMat);
1246
  win.position.set(i * 1.1, wallH * 0.6, d / 2 + 0.05);
1247
+ win.userData.isWindow = true; group.add(win);
1248
+ const sill = new THREE.Mesh(new THREE.BoxGeometry(0.7, 0.06, 0.12), mat(0xaa6848));
1249
+ sill.position.set(i * 1.1, wallH * 0.6 - 0.43, d / 2 + 0.08); group.add(sill);
1250
+ }
1251
+ for (let side of [-1, 1]) {
1252
+ for (let sz = -1; sz <= 1; sz++) {
1253
+ const sWin = new THREE.Mesh(new THREE.BoxGeometry(0.1, 0.7, 0.5), winMat);
1254
+ sWin.position.set(side * (w / 2 + 0.05), wallH * 0.6, sz * 1.3);
1255
+ sWin.userData.isWindow = true; group.add(sWin);
1256
+ }
1257
  }
1258
 
1259
+ const pole = new THREE.Mesh(new THREE.CylinderGeometry(0.05, 0.05, 4, 6), mat(0x888888));
1260
+ pole.position.set(w / 2 + 0.8, 2, d / 2 - 1); group.add(pole);
 
 
 
1261
  const flag = new THREE.Mesh(new THREE.PlaneGeometry(1, 0.6), mat(0xcc3333, { side: THREE.DoubleSide }));
1262
+ flag.position.set(w / 2 + 0.8 + 0.5, 3.5, d / 2 - 1); group.add(flag);
1263
+
1264
+ const roofCap = new THREE.Mesh(new THREE.BoxGeometry(w + 0.3, 0.2, d + 0.3), mat(BLDG_COLORS.school - 0x151515));
1265
+ roofCap.position.y = wallH; group.add(roofCap);
1266
 
1267
+ const label = createLabel(locData.label, group, entH + 2);
1268
+ const badge = createOccupantBadge(group, entH + 1);
1269
  group.userData = { id, type: 'school', label, badge, locData };
1270
  return group;
1271
  }
 
1273
  function createFactory(id, locData) {
1274
  const group = new THREE.Group();
1275
  const w = 7, d = 5, wallH = 4.5;
1276
+ const factMat = mat(BLDG_COLORS.factory);
1277
+ const metalMat = mat(0x666666, { metalness: 0.4, roughness: 0.4 });
1278
 
1279
+ const walls = new THREE.Mesh(new THREE.BoxGeometry(w, wallH, d), factMat);
1280
+ walls.position.y = wallH / 2; walls.castShadow = true; walls.receiveShadow = true;
1281
+ addEdges(walls); group.add(walls);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1282
 
1283
+ const sawTeeth = 3, toothW = w / sawTeeth, toothH = 1.2;
1284
+ for (let i = 0; i < sawTeeth; i++) {
1285
+ const tx = -w / 2 + toothW / 2 + i * toothW;
1286
+ const geo = new THREE.BufferGeometry();
1287
+ const hw = toothW / 2, hd = d / 2;
1288
+ const v = new Float32Array([
1289
+ -hw, wallH, -hd, hw, wallH, -hd, hw, wallH, hd, -hw, wallH, hd,
1290
+ -hw, wallH + toothH, -hd, hw, wallH + toothH, -hd,
1291
+ -hw, wallH + toothH, hd, hw, wallH, hd,
1292
+ ]);
1293
+ geo.setAttribute('position', new THREE.BufferAttribute(v, 3));
1294
+ geo.setIndex([0,1,5,0,5,4, 3,6,4,3,4,0, 1,2,7,1,7,5, 4,5,7,4,7,6, 3,2,1,3,1,0]);
1295
+ geo.computeVertexNormals();
1296
+ const tooth = new THREE.Mesh(geo, metalMat);
1297
+ tooth.position.x = tx; tooth.castShadow = true; group.add(tooth);
1298
+ const skylight = new THREE.Mesh(new THREE.PlaneGeometry(toothW * 0.6, d * 0.6),
1299
+ mat(0x88bbdd, { transparent: true, opacity: 0.4, side: THREE.DoubleSide }));
1300
+ skylight.position.set(tx, wallH + toothH * 0.5 + 0.01, 0);
1301
+ skylight.rotation.x = -Math.PI / 2 + 0.3;
1302
+ skylight.userData.isWindow = true; group.add(skylight);
1303
+ }
1304
+
1305
+ const chimney = new THREE.Mesh(new THREE.CylinderGeometry(0.5, 0.65, 5, 8), mat(0x555555));
1306
+ chimney.position.set(w / 2 - 1, wallH + 2.5, -d / 2 + 1);
1307
+ chimney.castShadow = true; group.add(chimney);
1308
+ const chimRing1 = new THREE.Mesh(new THREE.TorusGeometry(0.55, 0.06, 6, 12), metalMat);
1309
+ chimRing1.position.set(w / 2 - 1, wallH + 4.5, -d / 2 + 1);
1310
+ chimRing1.rotation.x = Math.PI / 2; group.add(chimRing1);
1311
+ const chimney2 = new THREE.Mesh(new THREE.CylinderGeometry(0.4, 0.5, 4, 8), mat(0x555555));
1312
+ chimney2.position.set(w / 2 - 2.8, wallH + 2, -d / 2 + 1);
1313
+ chimney2.castShadow = true; group.add(chimney2);
1314
+
1315
+ const dockW = 3, dockD = 1.5;
1316
+ const dockFloor = new THREE.Mesh(new THREE.BoxGeometry(dockW, 0.8, dockD), mat(0x606060));
1317
+ dockFloor.position.set(-w / 4, 0.4, d / 2 + dockD / 2); group.add(dockFloor);
1318
+ const dockRoof = new THREE.Mesh(new THREE.BoxGeometry(dockW + 0.5, 0.1, dockD + 0.5), metalMat);
1319
+ dockRoof.position.set(-w / 4, 3.5, d / 2 + dockD / 2); dockRoof.castShadow = true; group.add(dockRoof);
1320
+ for (let s of [-1, 1]) {
1321
+ const dPole = new THREE.Mesh(new THREE.CylinderGeometry(0.06, 0.06, 3, 6), metalMat);
1322
+ dPole.position.set(-w / 4 + s * dockW / 2, 2, d / 2 + dockD); group.add(dPole);
1323
+ }
1324
+ const rollDoor = new THREE.Mesh(new THREE.BoxGeometry(2.5, 2.8, 0.1), mat(0x888888, { metalness: 0.3 }));
1325
+ rollDoor.position.set(-w / 4, 2.2, d / 2 + 0.05); rollDoor.userData.isDoor = true; group.add(rollDoor);
1326
+
1327
+ const pipeMat = mat(0x999900, { metalness: 0.3 });
1328
+ const pipe1 = new THREE.Mesh(new THREE.CylinderGeometry(0.12, 0.12, wallH + 1, 8), pipeMat);
1329
+ pipe1.position.set(-w / 2 + 0.3, wallH / 2, -d / 2 + 0.3); group.add(pipe1);
1330
+ const pipe2 = new THREE.Mesh(new THREE.CylinderGeometry(0.1, 0.1, 3, 8), pipeMat);
1331
+ pipe2.rotation.z = Math.PI / 2;
1332
+ pipe2.position.set(-w / 2 + 1.8, wallH * 0.7, -d / 2 + 0.3); group.add(pipe2);
1333
+
1334
+ const label = createLabel(locData.label, group, wallH + toothH + 5);
1335
+ const badge = createOccupantBadge(group, wallH + toothH + 4);
1336
  group.userData = { id, type: 'factory', label, badge, locData };
1337
  return group;
1338
  }
 
1342
  const w = 5.5, d = 5, wallH = 5;
1343
 
1344
  const walls = new THREE.Mesh(new THREE.BoxGeometry(w, wallH, d), mat(BLDG_COLORS.cinema));
1345
+ walls.position.y = wallH / 2; walls.castShadow = true; walls.receiveShadow = true;
1346
+ addEdges(walls); group.add(walls);
 
 
 
 
 
 
 
 
 
 
 
1347
 
1348
+ const facadeH = 2;
1349
+ const facade = new THREE.Mesh(new THREE.BoxGeometry(w + 0.8, facadeH, 0.3), mat(0x6b2040));
1350
+ facade.position.set(0, wallH + facadeH / 2, d / 2); facade.castShadow = true; group.add(facade);
1351
+
1352
+ const marquee = new THREE.Mesh(new THREE.BoxGeometry(w + 1.2, 1, 1),
1353
+ mat(0xddcc44, { emissive: 0xaa8833, emissiveIntensity: 0.4 }));
1354
+ marquee.position.set(0, wallH * 0.75, d / 2 + 0.5); group.add(marquee);
1355
+ const marqueeBottom = new THREE.Mesh(new THREE.BoxGeometry(w + 1.3, 0.08, 1.1),
1356
+ mat(0xccbb33, { emissive: 0x887722, emissiveIntensity: 0.3 }));
1357
+ marqueeBottom.position.set(0, wallH * 0.75 - 0.5, d / 2 + 0.5); group.add(marqueeBottom);
1358
+
1359
+ const bulbMat = mat(0xffee88, { emissive: 0xffdd44, emissiveIntensity: 0.6 });
1360
+ for (let bx = -w / 2 - 0.3; bx <= w / 2 + 0.3; bx += 0.5) {
1361
+ const bulb = new THREE.Mesh(new THREE.SphereGeometry(0.06, 4, 4), bulbMat);
1362
+ bulb.position.set(bx, wallH * 0.75 + 0.52, d / 2 + 1.02); group.add(bulb);
1363
+ const bulb2 = new THREE.Mesh(new THREE.SphereGeometry(0.06, 4, 4), bulbMat);
1364
+ bulb2.position.set(bx, wallH * 0.75 - 0.52, d / 2 + 1.02); group.add(bulb2);
1365
+ }
1366
+
1367
+ const doors = new THREE.Mesh(new THREE.BoxGeometry(2.5, 2.5, 0.1), mat(0x4a2030));
1368
+ doors.position.set(0, 1.25, d / 2 + 0.06); doors.userData.isDoor = true; group.add(doors);
1369
+ for (let dx of [-0.5, 0.5]) {
1370
+ const dGlass = new THREE.Mesh(new THREE.BoxGeometry(0.7, 1.8, 0.06),
1371
+ mat(BLDG_COLORS.window, { transparent: true, opacity: 0.4 }));
1372
+ dGlass.position.set(dx, 1.4, d / 2 + 0.09); dGlass.userData.isWindow = true; group.add(dGlass);
1373
+ }
1374
+
1375
+ const boothW = 1, boothH = 2.2, boothD = 1;
1376
+ const booth = new THREE.Mesh(new THREE.BoxGeometry(boothW, boothH, boothD), mat(0x8b3050));
1377
+ booth.position.set(w / 2 + boothW / 2 + 0.3, boothH / 2, d / 2 - 0.5);
1378
+ booth.castShadow = true; group.add(booth);
1379
+ const boothWin = new THREE.Mesh(new THREE.BoxGeometry(0.06, 0.5, 0.6),
1380
+ mat(BLDG_COLORS.window, { transparent: true, opacity: 0.5 }));
1381
+ boothWin.position.set(w / 2 + 0.3, boothH * 0.6, d / 2 - 0.5);
1382
+ boothWin.userData.isWindow = true; group.add(boothWin);
1383
+ const boothRoof = new THREE.Mesh(new THREE.BoxGeometry(boothW + 0.2, 0.1, boothD + 0.2), mat(0x6b2040));
1384
+ boothRoof.position.set(w / 2 + boothW / 2 + 0.3, boothH + 0.05, d / 2 - 0.5); group.add(boothRoof);
1385
+
1386
+ const label = createLabel(locData.label, group, wallH + facadeH + 2);
1387
+ const badge = createOccupantBadge(group, wallH + facadeH + 1);
1388
  group.userData = { id, type: 'cinema', label, badge, locData };
1389
  return group;
1390
  }
1391
 
1392
  function createPark(id, locData) {
1393
  const group = new THREE.Group();
1394
+ const benchMat = mat(0x8b6040);
1395
+
1396
+ const parkGround = new THREE.Mesh(new THREE.BoxGeometry(8, 0.1, 6), mat(BLDG_COLORS.park));
1397
+ parkGround.position.y = 0.05; parkGround.receiveShadow = true; group.add(parkGround);
1398
+
1399
+ const pathMat = mat(0xccbbaa);
1400
+ const mainPath = new THREE.Mesh(new THREE.BoxGeometry(1, 0.12, 6), pathMat);
1401
+ mainPath.position.y = 0.06; group.add(mainPath);
1402
+ const crossPath = new THREE.Mesh(new THREE.BoxGeometry(8, 0.12, 0.8), pathMat);
1403
+ crossPath.position.y = 0.06; group.add(crossPath);
1404
+
1405
+ const gazR = 1.2, gazH = 2.8, gazPoles = 6;
1406
+ const gazebo = new THREE.Group();
1407
+ const gazFloor = new THREE.Mesh(new THREE.CylinderGeometry(gazR + 0.1, gazR + 0.1, 0.12, gazPoles), mat(0xc0b090));
1408
+ gazFloor.position.y = 0.2; gazebo.add(gazFloor);
1409
+ for (let i = 0; i < gazPoles; i++) {
1410
+ const ang = (i / gazPoles) * Math.PI * 2;
1411
+ const pole = new THREE.Mesh(new THREE.CylinderGeometry(0.05, 0.06, gazH, 6), mat(0xf0f0f0));
1412
+ pole.position.set(Math.cos(ang) * gazR, gazH / 2 + 0.2, Math.sin(ang) * gazR);
1413
+ gazebo.add(pole);
1414
+ }
1415
+ const gazRoof = new THREE.Mesh(new THREE.ConeGeometry(gazR + 0.4, 1.2, gazPoles), mat(0x885540));
1416
+ gazRoof.position.y = gazH + 0.8; gazRoof.castShadow = true; gazebo.add(gazRoof);
1417
+ const gazRail = new THREE.Mesh(new THREE.TorusGeometry(gazR, 0.04, 4, gazPoles), mat(0xf0f0f0));
1418
+ gazRail.rotation.x = Math.PI / 2; gazRail.position.y = 0.7; gazebo.add(gazRail);
1419
+ gazebo.position.set(-2.5, 0, 0); group.add(gazebo);
1420
+
1421
+ const flowerColors = [0xff6688, 0xffaa44, 0xcc66cc, 0xff4444, 0xffdd44];
1422
+ for (let fb = 0; fb < 2; fb++) {
1423
+ const fbz = fb === 0 ? -2 : 2;
1424
+ const bed = new THREE.Mesh(new THREE.BoxGeometry(2, 0.15, 0.8), mat(0x3a6a2a));
1425
+ bed.position.set(2.5, 0.12, fbz); group.add(bed);
1426
+ const border = new THREE.Mesh(new THREE.BoxGeometry(2.1, 0.2, 0.9), mat(0x8b7050));
1427
+ border.position.set(2.5, 0.06, fbz); group.add(border);
1428
+ for (let fx = 0; fx < 6; fx++) {
1429
+ const fc = flowerColors[(fb * 6 + fx) % flowerColors.length];
1430
+ const flower = new THREE.Mesh(new THREE.SphereGeometry(0.08, 4, 4), mat(fc));
1431
+ flower.position.set(2.5 + (fx - 2.5) * 0.35, 0.28, fbz + (Math.random() - 0.5) * 0.4);
1432
+ group.add(flower);
1433
+ const stem = new THREE.Mesh(new THREE.CylinderGeometry(0.015, 0.015, 0.12, 4), mat(0x2a6a2a));
1434
+ stem.position.set(flower.position.x, 0.2, flower.position.z); group.add(stem);
1435
+ }
1436
+ }
1437
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1438
  for (let i = 0; i < 5; i++) {
1439
  const tx = (Math.random() - 0.5) * 6;
1440
  const tz = (Math.random() - 0.5) * 4;
1441
+ if (Math.abs(tx) > 1 && Math.abs(tx + 2.5) > 1.5) {
1442
  const t = createTree(0, 0, 0.35 + Math.random() * 0.25);
1443
  t.position.set(tx, 0, tz);
1444
+ scene.remove(t); group.add(t);
 
1445
  }
1446
  }
1447
 
1448
+ for (let bPos of [[1.5, 1.2], [1.5, -1.2]]) {
1449
+ const bGroup = new THREE.Group();
1450
+ const seat = new THREE.Mesh(new THREE.BoxGeometry(1.4, 0.08, 0.45), benchMat);
1451
+ seat.position.y = 0.45; bGroup.add(seat);
1452
+ const back = new THREE.Mesh(new THREE.BoxGeometry(1.4, 0.5, 0.06), benchMat);
1453
+ back.position.set(0, 0.65, -0.2); bGroup.add(back);
1454
+ for (let lx of [-0.6, 0.6]) {
1455
+ const leg = new THREE.Mesh(new THREE.BoxGeometry(0.06, 0.45, 0.35), mat(0x444444));
1456
+ leg.position.set(lx, 0.22, 0); bGroup.add(leg);
1457
+ }
1458
+ bGroup.position.set(bPos[0], 0, bPos[1]);
1459
+ group.add(bGroup);
1460
+ }
1461
 
1462
  const label = createLabel(locData.label, group, 5);
1463
  const badge = createOccupantBadge(group, 4);
 
1504
  function createPolice(id, locData) {
1505
  const group = new THREE.Group();
1506
  const w = 5, d = 4, wallH = 3.5;
1507
+ const policeMat = mat(BLDG_COLORS.police);
1508
+
1509
+ const walls = new THREE.Mesh(new THREE.BoxGeometry(w, wallH, d), policeMat);
1510
  walls.position.y = wallH / 2; walls.castShadow = true; walls.receiveShadow = true;
1511
  addEdges(walls); group.add(walls);
1512
+
1513
  const roof = new THREE.Mesh(new THREE.BoxGeometry(w + 0.4, 0.25, d + 0.4), mat(0x2a4a6a));
1514
  roof.position.y = wallH; group.add(roof);
1515
+ const parapet = new THREE.Mesh(new THREE.BoxGeometry(w + 0.5, 0.4, 0.15), mat(0x2a4a6a));
1516
+ parapet.position.set(0, wallH + 0.2, d / 2 + 0.1); group.add(parapet);
1517
+
1518
+ const stripe = new THREE.Mesh(new THREE.BoxGeometry(w + 0.1, 0.5, 0.1), mat(0x4488cc));
1519
+ stripe.position.set(0, wallH * 0.88, d / 2 + 0.06); group.add(stripe);
1520
+ const lowerStripe = new THREE.Mesh(new THREE.BoxGeometry(w + 0.1, 0.15, 0.1), mat(0x4488cc));
1521
+ lowerStripe.position.set(0, 0.5, d / 2 + 0.06); group.add(lowerStripe);
1522
+
1523
+ const entW = 1.8;
1524
+ const canopy = new THREE.Mesh(new THREE.BoxGeometry(entW + 1, 0.1, 1.2), mat(0x2a4a6a));
1525
+ canopy.position.set(0, 2.5, d / 2 + 0.6); canopy.castShadow = true; group.add(canopy);
1526
+ const door = new THREE.Mesh(new THREE.BoxGeometry(entW, 2.2, 0.1), mat(0x2a3a5a));
1527
+ door.position.set(0, 1.1, d / 2 + 0.06); door.userData.isDoor = true; group.add(door);
1528
+ const dGlass = new THREE.Mesh(new THREE.BoxGeometry(entW * 0.7, 1.5, 0.06),
1529
+ mat(BLDG_COLORS.window, { transparent: true, opacity: 0.4 }));
1530
+ dGlass.position.set(0, 1.3, d / 2 + 0.09); dGlass.userData.isWindow = true; group.add(dGlass);
1531
+
1532
+ for (let sx of [-1.8, 1.8]) {
1533
+ const win = new THREE.Mesh(new THREE.BoxGeometry(0.7, 0.8, 0.1), mat(BLDG_COLORS.window, { roughness: 0.3 }));
1534
  win.position.set(sx, wallH * 0.6, d / 2 + 0.06); win.userData.isWindow = true; group.add(win);
1535
+ const bars = new THREE.Group();
1536
+ for (let bx = -0.25; bx <= 0.25; bx += 0.12) {
1537
+ const bar = new THREE.Mesh(new THREE.BoxGeometry(0.02, 0.8, 0.02), mat(0x444444));
1538
+ bar.position.set(bx, 0, 0.06); bars.add(bar);
1539
+ }
1540
+ bars.position.set(sx, wallH * 0.6, d / 2 + 0.06); group.add(bars);
1541
  }
1542
+
1543
+ const badgeR = 0.5;
1544
+ const shield = new THREE.Mesh(new THREE.CircleGeometry(badgeR, 6),
1545
+ mat(0xccaa44, { metalness: 0.5, side: THREE.DoubleSide }));
1546
+ shield.position.set(0, wallH * 0.65, d / 2 + 0.12); group.add(shield);
1547
+ const star = new THREE.Mesh(new THREE.CircleGeometry(badgeR * 0.5, 5),
1548
+ mat(0x4488cc, { side: THREE.DoubleSide }));
1549
+ star.position.set(0, wallH * 0.65, d / 2 + 0.13); group.add(star);
1550
+
1551
  const label = createLabel(locData.label, group, wallH + 2);
1552
  const badge = createOccupantBadge(group, wallH + 1);
1553
  group.userData = { id, type: 'police', label, badge, locData };
 
1577
 
1578
  function createMuseum(id, locData) {
1579
  const group = new THREE.Group();
1580
+ const w = 6, d = 5, wallH = 4.5;
1581
+ const museumMat = mat(BLDG_COLORS.museum);
1582
+ const colMat = mat(0xd8c898);
1583
+
1584
+ const walls = new THREE.Mesh(new THREE.BoxGeometry(w, wallH, d), museumMat);
1585
  walls.position.y = wallH / 2; walls.castShadow = true; walls.receiveShadow = true;
1586
  addEdges(walls); group.add(walls);
1587
+
1588
+ const porticoD = 1.5, porticoH = wallH + 0.5;
1589
+ const porticoRoof = new THREE.Mesh(new THREE.BoxGeometry(w + 0.8, 0.25, porticoD + 0.3), museumMat);
1590
+ porticoRoof.position.set(0, porticoH, d / 2 + porticoD / 2); porticoRoof.castShadow = true; group.add(porticoRoof);
1591
+
1592
+ const pedGeo = new THREE.BufferGeometry();
1593
+ const pw = w / 2 + 0.4, ph = 1.8;
1594
+ const pv = new Float32Array([
1595
+ -pw, porticoH + 0.25, d / 2, pw, porticoH + 0.25, d / 2, 0, porticoH + 0.25 + ph, d / 2,
1596
+ -pw, porticoH + 0.25, d / 2 + porticoD + 0.3, pw, porticoH + 0.25, d / 2 + porticoD + 0.3, 0, porticoH + 0.25 + ph, d / 2 + porticoD + 0.3,
1597
+ ]);
1598
+ pedGeo.setAttribute('position', new THREE.BufferAttribute(pv, 3));
1599
+ pedGeo.setIndex([0,1,2, 3,5,4, 0,2,5,0,5,3, 1,4,5,1,5,2]);
1600
+ pedGeo.computeVertexNormals();
1601
+ const pediment = new THREE.Mesh(pedGeo, museumMat);
1602
+ pediment.castShadow = true; group.add(pediment);
1603
+
1604
+ const cols = 5;
1605
+ for (let i = 0; i < cols; i++) {
1606
+ const cx = (i - (cols - 1) / 2) * (w / (cols - 1));
1607
+ const col = new THREE.Group();
1608
+ const colBase = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.3, 0.5), colMat);
1609
+ colBase.position.y = 0.5; col.add(colBase);
1610
+ const shaft = new THREE.Mesh(new THREE.CylinderGeometry(0.18, 0.22, porticoH - 0.8, 12), colMat);
1611
+ shaft.position.y = porticoH / 2 + 0.2; col.add(shaft);
1612
+ const capital = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.25, 0.5), colMat);
1613
+ capital.position.y = porticoH - 0.15; col.add(capital);
1614
+ col.position.set(cx, 0, d / 2 + porticoD - 0.2);
1615
+ col.castShadow = true; group.add(col);
1616
+ }
1617
+
1618
+ for (let s = 0; s < 3; s++) {
1619
+ const stepW = w + 1.5 - s * 0.3, stepD = 0.5;
1620
+ const step = new THREE.Mesh(new THREE.BoxGeometry(stepW, 0.2, stepD), mat(0xc8c0a0));
1621
+ step.position.set(0, 0.1 + s * 0.2, d / 2 + porticoD + 0.5 - s * 0.3);
1622
+ step.receiveShadow = true; group.add(step);
1623
+ }
1624
+
1625
+ const door = new THREE.Mesh(new THREE.BoxGeometry(1.5, 2.5, 0.1), mat(BLDG_COLORS.door));
1626
+ door.position.set(0, 1.55, d / 2 + 0.05); door.userData.isDoor = true; group.add(door);
1627
+
1628
+ const winMat = mat(isNight ? BLDG_COLORS.windowLit : BLDG_COLORS.window,
1629
+ isNight ? { emissive: BLDG_COLORS.windowLit, emissiveIntensity: 0.3 } : {});
1630
+ for (let side of [-1, 1]) {
1631
+ for (let sz = -1; sz <= 1; sz++) {
1632
+ const sWin = new THREE.Mesh(new THREE.BoxGeometry(0.1, 1.2, 0.6), winMat);
1633
+ sWin.position.set(side * (w / 2 + 0.05), wallH * 0.55, sz * 1.3);
1634
+ sWin.userData.isWindow = true; group.add(sWin);
1635
+ }
1636
+ }
1637
+
1638
+ const roofCap = new THREE.Mesh(new THREE.BoxGeometry(w + 0.3, 0.2, d + 0.3), mat(0xc0b890));
1639
+ roofCap.position.y = wallH; group.add(roofCap);
1640
+ const domeR = 1.2;
1641
+ const dome = new THREE.Mesh(new THREE.SphereGeometry(domeR, 16, 10, 0, Math.PI * 2, 0, Math.PI / 2),
1642
+ mat(0xa8b8a0, { metalness: 0.2 }));
1643
+ dome.position.y = wallH + 0.2; dome.castShadow = true; group.add(dome);
1644
+
1645
+ const label = createLabel(locData.label, group, porticoH + ph + 2);
1646
+ const badge = createOccupantBadge(group, porticoH + ph + 1);
1647
  group.userData = { id, type: 'museum', label, badge, locData };
1648
  return group;
1649
  }
 
1653
  const h = hash(id);
1654
  const w = 8, d = 6, wallH = 5;
1655
  const clr = Array.isArray(BLDG_COLORS.mall) ? BLDG_COLORS.mall[h % BLDG_COLORS.mall.length] : BLDG_COLORS.mall;
1656
+ const frameMat = mat(0x666666, { metalness: 0.3 });
1657
+
1658
  const walls = new THREE.Mesh(new THREE.BoxGeometry(w, wallH, d), mat(clr));
1659
  walls.position.y = wallH / 2; walls.castShadow = true; walls.receiveShadow = true;
1660
  addEdges(walls); group.add(walls);
1661
+
1662
+ const roof = new THREE.Mesh(new THREE.BoxGeometry(w + 0.5, 0.3, d + 0.5), frameMat);
1663
  roof.position.y = wallH; group.add(roof);
1664
+
1665
+ const atriumW = 3, atriumH = 2.5, atriumD = 2;
1666
+ const atriumGlass = mat(0x88ccee, { transparent: true, opacity: 0.35, roughness: 0.1, metalness: 0.3 });
1667
+ const atrium = new THREE.Mesh(new THREE.BoxGeometry(atriumW, atriumH, atriumD), atriumGlass);
1668
+ atrium.position.set(0, wallH + atriumH / 2, 0);
1669
+ atrium.userData.isWindow = true; group.add(atrium);
1670
+ const atriumFrame = new THREE.Mesh(new THREE.BoxGeometry(atriumW + 0.1, 0.08, atriumD + 0.1), frameMat);
1671
+ atriumFrame.position.set(0, wallH + atriumH, 0); group.add(atriumFrame);
1672
+ for (let mx of [-1, 0, 1]) {
1673
+ const mull = new THREE.Mesh(new THREE.BoxGeometry(0.05, atriumH, 0.05), frameMat);
1674
+ mull.position.set(mx * (atriumW / 3), wallH + atriumH / 2, atriumD / 2 + 0.02); group.add(mull);
1675
+ const mullB = mull.clone();
1676
+ mullB.position.z = -atriumD / 2 - 0.02; group.add(mullB);
1677
  }
1678
+
1679
+ const glassMat = mat(BLDG_COLORS.window, { roughness: 0.15, metalness: 0.2, transparent: true, opacity: 0.6 });
1680
+ const glass = new THREE.Mesh(new THREE.BoxGeometry(w * 0.85, wallH * 0.55, 0.08), glassMat);
1681
+ glass.position.set(0, wallH * 0.4, d / 2 + 0.05); glass.userData.isWindow = true; group.add(glass);
1682
+ for (let mx = -3; mx <= 3; mx++) {
1683
+ const mull = new THREE.Mesh(new THREE.BoxGeometry(0.05, wallH * 0.6, 0.05), frameMat);
1684
+ mull.position.set(mx * (w * 0.12), wallH * 0.4, d / 2 + 0.06); group.add(mull);
1685
+ }
1686
+
1687
+ const entrW = 3, entrH = 3;
1688
+ const canopy = new THREE.Mesh(new THREE.BoxGeometry(entrW + 1, 0.1, 2), frameMat);
1689
+ canopy.position.set(0, entrH, d / 2 + 1); canopy.castShadow = true; group.add(canopy);
1690
+ const doors = new THREE.Mesh(new THREE.BoxGeometry(entrW, entrH, 0.1), mat(0x3a3a4a));
1691
+ doors.position.set(0, entrH / 2, d / 2 + 0.05); doors.userData.isDoor = true; group.add(doors);
1692
+ for (let dx of [-0.8, 0, 0.8]) {
1693
+ const dGlass = new THREE.Mesh(new THREE.BoxGeometry(0.6, entrH * 0.7, 0.06), glassMat);
1694
+ dGlass.position.set(dx, entrH * 0.45, d / 2 + 0.08);
1695
+ dGlass.userData.isWindow = true; group.add(dGlass);
1696
+ }
1697
+
1698
+ for (let sx of [-2.8, 2.8]) {
1699
+ const awning = new THREE.Mesh(new THREE.BoxGeometry(1.8, 0.08, 1), mat(0xcc8844, { roughness: 0.6 }));
1700
+ awning.position.set(sx, wallH * 0.65, d / 2 + 0.5); awning.castShadow = true; group.add(awning);
1701
+ }
1702
+
1703
+ for (let side of [-1, 1]) {
1704
+ for (let sz = -1; sz <= 1; sz++) {
1705
+ const sWin = new THREE.Mesh(new THREE.BoxGeometry(0.08, 1.2, 0.8), glassMat);
1706
+ sWin.position.set(side * (w / 2 + 0.04), wallH * 0.5, sz * 1.5);
1707
+ sWin.userData.isWindow = true; group.add(sWin);
1708
+ }
1709
+ }
1710
+
1711
+ const label = createLabel(locData.label, group, wallH + atriumH + 2);
1712
+ const badge = createOccupantBadge(group, wallH + atriumH + 1);
1713
  group.userData = { id, type: 'mall', label, badge, locData };
1714
  return group;
1715
  }
 
1768
 
1769
  function createSquare(id, locData) {
1770
  const group = new THREE.Group();
1771
+ const stoneMat = mat(0x999988);
1772
+
1773
+ const plaza = new THREE.Mesh(new THREE.BoxGeometry(7, 0.1, 7), mat(BLDG_COLORS.square));
1774
+ plaza.position.y = 0.05; plaza.receiveShadow = true; group.add(plaza);
1775
+ for (let ring = 0; ring < 2; ring++) {
1776
+ const r = 2.5 + ring * 1.2;
1777
+ const border = new THREE.Mesh(new THREE.TorusGeometry(r, 0.06, 4, 24), mat(0x888878));
1778
+ border.rotation.x = Math.PI / 2; border.position.y = 0.12; group.add(border);
1779
+ }
1780
+
1781
+ const basinOuter = new THREE.Mesh(new THREE.CylinderGeometry(1.5, 1.7, 0.6, 16), stoneMat);
1782
+ basinOuter.position.y = 0.3; group.add(basinOuter);
1783
+ const basinInner = new THREE.Mesh(new THREE.CylinderGeometry(1.3, 1.3, 0.3, 16), mat(0x5588aa, { roughness: 0.2 }));
1784
+ basinInner.position.y = 0.45; group.add(basinInner);
1785
+ const waterTop = new THREE.Mesh(new THREE.CylinderGeometry(1.25, 1.25, 0.05, 16),
1786
+ mat(PALETTE.water, { roughness: 0.15, metalness: 0.15 }));
1787
+ waterTop.position.y = 0.55; group.add(waterTop);
1788
+
1789
+ const pedestal = new THREE.Mesh(new THREE.CylinderGeometry(0.25, 0.35, 1.5, 8), stoneMat);
1790
+ pedestal.position.y = 1.3; group.add(pedestal);
1791
+ const upperBasin = new THREE.Mesh(new THREE.CylinderGeometry(0.6, 0.5, 0.25, 12), stoneMat);
1792
+ upperBasin.position.y = 2.15; group.add(upperBasin);
1793
+ const upperWater = new THREE.Mesh(new THREE.CylinderGeometry(0.5, 0.5, 0.04, 12),
1794
+ mat(PALETTE.water, { roughness: 0.15, metalness: 0.15 }));
1795
+ upperWater.position.y = 2.3; group.add(upperWater);
1796
+ const finial = new THREE.Mesh(new THREE.SphereGeometry(0.15, 8, 6), stoneMat);
1797
+ finial.position.y = 2.55; group.add(finial);
1798
+
1799
+ const benchMat = mat(0x8b6040);
1800
+ for (let i = 0; i < 4; i++) {
1801
+ const ang = (i / 4) * Math.PI * 2 + Math.PI / 4;
1802
+ const br = 2.8;
1803
+ const bGroup = new THREE.Group();
1804
+ const seat = new THREE.Mesh(new THREE.BoxGeometry(1.4, 0.08, 0.45), benchMat);
1805
+ seat.position.y = 0.45; bGroup.add(seat);
1806
+ const back = new THREE.Mesh(new THREE.BoxGeometry(1.4, 0.5, 0.06), benchMat);
1807
+ back.position.set(0, 0.65, -0.2); bGroup.add(back);
1808
+ for (let lx of [-0.6, 0.6]) {
1809
+ const leg = new THREE.Mesh(new THREE.BoxGeometry(0.06, 0.45, 0.4), mat(0x444444));
1810
+ leg.position.set(lx, 0.22, 0); bGroup.add(leg);
1811
+ }
1812
+ bGroup.position.set(Math.cos(ang) * br, 0, Math.sin(ang) * br);
1813
+ bGroup.rotation.y = -ang + Math.PI / 2;
1814
+ group.add(bGroup);
1815
+ }
1816
 
1817
+ for (let i = 0; i < 4; i++) {
1818
+ const ang = (i / 4) * Math.PI * 2;
1819
+ const lamp = new THREE.Group();
1820
+ const lPole = new THREE.Mesh(new THREE.CylinderGeometry(0.04, 0.05, 2.5, 6), mat(0x444444));
1821
+ lPole.position.y = 1.25; lamp.add(lPole);
1822
+ const lTop = new THREE.Mesh(new THREE.SphereGeometry(0.1, 6, 4),
1823
+ mat(0xffeedd, { emissive: 0xffd860, emissiveIntensity: isNight ? 0.6 : 0 }));
1824
+ lTop.position.y = 2.5; lamp.add(lTop);
1825
+ lamp.position.set(Math.cos(ang) * 2, 0, Math.sin(ang) * 2);
1826
+ group.add(lamp);
1827
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1828
 
1829
+ const label = createLabel(locData.label, group, 5);
1830
+ const badge = createOccupantBadge(group, 4);
1831
  group.userData = { id, type: 'square', label, badge, locData };
1832
  return group;
1833
  }