SimCity-style toolbar: colored pictogram icons + grouped UX
- All 16 icons redrawn as rich multi-color SVGs (houses, towers, factory, plants with smoke, services with badges/crosses, park, fountain, stadium) — unique gradient ids, no collisions - Toolbar grouped under labeled sections: Tools / Zones / Power / Safety / Health / Leisure - Every button shows price (FREE for zones); unaffordable buildings dim and turn red as funds change - Keyboard-shortcut badges on buttons; bigger 30px icons; hover lift - Floating status chip above the bar shows active tool, price, upkeep and coverage radius (SimCity status-bar feel) - FIX: Game.ui was a method while input.js read it as a property, so tool-change UI updates silently never ran; now a proper getter
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 96 KiB |
+1
-1
@@ -87,7 +87,7 @@ export class Input {
|
|||||||
this.renderer.clearGhost();
|
this.renderer.clearGhost();
|
||||||
this.renderer.hideDragRect();
|
this.renderer.hideDragRect();
|
||||||
this.renderer.setHover(null, false);
|
this.renderer.setHover(null, false);
|
||||||
this.game.ui.onToolChanged?.(tool, sid);
|
this.game.ui?.onToolChanged(tool, sid);
|
||||||
}
|
}
|
||||||
|
|
||||||
_primaryButton(e) {
|
_primaryButton(e) {
|
||||||
|
|||||||
+8
-8
@@ -101,16 +101,16 @@ class Game {
|
|||||||
this.speedIdx = 1;
|
this.speedIdx = 1;
|
||||||
this.paused = false;
|
this.paused = false;
|
||||||
this.start(c);
|
this.start(c);
|
||||||
this.ui().toast(`Welcome to ${name}, Mayor!`, 'success');
|
this.ui?.toast(`Welcome to ${name}, Mayor!`, 'success');
|
||||||
this.ui().resetQuests();
|
this.ui?.resetQuests();
|
||||||
}
|
}
|
||||||
|
|
||||||
loadFrom(cityInstance) {
|
loadFrom(cityInstance) {
|
||||||
this.start(cityInstance);
|
this.start(cityInstance);
|
||||||
this.ui().toast('City loaded.', 'success');
|
this.ui?.toast('City loaded.', 'success');
|
||||||
}
|
}
|
||||||
|
|
||||||
ui() { return this._parts?.ui; }
|
get ui() { return this._parts?.ui; }
|
||||||
get input() { return this._parts?.input; }
|
get input() { return this._parts?.input; }
|
||||||
get renderer() { return this._parts?.renderer; }
|
get renderer() { return this._parts?.renderer; }
|
||||||
|
|
||||||
@@ -120,7 +120,7 @@ class Game {
|
|||||||
this.speedIdx = idx;
|
this.speedIdx = idx;
|
||||||
this.paused = idx === 0;
|
this.paused = idx === 0;
|
||||||
if (idx > 0) this.lastNonzeroSpeed = idx;
|
if (idx > 0) this.lastNonzeroSpeed = idx;
|
||||||
this.ui()?.setSpeedActive(idx);
|
this.ui?.setSpeedActive(idx);
|
||||||
}
|
}
|
||||||
|
|
||||||
togglePause() {
|
togglePause() {
|
||||||
@@ -137,16 +137,16 @@ class Game {
|
|||||||
|
|
||||||
queryTile(tile, x, y) {
|
queryTile(tile, x, y) {
|
||||||
const info = this.city.getTileInfo(tile.x, tile.z);
|
const info = this.city.getTileInfo(tile.x, tile.z);
|
||||||
if (info) this.ui()?.showInfoPopup(info, x, y);
|
if (info) this.ui?.showInfoPopup(info, x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
checkQuests() { this.ui()?.checkQuests(); }
|
checkQuests() { this.ui?.checkQuests(); }
|
||||||
|
|
||||||
_syncSoon() {
|
_syncSoon() {
|
||||||
if (this.city.tilesDirty && this._parts) {
|
if (this.city.tilesDirty && this._parts) {
|
||||||
this._parts.renderer.sync();
|
this._parts.renderer.sync();
|
||||||
this.city.tilesDirty = false;
|
this.city.tilesDirty = false;
|
||||||
this.ui()?.scheduleMinimap();
|
this.ui?.scheduleMinimap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+44
-11
@@ -77,25 +77,54 @@ html, body {
|
|||||||
/* =================== toolbar =================== */
|
/* =================== toolbar =================== */
|
||||||
#toolbar {
|
#toolbar {
|
||||||
position: absolute; bottom: 14px; left: 50%; transform: translateX(-50%);
|
position: absolute; bottom: 14px; left: 50%; transform: translateX(-50%);
|
||||||
display: flex; align-items: stretch; gap: 4px; max-width: calc(100vw - 24px);
|
display: flex; align-items: stretch; gap: 6px; max-width: calc(100vw - 24px);
|
||||||
background: var(--panel); backdrop-filter: blur(10px);
|
background: var(--panel); backdrop-filter: blur(10px);
|
||||||
border: 1px solid var(--line); border-radius: 16px; padding: 7px;
|
border: 1px solid var(--line); border-radius: 16px; padding: 6px 8px;
|
||||||
overflow-x: auto; scrollbar-width: none;
|
overflow-x: auto; scrollbar-width: none;
|
||||||
}
|
}
|
||||||
#toolbar::-webkit-scrollbar { display: none; }
|
#toolbar::-webkit-scrollbar { display: none; }
|
||||||
.tb-sep { width: 1px; background: var(--line); margin: 4px 3px; }
|
|
||||||
|
/* labeled sections, SimCity-style */
|
||||||
|
.tb-group { display: flex; flex-direction: column; align-items: center; gap: 3px; padding: 0 3px; }
|
||||||
|
.tb-group + .tb-group { border-left: 1px solid var(--line); padding-left: 9px; }
|
||||||
|
.tb-cap { font-size: 8.5px; font-weight: 700; letter-spacing: .14em; text-transform: uppercase;
|
||||||
|
color: var(--dim); opacity: .85; user-select: none; }
|
||||||
|
.tb-row { display: flex; gap: 4px; }
|
||||||
|
|
||||||
.tl-btn {
|
.tl-btn {
|
||||||
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px;
|
position: relative;
|
||||||
min-width: 58px; padding: 7px 6px 5px; border-radius: 11px; border: 1px solid transparent;
|
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px;
|
||||||
background: transparent; color: var(--text); cursor: pointer; transition: all .13s;
|
min-width: 64px; padding: 6px 6px 5px; border-radius: 11px; border: 1px solid transparent;
|
||||||
|
background: rgba(255,255,255,.03); color: var(--text); cursor: pointer; transition: all .13s;
|
||||||
}
|
}
|
||||||
.tl-btn:hover { background: rgba(255,255,255,.09); border-color: var(--line); }
|
.tl-btn:hover { background: rgba(255,255,255,.09); border-color: var(--line); transform: translateY(-1px); }
|
||||||
.tl-btn.active { background: rgba(53,224,138,.16); border-color: rgba(53,224,138,.65);
|
.tl-btn.active { background: rgba(53,224,138,.16); border-color: rgba(53,224,138,.65);
|
||||||
box-shadow: 0 0 14px rgba(53,224,138,.25) inset; }
|
box-shadow: 0 0 14px rgba(53,224,138,.25) inset; }
|
||||||
.tl-ico svg { width: 23px; height: 23px; }
|
.tl-ico svg { width: 30px; height: 30px; border-radius: 7px; }
|
||||||
.tl-label { font-size: 10.5px; color: var(--dim); display: flex; gap: 4px; align-items: baseline; white-space: nowrap; }
|
.tl-label { font-size: 10.5px; color: var(--text); white-space: nowrap; line-height: 1.1; }
|
||||||
.tl-btn.active .tl-label { color: var(--accent); }
|
.tl-btn.active .tl-label { color: var(--accent); }
|
||||||
.tl-cost { font-size: 9px; opacity: .75; }
|
.tl-cost { font-size: 9px; color: #7ddba3; opacity: .9; line-height: 1; }
|
||||||
|
.tl-key {
|
||||||
|
position: absolute; top: 4px; right: 5px; font-size: 8.5px; line-height: 13px;
|
||||||
|
color: var(--dim); border: 1px solid var(--line); border-radius: 4px; padding: 0 3px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.tl-btn.poor { opacity: .45; filter: saturate(.4); }
|
||||||
|
.tl-btn.poor .tl-cost { color: #ff7a7a; }
|
||||||
|
|
||||||
|
/* floating "current tool" chip above the bar */
|
||||||
|
#toolReadout {
|
||||||
|
position: absolute; bottom: 92px; left: 50%; transform: translate(-50%, 6px);
|
||||||
|
display: flex; align-items: center; gap: 8px;
|
||||||
|
background: var(--panel); border: 1px solid var(--line); border-radius: 12px;
|
||||||
|
padding: 6px 12px 6px 8px; pointer-events: none; opacity: 0;
|
||||||
|
transition: opacity .15s, transform .15s; max-width: min(70vw, 420px);
|
||||||
|
}
|
||||||
|
#toolReadout.show { opacity: 1; transform: translate(-50%, 0); }
|
||||||
|
#toolReadout .tr-ico svg { width: 24px; height: 24px; border-radius: 6px; }
|
||||||
|
#toolReadout b { font-size: 12.5px; white-space: nowrap; }
|
||||||
|
#toolReadout .tr-tip { font-size: 11px; color: var(--dim); white-space: nowrap;
|
||||||
|
overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
|
||||||
/* =================== toasts =================== */
|
/* =================== toasts =================== */
|
||||||
#toasts { position: absolute; top: 64px; left: 12px; display: flex; flex-direction: column; gap: 8px; max-width: 340px; }
|
#toasts { position: absolute; top: 64px; left: 12px; display: flex; flex-direction: column; gap: 8px; max-width: 340px; }
|
||||||
@@ -252,7 +281,11 @@ h4 { margin: 12px 0 4px; font-size: 12.5px; color: var(--dim); }
|
|||||||
#topbar { padding: 6px 8px; }
|
#topbar { padding: 6px 8px; }
|
||||||
.stat { padding: 3px 8px; font-size: 12.5px; }
|
.stat { padding: 3px 8px; font-size: 12.5px; }
|
||||||
#cityName { font-size: 14px; }
|
#cityName { font-size: 14px; }
|
||||||
.tl-btn { min-width: 48px; }
|
.tl-btn { min-width: 52px; }
|
||||||
|
.tl-ico svg { width: 26px; height: 26px; }
|
||||||
|
.tl-key { display: none; }
|
||||||
|
.tb-cap { font-size: 7.5px; }
|
||||||
|
#toolReadout { bottom: 96px; }
|
||||||
.tl-label { font-size: 9px; }
|
.tl-label { font-size: 9px; }
|
||||||
#questBox { display: none; }
|
#questBox { display: none; }
|
||||||
#minimapWrap { bottom: 110px; right: 8px; }
|
#minimapWrap { bottom: 110px; right: 8px; }
|
||||||
|
|||||||
+161
-21
@@ -1,25 +1,165 @@
|
|||||||
/** Inline stroke-style SVG icon set (24×24). */
|
/**
|
||||||
const S = 'fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"';
|
* Colorful SimCity-style pictogram icons (filled, multi-color, 32×32).
|
||||||
|
* Each gradient id is globally unique so any number can coexist inline.
|
||||||
function svg(inner) {
|
*/
|
||||||
return `<svg viewBox="0 0 24 24" ${S}>${inner}</svg>`;
|
function svg(inner, vb = '0 0 32 32') {
|
||||||
|
return `<svg viewBox="${vb}" xmlns="http://www.w3.org/2000/svg">${inner}</svg>`;
|
||||||
}
|
}
|
||||||
|
const sky = (id, c1 = '#bfe3ff', c2 = '#8fc7f2') =>
|
||||||
|
`<defs><linearGradient id="${id}" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="${c1}"/><stop offset="1" stop-color="${c2}"/></linearGradient></defs>`;
|
||||||
|
|
||||||
export const ICONS = {
|
export const ICONS = {
|
||||||
query: svg('<circle cx="10" cy="10" r="6"/><path d="M14.5 14.5L20 20"/>'),
|
/* ---------- tools ---------- */
|
||||||
bulldoze: svg('<rect x="3" y="12" width="8" height="6" rx="1"/><path d="M11 13h4l4 4v1h-8"/><circle cx="7" cy="19.5" r="1.8"/><circle cx="16.5" cy="19.5" r="1.5"/><path d="M5 12V9h4v3"/>'),
|
query: svg(`${sky('q-sky')}
|
||||||
road: svg('<path d="M9 3L5 21"/><path d="M15 3l4 18"/><path d="M12 4v3"/><path d="M12 11v3"/><path d="M12 18v3"/>'),
|
<rect x="2" y="2" width="28" height="28" rx="5" fill="url(#q-sky)"/>
|
||||||
res: svg('<path d="M4 11l8-7 8 7"/><path d="M6 10v10h12V10"/><path d="M10 20v-5h4v5"/>'),
|
<path d="M2 22h28v3a5 5 0 01-5 5H7a5 5 0 01-5-5z" fill="#79a86b"/>
|
||||||
com: svg('<rect x="4" y="8" width="16" height="12" rx="1"/><path d="M4 8l2-4h12l2 4"/><path d="M9 20v-6h6v6"/><path d="M4 12h16"/>'),
|
<circle cx="14" cy="13" r="7.2" fill="#ffffffcc"/>
|
||||||
ind: svg('<path d="M4 20V10l5 3v-3l5 3v-3l6 3v7z"/><rect x="16.5" y="4" width="3" height="6"/>'),
|
<circle cx="14" cy="13" r="7.2" fill="none" stroke="#1f7fd0" stroke-width="2.4"/>
|
||||||
coal: svg('<circle cx="12" cy="12" r="9"/><path d="M13 6l-5 7h4l-1 5 5-7h-4z"/>'),
|
<path d="M19.5 18.5L26 25" stroke="#1f7fd0" stroke-width="3.4" stroke-linecap="round"/>
|
||||||
solar: svg('<path d="M4 14h16l-2 6H6z"/><path d="M8 14l1.5 6"/><path d="M16 14l-1.5 6"/><path d="M5.5 17h13"/><circle cx="18" cy="5" r="2.4"/>'),
|
<path d="M11 11a3.4 3.4 0 013-2" stroke="#9fd0f5" stroke-width="1.6" stroke-linecap="round" fill="none"/>`),
|
||||||
wind: svg('<path d="M12 21v-8"/><path d="M12 13V4"/><path d="M12 13l7.8 4.5"/><path d="M12 13L4.2 17.5"/><circle cx="12" cy="13" r="1.6"/>'),
|
|
||||||
police: svg('<path d="M12 3l7 3v6c0 4.5-3 7.5-7 9-4-1.5-7-4.5-7-9V6z"/><path d="M9 12l2 2 4-4"/>'),
|
bulldoze: svg(`
|
||||||
fire: svg('<path d="M12 3c1 3 4.5 4.5 4.5 8.5a4.5 4.5 0 01-9 0c0-2 .9-3.2 1.8-4.3C10.2 9 12 6.5 12 3z"/><path d="M12 21a3 3 0 003-3c0-1.5-1.5-2.5-3-4-1.5 1.5-3 2.5-3 4a3 3 0 003 3z"/>'),
|
<rect x="2" y="20" width="28" height="8" rx="3" fill="#8a6b4a"/>
|
||||||
hospital: svg('<rect x="4" y="4" width="16" height="16" rx="2"/><path d="M12 8v8"/><path d="M8 12h8"/>'),
|
<rect x="2" y="20" width="28" height="8" rx="3" fill="none" stroke="#5f4630" stroke-width="1.2"/>
|
||||||
school: svg('<path d="M3 9l9-4 9 4-9 4z"/><path d="M7 11.5v3.5c0 1.6 2.2 3 5 3s5-1.4 5-3v-3.5"/><path d="M21 9v5"/>'),
|
<rect x="6" y="9" width="13" height="12" rx="2" fill="#f2b632"/>
|
||||||
park: svg('<path d="M12 3l5 7h-3l4 6H6l4-6H7z"/><path d="M12 16v5"/>'),
|
<rect x="6" y="9" width="13" height="5" rx="2" fill="#ffd45e"/>
|
||||||
plaza: svg('<path d="M12 4v4"/><path d="M12 8c-2 0-3.2 1-3.2 3h6.4c0-2-1.2-3-3.2-3z"/><path d="M5 14h14l-1.5 6h-11z"/><path d="M8.5 11.5h7"/>'),
|
<rect x="21" y="13" width="7" height="8" rx="1.5" fill="#e8912b"/>
|
||||||
stadium: svg('<ellipse cx="12" cy="13" rx="8.5" ry="5.5"/><ellipse cx="12" cy="13" rx="4" ry="2.2"/><path d="M3.8 10.5L2 7.5M20.2 10.5L22 7.5"/>')
|
<path d="M21 15l7-6" stroke="#5f4630" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<circle cx="10" cy="23.5" r="2.6" fill="#333"/><circle cx="17" cy="23.5" r="2.6" fill="#333"/>
|
||||||
|
<path d="M4 27h24" stroke="#3d2e1e" stroke-width="1.6" stroke-linecap="round"/>`),
|
||||||
|
|
||||||
|
/* ---------- zones ---------- */
|
||||||
|
road: svg(`${sky('rd-sky')}
|
||||||
|
<rect x="2" y="2" width="28" height="28" rx="5" fill="url(#rd-sky)"/>
|
||||||
|
<path d="M9 3h14L29 29H3z" fill="#4a4f57"/>
|
||||||
|
<path d="M11 3h4l6 26h-4z" fill="#585e68"/>
|
||||||
|
<path d="M16 5.5v3M16 12v3.4M16 19v3.4" stroke="#ffd23b" stroke-width="1.7" stroke-linecap="round"/>
|
||||||
|
<path d="M3 29c2-2.4 5-2.8 8-1.6L10 32H3zM29 29c-2-2.4-5-2.8-8-1.6L22 32h7z" fill="#57a04b"/>`),
|
||||||
|
|
||||||
|
res: svg(`${sky('r-sky')}
|
||||||
|
<rect x="2" y="2" width="28" height="28" rx="5" fill="url(#r-sky)"/>
|
||||||
|
<rect x="2" y="22" width="28" height="8" fill="#57a04b"/>
|
||||||
|
<rect x="7" y="14" width="11" height="10" fill="#f7f3e8" stroke="#3fae5a" stroke-width="1.4"/>
|
||||||
|
<path d="M5.5 14L12.5 7l7 7z" fill="#3fae5a"/>
|
||||||
|
<rect x="11" y="18" width="3.4" height="6" fill="#7a5230"/>
|
||||||
|
<circle cx="23" cy="17" r="4.4" fill="#2f8f43"/>
|
||||||
|
<rect x="22" y="19" width="2" height="5.4" fill="#6b4423"/>
|
||||||
|
<circle cx="25.5" cy="20" r="2.8" fill="#48b05c"/>`),
|
||||||
|
|
||||||
|
com: svg(`${sky('c-sky')}
|
||||||
|
<rect x="2" y="2" width="28" height="28" rx="5" fill="url(#c-sky)"/>
|
||||||
|
<rect x="2" y="24" width="28" height="6" fill="#6d7887"/>
|
||||||
|
<rect x="6" y="10" width="8" height="16" fill="#3d7be0"/>
|
||||||
|
<rect x="15" y="5" width="10" height="21" fill="#2f5fc4"/>
|
||||||
|
${[12, 15, 18].map(y => `<rect x="7.6" y="${y}" width="1.8" height="1.8" fill="#cfe4ff"/><rect x="10.6" y="${y}" width="1.8" height="1.8" fill="#cfe4ff"/>`).join('')}
|
||||||
|
${[7, 10, 13, 16, 19].map(y => `<rect x="17" y="${y}" width="2" height="2" fill="#bcd8ff"/><rect x="21" y="${y}" width="2" height="2" fill="#bcd8ff"/>`).join('')}
|
||||||
|
<rect x="19" y="2.6" width="2" height="3" fill="#e8b93c"/>`),
|
||||||
|
|
||||||
|
ind: svg(`${sky('i-sky', '#ffe1b0', '#f0b97a')}
|
||||||
|
<rect x="2" y="2" width="28" height="28" rx="5" fill="url(#i-sky)"/>
|
||||||
|
<rect x="2" y="24" width="28" height="6" fill="#8a7a63"/>
|
||||||
|
<path d="M5 24V14l6 3.4V14l6 3.4V14l6 3.4V24z" fill="#e8963c"/>
|
||||||
|
<path d="M5 24V14l6 3.4V14l6 3.4V14l6 3.4V24z" fill="none" stroke="#a35f1d" stroke-width="1.2"/>
|
||||||
|
<rect x="22" y="5" width="3.6" height="10" fill="#9a9aa5"/>
|
||||||
|
<ellipse cx="23.8" cy="3.4" rx="3.4" ry="2" fill="#e6e6ec" opacity=".9"/>
|
||||||
|
<rect x="8" y="18" width="3" height="3" fill="#5f3d16"/><rect x="14" y="18" width="3" height="3" fill="#5f3d16"/>`),
|
||||||
|
|
||||||
|
/* ---------- power ---------- */
|
||||||
|
coal: svg(`${sky('co-sky', '#d8dee8', '#aab4c4')}
|
||||||
|
<rect x="2" y="2" width="28" height="28" rx="5" fill="url(#co-sky)"/>
|
||||||
|
<rect x="2" y="23" width="28" height="7" fill="#5c6672"/>
|
||||||
|
<rect x="5" y="14" width="14" height="9" fill="#57606c"/>
|
||||||
|
<rect x="7" y="16" width="3" height="3" fill="#ffb03b"/><rect x="12" y="16" width="3" height="3" fill="#ffb03b"/>
|
||||||
|
<rect x="20" y="8" width="3.4" height="15" fill="#3f4854"/>
|
||||||
|
<rect x="25" y="11" width="3" height="12" fill="#3f4854"/>
|
||||||
|
<ellipse cx="21.7" cy="6" rx="3.2" ry="1.9" fill="#eef1f5"/>
|
||||||
|
<ellipse cx="26.5" cy="9" rx="2.6" ry="1.6" fill="#eef1f5"/>
|
||||||
|
<path d="M11.5 19l-2.4 3.6h2l-.8 2.6 2.8-3.6h-2z" fill="#ffd23b" stroke="#e8891b" stroke-width=".6"/>`),
|
||||||
|
|
||||||
|
solar: svg(`${sky('s-sky')}
|
||||||
|
<rect x="2" y="2" width="28" height="28" rx="5" fill="url(#s-sky)"/>
|
||||||
|
<rect x="2" y="23" width="28" height="7" fill="#57a04b"/>
|
||||||
|
<circle cx="24.5" cy="8" r="4" fill="#ffd23b"/>
|
||||||
|
<circle cx="24.5" cy="8" r="4" fill="none" stroke="#f0a92e" stroke-width="1.2"/>
|
||||||
|
<path d="M4 21l4-9h14l-4 9z" fill="#2b5fb8"/>
|
||||||
|
<path d="M4 21l4-9h14l-4 9z" fill="none" stroke="#16336e" stroke-width="1.2"/>
|
||||||
|
<path d="M9.2 16.5h13M8 19h13.4M10.8 12l-2.4 9M15 12l-2.2 9M19 12l-2 9" stroke="#7fb0ff" stroke-width="1.1"/>
|
||||||
|
<path d="M6 21v3M20 21v3" stroke="#3d2e1e" stroke-width="1.6"/>`),
|
||||||
|
|
||||||
|
wind: svg(`${sky('w-sky')}
|
||||||
|
<rect x="2" y="2" width="28" height="28" rx="5" fill="url(#w-sky)"/>
|
||||||
|
<path d="M2 25c5-4 9-3 14-1s10 1 14-1v7H2z" fill="#57a04b"/>
|
||||||
|
<rect x="14.8" y="10" width="2.4" height="16" rx="1" fill="#e8ecf2"/>
|
||||||
|
<path d="M16 10L16 2.6l1.9 6.8z" fill="#f8fafc"/>
|
||||||
|
<path d="M16 10l6.6 3.8-6.9-.9z" fill="#dde4ee"/>
|
||||||
|
<path d="M16 10l-6.6 3.8 6.9-.9z" fill="#cdd6e4"/>
|
||||||
|
<circle cx="16" cy="10" r="2.1" fill="#f8fafc" stroke="#94a3b8" stroke-width=".9"/>`),
|
||||||
|
|
||||||
|
/* ---------- civic ---------- */
|
||||||
|
police: svg(`${sky('p-sky')}
|
||||||
|
<rect x="2" y="2" width="28" height="28" rx="5" fill="url(#p-sky)"/>
|
||||||
|
<rect x="2" y="24" width="28" height="6" fill="#5c6672"/>
|
||||||
|
<rect x="6" y="9" width="20" height="15" fill="#274b8f"/>
|
||||||
|
<rect x="6" y="9" width="20" height="3.4" fill="#1d3a70"/>
|
||||||
|
<path d="M16 12.2l1.5 3 3.3.4-2.4 2.3.6 3.3-3-1.6-3 1.6.6-3.3-2.4-2.3 3.3-.4z" fill="#ffd23b"/>
|
||||||
|
<rect x="8" y="19" width="3" height="5" fill="#cfe4ff"/><rect x="21" y="19" width="3" height="5" fill="#cfe4ff"/>`),
|
||||||
|
|
||||||
|
fire: svg(`${sky('f-sky', '#ffd9c2', '#f2a37a')}
|
||||||
|
<rect x="2" y="2" width="28" height="28" rx="5" fill="url(#f-sky)"/>
|
||||||
|
<rect x="2" y="24" width="28" height="6" fill="#5c5050"/>
|
||||||
|
<rect x="5" y="12" width="22" height="12" fill="#c93b2c"/>
|
||||||
|
<rect x="5" y="12" width="22" height="3" fill="#a02a1e"/>
|
||||||
|
<rect x="12" y="16" width="8" height="8" fill="#3a2a26"/>
|
||||||
|
<path d="M16 4.6c1.2 2.6 3.9 3.8 3.9 7a3.9 3.9 0 11-7.8 0c0-1.6.7-2.7 1.5-3.7.6 1 1.4 1.6 2.4 1.7-.6-1.8-.6-3.4 0-5z" fill="#ff8c1a"/>
|
||||||
|
<path d="M16 8.4c.7 1.3 2 2 2 3.7a2 2 0 11-4 0c0-1.7 1.3-2.4 2-3.7z" fill="#ffd23b"/>`),
|
||||||
|
|
||||||
|
hospital: svg(`${sky('h-sky')}
|
||||||
|
<rect x="2" y="2" width="28" height="28" rx="5" fill="url(#h-sky)"/>
|
||||||
|
<rect x="2" y="24" width="28" height="6" fill="#6d7887"/>
|
||||||
|
<rect x="6" y="7" width="20" height="17" rx="1.5" fill="#f4f7fa" stroke="#b9c4cf" stroke-width="1.1"/>
|
||||||
|
<rect x="14" y="10" width="4" height="11" fill="#e03e3e"/>
|
||||||
|
<rect x="10.5" y="13.5" width="11" height="4" fill="#e03e3e"/>
|
||||||
|
<rect x="8" y="20" width="3.2" height="4" fill="#7fb0e8"/><rect x="20.8" y="20" width="3.2" height="4" fill="#7fb0e8"/>`),
|
||||||
|
|
||||||
|
school: svg(`${sky('sc-sky')}
|
||||||
|
<rect x="2" y="2" width="28" height="28" rx="5" fill="url(#sc-sky)"/>
|
||||||
|
<rect x="2" y="24" width="28" height="6" fill="#57a04b"/>
|
||||||
|
<rect x="5" y="13" width="22" height="11" fill="#e8963c"/>
|
||||||
|
<path d="M4 13l12-6.5L28 13z" fill="#b96a20"/>
|
||||||
|
<rect x="13.6" y="18" width="4.8" height="6" fill="#2b5fb8"/>
|
||||||
|
<rect x="8" y="17" width="3" height="3" fill="#fff3d6"/><rect x="21" y="17" width="3" height="3" fill="#fff3d6"/>
|
||||||
|
<path d="M16 6.5V3.4" stroke="#5f4630" stroke-width="1.3"/>
|
||||||
|
<path d="M16 3.4l4 1.2-4 1.4z" fill="#e03e3e"/>`),
|
||||||
|
|
||||||
|
/* ---------- leisure ---------- */
|
||||||
|
park: svg(`${sky('pk-sky')}
|
||||||
|
<rect x="2" y="2" width="28" height="28" rx="5" fill="url(#pk-sky)"/>
|
||||||
|
<path d="M2 24c6-2.4 22-2.4 28 0v6H2z" fill="#57a04b"/>
|
||||||
|
<circle cx="10.5" cy="14" r="6" fill="#2f8f43"/>
|
||||||
|
<circle cx="10.5" cy="11" r="4" fill="#48b05c"/>
|
||||||
|
<rect x="9.5" y="17" width="2" height="6" fill="#6b4423"/>
|
||||||
|
<circle cx="22" cy="16.5" r="5" fill="#3da355"/>
|
||||||
|
<circle cx="22" cy="14" r="3.4" fill="#59c06d"/>
|
||||||
|
<rect x="21.2" y="19" width="1.8" height="4.6" fill="#6b4423"/>`),
|
||||||
|
|
||||||
|
plaza: svg(`${sky('pl-sky')}
|
||||||
|
<rect x="2" y="2" width="28" height="28" rx="5" fill="#cfd8e2"/>
|
||||||
|
<rect x="2" y="2" width="28" height="28" rx="5" fill="url(#pl-sky)" opacity=".35"/>
|
||||||
|
<rect x="2" y="24" width="28" height="6" fill="#9aa7b5"/>
|
||||||
|
<ellipse cx="16" cy="19" rx="11" ry="4.6" fill="#8d99a8"/>
|
||||||
|
<ellipse cx="16" cy="18.2" rx="9" ry="3.6" fill="#3d9be0"/>
|
||||||
|
<ellipse cx="16" cy="18.2" rx="9" ry="3.6" fill="none" stroke="#2b6ea8" stroke-width="1"/>
|
||||||
|
<rect x="14.6" y="8" width="2.8" height="9" fill="#c2ccd8"/>
|
||||||
|
<ellipse cx="16" cy="8" rx="4.4" ry="1.9" fill="#aeb9c8"/>
|
||||||
|
<path d="M16 10.5c-1.6 1.6-1.6 3.4 0 5 1.6-1.6 1.6-3.4 0-5z" fill="#7fc4f5"/>
|
||||||
|
<circle cx="11" cy="16.6" r="1.1" fill="#9fd8fa"/><circle cx="21" cy="16.9" r="1.1" fill="#9fd8fa"/>`),
|
||||||
|
|
||||||
|
stadium: svg(`${sky('st-sky')}
|
||||||
|
<rect x="2" y="2" width="28" height="28" rx="5" fill="url(#st-sky)"/>
|
||||||
|
<ellipse cx="16" cy="18" rx="13.4" ry="9" fill="#b9c2ce"/>
|
||||||
|
<ellipse cx="16" cy="17.4" rx="11.6" ry="7.6" fill="#e8ecf2"/>
|
||||||
|
<ellipse cx="16" cy="17.8" rx="8.6" ry="5.2" fill="#3fae5a"/>
|
||||||
|
<path d="M16 13.2v9.2M10 17.8h12" stroke="#e8f5ec" stroke-width="1.1"/>
|
||||||
|
${[[-7, -4], [-3.5, -5], [0, -5.6], [3.5, -5], [7, -4]].map(([dx]) => `<circle cx="${16 + dx}" cy="11.6" r="1.15" fill="#5b6b80"/>`).join('')}
|
||||||
|
<rect x="13.8" y="24" width="4.4" height="2.6" fill="#8d99a8"/>`)
|
||||||
};
|
};
|
||||||
|
|||||||
+70
-29
@@ -13,27 +13,22 @@ const CITY_NAMES = ['Springvale', 'Port Aurora', 'New Meridian', 'Lakeharbor', '
|
|||||||
'Sunfield', 'Baycrest', 'Fernwood', 'Stonebrook', 'Havenport'];
|
'Sunfield', 'Baycrest', 'Fernwood', 'Stonebrook', 'Havenport'];
|
||||||
|
|
||||||
const TOOLBAR = [
|
const TOOLBAR = [
|
||||||
{ t: 'tool', id: 'query', icon: 'query', label: 'Inspect', key: 'Q', tip: 'Click tiles for details' },
|
{ t: 'tool', id: 'query', icon: 'query', label: 'Inspect', key: 'Q', tip: 'Click tiles for details', group: 'Tools' },
|
||||||
{ t: 'tool', id: 'bulldoze', icon: 'bulldoze', label: 'Bulldoze', key: 'B', tip: '$3 per tile' },
|
{ t: 'tool', id: 'bulldoze', icon: 'bulldoze', label: 'Bulldoze', key: 'B', tip: '$3 per tile', group: 'Tools', costLabel: '$3' },
|
||||||
{ t: 'tool', id: 'road', icon: 'road', label: 'Road', key: 'R', tip: '$10 · bridges $120' },
|
{ t: 'tool', id: 'road', icon: 'road', label: 'Road', key: 'R', tip: 'Drag an L-shape · bridges $120', group: 'Tools', costLabel: '$10' },
|
||||||
{ sep: true },
|
{ t: 'zone', id: 'zone_res', zname: 'res', icon: 'res', label: 'Homes', key: 'Z', tip: 'Free · drag a rectangle', group: 'Zones' },
|
||||||
{ t: 'zone', id: 'zone_res', zname: 'res', icon: 'res', label: 'Homes', key: 'Z', tip: 'Free · drag a rectangle' },
|
{ t: 'zone', id: 'zone_com', zname: 'com', icon: 'com', label: 'Shops', key: 'X', tip: 'Free · jobs & taxes', group: 'Zones' },
|
||||||
{ t: 'zone', id: 'zone_com', zname: 'com', icon: 'com', label: 'Shops', key: 'X', tip: 'Free · jobs & taxes' },
|
{ t: 'zone', id: 'zone_ind', zname: 'ind', icon: 'ind', label: 'Industry', key: 'C', tip: 'Free · jobs, pollution', group: 'Zones' },
|
||||||
{ t: 'zone', id: 'zone_ind', zname: 'ind', icon: 'ind', label: 'Industry', key: 'C', tip: 'Free · jobs, pollution' },
|
{ t: 'build', sid: STRUCT.COAL, icon: 'coal', label: 'Coal Plant', group: 'Power' },
|
||||||
{ sep: true },
|
{ t: 'build', sid: STRUCT.SOLAR, icon: 'solar', label: 'Solar Farm', group: 'Power' },
|
||||||
{ t: 'build', sid: STRUCT.COAL, icon: 'coal', label: 'Coal Plant' },
|
{ t: 'build', sid: STRUCT.WIND, icon: 'wind', label: 'Wind Turbine', group: 'Power' },
|
||||||
{ t: 'build', sid: STRUCT.SOLAR, icon: 'solar', label: 'Solar Farm' },
|
{ t: 'build', sid: STRUCT.POLICE, icon: 'police', label: 'Police', group: 'Safety' },
|
||||||
{ t: 'build', sid: STRUCT.WIND, icon: 'wind', label: 'Wind Turbine' },
|
{ t: 'build', sid: STRUCT.FIRE, icon: 'fire', label: 'Fire Stn', group: 'Safety' },
|
||||||
{ sep: true },
|
{ t: 'build', sid: STRUCT.HOSPITAL, icon: 'hospital', label: 'Hospital', group: 'Health' },
|
||||||
{ t: 'build', sid: STRUCT.POLICE, icon: 'police', label: 'Police' },
|
{ t: 'build', sid: STRUCT.SCHOOL, icon: 'school', label: 'School', group: 'Health' },
|
||||||
{ t: 'build', sid: STRUCT.FIRE, icon: 'fire', label: 'Fire Stn' },
|
{ t: 'build', sid: STRUCT.PARK, icon: 'park', label: 'Park', group: 'Leisure' },
|
||||||
{ sep: true },
|
{ t: 'build', sid: STRUCT.PLAZA, icon: 'plaza', label: 'Plaza', group: 'Leisure' },
|
||||||
{ t: 'build', sid: STRUCT.HOSPITAL, icon: 'hospital', label: 'Hospital' },
|
{ t: 'build', sid: STRUCT.STADIUM, icon: 'stadium', label: 'Stadium', group: 'Leisure' }
|
||||||
{ t: 'build', sid: STRUCT.SCHOOL, icon: 'school', label: 'School' },
|
|
||||||
{ sep: true },
|
|
||||||
{ t: 'build', sid: STRUCT.PARK, icon: 'park', label: 'Park' },
|
|
||||||
{ t: 'build', sid: STRUCT.PLAZA, icon: 'plaza', label: 'Plaza' },
|
|
||||||
{ t: 'build', sid: STRUCT.STADIUM, icon: 'stadium', label: 'Stadium' }
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export class UI {
|
export class UI {
|
||||||
@@ -88,26 +83,52 @@ export class UI {
|
|||||||
buildToolbar() {
|
buildToolbar() {
|
||||||
const bar = $('toolbar');
|
const bar = $('toolbar');
|
||||||
bar.innerHTML = '';
|
bar.innerHTML = '';
|
||||||
|
|
||||||
|
// floating readout: shows the active tool like SimCity's status chip
|
||||||
|
const chip = el('<div id="toolReadout"></div>');
|
||||||
|
bar.insertAdjacentElement('beforebegin', chip);
|
||||||
|
|
||||||
|
// group items under labeled sections (Tools · Zones · Power · …)
|
||||||
|
const groups = [];
|
||||||
for (const item of TOOLBAR) {
|
for (const item of TOOLBAR) {
|
||||||
if (item.sep) { bar.appendChild(el('<div class="tb-sep"></div>')); continue; }
|
let g = groups[groups.length - 1];
|
||||||
|
if (!g || g.name !== item.group) { g = { name: item.group, items: [] }; groups.push(g); }
|
||||||
|
g.items.push(item);
|
||||||
|
}
|
||||||
|
for (const g of groups) {
|
||||||
|
const box = el(`<div class="tb-group"><div class="tb-cap">${g.name}</div><div class="tb-row"></div></div>`);
|
||||||
|
const row = box.querySelector('.tb-row');
|
||||||
|
for (const item of g.items) {
|
||||||
|
const cost = item.t === 'build' ? BUILDINGS[item.sid].cost
|
||||||
|
: (item.t === 'zone' ? 0 : null);
|
||||||
|
const costText = item.costLabel ?? (cost === null ? '' : cost === 0 ? 'FREE' : '$' + fmtNum(cost));
|
||||||
const btn = el(`<button class="tl-btn" data-id="${item.id}">
|
const btn = el(`<button class="tl-btn" data-id="${item.id}">
|
||||||
|
${item.key ? `<span class="tl-key">${item.key}</span>` : ''}
|
||||||
<span class="tl-ico">${ICONS[item.icon]}</span>
|
<span class="tl-ico">${ICONS[item.icon]}</span>
|
||||||
<span class="tl-label">${item.label}</span>
|
<span class="tl-label">${item.label}</span>
|
||||||
|
<span class="tl-cost">${costText}</span>
|
||||||
</button>`);
|
</button>`);
|
||||||
btn.title = `${item.label}${item.key ? ` (${item.key})` : ''}${item.tip ? ' — ' + item.tip : ''}`;
|
btn.title = `${item.label}${item.tip ? ' — ' + item.tip : ''}`;
|
||||||
|
btn.dataset.kind = item.t === 'build' ? `build_${item.sid}` : item.id;
|
||||||
|
if (item.t === 'build') btn.dataset.cost = BUILDINGS[item.sid].cost;
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
this.game.audio.click();
|
this.game.audio.click();
|
||||||
if (item.t === 'build') this.game.input.setTool('build', item.sid);
|
if (item.t === 'build') this.game.input.setTool('build', item.sid);
|
||||||
else this.game.input.setTool(item.id);
|
else this.game.input.setTool(item.id);
|
||||||
});
|
});
|
||||||
btn.dataset.kind = item.t === 'build' ? `build_${item.sid}` : item.id;
|
row.appendChild(btn);
|
||||||
bar.appendChild(btn);
|
|
||||||
if (item.t === 'build') {
|
|
||||||
const cost = BUILDINGS[item.sid].cost;
|
|
||||||
btn.querySelector('.tl-label').insertAdjacentHTML('beforeend',
|
|
||||||
`<span class="tl-cost">$${fmtNum(cost)}</span>`);
|
|
||||||
}
|
}
|
||||||
|
bar.appendChild(box);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dim unaffordable buildings as funds change
|
||||||
|
const affordability = (money) => {
|
||||||
|
bar.querySelectorAll('.tl-btn[data-cost]').forEach(b => {
|
||||||
|
b.classList.toggle('poor', money < +b.dataset.cost);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
this.city.on('stats', s => affordability(s.money));
|
||||||
|
affordability(this.city.money);
|
||||||
}
|
}
|
||||||
|
|
||||||
onToolChanged(tool, sid) {
|
onToolChanged(tool, sid) {
|
||||||
@@ -116,6 +137,26 @@ export class UI {
|
|||||||
if (tool === 'build') sel = document.querySelector(`#toolbar [data-kind="build_${sid}"]`);
|
if (tool === 'build') sel = document.querySelector(`#toolbar [data-kind="build_${sid}"]`);
|
||||||
else sel = document.querySelector(`#toolbar [data-kind="${tool}"]`);
|
else sel = document.querySelector(`#toolbar [data-kind="${tool}"]`);
|
||||||
if (sel) sel.classList.add('active');
|
if (sel) sel.classList.add('active');
|
||||||
|
|
||||||
|
// status chip: what am I holding, and how do I use it?
|
||||||
|
const kind = tool === 'build' ? `build_${sid}` : tool;
|
||||||
|
const item = TOOLBAR.find(i => (i.t === 'build' ? `build_${i.sid}` : i.id) === kind);
|
||||||
|
const chip = document.getElementById('toolReadout');
|
||||||
|
if (chip) {
|
||||||
|
if (!item || tool === 'none') chip.classList.remove('show');
|
||||||
|
else {
|
||||||
|
let tip = item.tip;
|
||||||
|
if (!tip && item.t === 'build') {
|
||||||
|
const bmeta = BUILDINGS[item.sid];
|
||||||
|
tip = `$${fmtNum(bmeta.cost)} · $${bmeta.upkeep || 0}/mo` +
|
||||||
|
(bmeta.radius >= 10 ? ` · covers ${bmeta.radius} tiles around it` : '');
|
||||||
|
}
|
||||||
|
chip.innerHTML = `<span class="tr-ico">${ICONS[item.icon]}</span>
|
||||||
|
<b>${item.label}</b>${tip ? `<span class="tr-tip">${tip}</span>` : ''}`;
|
||||||
|
chip.classList.add('show');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const cursors = { bulldoze: 'not-allowed', query: 'help' };
|
const cursors = { bulldoze: 'not-allowed', query: 'help' };
|
||||||
this.game.renderer.domElement.style.cursor =
|
this.game.renderer.domElement.style.cursor =
|
||||||
tool === 'none' ? 'grab' : (cursors[tool] || 'crosshair');
|
tool === 'none' ? 'grab' : (cursors[tool] || 'crosshair');
|
||||||
|
|||||||
Reference in New Issue
Block a user