Initial commit.
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
class BuildingToolbox extends HTMLElement {
|
||||
static get observedAttributes() {
|
||||
return ['player-money', 'player-population'];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.app = null;
|
||||
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() {
|
||||
this.render();
|
||||
}
|
||||
|
||||
attributeChangedCallback() {
|
||||
this.render();
|
||||
}
|
||||
|
||||
render() {
|
||||
const money = parseInt(this.getAttribute('player-money') || '0');
|
||||
const population = parseInt(this.getAttribute('player-population') || '0');
|
||||
|
||||
this.innerHTML = `
|
||||
<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>
|
||||
`;
|
||||
|
||||
// Add click handlers
|
||||
this.querySelectorAll('.building-item').forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
const type = item.dataset.type;
|
||||
const building = this.buildings.find(b => b.type === type);
|
||||
|
||||
if (money >= building.cost && (!building.req || population >= building.req)) {
|
||||
if (this.app) {
|
||||
this.app.selectBuilding(type);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('building-toolbox', BuildingToolbox);
|
||||
@@ -0,0 +1,66 @@
|
||||
class ChatBox extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.app = null;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.innerHTML = `
|
||||
<div style="display: flex; flex-direction: column; height: 100%;">
|
||||
<div id="chatMessages" style="flex: 1; overflow-y: auto; padding: 5px; font-size: 12px; font-family: 'Courier New', monospace;">
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
id="chatInput"
|
||||
placeholder="Type message..."
|
||||
style="border: none; border-top: 1px solid var(--border-color); padding: 5px;"
|
||||
maxlength="200"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const input = this.querySelector('#chatInput');
|
||||
|
||||
input.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
const message = input.value.trim();
|
||||
if (message && this.app) {
|
||||
this.app.sendChatMessage(message);
|
||||
input.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addMessage(nickname, message, timestamp) {
|
||||
const messagesDiv = this.querySelector('#chatMessages');
|
||||
|
||||
const messageEl = document.createElement('div');
|
||||
messageEl.style.marginBottom = '3px';
|
||||
|
||||
const time = timestamp || new Date().toTimeString().slice(0, 5);
|
||||
const color = nickname === 'system' ? '#FFD700' : '#87CEEB';
|
||||
|
||||
messageEl.innerHTML = `
|
||||
<span style="color: #666;">${time}</span>
|
||||
<span style="color: ${color}; font-weight: bold;">${nickname}:</span>
|
||||
<span style="color: var(--text-color);">${this.escapeHtml(message)}</span>
|
||||
`;
|
||||
|
||||
messagesDiv.appendChild(messageEl);
|
||||
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
||||
|
||||
// Keep only last 100 messages
|
||||
while (messagesDiv.children.length > 100) {
|
||||
messagesDiv.removeChild(messagesDiv.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('chat-box', ChatBox);
|
||||
@@ -0,0 +1,114 @@
|
||||
class ContextMenu extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.app = null;
|
||||
this.currentX = null;
|
||||
this.currentY = null;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.innerHTML = `
|
||||
<div id="menuItems">
|
||||
<div class="menu-item" data-action="edit" style="padding: 8px; cursor: pointer; border-bottom: 1px solid var(--border-color);">
|
||||
Edit Name
|
||||
</div>
|
||||
<div class="menu-item" data-action="delete" style="padding: 8px; cursor: pointer;">
|
||||
Delete
|
||||
</div>
|
||||
</div>
|
||||
<div id="editForm" style="display: none; padding: 10px;">
|
||||
<input
|
||||
type="text"
|
||||
id="nameInput"
|
||||
placeholder="Building name..."
|
||||
style="width: 100%; margin-bottom: 8px;"
|
||||
maxlength="30"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Menu item clicks
|
||||
this.querySelectorAll('.menu-item').forEach(item => {
|
||||
item.addEventListener('mouseenter', (e) => {
|
||||
e.target.style.background = 'var(--bg-light)';
|
||||
});
|
||||
|
||||
item.addEventListener('mouseleave', (e) => {
|
||||
e.target.style.background = '';
|
||||
});
|
||||
|
||||
item.addEventListener('click', (e) => {
|
||||
const action = e.target.dataset.action;
|
||||
this.handleAction(action);
|
||||
});
|
||||
});
|
||||
|
||||
// Edit form
|
||||
const nameInput = this.querySelector('#nameInput');
|
||||
nameInput.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
this.submitEdit();
|
||||
}
|
||||
});
|
||||
|
||||
nameInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
this.hide();
|
||||
}
|
||||
});
|
||||
|
||||
// Click outside to close
|
||||
document.addEventListener('click', (e) => {
|
||||
if (this.style.display !== 'none' && !this.contains(e.target)) {
|
||||
this.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
show(x, y, tileX, tileY) {
|
||||
this.currentX = tileX;
|
||||
this.currentY = tileY;
|
||||
|
||||
this.style.left = x + 'px';
|
||||
this.style.top = y + 'px';
|
||||
this.style.display = 'block';
|
||||
|
||||
this.querySelector('#menuItems').style.display = 'block';
|
||||
this.querySelector('#editForm').style.display = 'none';
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.style.display = 'none';
|
||||
}
|
||||
|
||||
handleAction(action) {
|
||||
if (action === 'edit') {
|
||||
this.querySelector('#menuItems').style.display = 'none';
|
||||
this.querySelector('#editForm').style.display = 'block';
|
||||
|
||||
const input = this.querySelector('#nameInput');
|
||||
input.value = '';
|
||||
input.focus();
|
||||
} else if (action === 'delete') {
|
||||
if (confirm('Delete this building?')) {
|
||||
if (this.app) {
|
||||
this.app.removeBuilding(this.currentX, this.currentY);
|
||||
}
|
||||
this.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
submitEdit() {
|
||||
const input = this.querySelector('#nameInput');
|
||||
const name = input.value.trim();
|
||||
|
||||
if (name && this.app) {
|
||||
this.app.editBuilding(this.currentX, this.currentY, name);
|
||||
}
|
||||
|
||||
this.hide();
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('context-menu', ContextMenu);
|
||||
@@ -0,0 +1,60 @@
|
||||
class LoginScreen extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.app = null;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.innerHTML = `
|
||||
<div style="background: var(--bg-medium); padding: 40px; border: 2px solid var(--border-color); text-align: center;">
|
||||
<h1 style="margin-bottom: 30px; font-size: 32px;">City Builder</h1>
|
||||
<p style="margin-bottom: 20px;">Enter your nickname to start</p>
|
||||
<input
|
||||
type="text"
|
||||
id="nicknameInput"
|
||||
placeholder="Nickname"
|
||||
style="width: 300px; margin-bottom: 20px;"
|
||||
maxlength="20"
|
||||
/>
|
||||
<br>
|
||||
<button class="button" id="startButton">Start Game</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const input = this.querySelector('#nicknameInput');
|
||||
const button = this.querySelector('#startButton');
|
||||
|
||||
// Enter key to submit
|
||||
input.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
this.startGame();
|
||||
}
|
||||
});
|
||||
|
||||
button.addEventListener('click', () => this.startGame());
|
||||
|
||||
// Focus input
|
||||
setTimeout(() => input.focus(), 100);
|
||||
}
|
||||
|
||||
startGame() {
|
||||
const input = this.querySelector('#nicknameInput');
|
||||
const nickname = input.value.trim();
|
||||
|
||||
if (!nickname) {
|
||||
alert('Please enter a nickname');
|
||||
return;
|
||||
}
|
||||
|
||||
if (nickname.length < 2) {
|
||||
alert('Nickname must be at least 2 characters');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.app) {
|
||||
this.app.startGame(nickname);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('login-screen', LoginScreen);
|
||||
@@ -0,0 +1,37 @@
|
||||
class StatsDisplay extends HTMLElement {
|
||||
static get observedAttributes() {
|
||||
return ['money', 'population', 'nickname'];
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.render();
|
||||
}
|
||||
|
||||
attributeChangedCallback() {
|
||||
this.render();
|
||||
}
|
||||
|
||||
render() {
|
||||
const money = this.getAttribute('money') || '0';
|
||||
const population = this.getAttribute('population') || '0';
|
||||
const nickname = this.getAttribute('nickname') || 'Player';
|
||||
|
||||
const formattedMoney = parseInt(money).toLocaleString();
|
||||
|
||||
this.innerHTML = `
|
||||
<div style="font-size: 14px;">
|
||||
<div style="font-size: 18px; font-weight: bold; margin-bottom: 10px; color: #FFD700;">
|
||||
${nickname}
|
||||
</div>
|
||||
<div style="margin-bottom: 5px;">
|
||||
<span style="color: #90EE90;">$</span> ${formattedMoney}
|
||||
</div>
|
||||
<div>
|
||||
<span style="color: #87CEEB;">👥</span> ${population}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('stats-display', StatsDisplay);
|
||||
Reference in New Issue
Block a user