|
152 | 152 | lobby: { name:'Lobby', w:8, h:1, cost:200000, rent:0, color:'#D8C28A', textColor:'#5A4A2A', groundOnly:true, capacity:0 }, |
153 | 153 | office: { name:'Office', w:4, h:1, cost:50000, rent:3000, color:'#8AA0D0', textColor:'#1A2A50', capacity:6 }, |
154 | 154 | 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 }, |
155 | 158 | stairs: { name:'Stairs', w:2, h:1, cost:15000, rent:0, color:'#909898', textColor:'#303838', capacity:0 }, |
156 | 159 | elevator: { name:'Elevator', w:1, h:1, cost:60000, rent:0, color:'#7A7A82', textColor:'#FFF', capacity:0, isElevator:true } |
157 | 160 | }; |
158 | | -var PAL_ORDER = ['lobby','office','condo','stairs','elevator']; |
| 161 | +var PAL_ORDER = ['lobby','office','condo','hotel','restaurant','shop','stairs','elevator']; |
159 | 162 |
|
160 | 163 | /* ===== STATE ===== */ |
161 | 164 | var state = { |
162 | 165 | cash: 2000000, |
163 | 166 | yesterdayIncome: null, |
| 167 | + dayRevenue: 0, |
164 | 168 | units: {}, // id → { id, type, x, y, w, h, tenants, lastRent } |
165 | 169 | grid: {}, // "x,y" → unitId |
166 | 170 | nextUnitId: 1, |
|
323 | 327 | var uid = state.grid[gx+','+gy]; |
324 | 328 | if (!uid) return; |
325 | 329 | var unit = state.units[uid]; |
326 | | - // Kick out occupants |
327 | 330 | for (var i = state.agents.length - 1; i >= 0; i--) { |
328 | 331 | if (state.agents[i].unitId === uid) state.agents.splice(i, 1); |
329 | 332 | } |
330 | | - // Remove grid cells |
331 | 333 | for (var dx = 0; dx < unit.w; dx++) { |
332 | 334 | for (var dy = 0; dy < unit.h; dy++) { |
333 | 335 | delete state.grid[(unit.x+dx) + ',' + (unit.y+dy)]; |
334 | 336 | } |
335 | 337 | } |
336 | 338 | delete state.units[uid]; |
337 | 339 | var def = UNITS[unit.type]; |
| 340 | + // 50% refund |
| 341 | + var refund = Math.round(def.cost * 0.5); |
| 342 | + state.cash += refund; |
338 | 343 | if (def.isElevator) rebuildElevators(); |
339 | 344 | updatePopulation(); |
340 | 345 | updateStars(); |
341 | | - showToast('Bulldozed: ' + def.name); |
| 346 | + showToast('Bulldozed: ' + def.name + ' (+$' + formatMoney(refund) + ')'); |
342 | 347 | } |
343 | 348 |
|
344 | 349 | function rebuildElevators() { |
|
399 | 404 | /* ===== SIMULATION ===== */ |
400 | 405 | function simMinute() { |
401 | 406 | var h = Math.floor(state.minute / 60); |
402 | | - // Spawn office workers at ~8am and have them leave at ~5pm |
403 | 407 | if (state.minute === 8 * 60) spawnWorkers(); |
404 | 408 | if (state.minute === 17 * 60) releaseWorkers(); |
405 | | - // Condo residents: spawn at 6pm-ish, leave 8am |
406 | 409 | if (state.minute === 18 * 60) spawnResidents(); |
407 | 410 | 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(); |
408 | 423 |
|
409 | 424 | for (var i = state.agents.length - 1; i >= 0; i--) { |
410 | 425 | tickAgent(state.agents[i]); |
|
460 | 475 | } |
461 | 476 | } |
462 | 477 |
|
| 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 | + |
463 | 557 | function tickAgent(a) { |
464 | 558 | if (a.walkDelay && a.walkDelay > 0) { a.walkDelay--; return; } |
465 | 559 | var target = a.unitId && state.units[a.unitId]; |
|
471 | 565 | a.phase = (a.phase === 'toWork') ? 'atWork' : 'atHome'; |
472 | 566 | } |
473 | 567 | } 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 | + } |
475 | 576 | } else if (a.phase === 'leaving') { |
476 | 577 | if (stepToward(a, GRID_W/2, 0)) { |
477 | 578 | a.state = 'gone'; |
478 | 579 | 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; } |
479 | 582 | } |
480 | 583 | } |
481 | 584 | } |
|
643 | 746 | for (var id in state.units) { |
644 | 747 | var u = state.units[id]; |
645 | 748 | var def = UNITS[u.type]; |
646 | | - // Rent: proportional to occupancy |
| 749 | + // Rent from residents/workers: proportional to occupancy |
647 | 750 | if (def.rent && def.capacity > 0) { |
648 | 751 | var occ = u.tenants / def.capacity; |
649 | 752 | income += Math.round(def.rent * occ); |
650 | 753 | } |
651 | 754 | // Upkeep |
652 | 755 | if (u.type === 'lobby') expenses += 800; |
653 | 756 | 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; |
654 | 760 | } |
| 761 | + // Visit-based revenue accumulated during the day (shops, hotel checkouts, restaurants) |
| 762 | + income += Math.round(state.dayRevenue || 0); |
| 763 | + state.dayRevenue = 0; |
655 | 764 | var net = income - expenses; |
656 | 765 | state.cash += net; |
657 | 766 | state.yesterdayIncome = net; |
|
800 | 909 | ctx.font = 'bold 8px Georgia'; |
801 | 910 | ctx.textAlign = 'center'; |
802 | 911 | 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); |
803 | 948 | } else if (u.type === 'stairs') { |
804 | 949 | ctx.strokeStyle = '#404850'; |
805 | 950 | ctx.lineWidth = 1.5; |
|
840 | 985 | } |
841 | 986 | } |
842 | 987 |
|
| 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 | + |
843 | 997 | function drawAgent(a) { |
844 | 998 | var displayY = a.y; |
845 | 999 | if (a.inElevator) { |
846 | 1000 | displayY = a.inElevator.car.floatY; |
847 | 1001 | var epos = worldToScreen(a.inElevator.shaftX + 0.5, displayY); |
848 | | - ctx.fillStyle = a.type === 'worker' ? '#4A80D0' : '#D06060'; |
| 1002 | + ctx.fillStyle = agentColor(a); |
849 | 1003 | ctx.fillRect(epos.x - 1, epos.y - CELL_H + 6, 2, CELL_H - 8); |
850 | 1004 | return; |
851 | 1005 | } |
852 | 1006 | var ps = worldToScreen(a.x, displayY); |
853 | | - ctx.fillStyle = a.type === 'worker' ? '#4A80D0' : '#D06060'; |
| 1007 | + ctx.fillStyle = agentColor(a); |
854 | 1008 | ctx.fillRect(ps.x - 1, ps.y - CELL_H + 6, 2, CELL_H - 8); |
855 | 1009 | } |
856 | 1010 |
|
|
0 commit comments