let-them-talk 4.2.0 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +640 -540
- package/README.md +592 -415
- package/cli.js +1089 -589
- package/conversation-templates/autonomous-feature.json +22 -0
- package/conversation-templates/code-review.json +21 -11
- package/conversation-templates/debug-squad.json +21 -11
- package/conversation-templates/feature-build.json +21 -11
- package/conversation-templates/research-write.json +21 -11
- package/dashboard.html +9250 -7771
- package/dashboard.js +1232 -29
- package/office/agents.js +148 -4
- package/office/animation.js +68 -0
- package/office/assets.js +431 -0
- package/office/builder.js +355 -0
- package/office/building-interior.js +261 -0
- package/office/campus-env.js +119 -23
- package/office/car-hud.js +368 -0
- package/office/daynight.js +221 -0
- package/office/economy-hud.js +432 -0
- package/office/economy-ui.js +238 -0
- package/office/environment.js +818 -808
- package/office/face.js +65 -0
- package/office/fast-travel.js +215 -0
- package/office/hq-building.js +295 -0
- package/office/index.js +1095 -423
- package/office/instancing.js +160 -0
- package/office/lod-manager.js +165 -0
- package/office/multiplayer-hud.js +428 -0
- package/office/net-client.js +299 -0
- package/office/particles.js +172 -0
- package/office/player.js +658 -436
- package/office/post-processing.js +82 -0
- package/office/sky.js +319 -0
- package/office/street-furniture.js +308 -0
- package/office/vehicle.js +455 -0
- package/office/world-save.js +91 -0
- package/package.json +59 -59
- package/server.js +7190 -4685
- package/conversation-templates/managed-team.json +0 -12
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import { S } from './state.js';
|
|
3
|
+
|
|
4
|
+
// ============================================================
|
|
5
|
+
// DAY/NIGHT CYCLE — dynamic lighting, sky colors, atmosphere
|
|
6
|
+
// Phase 4: Persistent city feels alive 24/7
|
|
7
|
+
// Target: minimal performance impact, 120fps compatible
|
|
8
|
+
// ============================================================
|
|
9
|
+
|
|
10
|
+
var GAME_HOUR_SECONDS = 60; // 1 game hour = 60 real seconds (24 min full cycle)
|
|
11
|
+
var gameTime = 8.0; // start at 8 AM
|
|
12
|
+
|
|
13
|
+
var sunLight = null;
|
|
14
|
+
var moonLight = null;
|
|
15
|
+
var ambientLight = null;
|
|
16
|
+
var streetLights = [];
|
|
17
|
+
var windowMeshes = [];
|
|
18
|
+
|
|
19
|
+
// Sky color palette (lerp between these based on time)
|
|
20
|
+
var SKY_COLORS = {
|
|
21
|
+
dawn: new THREE.Color(0x2a1a3a), // 5-7
|
|
22
|
+
morning: new THREE.Color(0x4488cc), // 7-10
|
|
23
|
+
midday: new THREE.Color(0x5599dd), // 10-14
|
|
24
|
+
afternoon: new THREE.Color(0x6688bb), // 14-17
|
|
25
|
+
sunset: new THREE.Color(0xcc6633), // 17-19
|
|
26
|
+
dusk: new THREE.Color(0x1a1a3a), // 19-21
|
|
27
|
+
night: new THREE.Color(0x0a0e14), // 21-5
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
var FOG_COLORS = {
|
|
31
|
+
day: new THREE.Color(0x88aacc),
|
|
32
|
+
night: new THREE.Color(0x0a0e14),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// ============================================================
|
|
36
|
+
// INITIALIZATION
|
|
37
|
+
// ============================================================
|
|
38
|
+
|
|
39
|
+
export function initDayNight(options) {
|
|
40
|
+
options = options || {};
|
|
41
|
+
GAME_HOUR_SECONDS = options.hourSeconds || 60;
|
|
42
|
+
gameTime = options.startHour || 8.0;
|
|
43
|
+
|
|
44
|
+
// Find or create sun directional light
|
|
45
|
+
S.furnitureGroup.traverse(function(child) {
|
|
46
|
+
if (child.isDirectionalLight && child.castShadow && !sunLight) {
|
|
47
|
+
sunLight = child;
|
|
48
|
+
}
|
|
49
|
+
if (child.isDirectionalLight && !child.castShadow && !moonLight) {
|
|
50
|
+
moonLight = child;
|
|
51
|
+
}
|
|
52
|
+
if (child.isAmbientLight && !ambientLight) {
|
|
53
|
+
ambientLight = child;
|
|
54
|
+
}
|
|
55
|
+
// Collect street light bulbs (emissive meshes near light poles)
|
|
56
|
+
if (child.isPointLight) {
|
|
57
|
+
streetLights.push(child);
|
|
58
|
+
}
|
|
59
|
+
// Collect window meshes (identified by window material color)
|
|
60
|
+
if (child.isMesh && child.material && child.material.emissive &&
|
|
61
|
+
child.material.color && child.material.color.getHex() === 0x88bbee) {
|
|
62
|
+
windowMeshes.push(child);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ============================================================
|
|
68
|
+
// TIME HELPERS
|
|
69
|
+
// ============================================================
|
|
70
|
+
|
|
71
|
+
export function getGameTime() { return gameTime; }
|
|
72
|
+
export function getGameHour() { return Math.floor(gameTime) % 24; }
|
|
73
|
+
|
|
74
|
+
export function getTimeOfDay() {
|
|
75
|
+
var h = gameTime % 24;
|
|
76
|
+
if (h >= 5 && h < 7) return 'dawn';
|
|
77
|
+
if (h >= 7 && h < 10) return 'morning';
|
|
78
|
+
if (h >= 10 && h < 14) return 'midday';
|
|
79
|
+
if (h >= 14 && h < 17) return 'afternoon';
|
|
80
|
+
if (h >= 17 && h < 19) return 'sunset';
|
|
81
|
+
if (h >= 19 && h < 21) return 'dusk';
|
|
82
|
+
return 'night';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function isNightTime() {
|
|
86
|
+
var h = gameTime % 24;
|
|
87
|
+
return h >= 21 || h < 5;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function getTimeFactor() {
|
|
91
|
+
// Returns 0.0 (midnight) to 1.0 (noon) for smooth interpolation
|
|
92
|
+
var h = gameTime % 24;
|
|
93
|
+
if (h <= 12) return h / 12;
|
|
94
|
+
return (24 - h) / 12;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ============================================================
|
|
98
|
+
// SKY COLOR INTERPOLATION
|
|
99
|
+
// ============================================================
|
|
100
|
+
|
|
101
|
+
function getSkyColor() {
|
|
102
|
+
var tod = getTimeOfDay();
|
|
103
|
+
return SKY_COLORS[tod] || SKY_COLORS.night;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function getFogColor() {
|
|
107
|
+
var factor = getTimeFactor();
|
|
108
|
+
var color = new THREE.Color();
|
|
109
|
+
color.lerpColors(FOG_COLORS.night, FOG_COLORS.day, factor);
|
|
110
|
+
return color;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ============================================================
|
|
114
|
+
// MAIN UPDATE — called every frame
|
|
115
|
+
// ============================================================
|
|
116
|
+
|
|
117
|
+
export function updateDayNight(dt) {
|
|
118
|
+
// Advance game time
|
|
119
|
+
gameTime += dt / GAME_HOUR_SECONDS;
|
|
120
|
+
if (gameTime >= 24) gameTime -= 24;
|
|
121
|
+
|
|
122
|
+
var factor = getTimeFactor(); // 0=midnight, 1=noon
|
|
123
|
+
var night = isNightTime();
|
|
124
|
+
var h = gameTime % 24;
|
|
125
|
+
|
|
126
|
+
// === SKY COLOR ===
|
|
127
|
+
var skyColor = getSkyColor();
|
|
128
|
+
if (S.scene.background) {
|
|
129
|
+
S.scene.background.lerp(skyColor, 0.02);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// === FOG ===
|
|
133
|
+
if (S.scene.fog) {
|
|
134
|
+
var fogColor = getFogColor();
|
|
135
|
+
S.scene.fog.color.lerp(fogColor, 0.02);
|
|
136
|
+
// Fog distance: closer at night (atmospheric), farther at day
|
|
137
|
+
S.scene.fog.near = THREE.MathUtils.lerp(30, 60, factor);
|
|
138
|
+
S.scene.fog.far = THREE.MathUtils.lerp(120, 250, factor);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// === SUN POSITION (orbits overhead) ===
|
|
142
|
+
if (sunLight) {
|
|
143
|
+
var sunAngle = ((h - 6) / 12) * Math.PI; // 6AM=horizon, 12PM=zenith, 6PM=horizon
|
|
144
|
+
sunLight.position.set(
|
|
145
|
+
Math.cos(sunAngle) * 80,
|
|
146
|
+
Math.max(Math.sin(sunAngle) * 60, -10),
|
|
147
|
+
50
|
|
148
|
+
);
|
|
149
|
+
// Sun intensity: bright during day, off at night
|
|
150
|
+
sunLight.intensity = Math.max(0, Math.sin(sunAngle)) * 0.8;
|
|
151
|
+
// Warm sunrise/sunset color
|
|
152
|
+
if (h >= 5 && h < 8) {
|
|
153
|
+
sunLight.color.setHex(0xffaa66); // warm sunrise
|
|
154
|
+
} else if (h >= 16 && h < 19) {
|
|
155
|
+
sunLight.color.setHex(0xff8844); // warm sunset
|
|
156
|
+
} else {
|
|
157
|
+
sunLight.color.setHex(0xffeedd); // neutral day
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// === MOON ===
|
|
162
|
+
if (moonLight) {
|
|
163
|
+
moonLight.intensity = night ? 0.2 : 0;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// === AMBIENT LIGHT ===
|
|
167
|
+
if (ambientLight) {
|
|
168
|
+
ambientLight.intensity = THREE.MathUtils.lerp(0.15, 0.5, factor);
|
|
169
|
+
if (night) {
|
|
170
|
+
ambientLight.color.setHex(0x334466); // cool blue ambient at night
|
|
171
|
+
} else {
|
|
172
|
+
ambientLight.color.setHex(0xffffff);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// === STREET LIGHTS (on at night) ===
|
|
177
|
+
var targetIntensity = night ? 0.6 : 0;
|
|
178
|
+
streetLights.forEach(function(light) {
|
|
179
|
+
light.intensity = THREE.MathUtils.lerp(light.intensity, targetIntensity, 0.05);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// === WINDOW GLOW (emissive at night — boosted for bloom) ===
|
|
183
|
+
var windowEmissive = night ? 1.5 : 0.2;
|
|
184
|
+
windowMeshes.forEach(function(mesh) {
|
|
185
|
+
if (mesh.material.emissiveIntensity !== undefined) {
|
|
186
|
+
mesh.material.emissiveIntensity = THREE.MathUtils.lerp(
|
|
187
|
+
mesh.material.emissiveIntensity, windowEmissive, 0.03
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Bloom disabled for performance
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ============================================================
|
|
196
|
+
// TIME CONTROL — manual set / speed adjustment
|
|
197
|
+
// ============================================================
|
|
198
|
+
|
|
199
|
+
export function setGameTime(hour) {
|
|
200
|
+
gameTime = hour % 24;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function setTimeSpeed(hourSeconds) {
|
|
204
|
+
GAME_HOUR_SECONDS = Math.max(1, hourSeconds);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function getTimeSpeed() {
|
|
208
|
+
return GAME_HOUR_SECONDS;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ============================================================
|
|
212
|
+
// CLEANUP
|
|
213
|
+
// ============================================================
|
|
214
|
+
|
|
215
|
+
export function disposeDayNight() {
|
|
216
|
+
sunLight = null;
|
|
217
|
+
moonLight = null;
|
|
218
|
+
ambientLight = null;
|
|
219
|
+
streetLights = [];
|
|
220
|
+
windowMeshes = [];
|
|
221
|
+
}
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Economy HUD — Credit balance, upgrade popup, transaction history.
|
|
3
|
+
* HTML overlay on top of the Three.js canvas.
|
|
4
|
+
* Always visible in city mode. Polls /api/city/economy for data.
|
|
5
|
+
* Target: zero canvas impact (pure HTML/CSS).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
let hudEl = null;
|
|
9
|
+
let popupEl = null;
|
|
10
|
+
let historyEl = null;
|
|
11
|
+
let pollInterval = null;
|
|
12
|
+
let visible = false;
|
|
13
|
+
let currentBalance = 0;
|
|
14
|
+
let historyVisible = false;
|
|
15
|
+
|
|
16
|
+
const ECON_STYLES = `
|
|
17
|
+
.econ-hud {
|
|
18
|
+
position: fixed;
|
|
19
|
+
top: 12px;
|
|
20
|
+
right: 12px;
|
|
21
|
+
z-index: 100;
|
|
22
|
+
pointer-events: auto;
|
|
23
|
+
font-family: 'Segoe UI', sans-serif;
|
|
24
|
+
opacity: 0;
|
|
25
|
+
transition: opacity 0.3s ease;
|
|
26
|
+
}
|
|
27
|
+
.econ-hud.visible { opacity: 1; }
|
|
28
|
+
|
|
29
|
+
.econ-balance {
|
|
30
|
+
background: rgba(0,0,0,0.7);
|
|
31
|
+
border: 1px solid rgba(212,175,55,0.4);
|
|
32
|
+
border-radius: 8px;
|
|
33
|
+
padding: 8px 16px;
|
|
34
|
+
display: flex;
|
|
35
|
+
align-items: center;
|
|
36
|
+
gap: 8px;
|
|
37
|
+
cursor: pointer;
|
|
38
|
+
user-select: none;
|
|
39
|
+
backdrop-filter: blur(8px);
|
|
40
|
+
}
|
|
41
|
+
.econ-balance:hover { border-color: rgba(212,175,55,0.8); }
|
|
42
|
+
.econ-coin {
|
|
43
|
+
width: 20px;
|
|
44
|
+
height: 20px;
|
|
45
|
+
border-radius: 50%;
|
|
46
|
+
background: linear-gradient(135deg, #FFD700 0%, #D4AF37 50%, #B8860B 100%);
|
|
47
|
+
box-shadow: 0 0 6px rgba(212,175,55,0.5);
|
|
48
|
+
display: flex;
|
|
49
|
+
align-items: center;
|
|
50
|
+
justify-content: center;
|
|
51
|
+
font-size: 11px;
|
|
52
|
+
font-weight: 700;
|
|
53
|
+
color: #000;
|
|
54
|
+
}
|
|
55
|
+
.econ-amount {
|
|
56
|
+
font-size: 18px;
|
|
57
|
+
font-weight: 700;
|
|
58
|
+
font-variant-numeric: tabular-nums;
|
|
59
|
+
color: #FFD700;
|
|
60
|
+
text-shadow: 0 0 6px rgba(212,175,55,0.4);
|
|
61
|
+
}
|
|
62
|
+
.econ-label {
|
|
63
|
+
font-size: 10px;
|
|
64
|
+
color: #aaa;
|
|
65
|
+
text-transform: uppercase;
|
|
66
|
+
letter-spacing: 1px;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.econ-popup {
|
|
70
|
+
position: fixed;
|
|
71
|
+
top: 50%;
|
|
72
|
+
left: 50%;
|
|
73
|
+
transform: translate(-50%, -50%);
|
|
74
|
+
background: rgba(20,20,20,0.95);
|
|
75
|
+
border: 1px solid rgba(212,175,55,0.5);
|
|
76
|
+
border-radius: 12px;
|
|
77
|
+
padding: 24px;
|
|
78
|
+
min-width: 320px;
|
|
79
|
+
max-width: 400px;
|
|
80
|
+
z-index: 200;
|
|
81
|
+
pointer-events: auto;
|
|
82
|
+
backdrop-filter: blur(12px);
|
|
83
|
+
font-family: 'Segoe UI', sans-serif;
|
|
84
|
+
color: #fff;
|
|
85
|
+
display: none;
|
|
86
|
+
}
|
|
87
|
+
.econ-popup.open { display: block; }
|
|
88
|
+
.econ-popup-title {
|
|
89
|
+
font-size: 16px;
|
|
90
|
+
font-weight: 700;
|
|
91
|
+
color: #FFD700;
|
|
92
|
+
margin-bottom: 16px;
|
|
93
|
+
display: flex;
|
|
94
|
+
justify-content: space-between;
|
|
95
|
+
align-items: center;
|
|
96
|
+
}
|
|
97
|
+
.econ-popup-close {
|
|
98
|
+
cursor: pointer;
|
|
99
|
+
font-size: 18px;
|
|
100
|
+
color: #888;
|
|
101
|
+
background: none;
|
|
102
|
+
border: none;
|
|
103
|
+
padding: 4px 8px;
|
|
104
|
+
}
|
|
105
|
+
.econ-popup-close:hover { color: #fff; }
|
|
106
|
+
|
|
107
|
+
.econ-upgrade-item {
|
|
108
|
+
display: flex;
|
|
109
|
+
justify-content: space-between;
|
|
110
|
+
align-items: center;
|
|
111
|
+
padding: 10px 0;
|
|
112
|
+
border-bottom: 1px solid rgba(255,255,255,0.08);
|
|
113
|
+
}
|
|
114
|
+
.econ-upgrade-item:last-child { border-bottom: none; }
|
|
115
|
+
.econ-upgrade-name {
|
|
116
|
+
font-size: 13px;
|
|
117
|
+
color: #eee;
|
|
118
|
+
}
|
|
119
|
+
.econ-upgrade-desc {
|
|
120
|
+
font-size: 11px;
|
|
121
|
+
color: #888;
|
|
122
|
+
margin-top: 2px;
|
|
123
|
+
}
|
|
124
|
+
.econ-upgrade-btn {
|
|
125
|
+
background: linear-gradient(135deg, #D4AF37, #B8860B);
|
|
126
|
+
border: none;
|
|
127
|
+
border-radius: 6px;
|
|
128
|
+
padding: 6px 14px;
|
|
129
|
+
color: #000;
|
|
130
|
+
font-weight: 700;
|
|
131
|
+
font-size: 12px;
|
|
132
|
+
cursor: pointer;
|
|
133
|
+
white-space: nowrap;
|
|
134
|
+
}
|
|
135
|
+
.econ-upgrade-btn:hover { filter: brightness(1.2); }
|
|
136
|
+
.econ-upgrade-btn:disabled {
|
|
137
|
+
opacity: 0.4;
|
|
138
|
+
cursor: not-allowed;
|
|
139
|
+
filter: none;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.econ-history {
|
|
143
|
+
position: fixed;
|
|
144
|
+
top: 60px;
|
|
145
|
+
right: 12px;
|
|
146
|
+
width: 280px;
|
|
147
|
+
max-height: 300px;
|
|
148
|
+
overflow-y: auto;
|
|
149
|
+
background: rgba(0,0,0,0.85);
|
|
150
|
+
border: 1px solid rgba(212,175,55,0.3);
|
|
151
|
+
border-radius: 8px;
|
|
152
|
+
padding: 12px;
|
|
153
|
+
z-index: 150;
|
|
154
|
+
pointer-events: auto;
|
|
155
|
+
font-family: 'Segoe UI', sans-serif;
|
|
156
|
+
display: none;
|
|
157
|
+
backdrop-filter: blur(8px);
|
|
158
|
+
}
|
|
159
|
+
.econ-history.open { display: block; }
|
|
160
|
+
.econ-history-title {
|
|
161
|
+
font-size: 11px;
|
|
162
|
+
color: #D4AF37;
|
|
163
|
+
text-transform: uppercase;
|
|
164
|
+
letter-spacing: 1px;
|
|
165
|
+
margin-bottom: 8px;
|
|
166
|
+
}
|
|
167
|
+
.econ-tx {
|
|
168
|
+
display: flex;
|
|
169
|
+
justify-content: space-between;
|
|
170
|
+
padding: 4px 0;
|
|
171
|
+
font-size: 11px;
|
|
172
|
+
border-bottom: 1px solid rgba(255,255,255,0.05);
|
|
173
|
+
}
|
|
174
|
+
.econ-tx-desc { color: #ccc; }
|
|
175
|
+
.econ-tx-amount.credit { color: #3fb950; }
|
|
176
|
+
.econ-tx-amount.debit { color: #f85149; }
|
|
177
|
+
.econ-tx-time { color: #666; font-size: 10px; }
|
|
178
|
+
|
|
179
|
+
.econ-credit-pop {
|
|
180
|
+
position: fixed;
|
|
181
|
+
top: 20%;
|
|
182
|
+
left: 50%;
|
|
183
|
+
transform: translateX(-50%);
|
|
184
|
+
font-size: 24px;
|
|
185
|
+
font-weight: 700;
|
|
186
|
+
color: #3fb950;
|
|
187
|
+
text-shadow: 0 0 10px rgba(63,185,80,0.6);
|
|
188
|
+
pointer-events: none;
|
|
189
|
+
z-index: 300;
|
|
190
|
+
animation: creditPop 1.5s ease-out forwards;
|
|
191
|
+
}
|
|
192
|
+
@keyframes creditPop {
|
|
193
|
+
0% { opacity: 1; transform: translateX(-50%) translateY(0); }
|
|
194
|
+
100% { opacity: 0; transform: translateX(-50%) translateY(-40px); }
|
|
195
|
+
}
|
|
196
|
+
`;
|
|
197
|
+
|
|
198
|
+
const UPGRADES = [
|
|
199
|
+
{ id: 'speed_boost', name: 'Speed Boost', desc: 'Increase car max speed by 20%', cost: 50 },
|
|
200
|
+
{ id: 'building_glow', name: 'Building Neon', desc: 'Add neon glow to your buildings', cost: 100 },
|
|
201
|
+
{ id: 'extra_car', name: 'Extra Vehicle', desc: 'Spawn an additional car', cost: 75 },
|
|
202
|
+
{ id: 'night_vision', name: 'Night Mode', desc: 'Toggle day/night cycle', cost: 30 },
|
|
203
|
+
];
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Initialize the economy HUD. Call once.
|
|
207
|
+
*/
|
|
208
|
+
export function initEconomyHUD() {
|
|
209
|
+
if (hudEl) return;
|
|
210
|
+
|
|
211
|
+
const style = document.createElement('style');
|
|
212
|
+
style.textContent = ECON_STYLES;
|
|
213
|
+
document.head.appendChild(style);
|
|
214
|
+
|
|
215
|
+
// Balance display
|
|
216
|
+
hudEl = document.createElement('div');
|
|
217
|
+
hudEl.className = 'econ-hud';
|
|
218
|
+
hudEl.innerHTML = `
|
|
219
|
+
<div class="econ-balance" id="econ-balance-btn">
|
|
220
|
+
<div class="econ-coin">C</div>
|
|
221
|
+
<div>
|
|
222
|
+
<div class="econ-amount" id="econ-amount">0</div>
|
|
223
|
+
<div class="econ-label">Credits</div>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
`;
|
|
227
|
+
document.body.appendChild(hudEl);
|
|
228
|
+
|
|
229
|
+
// Transaction history panel
|
|
230
|
+
historyEl = document.createElement('div');
|
|
231
|
+
historyEl.className = 'econ-history';
|
|
232
|
+
historyEl.id = 'econ-history';
|
|
233
|
+
historyEl.innerHTML = `
|
|
234
|
+
<div class="econ-history-title">Transaction History</div>
|
|
235
|
+
<div id="econ-tx-list"></div>
|
|
236
|
+
`;
|
|
237
|
+
document.body.appendChild(historyEl);
|
|
238
|
+
|
|
239
|
+
// Upgrade popup
|
|
240
|
+
popupEl = document.createElement('div');
|
|
241
|
+
popupEl.className = 'econ-popup';
|
|
242
|
+
popupEl.id = 'econ-popup';
|
|
243
|
+
popupEl.innerHTML = `
|
|
244
|
+
<div class="econ-popup-title">
|
|
245
|
+
<span>Upgrade Shop</span>
|
|
246
|
+
<button class="econ-popup-close" id="econ-popup-close">×</button>
|
|
247
|
+
</div>
|
|
248
|
+
<div id="econ-upgrade-list"></div>
|
|
249
|
+
`;
|
|
250
|
+
document.body.appendChild(popupEl);
|
|
251
|
+
|
|
252
|
+
// Click handlers
|
|
253
|
+
document.getElementById('econ-balance-btn').addEventListener('click', toggleHistory);
|
|
254
|
+
document.getElementById('econ-popup-close').addEventListener('click', closeUpgradePopup);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Show the economy HUD (when entering city environment).
|
|
259
|
+
*/
|
|
260
|
+
export function showEconomyHUD() {
|
|
261
|
+
if (!hudEl) initEconomyHUD();
|
|
262
|
+
hudEl.classList.add('visible');
|
|
263
|
+
visible = true;
|
|
264
|
+
startPolling();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Hide the economy HUD.
|
|
269
|
+
*/
|
|
270
|
+
export function hideEconomyHUD() {
|
|
271
|
+
if (hudEl) hudEl.classList.remove('visible');
|
|
272
|
+
if (historyEl) historyEl.classList.remove('open');
|
|
273
|
+
if (popupEl) popupEl.classList.remove('open');
|
|
274
|
+
visible = false;
|
|
275
|
+
historyVisible = false;
|
|
276
|
+
stopPolling();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Update the balance display.
|
|
281
|
+
* @param {number} balance
|
|
282
|
+
*/
|
|
283
|
+
export function updateBalance(balance) {
|
|
284
|
+
currentBalance = balance;
|
|
285
|
+
const el = document.getElementById('econ-amount');
|
|
286
|
+
if (el) el.textContent = balance.toLocaleString();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Show a credit earned popup animation.
|
|
291
|
+
* @param {number} amount
|
|
292
|
+
* @param {string} reason
|
|
293
|
+
*/
|
|
294
|
+
export function showCreditEarned(amount, reason) {
|
|
295
|
+
const pop = document.createElement('div');
|
|
296
|
+
pop.className = 'econ-credit-pop';
|
|
297
|
+
pop.textContent = '+' + amount + ' ' + (reason || '');
|
|
298
|
+
document.body.appendChild(pop);
|
|
299
|
+
setTimeout(function() { pop.remove(); }, 1500);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Open the upgrade shop popup.
|
|
304
|
+
*/
|
|
305
|
+
export function openUpgradePopup() {
|
|
306
|
+
if (!popupEl) return;
|
|
307
|
+
renderUpgrades();
|
|
308
|
+
popupEl.classList.add('open');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Close the upgrade shop popup.
|
|
313
|
+
*/
|
|
314
|
+
export function closeUpgradePopup() {
|
|
315
|
+
if (popupEl) popupEl.classList.remove('open');
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function toggleHistory() {
|
|
319
|
+
historyVisible = !historyVisible;
|
|
320
|
+
if (historyEl) {
|
|
321
|
+
historyEl.classList.toggle('open', historyVisible);
|
|
322
|
+
if (historyVisible) fetchEconomy();
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function renderUpgrades() {
|
|
327
|
+
const list = document.getElementById('econ-upgrade-list');
|
|
328
|
+
if (!list) return;
|
|
329
|
+
list.innerHTML = UPGRADES.map(function(u) {
|
|
330
|
+
const canAfford = currentBalance >= u.cost;
|
|
331
|
+
return '<div class="econ-upgrade-item">' +
|
|
332
|
+
'<div>' +
|
|
333
|
+
'<div class="econ-upgrade-name">' + escapeHtml(u.name) + '</div>' +
|
|
334
|
+
'<div class="econ-upgrade-desc">' + escapeHtml(u.desc) + '</div>' +
|
|
335
|
+
'</div>' +
|
|
336
|
+
'<button class="econ-upgrade-btn" ' + (canAfford ? '' : 'disabled') +
|
|
337
|
+
' onclick="window._econBuy(\'' + u.id + '\',' + u.cost + ')">' +
|
|
338
|
+
u.cost + ' C</button>' +
|
|
339
|
+
'</div>';
|
|
340
|
+
}).join('');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function renderTransactions(transactions) {
|
|
344
|
+
const list = document.getElementById('econ-tx-list');
|
|
345
|
+
if (!list) return;
|
|
346
|
+
if (!transactions || !transactions.length) {
|
|
347
|
+
list.innerHTML = '<div style="color:#666;font-size:11px">No transactions yet</div>';
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
list.innerHTML = transactions.slice(-20).reverse().map(function(tx) {
|
|
351
|
+
const isCredit = tx.amount > 0;
|
|
352
|
+
const timeStr = tx.timestamp ? new Date(tx.timestamp).toLocaleTimeString() : '';
|
|
353
|
+
return '<div class="econ-tx">' +
|
|
354
|
+
'<div>' +
|
|
355
|
+
'<div class="econ-tx-desc">' + escapeHtml(tx.reason || tx.type || 'transaction') + '</div>' +
|
|
356
|
+
'<div class="econ-tx-time">' + timeStr + '</div>' +
|
|
357
|
+
'</div>' +
|
|
358
|
+
'<div class="econ-tx-amount ' + (isCredit ? 'credit' : 'debit') + '">' +
|
|
359
|
+
(isCredit ? '+' : '') + tx.amount + '</div>' +
|
|
360
|
+
'</div>';
|
|
361
|
+
}).join('');
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Purchase an upgrade via the API.
|
|
366
|
+
*/
|
|
367
|
+
window._econBuy = function(upgradeId, cost) {
|
|
368
|
+
if (currentBalance < cost) return;
|
|
369
|
+
fetch('/api/city/economy/spend', {
|
|
370
|
+
method: 'POST',
|
|
371
|
+
headers: { 'Content-Type': 'application/json' },
|
|
372
|
+
body: JSON.stringify({ upgrade: upgradeId, cost: cost })
|
|
373
|
+
})
|
|
374
|
+
.then(function(r) { return r.json(); })
|
|
375
|
+
.then(function(data) {
|
|
376
|
+
if (data.success) {
|
|
377
|
+
updateBalance(data.balance);
|
|
378
|
+
renderUpgrades();
|
|
379
|
+
fetchEconomy();
|
|
380
|
+
}
|
|
381
|
+
})
|
|
382
|
+
.catch(function() {});
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
function startPolling() {
|
|
386
|
+
if (pollInterval) return;
|
|
387
|
+
fetchEconomy();
|
|
388
|
+
pollInterval = setInterval(fetchEconomy, 5000);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function stopPolling() {
|
|
392
|
+
if (pollInterval) {
|
|
393
|
+
clearInterval(pollInterval);
|
|
394
|
+
pollInterval = null;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function fetchEconomy() {
|
|
399
|
+
fetch('/api/city/economy')
|
|
400
|
+
.then(function(r) { return r.ok ? r.json() : null; })
|
|
401
|
+
.then(function(data) {
|
|
402
|
+
if (!data) return;
|
|
403
|
+
if (typeof data.total_credits === 'number') updateBalance(data.total_credits);
|
|
404
|
+
if (data.recent_transactions) renderTransactions(data.recent_transactions);
|
|
405
|
+
})
|
|
406
|
+
.catch(function() {});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Get current balance.
|
|
411
|
+
* @returns {number}
|
|
412
|
+
*/
|
|
413
|
+
export function getBalance() {
|
|
414
|
+
return currentBalance;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Dispose the economy HUD.
|
|
419
|
+
*/
|
|
420
|
+
export function disposeEconomyHUD() {
|
|
421
|
+
hideEconomyHUD();
|
|
422
|
+
if (hudEl) { hudEl.remove(); hudEl = null; }
|
|
423
|
+
if (popupEl) { popupEl.remove(); popupEl = null; }
|
|
424
|
+
if (historyEl) { historyEl.remove(); historyEl = null; }
|
|
425
|
+
delete window._econBuy;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function escapeHtml(str) {
|
|
429
|
+
const div = document.createElement('div');
|
|
430
|
+
div.textContent = str;
|
|
431
|
+
return div.innerHTML;
|
|
432
|
+
}
|