class BuildingToolbox extends HTMLElement {
static get observedAttributes() {
return ['player-money', 'player-population'];
}
constructor() {
super();
this.app = null;
this.buildingItems = []; // A cache for the building DOM elements
this.buildings = [
{ type: 'small_house', name: 'Small House', cost: 5000, income: -50, pop: 10 },
{ type: 'medium_house', name: 'Medium House', cost: 12000, income: -120, pop: 25 },
{ type: 'large_house', name: 'Large House', cost: 25000, income: -250, pop: 50 },
{ type: 'small_shop', name: 'Small Shop', cost: 8000, income: 100, pop: -5, req: 20 },
{ type: 'supermarket', name: 'Supermarket', cost: 25000, income: 300, pop: -15, req: 50 },
{ type: 'mall', name: 'Shopping Mall', cost: 80000, income: 800, pop: -40, req: 100 },
{ type: 'small_factory', name: 'Small Factory', cost: 15000, income: 200, pop: -20 },
{ type: 'large_factory', name: 'Large Factory', cost: 50000, income: 500, pop: -50 },
{ type: 'road', name: 'Road', cost: 500, income: 0, pop: 0 },
{ type: 'park', name: 'Park', cost: 3000, income: -20, pop: 5 },
{ type: 'plaza', name: 'Plaza', cost: 8000, income: -40, pop: 10 },
{ type: 'town_hall', name: 'Town Hall', cost: 50000, income: -100, pop: 100 },
{ type: 'power_plant', name: 'Power Plant', cost: 100000, income: -500, pop: -30 }
];
}
connectedCallback() {
// 1. Initial full render happens only once
this.innerHTML = this.renderHTML();
// 2. Cache the DOM elements for efficient future updates
this.buildingItems = this.querySelectorAll('.building-item');
// 3. Add click handlers only once
this.addClickHandlers();
}
attributeChangedCallback() {
// When player stats change, run the lightweight update function
// instead of a full re-render.
if (this.buildingItems.length > 0) {
this.updateItemStates();
}
}
renderHTML() {
// This function generates the initial HTML string.
const money = parseInt(this.getAttribute('player-money') || '0');
const population = parseInt(this.getAttribute('player-population') || '0');
return `
<div style="font-weight: bold; margin-bottom: 10px; font-size: 16px; border-bottom: 1px solid var(--border-color); padding-bottom: 5px;">
Buildings
</div>
<div style="max-height: calc(100vh - 200px); overflow-y: auto;">
${this.buildings.map(building => {
const canAfford = money >= building.cost;
const meetsReq = !building.req || population >= building.req;
const enabled = canAfford && meetsReq;
return `
<div
class="building-item"
data-type="${building.type}"
style="
padding: 8px;
margin-bottom: 8px;
background: var(--bg-light);
border: 1px solid var(--border-color);
cursor: ${enabled ? 'pointer' : 'not-allowed'};
opacity: ${enabled ? '1' : '0.5'};"
>
<div style="font-weight: bold; margin-bottom: 4px;">
${building.name}
</div>
<div style="font-size: 12px; color: #90EE90;">
Cost: $${building.cost.toLocaleString()}
</div>
<div style="font-size: 11px; margin-top: 2px;">
${building.income !== 0 ? `Income: $${building.income}/tick` : ''}
${building.pop !== 0 ? `Pop: ${building.pop > 0 ? '+' : ''}${building.pop}` : ''}
${building.req ? `(Req: ${building.req} pop)` : ''}
</div>
</div>
`;
}).join('')}
</div>
`;
}
addClickHandlers() {
this.buildingItems.forEach(item => {
item.addEventListener('click', () => {
const type = item.dataset.type;
const building = this.buildings.find(b => b.type === type);
const money = parseInt(this.getAttribute('player-money') || '0');
const population = parseInt(this.getAttribute('player-population') || '0');
if (money >= building.cost && (!building.req || population >= building.req)) {
if (this.app) {
this.app.selectBuilding(type);
}
}
});
});
}
updateItemStates() {
// This lightweight function only updates styles, preserving the DOM and scroll position.
const money = parseInt(this.getAttribute('player-money') || '0');
const population = parseInt(this.getAttribute('player-population') || '0');
this.buildingItems.forEach(item => {
const type = item.dataset.type;
const building = this.buildings.find(b => b.type === type);
if (!building) return;
const canAfford = money >= building.cost;
const meetsReq = !building.req || population >= building.req;
const enabled = canAfford && meetsReq;
// Directly update only the styles that change
item.style.opacity = enabled ? '1' : '0.5';
item.style.cursor = enabled ? 'pointer' : 'not-allowed';
});
}
}
customElements.define('building-toolbox', BuildingToolbox);