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>
- 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
|
|
|
|
| 834 |
const c = hues[idx];
|
| 835 |
|
| 836 |
-
|
| 837 |
-
|
| 838 |
-
|
| 839 |
-
|
|
|
|
| 840 |
ctx.fillStyle = dk ? dim(c.roof, 0.4) : c.roof;
|
| 841 |
ctx.beginPath();
|
| 842 |
-
ctx.moveTo(x-w/2-
|
| 843 |
-
ctx.lineTo(x,
|
| 844 |
-
ctx.lineTo(x+w/2+
|
| 845 |
-
ctx.closePath();
|
| 846 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 847 |
// Door
|
| 848 |
ctx.fillStyle = dk ? dim(c.door, 0.4) : c.door;
|
| 849 |
-
ctx.fillRect(x-3,
|
| 850 |
-
// Doorknob
|
| 851 |
ctx.fillStyle = dk ? '#aa9060' : '#d4b070';
|
| 852 |
-
ctx.beginPath(); ctx.arc(x+2,
|
| 853 |
-
|
|
|
|
| 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,
|
| 857 |
-
ctx.fillRect(x+w/2-12,
|
| 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,
|
| 862 |
-
ctx.strokeRect(x+w/2-12,
|
|
|
|
| 863 |
// Chimney
|
| 864 |
ctx.fillStyle = dk ? dim(c.roof, 0.35) : dim(c.roof, 0.8);
|
| 865 |
-
ctx.fillRect(x+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 |
-
|
| 875 |
-
|
| 876 |
-
|
|
|
|
| 877 |
const awningColors = ['#c44', '#4a8', '#48a', '#a84'];
|
| 878 |
const ci = Math.floor(x*7 + y*3) % awningColors.length;
|
| 879 |
-
|
|
|
|
| 880 |
ctx.beginPath();
|
| 881 |
-
ctx.moveTo(x-w/2-3,
|
| 882 |
-
ctx.lineTo(x-w/2-6,
|
| 883 |
-
ctx.lineTo(x+w/2+6,
|
| 884 |
-
ctx.lineTo(x+w/2+3,
|
| 885 |
-
ctx.closePath();
|
| 886 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|
| 893 |
}
|
| 894 |
// Door
|
| 895 |
ctx.fillStyle = dk ? '#2a2520' : '#5a4a3a';
|
| 896 |
-
ctx.fillRect(x-4,
|
| 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,
|
| 901 |
-
ctx.fillRect(x+5,
|
| 902 |
}
|
| 903 |
|
| 904 |
function drawOffice(x, y, dk) {
|
| 905 |
const w = 50, h = 40;
|
| 906 |
-
|
| 907 |
-
|
| 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,
|
| 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 |
-
|
| 919 |
-
|
| 920 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 921 |
}
|
| 922 |
}
|
| 923 |
|
| 924 |
function drawPublicBuilding(x, y, dk) {
|
| 925 |
const w = 46, h = 34;
|
| 926 |
-
|
| 927 |
-
|
| 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,
|
| 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,
|
| 935 |
-
for (let c = 0; c < 4; c++) ctx.fillRect(x-w/2+4+c*11,
|
| 936 |
}
|
| 937 |
|
| 938 |
function drawPark(x, y, dk) {
|
| 939 |
-
//
|
| 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 =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 944 |
// Pond
|
| 945 |
ctx.fillStyle = dk ? '#1a2a3a' : '#5a9ac0';
|
| 946 |
-
ctx.beginPath(); ctx.ellipse(x+15, y+
|
| 947 |
ctx.strokeStyle = dk ? '#2a3a4a' : '#80b0d0'; ctx.lineWidth = 1; ctx.stroke();
|
| 948 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 949 |
ctx.fillStyle = dk ? '#3a2a15' : '#7a5a30';
|
| 950 |
-
ctx.fillRect(x-
|
| 951 |
-
ctx.
|
| 952 |
-
ctx.fillRect(x-
|
| 953 |
-
// Trees in park
|
| 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 |
-
|
| 969 |
-
|
| 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,
|
| 976 |
-
ctx.fillStyle = '#e94560'; ctx.beginPath(); ctx.arc(x,
|
| 977 |
// Door
|
| 978 |
ctx.fillStyle = dk ? '#0a0e18' : '#384858';
|
| 979 |
-
ctx.fillRect(x-5,
|
| 980 |
-
//
|
| 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,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 986 |
}
|
| 987 |
|
| 988 |
function drawFactory(x, y, dk) {
|
| 989 |
const w = 56, h = 34;
|
| 990 |
-
|
| 991 |
-
|
| 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,
|
| 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,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 =
|
| 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,
|
| 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,
|
| 1019 |
}
|
| 1020 |
|
| 1021 |
function drawSchool(x, y, dk) {
|
| 1022 |
const w = 52, h = 36;
|
| 1023 |
-
|
| 1024 |
-
|
| 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,
|
| 1031 |
-
// Flag
|
| 1032 |
ctx.fillStyle = '#e94560';
|
| 1033 |
ctx.beginPath();
|
| 1034 |
-
ctx.moveTo(x+w/2-4,
|
| 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,
|
| 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,
|
| 1047 |
}
|
| 1048 |
|
| 1049 |
function drawHospital(x, y, dk) {
|
| 1050 |
const w = 50, h = 40;
|
| 1051 |
-
|
| 1052 |
-
|
| 1053 |
-
|
| 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,
|
| 1060 |
-
ctx.fillRect(x-7,
|
| 1061 |
// Door
|
| 1062 |
ctx.fillStyle = dk ? '#0a1018' : '#4a5a6a';
|
| 1063 |
-
ctx.fillRect(x-5,
|
| 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,
|
| 1070 |
}
|
| 1071 |
|
| 1072 |
function drawChurch(x, y, dk) {
|
| 1073 |
const w = 40, h = 36;
|
| 1074 |
-
|
| 1075 |
-
|
| 1076 |
-
|
| 1077 |
-
|
|
|
|
|
|
|
| 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,
|
| 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,
|
| 1089 |
-
ctx.fillRect(x-4,
|
| 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,
|
| 1093 |
-
//
|
| 1094 |
ctx.fillStyle = dk ? '#1a1518' : '#5a4a3a';
|
| 1095 |
-
ctx.fillRect(x-5,
|
| 1096 |
-
ctx.beginPath(); ctx.arc(x,
|
| 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,
|
| 1101 |
-
ctx.fillRect(x+w/2-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,
|
| 1105 |
-
ctx.fillStyle = dk ? '#a0a8c0' : '#fff'; ctx.fillText("St. Mary's", x,
|
| 1106 |
}
|
| 1107 |
|
| 1108 |
function drawCinema(x, y, dk) {
|
| 1109 |
const w = 48, h = 34;
|
| 1110 |
-
|
| 1111 |
-
|
| 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,
|
| 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,
|
| 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,
|
| 1129 |
// Door
|
| 1130 |
ctx.fillStyle = dk ? '#1a0818' : '#3a1830';
|
| 1131 |
-
ctx.fillRect(x-5,
|
| 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,
|
| 1136 |
-
ctx.fillRect(x+w/2-16,
|
| 1137 |
}
|
| 1138 |
|
| 1139 |
function drawApartment(x, y, dk) {
|
| 1140 |
const w = 38, h = 48;
|
| 1141 |
-
|
| 1142 |
-
|
| 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,
|
| 1150 |
-
//
|
| 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.
|
|
|
|
| 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 =
|
| 1167 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1168 |
ctx.fillStyle = dk ? '#1a2a3a' : '#708898';
|
| 1169 |
-
ctx.beginPath(); ctx.ellipse(x, y, 12,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1170 |
ctx.fillStyle = dk ? '#2a3a5a' : '#88b0d0';
|
| 1171 |
-
ctx.beginPath(); ctx.ellipse(x, y-
|
| 1172 |
// Water spray
|
| 1173 |
if (!dk || animFrame % 3 < 2) {
|
| 1174 |
ctx.fillStyle = `rgba(100,180,220,${dk?0.3:0.5})`;
|
| 1175 |
-
|
|
|
|
|
|
|
|
|
|
| 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+
|
| 1187 |
-
ctx.fillStyle = dk ? '#a0a8b0' : '#fff'; ctx.fillText('Town Square', x, y+
|
| 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.
|
| 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=
|
| 1349 |
|
| 1350 |
-
const bounce = (isMoving||moving) ? Math.sin(animFrame*0.
|
| 1351 |
-
const armSwing = (isMoving||moving) ? Math.sin(animFrame*0.
|
| 1352 |
-
const legSwing = (isMoving||moving) ? Math.sin(animFrame*0.
|
| 1353 |
|
| 1354 |
-
//
|
| 1355 |
-
ctx.fillStyle = 'rgba(0,0,0,0.
|
| 1356 |
-
ctx.beginPath(); ctx.ellipse(
|
| 1357 |
|
| 1358 |
-
//
|
| 1359 |
-
|
| 1360 |
-
|
|
|
|
| 1361 |
|
| 1362 |
-
//
|
| 1363 |
-
|
| 1364 |
if (gender==='female') {
|
| 1365 |
-
ctx.
|
| 1366 |
-
ctx.beginPath();
|
| 1367 |
-
ctx.
|
| 1368 |
-
|
| 1369 |
-
ctx.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1370 |
} else {
|
| 1371 |
-
|
| 1372 |
-
ctx.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1373 |
}
|
| 1374 |
|
| 1375 |
-
//
|
| 1376 |
-
|
| 1377 |
-
|
| 1378 |
-
|
| 1379 |
-
|
| 1380 |
-
|
| 1381 |
-
|
| 1382 |
-
|
|
|
|
|
|
|
| 1383 |
|
| 1384 |
// Arms
|
| 1385 |
-
ctx.strokeStyle=
|
| 1386 |
-
ctx.beginPath(); ctx.moveTo(-
|
| 1387 |
-
ctx.beginPath(); ctx.moveTo(
|
| 1388 |
|
| 1389 |
-
//
|
| 1390 |
-
ctx.
|
| 1391 |
-
ctx.beginPath(); ctx.
|
| 1392 |
-
|
|
|
|
|
|
|
| 1393 |
|
| 1394 |
-
//
|
| 1395 |
-
|
| 1396 |
-
ctx.
|
| 1397 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 1405 |
-
ctx.fillStyle='#1a1a2e'; ctx.font='bold
|
| 1406 |
-
ctx.fillText('...',
|
| 1407 |
}
|
| 1408 |
|
| 1409 |
if (isSleeping) {
|
| 1410 |
const t=animFrame*0.04;
|
| 1411 |
-
ctx.font='bold
|
| 1412 |
for (let i=0;i<3;i++) {
|
| 1413 |
ctx.globalAlpha=0.3+i*0.25; ctx.fillStyle='#8ab4f8';
|
| 1414 |
-
ctx.fillText('z',
|
| 1415 |
}
|
| 1416 |
ctx.globalAlpha=1;
|
| 1417 |
}
|
| 1418 |
|
| 1419 |
if (agent.partner_id) {
|
| 1420 |
-
drawHeart(0, -
|
| 1421 |
}
|
| 1422 |
|
| 1423 |
ctx.restore();
|
| 1424 |
|
| 1425 |
-
// Name label
|
| 1426 |
const firstName = (agent.name||id).split(' ')[0];
|
| 1427 |
-
ctx.font=`${isSel?'bold ':''}
|
| 1428 |
-
ctx.fillStyle='rgba(0,0,0,0.
|
| 1429 |
-
ctx.fillStyle=isSel?'#fff':'#
|
| 1430 |
|
| 1431 |
-
// Mood bar
|
| 1432 |
const mood=agent.mood||0;
|
| 1433 |
-
const mw=
|
| 1434 |
-
ctx.fillStyle='rgba(15,52,96,0.
|
| 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,
|
| 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 |
// ============================================================
|