update.
This commit is contained in:
parent
d519cdfd0e
commit
5448cf2a89
73
app.py
73
app.py
@ -15,7 +15,7 @@ DB_FILE = Path("tycoon.db")
|
|||||||
|
|
||||||
# --- Database Management ---
|
# --- Database Management ---
|
||||||
def init_db():
|
def init_db():
|
||||||
"""Initializes the database and creates tables if they don't exist."""
|
"""Initializes the database and creates/alters tables if they don't exist."""
|
||||||
try:
|
try:
|
||||||
conn = sqlite3.connect(DB_FILE)
|
conn = sqlite3.connect(DB_FILE)
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
@ -23,9 +23,17 @@ def init_db():
|
|||||||
CREATE TABLE IF NOT EXISTS players (
|
CREATE TABLE IF NOT EXISTS players (
|
||||||
nickname TEXT PRIMARY KEY,
|
nickname TEXT PRIMARY KEY,
|
||||||
money INTEGER NOT NULL,
|
money INTEGER NOT NULL,
|
||||||
population INTEGER NOT NULL
|
population INTEGER NOT NULL,
|
||||||
|
happiness REAL NOT NULL DEFAULT 0.5
|
||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
|
# Add happiness column if it doesn't exist for migrations
|
||||||
|
try:
|
||||||
|
cursor.execute("ALTER TABLE players ADD COLUMN happiness REAL NOT NULL DEFAULT 0.5")
|
||||||
|
logger.info("Added 'happiness' column to players table.")
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
pass # Column already exists
|
||||||
|
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
CREATE TABLE IF NOT EXISTS buildings (
|
CREATE TABLE IF NOT EXISTS buildings (
|
||||||
key TEXT PRIMARY KEY,
|
key TEXT PRIMARY KEY,
|
||||||
@ -111,10 +119,12 @@ game_state: Dict[str, Any] = {
|
|||||||
building_data = {
|
building_data = {
|
||||||
'residential': { 'cost': 100, 'population': 10 },
|
'residential': { 'cost': 100, 'population': 10 },
|
||||||
'commercial': { 'cost': 250, 'income': 5 },
|
'commercial': { 'cost': 250, 'income': 5 },
|
||||||
'industrial': { 'cost': 500, 'income': 20 },
|
'industrial': { 'cost': 500, 'income': 20, 'happiness_impact': -0.02 },
|
||||||
'park': { 'cost': 80, 'population_bonus': 5 },
|
'park': { 'cost': 80, 'population_bonus': 5, 'happiness_impact': 0.01 },
|
||||||
'powerplant': { 'cost': 1000, 'income': 50 },
|
'powerplant': { 'cost': 1000, 'income': 50 },
|
||||||
'road': { 'cost': 20 }
|
'road': { 'cost': 20 },
|
||||||
|
'police': { 'cost': 600, 'happiness_bonus': 0.1 },
|
||||||
|
'stadium': {'cost': 5000, 'income': 150, 'happiness_impact': 0.05 }
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Game Logic ---
|
# --- Game Logic ---
|
||||||
@ -140,33 +150,52 @@ async def game_loop():
|
|||||||
player = game_state["players"][nickname]
|
player = game_state["players"][nickname]
|
||||||
income = 0
|
income = 0
|
||||||
population = 0
|
population = 0
|
||||||
|
base_happiness = 0.5 # Start with a neutral base happiness
|
||||||
|
|
||||||
player_buildings = {k: v for k, v in game_state["buildings"].items() if v["owner"] == nickname}
|
player_buildings = {k: v for k, v in game_state["buildings"].items() if v["owner"] == nickname}
|
||||||
|
|
||||||
# Calculate base income and population
|
# Calculate base income, population, and happiness impacts
|
||||||
for building in player_buildings.values():
|
for building in player_buildings.values():
|
||||||
b_type = building["type"]
|
b_type = building["type"]
|
||||||
if b_type == 'residential':
|
b_data = building_data.get(b_type, {})
|
||||||
population += building_data['residential']['population']
|
|
||||||
elif b_type == 'commercial':
|
|
||||||
income += building_data['commercial']['income']
|
|
||||||
elif b_type == 'industrial':
|
|
||||||
income += building_data['industrial']['income']
|
|
||||||
elif b_type == 'powerplant':
|
|
||||||
income += building_data['powerplant']['income']
|
|
||||||
|
|
||||||
# Calculate park adjacency bonuses
|
if b_type == 'residential':
|
||||||
|
population += b_data.get('population', 0)
|
||||||
|
elif 'income' in b_data:
|
||||||
|
if b_type == 'commercial':
|
||||||
|
# Commercial income is modified by happiness
|
||||||
|
happiness_multiplier = max(0.1, player.get('happiness', 0.5))
|
||||||
|
income += b_data['income'] * (1 + happiness_multiplier)
|
||||||
|
else:
|
||||||
|
income += b_data.get('income', 0)
|
||||||
|
|
||||||
|
if 'happiness_impact' in b_data:
|
||||||
|
base_happiness += b_data['happiness_impact']
|
||||||
|
|
||||||
|
# Calculate adjacency bonuses
|
||||||
parks = {k: v for k, v in player_buildings.items() if v["type"] == 'park'}
|
parks = {k: v for k, v in player_buildings.items() if v["type"] == 'park'}
|
||||||
|
police_stations = {k: v for k, v in player_buildings.items() if v["type"] == 'police'}
|
||||||
|
|
||||||
for park_key in parks:
|
for park_key in parks:
|
||||||
for neighbor_key in get_neighbors(park_key):
|
for neighbor_key in get_neighbors(park_key):
|
||||||
neighbor = player_buildings.get(neighbor_key)
|
neighbor = player_buildings.get(neighbor_key)
|
||||||
if neighbor and neighbor["type"] == 'residential':
|
if neighbor and neighbor["type"] == 'residential':
|
||||||
population += building_data['park']['population_bonus']
|
population += building_data['park']['population_bonus']
|
||||||
|
|
||||||
|
for police_key in police_stations:
|
||||||
|
for neighbor_key in get_neighbors(police_key):
|
||||||
|
neighbor = player_buildings.get(neighbor_key)
|
||||||
|
if neighbor and neighbor["type"] == 'residential':
|
||||||
|
base_happiness += building_data['police']['happiness_bonus'] / 4 # Distribute bonus over 4 neighbors
|
||||||
|
|
||||||
|
# Finalize and clamp values
|
||||||
|
final_happiness = max(0, min(1, base_happiness))
|
||||||
|
|
||||||
player["money"] += income
|
player["money"] += income
|
||||||
player["population"] = population
|
player["population"] = population
|
||||||
|
player["happiness"] = final_happiness
|
||||||
|
|
||||||
db_execute("UPDATE players SET money = ?, population = ? WHERE nickname = ?", (player["money"], player["population"], nickname))
|
db_execute("UPDATE players SET money = ?, population = ?, happiness = ? WHERE nickname = ?", (player["money"], player["population"], player["happiness"], nickname))
|
||||||
|
|
||||||
if game_state["players"]:
|
if game_state["players"]:
|
||||||
await manager.broadcast(json.dumps({
|
await manager.broadcast(json.dumps({
|
||||||
@ -187,14 +216,15 @@ async def on_startup():
|
|||||||
async def websocket_endpoint(websocket: WebSocket, nickname: str):
|
async def websocket_endpoint(websocket: WebSocket, nickname: str):
|
||||||
await manager.connect(websocket, nickname)
|
await manager.connect(websocket, nickname)
|
||||||
|
|
||||||
player_data = db_fetchone("SELECT money, population FROM players WHERE nickname = ?", (nickname,))
|
player_data = db_fetchone("SELECT money, population, happiness FROM players WHERE nickname = ?", (nickname,))
|
||||||
if player_data:
|
if player_data:
|
||||||
game_state["players"][nickname] = { "money": player_data["money"], "population": player_data["population"] }
|
game_state["players"][nickname] = { "money": player_data["money"], "population": player_data["population"], "happiness": player_data["happiness"] }
|
||||||
else:
|
else:
|
||||||
initial_money = 1500
|
initial_money = 1500
|
||||||
initial_pop = 0
|
initial_pop = 0
|
||||||
db_execute("INSERT INTO players (nickname, money, population) VALUES (?, ?, ?)", (nickname, initial_money, initial_pop))
|
initial_happiness = 0.5
|
||||||
game_state["players"][nickname] = {"money": initial_money, "population": initial_pop}
|
db_execute("INSERT INTO players (nickname, money, population, happiness) VALUES (?, ?, ?, ?)", (nickname, initial_money, initial_pop, initial_happiness))
|
||||||
|
game_state["players"][nickname] = {"money": initial_money, "population": initial_pop, "happiness": initial_happiness}
|
||||||
|
|
||||||
await websocket.send_text(json.dumps({ "type": "full_state", "buildings": game_state["buildings"] }))
|
await websocket.send_text(json.dumps({ "type": "full_state", "buildings": game_state["buildings"] }))
|
||||||
await manager.broadcast(json.dumps({ "type": "status_update", "message": f"'{nickname}' has joined the game!" }))
|
await manager.broadcast(json.dumps({ "type": "status_update", "message": f"'{nickname}' has joined the game!" }))
|
||||||
@ -248,4 +278,3 @@ async def websocket_endpoint(websocket: WebSocket, nickname: str):
|
|||||||
async def root():
|
async def root():
|
||||||
return FileResponse("index.html")
|
return FileResponse("index.html")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
70
index.html
70
index.html
@ -1,39 +1,39 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Tiny Tycoon 3D (Multiplayer)</title>
|
<title>Tiny Tycoon 3D (Multiplayer)</title>
|
||||||
<style>
|
<style>
|
||||||
/* --- General Setup --- */
|
/* --- General Setup --- */
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||||
background-color: #1a202c;
|
background-color: #1a202c;
|
||||||
color: #e2e8f0;
|
color: #e2e8f0;
|
||||||
}
|
}
|
||||||
#game-canvas { display: block; }
|
#game-canvas { display: block; }
|
||||||
|
|
||||||
/* --- UI Panels --- */
|
/* --- UI Panels --- */
|
||||||
.ui-panel {
|
.ui-panel {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background-color: rgba(45, 55, 72, 0.9);
|
background-color: rgba(45, 55, 72, 0.9);
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
}
|
}
|
||||||
#title-card { top: 20px; left: 50%; transform: translateX(-50%); padding: 8px 16px; font-size: 1.25rem; font-weight: bold; }
|
|
||||||
#info-card { top: 20px; right: 20px; max-width: 250px; font-size: 0.875rem; }
|
#info-card { top: 20px; right: 20px; max-width: 250px; font-size: 0.875rem; }
|
||||||
#info-card h3, #stats-panel h3, #players-panel h3 { font-weight: bold; font-size: 1.125rem; margin-top: 0; margin-bottom: 8px; color: white; }
|
#info-card h3, #stats-panel h3, #players-panel h3 { font-weight: bold; font-size: 1.125rem; margin-top: 0; margin-bottom: 8px; color: white; }
|
||||||
#info-card strong { color: #63b3ed; }
|
#info-card strong { color: #63b3ed; }
|
||||||
#stats-panel { top: 80px; left: 20px; font-size: 1rem; min-width: 240px; }
|
#stats-panel { top: 20px; left: 20px; font-size: 1rem; min-width: 240px; }
|
||||||
#stats-panel div { margin-bottom: 4px; }
|
#stats-panel div { margin-bottom: 4px; }
|
||||||
#stats-panel span { font-weight: bold; }
|
#stats-panel span { font-weight: bold; }
|
||||||
#money-stat { color: #48bb78; }
|
#money-stat { color: #48bb78; }
|
||||||
#population-stat { color: #63b3ed; }
|
#population-stat { color: #63b3ed; }
|
||||||
#players-panel { top: 200px; left: 20px; max-width: 240px; }
|
#happiness-stat { color: #f6e05e; }
|
||||||
|
#players-panel { top: 220px; left: 20px; max-width: 240px; }
|
||||||
#players-list .player-div { padding: 4px; font-size: 0.875rem; }
|
#players-list .player-div { padding: 4px; font-size: 0.875rem; }
|
||||||
#players-list .player-div.is-self { background-color: rgba(66, 153, 225, 0.5); border-radius: 4px; }
|
#players-list .player-div.is-self { background-color: rgba(66, 153, 225, 0.5); border-radius: 4px; }
|
||||||
#players-list .player-div strong { font-weight: bold; }
|
#players-list .player-div strong { font-weight: bold; }
|
||||||
@ -41,7 +41,7 @@
|
|||||||
/* --- Build Menu --- */
|
/* --- Build Menu --- */
|
||||||
#ui-panel-bottom { bottom: 20px; left: 50%; transform: translateX(-50%); display: flex; gap: 10px; }
|
#ui-panel-bottom { bottom: 20px; left: 50%; transform: translateX(-50%); display: flex; gap: 10px; }
|
||||||
.build-btn {
|
.build-btn {
|
||||||
padding: 10px 16px;
|
padding: 10px 16px;
|
||||||
border: 2px solid transparent;
|
border: 2px solid transparent;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background-color: #4a5568;
|
background-color: #4a5568;
|
||||||
@ -49,7 +49,7 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease-in-out;
|
transition: all 0.2s ease-in-out;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
@ -82,7 +82,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="game-container" class="hidden">
|
<div id="game-container" class="hidden">
|
||||||
<div id="title-card" class="ui-panel">Tiny Tycoon 3D</div>
|
|
||||||
<div id="info-card" class="ui-panel">
|
<div id="info-card" class="ui-panel">
|
||||||
<h3>Controls</h3>
|
<h3>Controls</h3>
|
||||||
<p><strong>Left-Click + Drag:</strong> Rotate</p>
|
<p><strong>Left-Click + Drag:</strong> Rotate</p>
|
||||||
@ -93,6 +92,7 @@
|
|||||||
<h3>My City</h3>
|
<h3>My City</h3>
|
||||||
<div>💰 Money: <span id="money-stat"></span></div>
|
<div>💰 Money: <span id="money-stat"></span></div>
|
||||||
<div>👥 Population: <span id="population-stat"></span></div>
|
<div>👥 Population: <span id="population-stat"></span></div>
|
||||||
|
<div>😊 Happiness: <span id="happiness-stat"></span></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="players-panel" class="ui-panel">
|
<div id="players-panel" class="ui-panel">
|
||||||
<h3>Players Online</h3>
|
<h3>Players Online</h3>
|
||||||
@ -100,12 +100,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="status-feed" class="ui-panel"></div>
|
<div id="status-feed" class="ui-panel"></div>
|
||||||
<div id="ui-panel-bottom" class="ui-panel">
|
<div id="ui-panel-bottom" class="ui-panel">
|
||||||
<!-- Icons from feathericons.com -->
|
|
||||||
<button class="build-btn" data-type="residential" title="Cost: $100"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>House</button>
|
<button class="build-btn" data-type="residential" title="Cost: $100"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>House</button>
|
||||||
<button class="build-btn" data-type="commercial" title="Cost: $250"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"></path><rect x="3" y="10" width="18" height="12" rx="2"></rect><path d="M7 15h.01"></path><path d="M17 15h.01"></path></svg>Shop</button>
|
<button class="build-btn" data-type="commercial" title="Cost: $250"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"></path><rect x="3" y="10" width="18" height="12" rx="2"></rect><path d="M7 15h.01"></path><path d="M17 15h.01"></path></svg>Shop</button>
|
||||||
<button class="build-btn" data-type="industrial" title="Cost: $500"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 22h16"/><path d="M6 18V8.103a2 2 0 0 1 .91-1.657l6-3.79a2 2 0 0 1 2.18 0l6 3.79A2 2 0 0 1 22 8.103V18"/><path d="m14 14-2-1-2 1"/><path d="M18 18h-4v-2h4v2Z"/></svg>Factory</button>
|
<button class="build-btn" data-type="industrial" title="Cost: $500"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 22h16"/><path d="M6 18V8.103a2 2 0 0 1 .91-1.657l6-3.79a2 2 0 0 1 2.18 0l6 3.79A2 2 0 0 1 22 8.103V18"/><path d="m14 14-2-1-2 1"/><path d="M18 18h-4v-2h4v2Z"/></svg>Factory</button>
|
||||||
<button class="build-btn" data-type="park" title="Cost: $80"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22v-8"/><path d="m17 8-1.5-1.5c-.9-.9-2.5-.9-3.4 0L12 6.6c-.9-.9-2.5-.9-3.4 0L7 8"/><path d="M12 22h-1a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-1Z"/></svg>Park</button>
|
<button class="build-btn" data-type="park" title="Cost: $80"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22v-8"/><path d="m17 8-1.5-1.5c-.9-.9-2.5-.9-3.4 0L12 6.6c-.9-.9-2.5-.9-3.4 0L7 8"/><path d="M12 22h-1a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-1Z"/></svg>Park</button>
|
||||||
<button class="build-btn" data-type="powerplant" title="Cost: $1000"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 12v3"/><path d="M16.24 7.76 14.12 9.88"/><path d="m7.76 7.76 2.12 2.12"/><path d="m12 2 3.5 3.5"/><path d="m12 2-3.5 3.5"/><path d="M22 12h-3"/><path d="M5 12H2"/><path d="M19.07 19.07 16.95 16.95"/><path d="m4.93 19.07 2.12-2.12"/><path d="M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20Z"/></svg>Power</button>
|
<button class="build-btn" data-type="powerplant" title="Cost: $1000"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 12v3"/><path d="M16.24 7.76 14.12 9.88"/><path d="m7.76 7.76 2.12 2.12"/><path d="m12 2 3.5 3.5"/><path d="m12 2-3.5 3.5"/><path d="M22 12h-3"/><path d="M5 12H2"/><path d="M19.07 19.07 16.95 16.95"/><path d="m4.93 19.07 2.12-2.12"/><path d="M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20Z"/></svg>Power</button>
|
||||||
|
<button class="build-btn" data-type="police" title="Cost: $600"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg>Police</button>
|
||||||
|
<button class="build-btn" data-type="stadium" title="Cost: $5000"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 8c-2.209 0-4 1.791-4 4s1.791 4 4 4 4-1.791 4-4-1.791-4-4-4z"/><path d="M22 12c0-5.523-4.477-10-10-10S2 6.477 2 12s4.477 10 10 10 10-4.477 10-10z"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="M22 12h-2"/><path d="M4 12H2"/><path d="M19.071 4.929l-1.414 1.414"/><path d="M6.343 17.657l-1.414 1.414"/><path d="M19.071 19.071l-1.414-1.414"/><path d="M6.343 6.343l-1.414-1.414"/></svg>Stadium</button>
|
||||||
<button class="build-btn" data-type="road" title="Cost: $20"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12h16"/><path d="M4 12v-2a4 4 0 0 1 4-4h8a4 4 0 0 1 4 4v2"/><path d="M4 12v2a4 4 0 0 0 4 4h8a4 4 0 0 0 4-4v-2"/></svg>Road</button>
|
<button class="build-btn" data-type="road" title="Cost: $20"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12h16"/><path d="M4 12v-2a4 4 0 0 1 4-4h8a4 4 0 0 1 4 4v2"/><path d="M4 12v2a4 4 0 0 0 4 4h8a4 4 0 0 0 4-4v-2"/></svg>Road</button>
|
||||||
<button class="build-btn" data-type="remove" title="Refund: 50%"><svg viewBox="0 0 24 24" fill="none" stroke="#fca5a5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 4H8l-7 8 7 8h13a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2Z"></path><line x1="18" x2="12" y1="9" y2="15"></line><line x1="12" x2="18" y1="9" y2="15"></line></svg>Remove</button>
|
<button class="build-btn" data-type="remove" title="Refund: 50%"><svg viewBox="0 0 24 24" fill="none" stroke="#fca5a5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 4H8l-7 8 7 8h13a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2Z"></path><line x1="18" x2="12" y1="9" y2="15"></line><line x1="12" x2="18" y1="9" y2="15"></line></svg>Remove</button>
|
||||||
</div>
|
</div>
|
||||||
@ -122,7 +123,6 @@
|
|||||||
let scene, camera, renderer, controls;
|
let scene, camera, renderer, controls;
|
||||||
let groundPlane, placementIndicator;
|
let groundPlane, placementIndicator;
|
||||||
let websocket, nickname;
|
let websocket, nickname;
|
||||||
|
|
||||||
let currentBuildType = null;
|
let currentBuildType = null;
|
||||||
const gridCellSize = 2;
|
const gridCellSize = 2;
|
||||||
const gridDivisions = 30;
|
const gridDivisions = 30;
|
||||||
@ -132,13 +132,12 @@
|
|||||||
const buildingData = {
|
const buildingData = {
|
||||||
'residential': { cost: 100 }, 'commercial': { cost: 250 },
|
'residential': { cost: 100 }, 'commercial': { cost: 250 },
|
||||||
'industrial': { cost: 500 }, 'park': { cost: 80 },
|
'industrial': { cost: 500 }, 'park': { cost: 80 },
|
||||||
'powerplant': { cost: 1000 }, 'road': { cost: 20 }
|
'powerplant': { cost: 1000 }, 'road': { cost: 20 },
|
||||||
|
'police': { cost: 600 }, 'stadium': { cost: 5000 }
|
||||||
};
|
};
|
||||||
|
|
||||||
const nicknameModal = document.getElementById('nickname-modal');
|
const nicknameModal = document.getElementById('nickname-modal');
|
||||||
const nicknameInput = document.getElementById('nickname-input');
|
const nicknameInput = document.getElementById('nickname-input');
|
||||||
const playBtn = document.getElementById('play-btn');
|
const playBtn = document.getElementById('play-btn');
|
||||||
|
|
||||||
playBtn.addEventListener('click', () => {
|
playBtn.addEventListener('click', () => {
|
||||||
nickname = nicknameInput.value.trim();
|
nickname = nicknameInput.value.trim();
|
||||||
if (nickname && /^[a-zA-Z0-9_]+$/.test(nickname)) {
|
if (nickname && /^[a-zA-Z0-9_]+$/.test(nickname)) {
|
||||||
@ -149,7 +148,6 @@
|
|||||||
alert("Please enter a valid nickname (letters, numbers, and underscores only).");
|
alert("Please enter a valid nickname (letters, numbers, and underscores only).");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Core 3D and Game Initialization ---
|
// --- Core 3D and Game Initialization ---
|
||||||
function initGame() {
|
function initGame() {
|
||||||
scene = new THREE.Scene();
|
scene = new THREE.Scene();
|
||||||
@ -176,7 +174,6 @@
|
|||||||
directionalLight.shadow.mapSize.width = 2048;
|
directionalLight.shadow.mapSize.width = 2048;
|
||||||
directionalLight.shadow.mapSize.height = 2048;
|
directionalLight.shadow.mapSize.height = 2048;
|
||||||
scene.add(directionalLight);
|
scene.add(directionalLight);
|
||||||
|
|
||||||
groundPlane = new THREE.Mesh(new THREE.PlaneGeometry(gridSize, gridSize), new THREE.MeshLambertMaterial({ color: 0x50c878 }));
|
groundPlane = new THREE.Mesh(new THREE.PlaneGeometry(gridSize, gridSize), new THREE.MeshLambertMaterial({ color: 0x50c878 }));
|
||||||
groundPlane.rotation.x = -Math.PI / 2;
|
groundPlane.rotation.x = -Math.PI / 2;
|
||||||
groundPlane.receiveShadow = true;
|
groundPlane.receiveShadow = true;
|
||||||
@ -198,7 +195,11 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function animate() { requestAnimationFrame(animate); controls.update(); renderer.render(scene, camera); }
|
function animate() {
|
||||||
|
requestAnimationFrame(animate);
|
||||||
|
controls.update();
|
||||||
|
renderer.render(scene, camera);
|
||||||
|
}
|
||||||
|
|
||||||
// --- WebSocket Logic ---
|
// --- WebSocket Logic ---
|
||||||
function connectWebSocket() {
|
function connectWebSocket() {
|
||||||
@ -243,7 +244,6 @@
|
|||||||
if (buildings.has(key)) return;
|
if (buildings.has(key)) return;
|
||||||
const buildingGroup = new THREE.Group();
|
const buildingGroup = new THREE.Group();
|
||||||
const mainMaterial = new THREE.MeshLambertMaterial();
|
const mainMaterial = new THREE.MeshLambertMaterial();
|
||||||
|
|
||||||
// Set color based on building type for self, or player color for others
|
// Set color based on building type for self, or player color for others
|
||||||
if (owner !== nickname) {
|
if (owner !== nickname) {
|
||||||
mainMaterial.color.set(getPlayerColor(owner));
|
mainMaterial.color.set(getPlayerColor(owner));
|
||||||
@ -254,6 +254,8 @@
|
|||||||
case 'industrial': mainMaterial.color.set(0xa0aec0); break;
|
case 'industrial': mainMaterial.color.set(0xa0aec0); break;
|
||||||
case 'park': mainMaterial.color.set(0x48bb78); break;
|
case 'park': mainMaterial.color.set(0x48bb78); break;
|
||||||
case 'powerplant': mainMaterial.color.set(0xf56565); break;
|
case 'powerplant': mainMaterial.color.set(0xf56565); break;
|
||||||
|
case 'police': mainMaterial.color.set(0x4299e1); break;
|
||||||
|
case 'stadium': mainMaterial.color.set(0xed8936); break;
|
||||||
default: mainMaterial.color.set(0xffffff); break;
|
default: mainMaterial.color.set(0xffffff); break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -262,7 +264,8 @@
|
|||||||
case 'residential': {
|
case 'residential': {
|
||||||
const base = new THREE.Mesh(new THREE.BoxGeometry(gridCellSize * 0.8, 2.5, gridCellSize * 0.8), mainMaterial);
|
const base = new THREE.Mesh(new THREE.BoxGeometry(gridCellSize * 0.8, 2.5, gridCellSize * 0.8), mainMaterial);
|
||||||
const roof = new THREE.Mesh(new THREE.CylinderGeometry(0, gridCellSize * 0.6, 1.5, 4), new THREE.MeshLambertMaterial({ color: 0x8b4513 }));
|
const roof = new THREE.Mesh(new THREE.CylinderGeometry(0, gridCellSize * 0.6, 1.5, 4), new THREE.MeshLambertMaterial({ color: 0x8b4513 }));
|
||||||
base.position.y = 1.25; roof.position.y = 3.25; roof.rotation.y = Math.PI / 4;
|
base.position.y = 1.25;
|
||||||
|
roof.position.y = 3.25; roof.rotation.y = Math.PI / 4;
|
||||||
buildingGroup.add(base, roof);
|
buildingGroup.add(base, roof);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -288,7 +291,8 @@
|
|||||||
trunk.add(leaves);
|
trunk.add(leaves);
|
||||||
buildingGroup.add(trunk);
|
buildingGroup.add(trunk);
|
||||||
}
|
}
|
||||||
base.position.y = 0.05; buildingGroup.add(base);
|
base.position.y = 0.05;
|
||||||
|
buildingGroup.add(base);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'powerplant': {
|
case 'powerplant': {
|
||||||
@ -303,6 +307,22 @@
|
|||||||
roadSurface.position.y = 0.05; buildingGroup.add(roadSurface);
|
roadSurface.position.y = 0.05; buildingGroup.add(roadSurface);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'police': {
|
||||||
|
const base = new THREE.Mesh(new THREE.BoxGeometry(gridCellSize * 0.8, 2, gridCellSize * 0.8), new THREE.MeshLambertMaterial({color: 0xedf2f7}));
|
||||||
|
const roof = new THREE.Mesh(new THREE.BoxGeometry(gridCellSize * 0.9, 0.3, gridCellSize * 0.9), mainMaterial);
|
||||||
|
base.position.y = 1;
|
||||||
|
roof.position.y = 2.15;
|
||||||
|
buildingGroup.add(base, roof);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'stadium': {
|
||||||
|
const base = new THREE.Mesh(new THREE.CylinderGeometry(gridCellSize * 0.8, gridCellSize, 3, 32), mainMaterial);
|
||||||
|
const field = new THREE.Mesh(new THREE.CylinderGeometry(gridCellSize * 0.5, gridCellSize * 0.6, 0.1, 32), new THREE.MeshLambertMaterial({color: 0x48bb78}));
|
||||||
|
base.position.y = 1.5;
|
||||||
|
field.position.y = 3.05;
|
||||||
|
buildingGroup.add(base, field);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
buildingGroup.position.set(position.x, 0, position.z);
|
buildingGroup.position.set(position.x, 0, position.z);
|
||||||
buildingGroup.traverse(c => { if(c.isMesh) c.castShadow = true; });
|
buildingGroup.traverse(c => { if(c.isMesh) c.castShadow = true; });
|
||||||
@ -324,6 +344,7 @@
|
|||||||
document.querySelectorAll('.build-btn').forEach(button => {
|
document.querySelectorAll('.build-btn').forEach(button => {
|
||||||
button.addEventListener('click', () => {
|
button.addEventListener('click', () => {
|
||||||
const type = button.dataset.type;
|
const type = button.dataset.type;
|
||||||
|
|
||||||
currentBuildType = (currentBuildType === type) ? null : type;
|
currentBuildType = (currentBuildType === type) ? null : type;
|
||||||
document.querySelectorAll('.build-btn').forEach(btn => btn.classList.toggle('active', btn === button && currentBuildType !== null));
|
document.querySelectorAll('.build-btn').forEach(btn => btn.classList.toggle('active', btn === button && currentBuildType !== null));
|
||||||
});
|
});
|
||||||
@ -346,12 +367,13 @@
|
|||||||
const playerDiv = document.createElement('div');
|
const playerDiv = document.createElement('div');
|
||||||
playerDiv.className = 'player-div';
|
playerDiv.className = 'player-div';
|
||||||
if(name === nickname) playerDiv.classList.add('is-self');
|
if(name === nickname) playerDiv.classList.add('is-self');
|
||||||
playerDiv.innerHTML = `<strong style="color: #${getPlayerColor(name).toString(16)}">${name}</strong>: $${Math.floor(p.money)} | Pop: ${p.population}`;
|
playerDiv.innerHTML = `<strong style="color: #${getPlayerColor(name).toString(16)}">${name}</strong>: $${Math.floor(p.money)} | Pop: ${p.population} | Hap: ${Math.round(p.happiness * 100)}%`;
|
||||||
listDiv.appendChild(playerDiv);
|
listDiv.appendChild(playerDiv);
|
||||||
|
|
||||||
if (name === nickname) {
|
if (name === nickname) {
|
||||||
document.getElementById('money-stat').textContent = `$${Math.floor(p.money)}`;
|
document.getElementById('money-stat').textContent = `$${Math.floor(p.money)}`;
|
||||||
document.getElementById('population-stat').textContent = p.population;
|
document.getElementById('population-stat').textContent = p.population;
|
||||||
|
document.getElementById('happiness-stat').textContent = `${Math.round(p.happiness * 100)}%`;
|
||||||
document.querySelectorAll('.build-btn[data-type]').forEach(btn => {
|
document.querySelectorAll('.build-btn[data-type]').forEach(btn => {
|
||||||
const type = btn.dataset.type;
|
const type = btn.dataset.type;
|
||||||
if (type !== 'remove' && buildingData[type]) {
|
if (type !== 'remove' && buildingData[type]) {
|
||||||
@ -387,7 +409,6 @@
|
|||||||
placementIndicator.visible = false;
|
placementIndicator.visible = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('game-canvas').addEventListener('mousedown', (event) => {
|
document.getElementById('game-canvas').addEventListener('mousedown', (event) => {
|
||||||
if (event.button !== 0 || !currentBuildType) return;
|
if (event.button !== 0 || !currentBuildType) return;
|
||||||
const gridPos = getGridPosition(event);
|
const gridPos = getGridPosition(event);
|
||||||
@ -404,4 +425,3 @@
|
|||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user