RayMelius Claude Opus 4.6 commited on
Commit
67f71f5
·
1 Parent(s): e801152

Add mountain horizon, 2.5D perspective buildings and smaller agent figures

Browse files

- Mountains with snow caps, near/far layers, dawn/night color adaptation
- All buildings now 2.5D with right-side face and top face perspective
- Agent figures 30% smaller with isometric body, varied skin tones, eyes
- Town square fountain with 3D column and water spray
- Park with walking paths, raised edges, water shimmer
- Factory chimney, church steeple all with perspective depth

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Files changed (1) hide show
  1. web/index.html +392 -218
web/index.html CHANGED
@@ -566,6 +566,70 @@ function drawSky(W, H) {
566
  else if (s.sun === 'high') drawSun(W*0.78, hLine*0.25, 16);
567
  else if (s.sun === 'mid') drawSun(W*0.80, hLine*0.45, 16);
568
  else if (s.sun === 'low') drawSun(W*0.82, hLine*0.7, 16);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
569
  }
570
 
571
  function drawSun(x, y, r) {
@@ -820,137 +884,197 @@ function drawBuilding(id, pos, W, H) {
820
  }
821
  }
822
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
823
  function drawHouse(x, y, dk, id) {
824
  const w = 36, h = 24;
825
- // Vary house colors by position for variety
826
  const hues = [
827
- {wall:'#c8a882', roof:'#8b4513', door:'#6b3410'},
828
- {wall:'#a0b8a0', roof:'#4a6a4a', door:'#3a4a3a'},
829
- {wall:'#b8a0a0', roof:'#7a3a3a', door:'#5a2a2a'},
830
- {wall:'#a0a8c0', roof:'#4a5a7a', door:'#3a4a6a'},
831
- {wall:'#c8b878', roof:'#8a7a40', door:'#6a5a30'},
832
  ];
833
- const idx = Object.keys(LOCATION_POSITIONS).indexOf(id) % hues.length;
 
834
  const c = hues[idx];
835
 
836
- // Wall
837
- ctx.fillStyle = dk ? dim(c.wall, 0.4) : c.wall;
838
- ctx.fillRect(x-w/2, y-h/2, w, h);
839
- // Roof
 
840
  ctx.fillStyle = dk ? dim(c.roof, 0.4) : c.roof;
841
  ctx.beginPath();
842
- ctx.moveTo(x-w/2-4, y-h/2);
843
- ctx.lineTo(x, y-h/2-14);
844
- ctx.lineTo(x+w/2+4, y-h/2);
845
- ctx.closePath();
846
- ctx.fill();
 
 
 
 
 
 
 
 
847
  // Door
848
  ctx.fillStyle = dk ? dim(c.door, 0.4) : c.door;
849
- ctx.fillRect(x-3, y+h/2-10, 6, 10);
850
- // Doorknob
851
  ctx.fillStyle = dk ? '#aa9060' : '#d4b070';
852
- ctx.beginPath(); ctx.arc(x+2, y+h/2-4, 1, 0, 6.28); ctx.fill();
853
- // Windows (lit at night)
 
854
  const wc = dk ? 'rgba(255,210,100,0.75)' : 'rgba(180,220,255,0.55)';
855
  ctx.fillStyle = wc;
856
- ctx.fillRect(x-w/2+4, y-h/2+4, 8, 6);
857
- ctx.fillRect(x+w/2-12, y-h/2+4, 8, 6);
858
- // Window frames
859
  ctx.strokeStyle = dk ? 'rgba(100,80,50,0.4)' : 'rgba(80,70,60,0.3)';
860
  ctx.lineWidth = 0.5;
861
- ctx.strokeRect(x-w/2+4, y-h/2+4, 8, 6);
862
- ctx.strokeRect(x+w/2-12, y-h/2+4, 8, 6);
 
863
  // Chimney
864
  ctx.fillStyle = dk ? dim(c.roof, 0.35) : dim(c.roof, 0.8);
865
- ctx.fillRect(x+8, y-h/2-12, 5, 8);
866
- // Small garden/fence
867
- ctx.strokeStyle = dk ? 'rgba(80,60,40,0.3)' : 'rgba(120,90,60,0.4)';
868
- ctx.lineWidth = 1;
869
- ctx.strokeRect(x-w/2-2, y+h/2, w+4, 4);
870
  }
871
 
872
  function drawShop(x, y, dk) {
873
  const w = 48, h = 32;
874
- ctx.fillStyle = dk ? '#3a3530' : '#d4c4a8';
875
- ctx.fillRect(x-w/2, y-h/2, w, h);
876
- // Awning
 
877
  const awningColors = ['#c44', '#4a8', '#48a', '#a84'];
878
  const ci = Math.floor(x*7 + y*3) % awningColors.length;
879
- ctx.fillStyle = dk ? dim(awningColors[ci], 0.35) : awningColors[ci];
 
880
  ctx.beginPath();
881
- ctx.moveTo(x-w/2-3, y-h/2);
882
- ctx.lineTo(x-w/2-6, y-h/2+12);
883
- ctx.lineTo(x+w/2+6, y-h/2+12);
884
- ctx.lineTo(x+w/2+3, y-h/2);
885
- ctx.closePath();
886
- ctx.fill();
 
 
 
 
 
 
 
 
887
  // Awning stripes
888
  ctx.strokeStyle = dk ? 'rgba(255,255,255,0.08)' : 'rgba(255,255,255,0.25)';
889
  ctx.lineWidth = 1;
890
  for (let i = 0; i < 5; i++) {
891
  const sx = x-w/2 + i*(w/4);
892
- ctx.beginPath(); ctx.moveTo(sx, y-h/2); ctx.lineTo(sx-1.5, y-h/2+12); ctx.stroke();
893
  }
894
  // Door
895
  ctx.fillStyle = dk ? '#2a2520' : '#5a4a3a';
896
- ctx.fillRect(x-4, y+h/2-12, 8, 12);
897
  // Windows
898
  const wc = dk ? 'rgba(255,210,100,0.65)' : 'rgba(200,230,255,0.55)';
899
  ctx.fillStyle = wc;
900
- ctx.fillRect(x-w/2+4, y-h/2+14, w/2-10, 10);
901
- ctx.fillRect(x+5, y-h/2+14, w/2-10, 10);
902
  }
903
 
904
  function drawOffice(x, y, dk) {
905
  const w = 50, h = 40;
906
- ctx.fillStyle = dk ? '#2a2a35' : '#8090a8';
907
- ctx.fillRect(x-w/2, y-h/2, w, h);
908
- // Flat roof edge
909
- ctx.fillStyle = dk ? '#1a1a25' : '#607080';
910
- ctx.fillRect(x-w/2-2, y-h/2-3, w+4, 5);
911
  // Door
912
  ctx.fillStyle = dk ? '#1a1a25' : '#4a5a6a';
913
- ctx.fillRect(x-4, y+h/2-12, 8, 12);
914
  // Windows grid
915
  const wc = dk ? 'rgba(255,210,100,0.6)' : 'rgba(180,220,255,0.5)';
916
  ctx.fillStyle = wc;
 
 
 
 
 
 
917
  for (let r = 0; r < 3; r++) {
918
- for (let c = 0; c < 3; c++) {
919
- ctx.fillRect(x-w/2+6+c*15, y-h/2+5+r*10, 9, 6);
920
- }
 
 
 
 
921
  }
922
  }
923
 
924
  function drawPublicBuilding(x, y, dk) {
925
  const w = 46, h = 34;
926
- ctx.fillStyle = dk ? '#2a3525' : '#7a9a6a';
927
- ctx.fillRect(x-w/2, y-h/2, w, h);
928
- ctx.fillStyle = dk ? '#1a2a18' : '#5a7a4a';
929
- ctx.fillRect(x-w/2-2, y-h/2-3, w+4, 5);
930
  ctx.fillStyle = dk ? '#1a2018' : '#4a6a3a';
931
- ctx.fillRect(x-4, y+h/2-12, 8, 12);
932
  const wc = dk ? 'rgba(255,210,100,0.55)' : 'rgba(200,240,200,0.5)';
933
  ctx.fillStyle = wc;
934
- for (let c = 0; c < 4; c++) ctx.fillRect(x-w/2+4+c*11, y-h/2+6, 8, 8);
935
- for (let c = 0; c < 4; c++) ctx.fillRect(x-w/2+4+c*11, y-h/2+18, 8, 8);
936
  }
937
 
938
  function drawPark(x, y, dk) {
939
- // Large green area
940
  ctx.fillStyle = dk ? '#1a3018' : '#4a9040';
941
  ctx.beginPath(); ctx.ellipse(x, y, 55, 25, 0, 0, 6.28); ctx.fill();
 
 
 
942
  ctx.strokeStyle = dk ? '#2a4a25' : '#6ab850';
943
- ctx.lineWidth = 2; ctx.stroke();
 
 
 
 
 
 
944
  // Pond
945
  ctx.fillStyle = dk ? '#1a2a3a' : '#5a9ac0';
946
- ctx.beginPath(); ctx.ellipse(x+15, y+5, 12, 6, 0.2, 0, 6.28); ctx.fill();
947
  ctx.strokeStyle = dk ? '#2a3a4a' : '#80b0d0'; ctx.lineWidth = 1; ctx.stroke();
948
- // Benches
 
 
 
 
 
949
  ctx.fillStyle = dk ? '#3a2a15' : '#7a5a30';
950
- ctx.fillRect(x-20, y+8, 14, 3);
951
- ctx.fillRect(x-18, y+11, 2, 3);
952
- ctx.fillRect(x-8, y+11, 2, 3);
953
- // Trees in park (extra)
954
  for (let i = -1; i <= 1; i++) {
955
  const tx = x + i*22;
956
  ctx.fillStyle = dk ? '#3a2a15' : '#6b4226'; ctx.fillRect(tx-2, y-4, 4, 10);
@@ -965,226 +1089,244 @@ function drawPark(x, y, dk) {
965
 
966
  function drawTower(x, y, dk) {
967
  const w = 36, h = 56;
968
- // Glass facade
969
- ctx.fillStyle = dk ? '#1e2038' : '#6880a0';
970
- ctx.fillRect(x-w/2, y-h/2, w, h);
971
- // Top cap
972
- ctx.fillStyle = dk ? '#141828' : '#506880';
973
- ctx.fillRect(x-w/2-2, y-h/2-3, w+4, 5);
974
  // Antenna
975
- ctx.fillStyle = '#888'; ctx.fillRect(x-1, y-h/2-12, 2, 10);
976
- ctx.fillStyle = '#e94560'; ctx.beginPath(); ctx.arc(x, y-h/2-12, 2, 0, 6.28); ctx.fill();
977
  // Door
978
  ctx.fillStyle = dk ? '#0a0e18' : '#384858';
979
- ctx.fillRect(x-5, y+h/2-14, 10, 14);
980
- // Window grid (5 rows x 4 cols)
981
  const wc = dk ? 'rgba(255,210,100,0.55)' : 'rgba(180,220,255,0.5)';
982
  ctx.fillStyle = wc;
983
  for (let r = 0; r < 5; r++)
984
  for (let c = 0; c < 4; c++)
985
- ctx.fillRect(x-w/2+3+c*8.5, y-h/2+5+r*10, 6, 6);
 
 
 
 
 
 
 
 
 
 
 
 
986
  }
987
 
988
  function drawFactory(x, y, dk) {
989
  const w = 56, h = 34;
990
- // Main building
991
- ctx.fillStyle = dk ? '#2a2520' : '#8a7a6a';
992
- ctx.fillRect(x-w/2, y-h/2, w, h);
993
  // Saw-tooth roof
994
  ctx.fillStyle = dk ? '#1a1815' : '#6a5a4a';
995
  for (let i = 0; i < 3; i++) {
996
  const rx = x - w/2 + i*(w/3);
997
  ctx.beginPath();
998
- ctx.moveTo(rx, y-h/2);
999
- ctx.lineTo(rx + w/6, y-h/2-10);
1000
- ctx.lineTo(rx + w/3, y-h/2);
1001
  ctx.closePath(); ctx.fill();
1002
  }
1003
  // Chimney with smoke
1004
  ctx.fillStyle = dk ? '#3a3020' : '#706050';
1005
- ctx.fillRect(x+w/2-10, y-h/2-18, 8, 14);
 
 
 
 
 
 
1006
  // Smoke
1007
  ctx.fillStyle = `rgba(180,180,180,${dk?0.15:0.25})`;
1008
  for (let i = 0; i < 3; i++) {
1009
- const sy = y-h/2-22-i*8 + Math.sin(animFrame*0.03+i)*3;
1010
  ctx.beginPath(); ctx.arc(x+w/2-6+i*3, sy, 4+i*2, 0, 6.28); ctx.fill();
1011
  }
1012
  // Loading door
1013
  ctx.fillStyle = dk ? '#1a1815' : '#5a4a3a';
1014
- ctx.fillRect(x-8, y+h/2-16, 16, 16);
1015
  // Windows
1016
  const wc = dk ? 'rgba(255,180,80,0.5)' : 'rgba(200,220,240,0.4)';
1017
  ctx.fillStyle = wc;
1018
- for (let c = 0; c < 4; c++) ctx.fillRect(x-w/2+4+c*14, y-h/2+6, 10, 8);
1019
  }
1020
 
1021
  function drawSchool(x, y, dk) {
1022
  const w = 52, h = 36;
1023
- // Main building
1024
- ctx.fillStyle = dk ? '#2a2225' : '#c4a088';
1025
- ctx.fillRect(x-w/2, y-h/2, w, h);
1026
- // Roof
1027
- ctx.fillStyle = dk ? '#1a1518' : '#8a5a3a';
1028
- ctx.fillRect(x-w/2-2, y-h/2-3, w+4, 5);
1029
  // Flagpole
1030
- ctx.fillStyle = '#888'; ctx.fillRect(x+w/2-6, y-h/2-20, 2, 18);
1031
- // Flag
1032
  ctx.fillStyle = '#e94560';
1033
  ctx.beginPath();
1034
- ctx.moveTo(x+w/2-4, y-h/2-20);
1035
- ctx.lineTo(x+w/2+8, y-h/2-16);
1036
- ctx.lineTo(x+w/2-4, y-h/2-12);
1037
  ctx.closePath(); ctx.fill();
1038
  // Door
1039
  ctx.fillStyle = dk ? '#1a1218' : '#6a4a3a';
1040
- ctx.fillRect(x-5, y+h/2-14, 10, 14);
1041
  // Windows (2 rows)
1042
  const wc = dk ? 'rgba(255,210,100,0.6)' : 'rgba(200,230,255,0.5)';
1043
  ctx.fillStyle = wc;
1044
  for (let r = 0; r < 2; r++)
1045
  for (let c = 0; c < 4; c++)
1046
- ctx.fillRect(x-w/2+4+c*12, y-h/2+5+r*12, 8, 8);
1047
  }
1048
 
1049
  function drawHospital(x, y, dk) {
1050
  const w = 50, h = 40;
1051
- // Main building
1052
- ctx.fillStyle = dk ? '#1e2228' : '#c8ccd0';
1053
- ctx.fillRect(x-w/2, y-h/2, w, h);
1054
- // Flat roof
1055
- ctx.fillStyle = dk ? '#141820' : '#a0a4a8';
1056
- ctx.fillRect(x-w/2-2, y-h/2-3, w+4, 5);
1057
- // Red cross
1058
  ctx.fillStyle = '#e94560';
1059
- ctx.fillRect(x-3, y-h/2+4, 6, 14);
1060
- ctx.fillRect(x-7, y-h/2+8, 14, 6);
1061
  // Door
1062
  ctx.fillStyle = dk ? '#0a1018' : '#4a5a6a';
1063
- ctx.fillRect(x-5, y+h/2-12, 10, 12);
1064
  // Windows
1065
  const wc = dk ? 'rgba(200,240,255,0.55)' : 'rgba(200,230,255,0.5)';
1066
  ctx.fillStyle = wc;
1067
  for (let r = 0; r < 2; r++)
1068
  for (let c = 0; c < 4; c++)
1069
- ctx.fillRect(x-w/2+4+c*12, y-h/2+20+r*8, 8, 5);
1070
  }
1071
 
1072
  function drawChurch(x, y, dk) {
1073
  const w = 40, h = 36;
1074
- // Main building
1075
- ctx.fillStyle = dk ? '#2a2828' : '#c0b8a8';
1076
- ctx.fillRect(x-w/2, y-h/2, w, h);
1077
- // Steeple
 
 
1078
  ctx.fillStyle = dk ? '#1a1818' : '#908880';
1079
- ctx.fillRect(x-6, y-h/2-18, 12, 18);
1080
- // Steeple top
1081
  ctx.beginPath();
1082
- ctx.moveTo(x-8, y-h/2-18);
1083
- ctx.lineTo(x, y-h/2-30);
1084
- ctx.lineTo(x+8, y-h/2-18);
1085
  ctx.closePath(); ctx.fill();
1086
  // Cross
1087
  ctx.fillStyle = dk ? '#888' : '#d4c8a0';
1088
- ctx.fillRect(x-1.5, y-h/2-38, 3, 10);
1089
- ctx.fillRect(x-4, y-h/2-36, 8, 3);
1090
- // Stained glass window
1091
  ctx.fillStyle = dk ? 'rgba(100,150,255,0.5)' : 'rgba(80,120,200,0.4)';
1092
- ctx.beginPath(); ctx.arc(x, y-h/2-8, 4, 0, 6.28); ctx.fill();
1093
- // Door (arched)
1094
  ctx.fillStyle = dk ? '#1a1518' : '#5a4a3a';
1095
- ctx.fillRect(x-5, y+h/2-14, 10, 14);
1096
- ctx.beginPath(); ctx.arc(x, y+h/2-14, 5, Math.PI, 0); ctx.fill();
1097
  // Side windows
1098
  const wc = dk ? 'rgba(255,200,100,0.5)' : 'rgba(200,180,120,0.45)';
1099
  ctx.fillStyle = wc;
1100
- ctx.fillRect(x-w/2+4, y-h/2+6, 6, 10);
1101
- ctx.fillRect(x+w/2-10, y-h/2+6, 6, 10);
1102
  // Label
1103
  ctx.font = 'bold 9px Segoe UI'; ctx.textAlign = 'center'; ctx.textBaseline = 'top';
1104
- ctx.fillStyle = 'rgba(0,0,0,0.5)'; ctx.fillText("St. Mary's", x+1, y+h/2+3);
1105
- ctx.fillStyle = dk ? '#a0a8c0' : '#fff'; ctx.fillText("St. Mary's", x, y+h/2+2);
1106
  }
1107
 
1108
  function drawCinema(x, y, dk) {
1109
  const w = 48, h = 34;
1110
- // Main building
1111
- ctx.fillStyle = dk ? '#2a1828' : '#5a3060';
1112
- ctx.fillRect(x-w/2, y-h/2, w, h);
1113
  // Marquee sign
1114
  ctx.fillStyle = dk ? '#4a2040' : '#8a4080';
1115
- ctx.fillRect(x-w/2+4, y-h/2-8, w-8, 10);
1116
  // Marquee lights
1117
  ctx.fillStyle = dk ? '#f0c040' : '#ffe880';
1118
  for (let i = 0; i < 8; i++) {
1119
  const lx = x-w/2+8+i*5;
1120
  const flicker = Math.sin(animFrame*0.1+i*0.8) > 0;
1121
  if (flicker || !dk) {
1122
- ctx.beginPath(); ctx.arc(lx, y-h/2-3, 1.5, 0, 6.28); ctx.fill();
1123
  }
1124
  }
1125
- // "CINEMA" text
1126
  ctx.fillStyle = dk ? '#f0c040' : '#fff';
1127
  ctx.font = 'bold 7px Segoe UI'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
1128
- ctx.fillText('CINEMA', x, y-h/2-2);
1129
  // Door
1130
  ctx.fillStyle = dk ? '#1a0818' : '#3a1830';
1131
- ctx.fillRect(x-5, y+h/2-12, 10, 12);
1132
  // Poster frames
1133
  const wc = dk ? 'rgba(255,200,100,0.4)' : 'rgba(200,180,240,0.5)';
1134
  ctx.fillStyle = wc;
1135
- ctx.fillRect(x-w/2+4, y-h/2+8, 12, 16);
1136
- ctx.fillRect(x+w/2-16, y-h/2+8, 12, 16);
1137
  }
1138
 
1139
  function drawApartment(x, y, dk) {
1140
  const w = 38, h = 48;
1141
- // Main building (tall)
1142
- ctx.fillStyle = dk ? '#2a2830' : '#a09890';
1143
- ctx.fillRect(x-w/2, y-h/2, w, h);
1144
- // Flat roof
1145
- ctx.fillStyle = dk ? '#1a1820' : '#807870';
1146
- ctx.fillRect(x-w/2-2, y-h/2-3, w+4, 5);
1147
  // Door
1148
  ctx.fillStyle = dk ? '#1a1520' : '#605850';
1149
- ctx.fillRect(x-4, y+h/2-10, 8, 10);
1150
- // Windows (4 rows x 3 cols)
1151
  const wc = dk ? 'rgba(255,210,100,0.55)' : 'rgba(180,220,255,0.45)';
1152
- ctx.fillStyle = wc;
1153
  for (let r = 0; r < 4; r++)
1154
  for (let c = 0; c < 3; c++) {
1155
- // Some windows dark at night
1156
  if (dk && ((r*3+c+animFrame) % 7 < 2)) continue;
1157
- ctx.fillRect(x-w/2+4+c*12, y-h/2+5+r*11, 8, 7);
 
1158
  }
 
 
 
 
 
 
 
 
 
 
 
1159
  }
1160
 
1161
  function drawSquare(x, y, dk) {
1162
- // Cobblestone plaza
1163
  ctx.fillStyle = dk ? '#2a2820' : '#b0a890';
1164
  ctx.beginPath(); ctx.ellipse(x, y, 50, 30, 0, 0, 6.28); ctx.fill();
 
 
 
1165
  ctx.strokeStyle = dk ? '#3a3828' : '#c0b898';
1166
- ctx.lineWidth = 2; ctx.stroke();
1167
- // Fountain
 
 
 
 
 
 
 
 
 
1168
  ctx.fillStyle = dk ? '#1a2a3a' : '#708898';
1169
- ctx.beginPath(); ctx.ellipse(x, y, 12, 7, 0, 0, 6.28); ctx.fill();
 
 
 
 
1170
  ctx.fillStyle = dk ? '#2a3a5a' : '#88b0d0';
1171
- ctx.beginPath(); ctx.ellipse(x, y-2, 6, 4, 0, 0, 6.28); ctx.fill();
1172
  // Water spray
1173
  if (!dk || animFrame % 3 < 2) {
1174
  ctx.fillStyle = `rgba(100,180,220,${dk?0.3:0.5})`;
1175
- ctx.beginPath(); ctx.arc(x, y-8+Math.sin(animFrame*0.06)*2, 3, 0, 6.28); ctx.fill();
 
 
 
1176
  }
1177
  // Benches
1178
  ctx.fillStyle = dk ? '#3a2a15' : '#7a5a30';
1179
  ctx.fillRect(x-30, y+12, 12, 3);
1180
  ctx.fillRect(x+18, y+12, 12, 3);
1181
- // Notice board
1182
- ctx.fillStyle = dk ? '#2a2520' : '#8a7a60';
1183
- ctx.fillRect(x+30, y-8, 8, 12);
1184
  // Label
1185
  ctx.font = 'bold 10px Segoe UI'; ctx.textAlign = 'center'; ctx.textBaseline = 'top';
1186
- ctx.fillStyle = 'rgba(0,0,0,0.5)'; ctx.fillText('Town Square', x+1, y+21);
1187
- ctx.fillStyle = dk ? '#a0a8b0' : '#fff'; ctx.fillText('Town Square', x, y+20);
1188
  }
1189
 
1190
  function drawSportsField(x, y, dk) {
@@ -1334,7 +1476,7 @@ function drawPerson(id, agent, globalIdx, W, H) {
1334
  const isSel = id === selectedAgentId;
1335
  const isHov = id === hoveredAgent;
1336
  const gender = agent.gender || 'unknown';
1337
- const scale = isSel ? 1.3 : (isHov ? 1.15 : 1.0);
1338
  const isMoving = agent.state === 'moving';
1339
  const isSleeping = agent.state === 'sleeping';
1340
 
@@ -1345,96 +1487,128 @@ function drawPerson(id, agent, globalIdx, W, H) {
1345
  ctx.translate(ax, ay);
1346
  ctx.scale(scale, scale);
1347
 
1348
- if (isSel) { ctx.shadowColor=color; ctx.shadowBlur=16; }
1349
 
1350
- const bounce = (isMoving||moving) ? Math.sin(animFrame*0.15)*2 : 0;
1351
- const armSwing = (isMoving||moving) ? Math.sin(animFrame*0.15)*8 : 0;
1352
- const legSwing = (isMoving||moving) ? Math.sin(animFrame*0.15)*5 : 0;
1353
 
1354
- // Shadow
1355
- ctx.fillStyle = 'rgba(0,0,0,0.15)';
1356
- ctx.beginPath(); ctx.ellipse(0, 13, 7, 3, 0, 0, 6.28); ctx.fill();
1357
 
1358
- // Head
1359
- ctx.fillStyle = '#f0d0b0';
1360
- ctx.beginPath(); ctx.arc(0, -18+bounce, 6.5, 0, 6.28); ctx.fill();
 
1361
 
1362
- // Hair
1363
- ctx.fillStyle = dim(color, 0.55);
1364
  if (gender==='female') {
1365
- ctx.beginPath(); ctx.ellipse(0,-21+bounce,7.5,4.5,0,Math.PI,0); ctx.fill();
1366
- ctx.beginPath(); ctx.ellipse(-4,-16+bounce,2.5,7,0.3,0,6.28); ctx.fill();
1367
- ctx.beginPath(); ctx.ellipse(4,-16+bounce,2.5,7,-0.3,0,6.28); ctx.fill();
1368
- } else if (gender==='male') {
1369
- ctx.beginPath(); ctx.ellipse(0,-21+bounce,7,4,0,Math.PI,0); ctx.fill();
 
 
 
 
 
 
1370
  } else {
1371
- ctx.beginPath(); ctx.ellipse(0,-21+bounce,7.5,4.5,0,Math.PI,0); ctx.fill();
1372
- ctx.beginPath(); ctx.ellipse(-4.5,-17+bounce,2,5,0.2,0,6.28); ctx.fill();
 
 
 
 
 
 
 
1373
  }
1374
 
1375
- // Body
1376
- if (gender==='female') {
1377
- ctx.fillStyle=color;
1378
- ctx.beginPath(); ctx.moveTo(-3,-10+bounce); ctx.lineTo(3,-10+bounce); ctx.lineTo(7,4+bounce); ctx.lineTo(-7,4+bounce); ctx.closePath(); ctx.fill();
1379
- } else {
1380
- ctx.fillStyle=color; ctx.fillRect(-4,-10+bounce,8,8);
1381
- ctx.fillStyle=dim(color,0.6); ctx.fillRect(-4,-2+bounce,3.5,6); ctx.fillRect(0.5,-2+bounce,3.5,6);
1382
- }
 
 
1383
 
1384
  // Arms
1385
- ctx.strokeStyle='#f0d0b0'; ctx.lineWidth=2;
1386
- ctx.beginPath(); ctx.moveTo(-4,-8+bounce); ctx.lineTo(-9,-1+bounce+armSwing); ctx.stroke();
1387
- ctx.beginPath(); ctx.moveTo(4,-8+bounce); ctx.lineTo(9,-1+bounce-armSwing); ctx.stroke();
1388
 
1389
- // Legs
1390
- ctx.strokeStyle=gender==='female'?'#f0d0b0':dim(color,0.5); ctx.lineWidth=2;
1391
- ctx.beginPath(); ctx.moveTo(-2.5,4+bounce); ctx.lineTo(-4,11+bounce+legSwing); ctx.stroke();
1392
- ctx.beginPath(); ctx.moveTo(2.5,4+bounce); ctx.lineTo(4,11+bounce-legSwing); ctx.stroke();
 
 
1393
 
1394
- // Feet
1395
- ctx.fillStyle='#3a3a3a';
1396
- ctx.fillRect(-5.5,10+bounce+legSwing,3.5,2);
1397
- ctx.fillRect(1.5,10+bounce-legSwing,3.5,2);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1398
 
1399
  ctx.shadowColor='transparent'; ctx.shadowBlur=0;
1400
 
1401
  // State effects
1402
  if (agent.state==='in_conversation') {
1403
  ctx.fillStyle='rgba(240,192,64,0.85)';
1404
- ctx.beginPath(); ctx.ellipse(13,-22+bounce,9,6,0,0,6.28); ctx.fill();
1405
- ctx.fillStyle='#1a1a2e'; ctx.font='bold 8px Segoe UI'; ctx.textAlign='center'; ctx.textBaseline='middle';
1406
- ctx.fillText('...',13,-22+bounce);
1407
  }
1408
 
1409
  if (isSleeping) {
1410
  const t=animFrame*0.04;
1411
- ctx.font='bold 9px Segoe UI'; ctx.textAlign='left';
1412
  for (let i=0;i<3;i++) {
1413
  ctx.globalAlpha=0.3+i*0.25; ctx.fillStyle='#8ab4f8';
1414
- ctx.fillText('z',9+i*4,-24-i*6+Math.sin(t+i)*2);
1415
  }
1416
  ctx.globalAlpha=1;
1417
  }
1418
 
1419
  if (agent.partner_id) {
1420
- drawHeart(0, -30+bounce+Math.sin(animFrame*0.04)*2, 4, 'rgba(233,30,144,0.7)');
1421
  }
1422
 
1423
  ctx.restore();
1424
 
1425
- // Name label
1426
  const firstName = (agent.name||id).split(' ')[0];
1427
- ctx.font=`${isSel?'bold ':''}10px Segoe UI`; ctx.textAlign='center'; ctx.textBaseline='top';
1428
- ctx.fillStyle='rgba(0,0,0,0.5)'; ctx.fillText(firstName,ax+1,ay+15*scale+1);
1429
- ctx.fillStyle=isSel?'#fff':'#d0d0e0'; ctx.fillText(firstName,ax,ay+15*scale);
1430
 
1431
- // Mood bar
1432
  const mood=agent.mood||0;
1433
- const mw=20, mx=ax-mw/2, my=ay+15*scale+13;
1434
- ctx.fillStyle='rgba(15,52,96,0.5)'; ctx.fillRect(mx,my,mw,3);
1435
  const mf=(mood+1)/2;
1436
  ctx.fillStyle=mf>0.6?'#4ecca3':(mf>0.3?'#f0c040':'#e94560');
1437
- ctx.fillRect(mx,my,mw*mf,3);
1438
  }
1439
 
1440
  // ============================================================
 
566
  else if (s.sun === 'high') drawSun(W*0.78, hLine*0.25, 16);
567
  else if (s.sun === 'mid') drawSun(W*0.80, hLine*0.45, 16);
568
  else if (s.sun === 'low') drawSun(W*0.82, hLine*0.7, 16);
569
+
570
+ // Mountains along the horizon
571
+ drawMountains(W, hLine);
572
+ }
573
+
574
+ function drawMountains(W, hLine) {
575
+ const isDark = currentTimeOfDay === 'night' || currentTimeOfDay === 'evening';
576
+ const isDawn = currentTimeOfDay === 'dawn';
577
+
578
+ // Far mountains (larger, darker)
579
+ const farPeaks = [
580
+ {x:0.00,h:0.55},{x:0.08,h:0.85},{x:0.18,h:0.65},{x:0.28,h:0.95},
581
+ {x:0.38,h:0.70},{x:0.48,h:1.0},{x:0.58,h:0.75},{x:0.68,h:0.90},
582
+ {x:0.78,h:0.80},{x:0.88,h:0.70},{x:0.98,h:0.60},{x:1.05,h:0.50},
583
+ ];
584
+ ctx.fillStyle = isDark ? '#0a0e18' : (isDawn ? '#4a3858' : '#5a6878');
585
+ ctx.beginPath();
586
+ ctx.moveTo(0, hLine);
587
+ for (const p of farPeaks) {
588
+ ctx.lineTo(p.x * W, hLine - p.h * hLine * 0.6);
589
+ }
590
+ ctx.lineTo(W, hLine);
591
+ ctx.closePath();
592
+ ctx.fill();
593
+
594
+ // Near mountains (smaller, lighter)
595
+ const nearPeaks = [
596
+ {x:-0.02,h:0.30},{x:0.05,h:0.55},{x:0.14,h:0.40},{x:0.22,h:0.65},
597
+ {x:0.32,h:0.45},{x:0.42,h:0.60},{x:0.52,h:0.50},{x:0.60,h:0.70},
598
+ {x:0.70,h:0.55},{x:0.80,h:0.50},{x:0.90,h:0.62},{x:1.02,h:0.35},
599
+ ];
600
+ ctx.fillStyle = isDark ? '#141828' : (isDawn ? '#5a4868' : '#6a7888');
601
+ ctx.beginPath();
602
+ ctx.moveTo(0, hLine);
603
+ for (const p of nearPeaks) {
604
+ ctx.lineTo(p.x * W, hLine - p.h * hLine * 0.45);
605
+ }
606
+ ctx.lineTo(W, hLine);
607
+ ctx.closePath();
608
+ ctx.fill();
609
+
610
+ // Snow caps on far peaks (daytime only)
611
+ if (!isDark) {
612
+ ctx.fillStyle = isDawn ? 'rgba(220,200,220,0.5)' : 'rgba(240,245,255,0.6)';
613
+ for (const p of farPeaks) {
614
+ if (p.h > 0.75) {
615
+ const px = p.x * W;
616
+ const py = hLine - p.h * hLine * 0.6;
617
+ ctx.beginPath();
618
+ ctx.moveTo(px, py);
619
+ ctx.lineTo(px - 12, py + hLine * 0.06);
620
+ ctx.lineTo(px + 12, py + hLine * 0.06);
621
+ ctx.closePath();
622
+ ctx.fill();
623
+ }
624
+ }
625
+ }
626
+
627
+ // Haze at base of mountains
628
+ const hazeGrad = ctx.createLinearGradient(0, hLine - 15, 0, hLine + 5);
629
+ hazeGrad.addColorStop(0, 'rgba(0,0,0,0)');
630
+ hazeGrad.addColorStop(1, isDark ? 'rgba(10,15,25,0.5)' : (isDawn ? 'rgba(180,140,120,0.3)' : 'rgba(140,160,180,0.25)'));
631
+ ctx.fillStyle = hazeGrad;
632
+ ctx.fillRect(0, hLine - 15, W, 20);
633
  }
634
 
635
  function drawSun(x, y, r) {
 
884
  }
885
  }
886
 
887
+ // --- 2.5D PERSPECTIVE HELPERS ---
888
+ const ISO_DX = 6; // horizontal offset for side face
889
+ const ISO_DY = 4; // vertical offset for top face
890
+
891
+ function draw25DBox(x, y, w, h, frontColor, sideColor, topColor, dk) {
892
+ // Front face
893
+ ctx.fillStyle = dk ? dim(frontColor, 0.4) : frontColor;
894
+ ctx.fillRect(x - w/2, y - h, w, h);
895
+ // Right side face (perspective)
896
+ ctx.fillStyle = dk ? dim(sideColor, 0.35) : sideColor;
897
+ ctx.beginPath();
898
+ ctx.moveTo(x + w/2, y - h);
899
+ ctx.lineTo(x + w/2 + ISO_DX, y - h - ISO_DY);
900
+ ctx.lineTo(x + w/2 + ISO_DX, y - ISO_DY);
901
+ ctx.lineTo(x + w/2, y);
902
+ ctx.closePath(); ctx.fill();
903
+ // Top face
904
+ ctx.fillStyle = dk ? dim(topColor, 0.45) : topColor;
905
+ ctx.beginPath();
906
+ ctx.moveTo(x - w/2, y - h);
907
+ ctx.lineTo(x - w/2 + ISO_DX, y - h - ISO_DY);
908
+ ctx.lineTo(x + w/2 + ISO_DX, y - h - ISO_DY);
909
+ ctx.lineTo(x + w/2, y - h);
910
+ ctx.closePath(); ctx.fill();
911
+ }
912
+
913
  function drawHouse(x, y, dk, id) {
914
  const w = 36, h = 24;
 
915
  const hues = [
916
+ {wall:'#c8a882', side:'#a88862', roof:'#8b4513', door:'#6b3410'},
917
+ {wall:'#a0b8a0', side:'#809880', roof:'#4a6a4a', door:'#3a4a3a'},
918
+ {wall:'#b8a0a0', side:'#988080', roof:'#7a3a3a', door:'#5a2a2a'},
919
+ {wall:'#a0a8c0', side:'#8088a0', roof:'#4a5a7a', door:'#3a4a6a'},
920
+ {wall:'#c8b878', side:'#a89858', roof:'#8a7a40', door:'#6a5a30'},
921
  ];
922
+ const allPos = getEffectivePositions();
923
+ const idx = Object.keys(allPos).indexOf(id) % hues.length;
924
  const c = hues[idx];
925
 
926
+ const baseY = y + h/2;
927
+ // 2.5D wall
928
+ draw25DBox(x, baseY, w, h, c.wall, c.side, c.roof, dk);
929
+
930
+ // Pitched roof
931
  ctx.fillStyle = dk ? dim(c.roof, 0.4) : c.roof;
932
  ctx.beginPath();
933
+ ctx.moveTo(x - w/2 - 3, baseY - h);
934
+ ctx.lineTo(x, baseY - h - 14);
935
+ ctx.lineTo(x + w/2 + 3, baseY - h);
936
+ ctx.closePath(); ctx.fill();
937
+ // Roof right side
938
+ ctx.fillStyle = dk ? dim(c.roof, 0.3) : dim(c.roof, 0.8);
939
+ ctx.beginPath();
940
+ ctx.moveTo(x + w/2 + 3, baseY - h);
941
+ ctx.lineTo(x, baseY - h - 14);
942
+ ctx.lineTo(x + ISO_DX, baseY - h - 14 - ISO_DY);
943
+ ctx.lineTo(x + w/2 + 3 + ISO_DX, baseY - h - ISO_DY);
944
+ ctx.closePath(); ctx.fill();
945
+
946
  // Door
947
  ctx.fillStyle = dk ? dim(c.door, 0.4) : c.door;
948
+ ctx.fillRect(x - 3, baseY - 10, 6, 10);
 
949
  ctx.fillStyle = dk ? '#aa9060' : '#d4b070';
950
+ ctx.beginPath(); ctx.arc(x + 2, baseY - 4, 1, 0, 6.28); ctx.fill();
951
+
952
+ // Windows
953
  const wc = dk ? 'rgba(255,210,100,0.75)' : 'rgba(180,220,255,0.55)';
954
  ctx.fillStyle = wc;
955
+ ctx.fillRect(x - w/2 + 4, baseY - h + 5, 8, 6);
956
+ ctx.fillRect(x + w/2 - 12, baseY - h + 5, 8, 6);
 
957
  ctx.strokeStyle = dk ? 'rgba(100,80,50,0.4)' : 'rgba(80,70,60,0.3)';
958
  ctx.lineWidth = 0.5;
959
+ ctx.strokeRect(x - w/2 + 4, baseY - h + 5, 8, 6);
960
+ ctx.strokeRect(x + w/2 - 12, baseY - h + 5, 8, 6);
961
+
962
  // Chimney
963
  ctx.fillStyle = dk ? dim(c.roof, 0.35) : dim(c.roof, 0.8);
964
+ ctx.fillRect(x + 8, baseY - h - 10, 5, 8);
 
 
 
 
965
  }
966
 
967
  function drawShop(x, y, dk) {
968
  const w = 48, h = 32;
969
+ const baseY = y + h/2;
970
+ draw25DBox(x, baseY, w, h, '#d4c4a8', '#b4a488', '#e0d4c0', dk);
971
+
972
+ // Awning with 2.5D
973
  const awningColors = ['#c44', '#4a8', '#48a', '#a84'];
974
  const ci = Math.floor(x*7 + y*3) % awningColors.length;
975
+ const ac = awningColors[ci];
976
+ ctx.fillStyle = dk ? dim(ac, 0.35) : ac;
977
  ctx.beginPath();
978
+ ctx.moveTo(x-w/2-3, baseY-h);
979
+ ctx.lineTo(x-w/2-6, baseY-h+12);
980
+ ctx.lineTo(x+w/2+6, baseY-h+12);
981
+ ctx.lineTo(x+w/2+3, baseY-h);
982
+ ctx.closePath(); ctx.fill();
983
+ // Awning side
984
+ ctx.fillStyle = dk ? dim(ac, 0.25) : dim(ac, 0.7);
985
+ ctx.beginPath();
986
+ ctx.moveTo(x+w/2+3, baseY-h);
987
+ ctx.lineTo(x+w/2+6, baseY-h+12);
988
+ ctx.lineTo(x+w/2+6+ISO_DX, baseY-h+12-ISO_DY);
989
+ ctx.lineTo(x+w/2+3+ISO_DX, baseY-h-ISO_DY);
990
+ ctx.closePath(); ctx.fill();
991
+
992
  // Awning stripes
993
  ctx.strokeStyle = dk ? 'rgba(255,255,255,0.08)' : 'rgba(255,255,255,0.25)';
994
  ctx.lineWidth = 1;
995
  for (let i = 0; i < 5; i++) {
996
  const sx = x-w/2 + i*(w/4);
997
+ ctx.beginPath(); ctx.moveTo(sx, baseY-h); ctx.lineTo(sx-1.5, baseY-h+12); ctx.stroke();
998
  }
999
  // Door
1000
  ctx.fillStyle = dk ? '#2a2520' : '#5a4a3a';
1001
+ ctx.fillRect(x-4, baseY-12, 8, 12);
1002
  // Windows
1003
  const wc = dk ? 'rgba(255,210,100,0.65)' : 'rgba(200,230,255,0.55)';
1004
  ctx.fillStyle = wc;
1005
+ ctx.fillRect(x-w/2+4, baseY-h+14, w/2-10, 10);
1006
+ ctx.fillRect(x+5, baseY-h+14, w/2-10, 10);
1007
  }
1008
 
1009
  function drawOffice(x, y, dk) {
1010
  const w = 50, h = 40;
1011
+ const baseY = y + h/2;
1012
+ draw25DBox(x, baseY, w, h, '#8090a8', '#607088', '#90a0b8', dk);
 
 
 
1013
  // Door
1014
  ctx.fillStyle = dk ? '#1a1a25' : '#4a5a6a';
1015
+ ctx.fillRect(x-4, baseY-12, 8, 12);
1016
  // Windows grid
1017
  const wc = dk ? 'rgba(255,210,100,0.6)' : 'rgba(180,220,255,0.5)';
1018
  ctx.fillStyle = wc;
1019
+ for (let r = 0; r < 3; r++)
1020
+ for (let c = 0; c < 3; c++)
1021
+ ctx.fillRect(x-w/2+6+c*15, baseY-h+5+r*10, 9, 6);
1022
+ // Side windows
1023
+ const swc = dk ? 'rgba(255,200,80,0.4)' : 'rgba(160,200,240,0.35)';
1024
+ ctx.fillStyle = swc;
1025
  for (let r = 0; r < 3; r++) {
1026
+ const wy = baseY - h + 6 + r*10;
1027
+ ctx.beginPath();
1028
+ ctx.moveTo(x+w/2+1, wy);
1029
+ ctx.lineTo(x+w/2+ISO_DX, wy-ISO_DY);
1030
+ ctx.lineTo(x+w/2+ISO_DX, wy-ISO_DY+6);
1031
+ ctx.lineTo(x+w/2+1, wy+6);
1032
+ ctx.closePath(); ctx.fill();
1033
  }
1034
  }
1035
 
1036
  function drawPublicBuilding(x, y, dk) {
1037
  const w = 46, h = 34;
1038
+ const baseY = y + h/2;
1039
+ draw25DBox(x, baseY, w, h, '#7a9a6a', '#5a7a4a', '#8aaa7a', dk);
 
 
1040
  ctx.fillStyle = dk ? '#1a2018' : '#4a6a3a';
1041
+ ctx.fillRect(x-4, baseY-12, 8, 12);
1042
  const wc = dk ? 'rgba(255,210,100,0.55)' : 'rgba(200,240,200,0.5)';
1043
  ctx.fillStyle = wc;
1044
+ for (let c = 0; c < 4; c++) ctx.fillRect(x-w/2+4+c*11, baseY-h+6, 8, 8);
1045
+ for (let c = 0; c < 4; c++) ctx.fillRect(x-w/2+4+c*11, baseY-h+18, 8, 8);
1046
  }
1047
 
1048
  function drawPark(x, y, dk) {
1049
+ // Green area with slight elevation
1050
  ctx.fillStyle = dk ? '#1a3018' : '#4a9040';
1051
  ctx.beginPath(); ctx.ellipse(x, y, 55, 25, 0, 0, 6.28); ctx.fill();
1052
+ // Raised edge (2.5D)
1053
+ ctx.fillStyle = dk ? '#122810' : '#3a7030';
1054
+ ctx.beginPath(); ctx.ellipse(x, y+3, 55, 25, 0, 0, Math.PI); ctx.fill();
1055
  ctx.strokeStyle = dk ? '#2a4a25' : '#6ab850';
1056
+ ctx.lineWidth = 1.5;
1057
+ ctx.beginPath(); ctx.ellipse(x, y, 55, 25, 0, 0, 6.28); ctx.stroke();
1058
+ // Walking path
1059
+ ctx.strokeStyle = dk ? '#2a2820' : '#c8c0a8';
1060
+ ctx.lineWidth = 3; ctx.setLineDash([6,4]);
1061
+ ctx.beginPath(); ctx.ellipse(x, y, 32, 14, 0, 0, 6.28); ctx.stroke();
1062
+ ctx.setLineDash([]);
1063
  // Pond
1064
  ctx.fillStyle = dk ? '#1a2a3a' : '#5a9ac0';
1065
+ ctx.beginPath(); ctx.ellipse(x+15, y+4, 12, 6, 0.2, 0, 6.28); ctx.fill();
1066
  ctx.strokeStyle = dk ? '#2a3a4a' : '#80b0d0'; ctx.lineWidth = 1; ctx.stroke();
1067
+ // Water shimmer
1068
+ if (!dk) {
1069
+ ctx.fillStyle = `rgba(255,255,255,${0.15+Math.sin(animFrame*0.05)*0.1})`;
1070
+ ctx.beginPath(); ctx.ellipse(x+17, y+2, 4, 2, 0.3, 0, 6.28); ctx.fill();
1071
+ }
1072
+ // Benches (2.5D)
1073
  ctx.fillStyle = dk ? '#3a2a15' : '#7a5a30';
1074
+ ctx.fillRect(x-22, y+7, 14, 3);
1075
+ ctx.fillStyle = dk ? '#2a1a08' : '#5a3a18';
1076
+ ctx.fillRect(x-22, y+10, 14, 2); // bench shadow/depth
1077
+ // Trees in park
1078
  for (let i = -1; i <= 1; i++) {
1079
  const tx = x + i*22;
1080
  ctx.fillStyle = dk ? '#3a2a15' : '#6b4226'; ctx.fillRect(tx-2, y-4, 4, 10);
 
1089
 
1090
  function drawTower(x, y, dk) {
1091
  const w = 36, h = 56;
1092
+ const baseY = y + h/2;
1093
+ draw25DBox(x, baseY, w, h, '#6880a0', '#486078', '#7890b0', dk);
 
 
 
 
1094
  // Antenna
1095
+ ctx.fillStyle = '#888'; ctx.fillRect(x-1, baseY-h-10, 2, 10);
1096
+ ctx.fillStyle = '#e94560'; ctx.beginPath(); ctx.arc(x, baseY-h-10, 2, 0, 6.28); ctx.fill();
1097
  // Door
1098
  ctx.fillStyle = dk ? '#0a0e18' : '#384858';
1099
+ ctx.fillRect(x-5, baseY-14, 10, 14);
1100
+ // Front windows (5 rows x 4 cols)
1101
  const wc = dk ? 'rgba(255,210,100,0.55)' : 'rgba(180,220,255,0.5)';
1102
  ctx.fillStyle = wc;
1103
  for (let r = 0; r < 5; r++)
1104
  for (let c = 0; c < 4; c++)
1105
+ ctx.fillRect(x-w/2+3+c*8.5, baseY-h+5+r*10, 6, 6);
1106
+ // Side windows
1107
+ const swc = dk ? 'rgba(255,200,80,0.35)' : 'rgba(160,200,240,0.3)';
1108
+ ctx.fillStyle = swc;
1109
+ for (let r = 0; r < 5; r++) {
1110
+ const wy = baseY-h+6+r*10;
1111
+ ctx.beginPath();
1112
+ ctx.moveTo(x+w/2+1, wy);
1113
+ ctx.lineTo(x+w/2+ISO_DX, wy-ISO_DY);
1114
+ ctx.lineTo(x+w/2+ISO_DX, wy-ISO_DY+5);
1115
+ ctx.lineTo(x+w/2+1, wy+5);
1116
+ ctx.closePath(); ctx.fill();
1117
+ }
1118
  }
1119
 
1120
  function drawFactory(x, y, dk) {
1121
  const w = 56, h = 34;
1122
+ const baseY = y + h/2;
1123
+ draw25DBox(x, baseY, w, h, '#8a7a6a', '#6a5a4a', '#9a8a78', dk);
 
1124
  // Saw-tooth roof
1125
  ctx.fillStyle = dk ? '#1a1815' : '#6a5a4a';
1126
  for (let i = 0; i < 3; i++) {
1127
  const rx = x - w/2 + i*(w/3);
1128
  ctx.beginPath();
1129
+ ctx.moveTo(rx, baseY-h); ctx.lineTo(rx+w/6, baseY-h-10); ctx.lineTo(rx+w/3, baseY-h);
 
 
1130
  ctx.closePath(); ctx.fill();
1131
  }
1132
  // Chimney with smoke
1133
  ctx.fillStyle = dk ? '#3a3020' : '#706050';
1134
+ ctx.fillRect(x+w/2-10, baseY-h-16, 8, 14);
1135
+ // 2.5D chimney side
1136
+ ctx.fillStyle = dk ? '#2a2018' : '#605040';
1137
+ ctx.beginPath();
1138
+ ctx.moveTo(x+w/2-2, baseY-h-16); ctx.lineTo(x+w/2-2+ISO_DX, baseY-h-16-ISO_DY);
1139
+ ctx.lineTo(x+w/2-2+ISO_DX, baseY-h-2-ISO_DY); ctx.lineTo(x+w/2-2, baseY-h-2);
1140
+ ctx.closePath(); ctx.fill();
1141
  // Smoke
1142
  ctx.fillStyle = `rgba(180,180,180,${dk?0.15:0.25})`;
1143
  for (let i = 0; i < 3; i++) {
1144
+ const sy = baseY-h-20-i*8+Math.sin(animFrame*0.03+i)*3;
1145
  ctx.beginPath(); ctx.arc(x+w/2-6+i*3, sy, 4+i*2, 0, 6.28); ctx.fill();
1146
  }
1147
  // Loading door
1148
  ctx.fillStyle = dk ? '#1a1815' : '#5a4a3a';
1149
+ ctx.fillRect(x-8, baseY-16, 16, 16);
1150
  // Windows
1151
  const wc = dk ? 'rgba(255,180,80,0.5)' : 'rgba(200,220,240,0.4)';
1152
  ctx.fillStyle = wc;
1153
+ for (let c = 0; c < 4; c++) ctx.fillRect(x-w/2+4+c*14, baseY-h+6, 10, 8);
1154
  }
1155
 
1156
  function drawSchool(x, y, dk) {
1157
  const w = 52, h = 36;
1158
+ const baseY = y + h/2;
1159
+ draw25DBox(x, baseY, w, h, '#c4a088', '#a48068', '#d4b098', dk);
 
 
 
 
1160
  // Flagpole
1161
+ ctx.fillStyle = '#888'; ctx.fillRect(x+w/2-6, baseY-h-18, 2, 18);
 
1162
  ctx.fillStyle = '#e94560';
1163
  ctx.beginPath();
1164
+ ctx.moveTo(x+w/2-4, baseY-h-18); ctx.lineTo(x+w/2+8, baseY-h-14); ctx.lineTo(x+w/2-4, baseY-h-10);
 
 
1165
  ctx.closePath(); ctx.fill();
1166
  // Door
1167
  ctx.fillStyle = dk ? '#1a1218' : '#6a4a3a';
1168
+ ctx.fillRect(x-5, baseY-14, 10, 14);
1169
  // Windows (2 rows)
1170
  const wc = dk ? 'rgba(255,210,100,0.6)' : 'rgba(200,230,255,0.5)';
1171
  ctx.fillStyle = wc;
1172
  for (let r = 0; r < 2; r++)
1173
  for (let c = 0; c < 4; c++)
1174
+ ctx.fillRect(x-w/2+4+c*12, baseY-h+5+r*12, 8, 8);
1175
  }
1176
 
1177
  function drawHospital(x, y, dk) {
1178
  const w = 50, h = 40;
1179
+ const baseY = y + h/2;
1180
+ draw25DBox(x, baseY, w, h, '#c8ccd0', '#a0a4a8', '#d8dce0', dk);
1181
+ // Red cross on front
 
 
 
 
1182
  ctx.fillStyle = '#e94560';
1183
+ ctx.fillRect(x-3, baseY-h+4, 6, 14);
1184
+ ctx.fillRect(x-7, baseY-h+8, 14, 6);
1185
  // Door
1186
  ctx.fillStyle = dk ? '#0a1018' : '#4a5a6a';
1187
+ ctx.fillRect(x-5, baseY-12, 10, 12);
1188
  // Windows
1189
  const wc = dk ? 'rgba(200,240,255,0.55)' : 'rgba(200,230,255,0.5)';
1190
  ctx.fillStyle = wc;
1191
  for (let r = 0; r < 2; r++)
1192
  for (let c = 0; c < 4; c++)
1193
+ ctx.fillRect(x-w/2+4+c*12, baseY-h+20+r*8, 8, 5);
1194
  }
1195
 
1196
  function drawChurch(x, y, dk) {
1197
  const w = 40, h = 36;
1198
+ const baseY = y + h/2;
1199
+ draw25DBox(x, baseY, w, h, '#c0b8a8', '#a09888', '#d0c8b8', dk);
1200
+ // Steeple (2.5D)
1201
+ const stW = 12, stH = 18;
1202
+ draw25DBox(x, baseY-h, stW, stH, '#908880', '#706860', '#a09890', dk);
1203
+ // Steeple pointed top
1204
  ctx.fillStyle = dk ? '#1a1818' : '#908880';
 
 
1205
  ctx.beginPath();
1206
+ ctx.moveTo(x-8, baseY-h-stH); ctx.lineTo(x, baseY-h-stH-12); ctx.lineTo(x+8, baseY-h-stH);
 
 
1207
  ctx.closePath(); ctx.fill();
1208
  // Cross
1209
  ctx.fillStyle = dk ? '#888' : '#d4c8a0';
1210
+ ctx.fillRect(x-1.5, baseY-h-stH-20, 3, 10);
1211
+ ctx.fillRect(x-4, baseY-h-stH-18, 8, 3);
1212
+ // Stained glass (rose window)
1213
  ctx.fillStyle = dk ? 'rgba(100,150,255,0.5)' : 'rgba(80,120,200,0.4)';
1214
+ ctx.beginPath(); ctx.arc(x, baseY-h-8, 4, 0, 6.28); ctx.fill();
1215
+ // Arched door
1216
  ctx.fillStyle = dk ? '#1a1518' : '#5a4a3a';
1217
+ ctx.fillRect(x-5, baseY-14, 10, 14);
1218
+ ctx.beginPath(); ctx.arc(x, baseY-14, 5, Math.PI, 0); ctx.fill();
1219
  // Side windows
1220
  const wc = dk ? 'rgba(255,200,100,0.5)' : 'rgba(200,180,120,0.45)';
1221
  ctx.fillStyle = wc;
1222
+ ctx.fillRect(x-w/2+4, baseY-h+6, 6, 10);
1223
+ ctx.fillRect(x+w/2-10, baseY-h+6, 6, 10);
1224
  // Label
1225
  ctx.font = 'bold 9px Segoe UI'; ctx.textAlign = 'center'; ctx.textBaseline = 'top';
1226
+ ctx.fillStyle = 'rgba(0,0,0,0.5)'; ctx.fillText("St. Mary's", x+1, baseY+3);
1227
+ ctx.fillStyle = dk ? '#a0a8c0' : '#fff'; ctx.fillText("St. Mary's", x, baseY+2);
1228
  }
1229
 
1230
  function drawCinema(x, y, dk) {
1231
  const w = 48, h = 34;
1232
+ const baseY = y + h/2;
1233
+ draw25DBox(x, baseY, w, h, '#5a3060', '#3a1840', '#6a4070', dk);
 
1234
  // Marquee sign
1235
  ctx.fillStyle = dk ? '#4a2040' : '#8a4080';
1236
+ ctx.fillRect(x-w/2+4, baseY-h-8, w-8, 10);
1237
  // Marquee lights
1238
  ctx.fillStyle = dk ? '#f0c040' : '#ffe880';
1239
  for (let i = 0; i < 8; i++) {
1240
  const lx = x-w/2+8+i*5;
1241
  const flicker = Math.sin(animFrame*0.1+i*0.8) > 0;
1242
  if (flicker || !dk) {
1243
+ ctx.beginPath(); ctx.arc(lx, baseY-h-3, 1.5, 0, 6.28); ctx.fill();
1244
  }
1245
  }
 
1246
  ctx.fillStyle = dk ? '#f0c040' : '#fff';
1247
  ctx.font = 'bold 7px Segoe UI'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
1248
+ ctx.fillText('CINEMA', x, baseY-h-2);
1249
  // Door
1250
  ctx.fillStyle = dk ? '#1a0818' : '#3a1830';
1251
+ ctx.fillRect(x-5, baseY-12, 10, 12);
1252
  // Poster frames
1253
  const wc = dk ? 'rgba(255,200,100,0.4)' : 'rgba(200,180,240,0.5)';
1254
  ctx.fillStyle = wc;
1255
+ ctx.fillRect(x-w/2+4, baseY-h+8, 12, 16);
1256
+ ctx.fillRect(x+w/2-16, baseY-h+8, 12, 16);
1257
  }
1258
 
1259
  function drawApartment(x, y, dk) {
1260
  const w = 38, h = 48;
1261
+ const baseY = y + h/2;
1262
+ draw25DBox(x, baseY, w, h, '#a09890', '#807870', '#b0a8a0', dk);
 
 
 
 
1263
  // Door
1264
  ctx.fillStyle = dk ? '#1a1520' : '#605850';
1265
+ ctx.fillRect(x-4, baseY-10, 8, 10);
1266
+ // Front windows (4 rows x 3 cols)
1267
  const wc = dk ? 'rgba(255,210,100,0.55)' : 'rgba(180,220,255,0.45)';
 
1268
  for (let r = 0; r < 4; r++)
1269
  for (let c = 0; c < 3; c++) {
 
1270
  if (dk && ((r*3+c+animFrame) % 7 < 2)) continue;
1271
+ ctx.fillStyle = wc;
1272
+ ctx.fillRect(x-w/2+4+c*12, baseY-h+5+r*11, 8, 7);
1273
  }
1274
+ // Side windows
1275
+ const swc = dk ? 'rgba(255,200,80,0.35)' : 'rgba(160,200,240,0.3)';
1276
+ for (let r = 0; r < 4; r++) {
1277
+ if (dk && ((r+animFrame) % 5 < 2)) continue;
1278
+ ctx.fillStyle = swc;
1279
+ const wy = baseY-h+6+r*11;
1280
+ ctx.beginPath();
1281
+ ctx.moveTo(x+w/2+1, wy); ctx.lineTo(x+w/2+ISO_DX, wy-ISO_DY);
1282
+ ctx.lineTo(x+w/2+ISO_DX, wy-ISO_DY+6); ctx.lineTo(x+w/2+1, wy+6);
1283
+ ctx.closePath(); ctx.fill();
1284
+ }
1285
  }
1286
 
1287
  function drawSquare(x, y, dk) {
1288
+ // Cobblestone plaza with raised edge
1289
  ctx.fillStyle = dk ? '#2a2820' : '#b0a890';
1290
  ctx.beginPath(); ctx.ellipse(x, y, 50, 30, 0, 0, 6.28); ctx.fill();
1291
+ // Raised edge (2.5D)
1292
+ ctx.fillStyle = dk ? '#1a1810' : '#908870';
1293
+ ctx.beginPath(); ctx.ellipse(x, y+3, 50, 30, 0, 0, Math.PI); ctx.fill();
1294
  ctx.strokeStyle = dk ? '#3a3828' : '#c0b898';
1295
+ ctx.lineWidth = 1.5;
1296
+ ctx.beginPath(); ctx.ellipse(x, y, 50, 30, 0, 0, 6.28); ctx.stroke();
1297
+ // Cobblestone pattern
1298
+ ctx.strokeStyle = dk ? 'rgba(60,55,45,0.3)' : 'rgba(160,150,130,0.3)';
1299
+ ctx.lineWidth = 0.5;
1300
+ for (let i = -3; i <= 3; i++) {
1301
+ ctx.beginPath(); ctx.moveTo(x+i*12, y-20); ctx.lineTo(x+i*12, y+20); ctx.stroke();
1302
+ }
1303
+ // Fountain (2.5D — circular base + column + basin)
1304
+ ctx.fillStyle = dk ? '#4a5868' : '#788898';
1305
+ ctx.beginPath(); ctx.ellipse(x, y+4, 14, 7, 0, 0, 6.28); ctx.fill();
1306
  ctx.fillStyle = dk ? '#1a2a3a' : '#708898';
1307
+ ctx.beginPath(); ctx.ellipse(x, y+1, 12, 6, 0, 0, 6.28); ctx.fill();
1308
+ // Fountain column
1309
+ ctx.fillStyle = dk ? '#3a4858' : '#8098a8';
1310
+ ctx.fillRect(x-2, y-8, 4, 10);
1311
+ // Water basin top
1312
  ctx.fillStyle = dk ? '#2a3a5a' : '#88b0d0';
1313
+ ctx.beginPath(); ctx.ellipse(x, y-8, 7, 3, 0, 0, 6.28); ctx.fill();
1314
  // Water spray
1315
  if (!dk || animFrame % 3 < 2) {
1316
  ctx.fillStyle = `rgba(100,180,220,${dk?0.3:0.5})`;
1317
+ const wy = y-14+Math.sin(animFrame*0.06)*2;
1318
+ ctx.beginPath(); ctx.arc(x, wy, 2, 0, 6.28); ctx.fill();
1319
+ ctx.beginPath(); ctx.arc(x-3, wy+2, 1.5, 0, 6.28); ctx.fill();
1320
+ ctx.beginPath(); ctx.arc(x+3, wy+2, 1.5, 0, 6.28); ctx.fill();
1321
  }
1322
  // Benches
1323
  ctx.fillStyle = dk ? '#3a2a15' : '#7a5a30';
1324
  ctx.fillRect(x-30, y+12, 12, 3);
1325
  ctx.fillRect(x+18, y+12, 12, 3);
 
 
 
1326
  // Label
1327
  ctx.font = 'bold 10px Segoe UI'; ctx.textAlign = 'center'; ctx.textBaseline = 'top';
1328
+ ctx.fillStyle = 'rgba(0,0,0,0.5)'; ctx.fillText('Town Square', x+1, y+23);
1329
+ ctx.fillStyle = dk ? '#a0a8b0' : '#fff'; ctx.fillText('Town Square', x, y+22);
1330
  }
1331
 
1332
  function drawSportsField(x, y, dk) {
 
1476
  const isSel = id === selectedAgentId;
1477
  const isHov = id === hoveredAgent;
1478
  const gender = agent.gender || 'unknown';
1479
+ const scale = (isSel ? 1.0 : (isHov ? 0.9 : 0.72));
1480
  const isMoving = agent.state === 'moving';
1481
  const isSleeping = agent.state === 'sleeping';
1482
 
 
1487
  ctx.translate(ax, ay);
1488
  ctx.scale(scale, scale);
1489
 
1490
+ if (isSel) { ctx.shadowColor=color; ctx.shadowBlur=12; }
1491
 
1492
+ const bounce = (isMoving||moving) ? Math.sin(animFrame*0.18)*1.5 : 0;
1493
+ const armSwing = (isMoving||moving) ? Math.sin(animFrame*0.18)*6 : 0;
1494
+ const legSwing = (isMoving||moving) ? Math.sin(animFrame*0.18)*4 : 0;
1495
 
1496
+ // Ground shadow (isometric ellipse)
1497
+ ctx.fillStyle = 'rgba(0,0,0,0.18)';
1498
+ ctx.beginPath(); ctx.ellipse(1, 10, 6, 2.5, 0.15, 0, 6.28); ctx.fill();
1499
 
1500
+ // Determine skin tones for variety
1501
+ const skinTones = ['#f0d0b0','#d4a574','#c68642','#8d5524','#e8c4a0','#f5d6b8'];
1502
+ const skinIdx = (globalIdx * 7 + 3) % skinTones.length;
1503
+ const skin = skinTones[skinIdx];
1504
 
1505
+ // --- BODY (2.5D: front + slight right side visible) ---
1506
+ // Torso front face
1507
  if (gender==='female') {
1508
+ ctx.fillStyle = color;
1509
+ ctx.beginPath();
1510
+ ctx.moveTo(-3, -8+bounce); ctx.lineTo(3, -8+bounce);
1511
+ ctx.lineTo(5, 3+bounce); ctx.lineTo(-5, 3+bounce);
1512
+ ctx.closePath(); ctx.fill();
1513
+ // Torso side (2.5D)
1514
+ ctx.fillStyle = dim(color, 0.7);
1515
+ ctx.beginPath();
1516
+ ctx.moveTo(3, -8+bounce); ctx.lineTo(5, -9+bounce);
1517
+ ctx.lineTo(7, 2+bounce); ctx.lineTo(5, 3+bounce);
1518
+ ctx.closePath(); ctx.fill();
1519
  } else {
1520
+ // Male/NB — blocky torso
1521
+ ctx.fillStyle = color;
1522
+ ctx.fillRect(-3.5, -8+bounce, 7, 10);
1523
+ // Torso side (2.5D)
1524
+ ctx.fillStyle = dim(color, 0.65);
1525
+ ctx.beginPath();
1526
+ ctx.moveTo(3.5, -8+bounce); ctx.lineTo(5.5, -9.5+bounce);
1527
+ ctx.lineTo(5.5, 0.5+bounce); ctx.lineTo(3.5, 2+bounce);
1528
+ ctx.closePath(); ctx.fill();
1529
  }
1530
 
1531
+ // Legs (with perspective offset)
1532
+ ctx.strokeStyle = gender==='female' ? skin : dim(color, 0.5);
1533
+ ctx.lineWidth = 1.8;
1534
+ ctx.beginPath(); ctx.moveTo(-2, 3+bounce); ctx.lineTo(-3, 8+bounce+legSwing); ctx.stroke();
1535
+ ctx.beginPath(); ctx.moveTo(2, 3+bounce); ctx.lineTo(3, 8+bounce-legSwing); ctx.stroke();
1536
+
1537
+ // Feet
1538
+ ctx.fillStyle = '#2a2a2a';
1539
+ ctx.fillRect(-4.5, 7+bounce+legSwing, 3, 1.5);
1540
+ ctx.fillRect(1.5, 7+bounce-legSwing, 3, 1.5);
1541
 
1542
  // Arms
1543
+ ctx.strokeStyle = skin; ctx.lineWidth = 1.5;
1544
+ ctx.beginPath(); ctx.moveTo(-3.5, -6+bounce); ctx.lineTo(-7, 0+bounce+armSwing); ctx.stroke();
1545
+ ctx.beginPath(); ctx.moveTo(3.5, -6+bounce); ctx.lineTo(7, 0+bounce-armSwing); ctx.stroke();
1546
 
1547
+ // Head (slightly offset for 2.5D)
1548
+ ctx.fillStyle = skin;
1549
+ ctx.beginPath(); ctx.arc(0.5, -13+bounce, 5, 0, 6.28); ctx.fill();
1550
+ // Head side shading
1551
+ ctx.fillStyle = dim(skin, 0.85);
1552
+ ctx.beginPath(); ctx.arc(2, -13+bounce, 4.5, -0.3, 1.2); ctx.fill();
1553
 
1554
+ // Hair
1555
+ const hairColor = dim(color, 0.5);
1556
+ ctx.fillStyle = hairColor;
1557
+ if (gender==='female') {
1558
+ ctx.beginPath(); ctx.ellipse(0.5, -15.5+bounce, 6, 3.5, 0, Math.PI, 0); ctx.fill();
1559
+ ctx.beginPath(); ctx.ellipse(-3, -11+bounce, 2, 5, 0.3, 0, 6.28); ctx.fill();
1560
+ ctx.beginPath(); ctx.ellipse(4, -11+bounce, 2, 5, -0.3, 0, 6.28); ctx.fill();
1561
+ } else if (gender==='male') {
1562
+ ctx.beginPath(); ctx.ellipse(0.5, -15.5+bounce, 5.5, 3, 0, Math.PI, 0); ctx.fill();
1563
+ } else {
1564
+ ctx.beginPath(); ctx.ellipse(0.5, -15.5+bounce, 6, 3.5, 0, Math.PI, 0); ctx.fill();
1565
+ ctx.beginPath(); ctx.ellipse(-3.5, -12+bounce, 1.5, 4, 0.2, 0, 6.28); ctx.fill();
1566
+ }
1567
+
1568
+ // Eyes (tiny dots)
1569
+ ctx.fillStyle = '#222';
1570
+ ctx.fillRect(-1.5, -14+bounce, 1, 1);
1571
+ ctx.fillRect(2, -14+bounce, 1, 1);
1572
 
1573
  ctx.shadowColor='transparent'; ctx.shadowBlur=0;
1574
 
1575
  // State effects
1576
  if (agent.state==='in_conversation') {
1577
  ctx.fillStyle='rgba(240,192,64,0.85)';
1578
+ ctx.beginPath(); ctx.ellipse(10, -17+bounce, 7, 5, 0, 0, 6.28); ctx.fill();
1579
+ ctx.fillStyle='#1a1a2e'; ctx.font='bold 6px Segoe UI'; ctx.textAlign='center'; ctx.textBaseline='middle';
1580
+ ctx.fillText('...', 10, -17+bounce);
1581
  }
1582
 
1583
  if (isSleeping) {
1584
  const t=animFrame*0.04;
1585
+ ctx.font='bold 7px Segoe UI'; ctx.textAlign='left';
1586
  for (let i=0;i<3;i++) {
1587
  ctx.globalAlpha=0.3+i*0.25; ctx.fillStyle='#8ab4f8';
1588
+ ctx.fillText('z', 7+i*3, -18-i*5+Math.sin(t+i)*2);
1589
  }
1590
  ctx.globalAlpha=1;
1591
  }
1592
 
1593
  if (agent.partner_id) {
1594
+ drawHeart(0, -22+bounce+Math.sin(animFrame*0.04)*2, 3, 'rgba(233,30,144,0.7)');
1595
  }
1596
 
1597
  ctx.restore();
1598
 
1599
+ // Name label (smaller)
1600
  const firstName = (agent.name||id).split(' ')[0];
1601
+ ctx.font=`${isSel?'bold ':''}8px Segoe UI`; ctx.textAlign='center'; ctx.textBaseline='top';
1602
+ ctx.fillStyle='rgba(0,0,0,0.45)'; ctx.fillText(firstName, ax+1, ay+10*scale+1);
1603
+ ctx.fillStyle=isSel?'#fff':'#c0c0d0'; ctx.fillText(firstName, ax, ay+10*scale);
1604
 
1605
+ // Mood bar (smaller)
1606
  const mood=agent.mood||0;
1607
+ const mw=14, mx=ax-mw/2, my=ay+10*scale+11;
1608
+ ctx.fillStyle='rgba(15,52,96,0.4)'; ctx.fillRect(mx,my,mw,2);
1609
  const mf=(mood+1)/2;
1610
  ctx.fillStyle=mf>0.6?'#4ecca3':(mf>0.3?'#f0c040':'#e94560');
1611
+ ctx.fillRect(mx,my,mw*mf,2);
1612
  }
1613
 
1614
  // ============================================================