Skip to content

Commit 2af5be3

Browse files
mf4633claude
andcommitted
Tower Phase 2: hotels, restaurants, shops + bulldoze refund
New unit types: - Hotel Rm (3x1, $45k): guests arrive 6-10pm, check out 9am, pay $180/night per guest, capacity 2 - Restaurant (6x1, $180k): lunch rush 11:30-12:30, dinner rush 17:30-19:30, $22/diner, capacity 24 - Shop (4x1, $90k): patrons trickle 10am-9pm, stay 1-3 minutes, $14/visit, capacity 12 Each unit type has upkeep (hotel $120, restaurant $1200, shop $400) and its own render style. Agents color-coded: workers blue, residents red, hotel purple, patrons orange, shoppers green. Bulldoze now refunds 50% of the build cost. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 4a40d5d commit 2af5be3

1 file changed

Lines changed: 164 additions & 10 deletions

File tree

Tower.html

Lines changed: 164 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -152,15 +152,19 @@
152152
lobby: { name:'Lobby', w:8, h:1, cost:200000, rent:0, color:'#D8C28A', textColor:'#5A4A2A', groundOnly:true, capacity:0 },
153153
office: { name:'Office', w:4, h:1, cost:50000, rent:3000, color:'#8AA0D0', textColor:'#1A2A50', capacity:6 },
154154
condo: { name:'Condo', w:4, h:1, cost:70000, rent:2200, color:'#D0A090', textColor:'#5A2A20', capacity:3 },
155+
hotel: { name:'Hotel Rm', w:3, h:1, cost:45000, rent:0, color:'#C098B8', textColor:'#3A1A30', capacity:2, nightly:180 },
156+
restaurant:{ name:'Restaurant',w:6, h:1, cost:180000, rent:0, color:'#E0A860', textColor:'#5A3010', capacity:24, perVisit:22 },
157+
shop: { name:'Shop', w:4, h:1, cost:90000, rent:0, color:'#98D0A8', textColor:'#204028', capacity:12, perVisit:14 },
155158
stairs: { name:'Stairs', w:2, h:1, cost:15000, rent:0, color:'#909898', textColor:'#303838', capacity:0 },
156159
elevator: { name:'Elevator', w:1, h:1, cost:60000, rent:0, color:'#7A7A82', textColor:'#FFF', capacity:0, isElevator:true }
157160
};
158-
var PAL_ORDER = ['lobby','office','condo','stairs','elevator'];
161+
var PAL_ORDER = ['lobby','office','condo','hotel','restaurant','shop','stairs','elevator'];
159162

160163
/* ===== STATE ===== */
161164
var state = {
162165
cash: 2000000,
163166
yesterdayIncome: null,
167+
dayRevenue: 0,
164168
units: {}, // id → { id, type, x, y, w, h, tenants, lastRent }
165169
grid: {}, // "x,y" → unitId
166170
nextUnitId: 1,
@@ -323,22 +327,23 @@
323327
var uid = state.grid[gx+','+gy];
324328
if (!uid) return;
325329
var unit = state.units[uid];
326-
// Kick out occupants
327330
for (var i = state.agents.length - 1; i >= 0; i--) {
328331
if (state.agents[i].unitId === uid) state.agents.splice(i, 1);
329332
}
330-
// Remove grid cells
331333
for (var dx = 0; dx < unit.w; dx++) {
332334
for (var dy = 0; dy < unit.h; dy++) {
333335
delete state.grid[(unit.x+dx) + ',' + (unit.y+dy)];
334336
}
335337
}
336338
delete state.units[uid];
337339
var def = UNITS[unit.type];
340+
// 50% refund
341+
var refund = Math.round(def.cost * 0.5);
342+
state.cash += refund;
338343
if (def.isElevator) rebuildElevators();
339344
updatePopulation();
340345
updateStars();
341-
showToast('Bulldozed: ' + def.name);
346+
showToast('Bulldozed: ' + def.name + ' (+$' + formatMoney(refund) + ')');
342347
}
343348

344349
function rebuildElevators() {
@@ -399,12 +404,22 @@
399404
/* ===== SIMULATION ===== */
400405
function simMinute() {
401406
var h = Math.floor(state.minute / 60);
402-
// Spawn office workers at ~8am and have them leave at ~5pm
403407
if (state.minute === 8 * 60) spawnWorkers();
404408
if (state.minute === 17 * 60) releaseWorkers();
405-
// Condo residents: spawn at 6pm-ish, leave 8am
406409
if (state.minute === 18 * 60) spawnResidents();
407410
if (state.minute === 8 * 60 + 30) releaseResidents();
411+
// Hotel guests: arrive 6pm-10pm random, check out 8am-10am
412+
if (state.minute >= 18 * 60 && state.minute <= 22 * 60 && state.minute % 5 === 0) spawnHotelGuests();
413+
if (state.minute === 9 * 60) checkoutHotelGuests();
414+
// Restaurant rushes: noon-1pm (lunch), 6pm-8pm (dinner)
415+
if (state.minute === 11 * 60 + 30) startRushLunch();
416+
if (state.minute === 12 * 60 + 30) stopRushLunch();
417+
if (state.minute === 17 * 60 + 30) startRushDinner();
418+
if (state.minute === 19 * 60 + 30) stopRushDinner();
419+
// Shop patrons trickle 10am-9pm
420+
if (state.minute >= 10 * 60 && state.minute <= 21 * 60 && state.minute % 6 === 0) spawnShopPatrons();
421+
// Restaurant patrons trickle during rushes
422+
if ((rushActive.lunch || rushActive.dinner) && state.minute % 4 === 0) spawnRestaurantPatrons();
408423

409424
for (var i = state.agents.length - 1; i >= 0; i--) {
410425
tickAgent(state.agents[i]);
@@ -460,6 +475,85 @@
460475
}
461476
}
462477

478+
function spawnHotelGuests() {
479+
for (var id in state.units) {
480+
var u = state.units[id];
481+
if (u.type !== 'hotel') continue;
482+
var def = UNITS.hotel;
483+
var free = def.capacity - u.tenants;
484+
if (free <= 0) continue;
485+
if (Math.random() < 0.35) {
486+
state.agents.push({
487+
id: state.nextAgentId++, type: 'hotelGuest',
488+
x: GRID_W/2, y: 0, phase: 'toHome', unitId: u.id, walkDelay: Math.random() * 40,
489+
payment: def.nightly
490+
});
491+
u.tenants++;
492+
}
493+
}
494+
}
495+
function checkoutHotelGuests() {
496+
for (var i = 0; i < state.agents.length; i++) {
497+
var a = state.agents[i];
498+
if (a.type === 'hotelGuest') {
499+
a.phase = 'leaving';
500+
if (a.payment) { state.dayRevenue = (state.dayRevenue || 0) + a.payment; a.payment = 0; }
501+
}
502+
}
503+
}
504+
505+
var rushActive = { lunch:false, dinner:false };
506+
function startRushLunch() { rushActive.lunch = true; }
507+
function stopRushLunch() { rushActive.lunch = false; releaseRestaurantPatrons('lunch'); }
508+
function startRushDinner() { rushActive.dinner = true; }
509+
function stopRushDinner() { rushActive.dinner = false; releaseRestaurantPatrons('dinner'); }
510+
function releaseRestaurantPatrons(meal) {
511+
for (var i = 0; i < state.agents.length; i++) {
512+
var a = state.agents[i];
513+
if (a.type === 'patron' && a.meal === meal) a.phase = 'leaving';
514+
}
515+
}
516+
517+
// Tick-called spawning: restaurant patrons spawn every few minutes during a rush
518+
function spawnRestaurantPatrons() {
519+
if (!rushActive.lunch && !rushActive.dinner) return;
520+
var meal = rushActive.lunch ? 'lunch' : 'dinner';
521+
for (var id in state.units) {
522+
var u = state.units[id];
523+
if (u.type !== 'restaurant') continue;
524+
var def = UNITS.restaurant;
525+
var free = def.capacity - u.tenants;
526+
if (free <= 0) continue;
527+
var want = Math.min(free, 2 + Math.floor(Math.random() * 3));
528+
for (var k = 0; k < want; k++) {
529+
state.agents.push({
530+
id: state.nextAgentId++, type: 'patron', meal: meal,
531+
x: GRID_W/2, y: 0, phase: 'toHome', unitId: u.id, walkDelay: Math.random() * 20,
532+
payment: def.perVisit
533+
});
534+
u.tenants++;
535+
}
536+
}
537+
}
538+
539+
function spawnShopPatrons() {
540+
for (var id in state.units) {
541+
var u = state.units[id];
542+
if (u.type !== 'shop') continue;
543+
var def = UNITS.shop;
544+
var free = def.capacity - u.tenants;
545+
if (free <= 0) continue;
546+
if (Math.random() < 0.4) {
547+
state.agents.push({
548+
id: state.nextAgentId++, type: 'shopper',
549+
x: GRID_W/2, y: 0, phase: 'toHome', unitId: u.id, walkDelay: Math.random() * 15,
550+
payment: def.perVisit, visitTimer: 60 + Math.floor(Math.random() * 120)
551+
});
552+
u.tenants++;
553+
}
554+
}
555+
}
556+
463557
function tickAgent(a) {
464558
if (a.walkDelay && a.walkDelay > 0) { a.walkDelay--; return; }
465559
var target = a.unitId && state.units[a.unitId];
@@ -471,11 +565,20 @@
471565
a.phase = (a.phase === 'toWork') ? 'atWork' : 'atHome';
472566
}
473567
} else if (a.phase === 'atWork' || a.phase === 'atHome') {
474-
// Idle at target unit
568+
// Shoppers time out and leave after visiting
569+
if (a.type === 'shopper') {
570+
a.visitTimer--;
571+
if (a.visitTimer <= 0) {
572+
a.phase = 'leaving';
573+
if (a.payment) { state.dayRevenue = (state.dayRevenue || 0) + a.payment; a.payment = 0; }
574+
}
575+
}
475576
} else if (a.phase === 'leaving') {
476577
if (stepToward(a, GRID_W/2, 0)) {
477578
a.state = 'gone';
478579
if (target) target.tenants = Math.max(0, target.tenants - 1);
580+
// Patron / visitor pays on departure if not already paid
581+
if (a.payment) { state.dayRevenue = (state.dayRevenue || 0) + a.payment; a.payment = 0; }
479582
}
480583
}
481584
}
@@ -643,15 +746,21 @@
643746
for (var id in state.units) {
644747
var u = state.units[id];
645748
var def = UNITS[u.type];
646-
// Rent: proportional to occupancy
749+
// Rent from residents/workers: proportional to occupancy
647750
if (def.rent && def.capacity > 0) {
648751
var occ = u.tenants / def.capacity;
649752
income += Math.round(def.rent * occ);
650753
}
651754
// Upkeep
652755
if (u.type === 'lobby') expenses += 800;
653756
if (u.type === 'elevator') expenses += 200;
757+
if (u.type === 'hotel') expenses += 120;
758+
if (u.type === 'restaurant') expenses += 1200;
759+
if (u.type === 'shop') expenses += 400;
654760
}
761+
// Visit-based revenue accumulated during the day (shops, hotel checkouts, restaurants)
762+
income += Math.round(state.dayRevenue || 0);
763+
state.dayRevenue = 0;
655764
var net = income - expenses;
656765
state.cash += net;
657766
state.yesterdayIncome = net;
@@ -800,6 +909,42 @@
800909
ctx.font = 'bold 8px Georgia';
801910
ctx.textAlign = 'center';
802911
ctx.fillText('CONDO', x + w/2, y + h - 4);
912+
} else if (u.type === 'hotel') {
913+
var h4 = state.minute / 60;
914+
var lit3 = (u.tenants > 0 && (h4 < 7 || h4 >= 21));
915+
ctx.fillStyle = lit3 ? '#FFB880' : '#3A1A30';
916+
for (var xh = 0; xh < 3; xh++) {
917+
var wx3 = x + 4 + xh * ((w - 8) / 3);
918+
ctx.fillRect(wx3, y + 4, (w - 8) / 3 - 2, h - 8);
919+
}
920+
ctx.fillStyle = def.textColor;
921+
ctx.font = 'bold 8px Georgia';
922+
ctx.textAlign = 'center';
923+
ctx.fillText('HOTEL', x + w/2, y + h - 4);
924+
} else if (u.type === 'restaurant') {
925+
// Awning + tables
926+
ctx.fillStyle = '#FFE0B0';
927+
ctx.fillRect(x + 3, y + 5, w - 6, h - 10);
928+
ctx.fillStyle = '#A04020';
929+
for (var rx = 0; rx < 4; rx++) { ctx.fillRect(x + 6 + rx*((w-12)/4), y + 5, 3, 3); }
930+
ctx.fillStyle = def.textColor;
931+
ctx.font = 'bold 9px Georgia';
932+
ctx.textAlign = 'center';
933+
ctx.fillText('RESTAURANT', x + w/2, y + h/2 + 1);
934+
if (u.tenants > 0) {
935+
ctx.fillStyle = '#805030';
936+
ctx.font = 'bold 7px monospace';
937+
ctx.fillText(u.tenants + ' diners', x + w/2, y + h - 3);
938+
}
939+
} else if (u.type === 'shop') {
940+
ctx.fillStyle = '#FFFFE0';
941+
ctx.fillRect(x + 3, y + 4, w - 6, h - 8);
942+
ctx.fillStyle = '#406050';
943+
ctx.fillRect(x + 3, y + 4, w - 6, 3);
944+
ctx.fillStyle = def.textColor;
945+
ctx.font = 'bold 9px Georgia';
946+
ctx.textAlign = 'center';
947+
ctx.fillText('SHOP', x + w/2, y + h/2 + 2);
803948
} else if (u.type === 'stairs') {
804949
ctx.strokeStyle = '#404850';
805950
ctx.lineWidth = 1.5;
@@ -840,17 +985,26 @@
840985
}
841986
}
842987

988+
function agentColor(a) {
989+
if (a.type === 'worker') return '#4A80D0';
990+
if (a.type === 'resident') return '#D06060';
991+
if (a.type === 'hotelGuest') return '#B080C0';
992+
if (a.type === 'patron') return '#E0A040';
993+
if (a.type === 'shopper') return '#60C080';
994+
return '#FFF';
995+
}
996+
843997
function drawAgent(a) {
844998
var displayY = a.y;
845999
if (a.inElevator) {
8461000
displayY = a.inElevator.car.floatY;
8471001
var epos = worldToScreen(a.inElevator.shaftX + 0.5, displayY);
848-
ctx.fillStyle = a.type === 'worker' ? '#4A80D0' : '#D06060';
1002+
ctx.fillStyle = agentColor(a);
8491003
ctx.fillRect(epos.x - 1, epos.y - CELL_H + 6, 2, CELL_H - 8);
8501004
return;
8511005
}
8521006
var ps = worldToScreen(a.x, displayY);
853-
ctx.fillStyle = a.type === 'worker' ? '#4A80D0' : '#D06060';
1007+
ctx.fillStyle = agentColor(a);
8541008
ctx.fillRect(ps.x - 1, ps.y - CELL_H + 6, 2, CELL_H - 8);
8551009
}
8561010

0 commit comments

Comments
 (0)