This proof of concept demonstrates a living economy, not a static shop.
Prices are not fixed.
Money is not a score.
Trading decisions create consequences.
The purpose of this POC is to show that resources have intent:
Money → enables action → action reshapes the market → profit enables scale
What This POC Is Demonstrating
This system is designed to prove four core ideas:
- Location matters
Each station has different supply and demand. The same commodity can be cheap in one place and valuable in another. - Time matters
Markets drift continuously. While you travel, prices change. Waiting has a cost. - Actions matter
Your trades directly affect prices. Buying increases demand and reduces supply. Selling does the opposite. You can see this impact immediately. - Scale compounds
Profit is not the end goal. Profit buys upgrades. Upgrades increase cargo, speed, and information—raising profit per minute, not just total profit.
How to Use the Simulation
A simple loop drives the entire system:
- Select a commodity
Choose what you want to trade and observe its buy/sell prices. - Buy at the current station
Your purchase immediately shifts supply and demand. - Check the route intelligence
The system suggests the best nearby sell destination based on price, distance, and your intel level. - Travel to another station
While traveling, markets continue to drift. Missed timing is real risk. - Sell and realize profit
Selling impacts the destination market and updates your profit metrics. - Upgrade to scale
Use profit to increase cargo capacity, travel speed, or market visibility.
Repeat the loop at a larger scale.
What to Pay Attention To
As you interact with the POC, watch for these signals:
- Supply/Demand bars move when you trade
This is intentional and visible causality. - The price chart reacts to your actions
Buy and sell markers show exactly when you caused changes. - Profit Preview changes by location
Selling in the wrong place can be unprofitable—even if prices look “high.” - Profit per minute improves with upgrades
Scaling is about efficiency over time, not single transactions.
If you can clearly see cause → effect in the chart and numbers, the economy is working.
What This POC Is Not
This is intentionally scoped:
- No factions
- No contracts
- No production chains
- No NPC trading fleets
- No combat or piracy
Those systems come later.
This POC exists solely to prove that trading has meaning and money has purpose.
Why This Matters
A static market is decoration.
A dynamic market is gameplay.
This prototype shows how economic pressure can drive:
- movement,
- decision-making,
- risk,
- and long-term progression
without scripted events or artificial rewards.
Everything flows from player choice.
Code for Above
<div style="max-width:1500px;margin:0 auto;">
<iframe
title="POC 10 — Market Buy/Sell (Redesigned UI)"
scrolling="no"
style="width:1500px;height:1500px;border:0;display:block;background:#000;border-radius:14px;box-shadow:0 20px 60px rgba(0,0,0,.55);"
sandbox="allow-scripts allow-same-origin"
srcdoc='
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>POC 10 — Market Buy/Sell (Redesigned UI)</title>
<style>
:root{
--bg0:#05070D;
--bg1:#070B14;
--panel:rgba(14,20,34,.86);
--panel2:rgba(10,14,24,.80);
--stroke:rgba(255,255,255,.10);
--stroke2:rgba(77,163,255,.22);
--text:#EAF1FF;
--muted:#A8B6D6;
--good:#2BE4A7;
--warn:#FFD166;
--bad:#FF4D6D;
--blue:#4DA3FF;
--gold:#FFC857;
--shadow: 0 18px 55px rgba(0,0,0,.50);
--r:16px;
}
*{box-sizing:border-box}
html,body{width:100%;height:100%;margin:0;overflow:hidden;background:#000;color:var(--text);font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif}
#app{
width:1500px;height:1500px;margin:0 auto;position:relative;overflow:hidden;
background:
radial-gradient(1100px 900px at 50% 15%, #0E1A3D 0%, #070A12 55%, #04050A 100%);
}
/* starfield */
#stars{position:absolute;inset:0;opacity:.50}
/* layout */
.shell{position:absolute;inset:14px;display:grid;grid-template-rows:76px 1fr;gap:12px;z-index:2}
.topbar{
border-radius:18px;
background:linear-gradient(90deg, rgba(77,163,255,.16), rgba(255,200,87,.08));
border:1px solid rgba(255,255,255,.12);
box-shadow: var(--shadow);
display:flex;align-items:center;justify-content:space-between;gap:12px;
padding:14px 16px;
}
.brand{display:flex;flex-direction:column;gap:3px}
.brand .t{font-weight:900;letter-spacing:.2px}
.brand .s{color:var(--muted);font-size:12px}
.statusRow{display:flex;align-items:center;gap:10px;flex-wrap:wrap;justify-content:flex-end}
.chip{
display:inline-flex;align-items:center;gap:8px;
padding:8px 10px;border-radius:999px;
border:1px solid rgba(255,255,255,.14);
background:rgba(0,0,0,.22);
font-size:12px;font-weight:800;
}
.chip .dot{width:9px;height:9px;border-radius:999px;background:var(--blue)}
.chip.good .dot{background:var(--good)}
.chip.warn .dot{background:var(--warn)}
.chip.bad .dot{background:var(--bad)}
.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace}
.grid{
display:grid;
grid-template-columns: 360px 1fr 420px;
grid-template-rows: 1fr 420px;
gap:12px;
min-height:0;
}
/* cards */
.card{
border-radius:18px;
background:linear-gradient(180deg, rgba(18,25,45,.90), rgba(8,10,18,.86));
border:1px solid rgba(255,255,255,.12);
box-shadow: var(--shadow);
overflow:hidden;
min-height:0;
}
.hd{
padding:12px 14px;
border-bottom:1px solid rgba(255,255,255,.08);
display:flex;align-items:center;justify-content:space-between;gap:10px;
background:linear-gradient(90deg, rgba(77,163,255,.10), rgba(0,0,0,.02));
}
.hd .ttl{font-weight:900}
.hd .sub{color:var(--muted);font-size:12px;margin-top:2px}
.hd .right{display:flex;align-items:center;gap:10px}
.body{padding:14px;min-height:0;height:100%}
.stack{display:flex;flex-direction:column;gap:12px;min-height:0;height:100%}
.row{display:flex;align-items:center;justify-content:space-between;gap:10px}
.k{color:var(--muted);font-size:12px}
.v{font-weight:900}
.hint{color:var(--muted);font-size:12px;line-height:1.35}
.sep{height:1px;background:rgba(255,255,255,.08);margin:8px 0}
/* mini panels */
.mini{
border:1px solid rgba(255,255,255,.10);
background:rgba(0,0,0,.18);
border-radius:16px;
padding:12px;
}
/* table */
.table{
width:100%;
border-collapse:collapse;
border-radius:14px;
overflow:hidden;
border:1px solid rgba(255,255,255,.10);
background:rgba(0,0,0,.16);
}
.table th,.table td{
padding:10px 10px;
border-bottom:1px solid rgba(255,255,255,.08);
font-size:12px;
text-align:left;
vertical-align:middle;
}
.table th{color:var(--muted);font-weight:900;background:rgba(255,255,255,.04)}
.table tr:last-child td{border-bottom:0}
.rightTxt{text-align:right}
/* controls */
.select, .input{
width:100%;
padding:12px 12px;
border-radius:14px;
border:1px solid rgba(255,255,255,.14);
background:rgba(0,0,0,.26);
color:var(--text);
outline:none;
font-weight:850;
}
.btn{
appearance:none;
border:1px solid rgba(255,255,255,.14);
background:rgba(255,255,255,.06);
color:var(--text);
padding:12px 12px;
border-radius:14px;
cursor:pointer;
font-weight:900;
transition:transform .06s ease, background .15s ease, border-color .15s ease, filter .15s ease;
user-select:none;
}
.btn:hover{background:rgba(255,255,255,.10);border-color:rgba(255,255,255,.22)}
.btn:active{transform:translateY(1px)}
.btn:disabled{opacity:.45;cursor:not-allowed}
.btn.primary{background:rgba(77,163,255,.18);border-color:rgba(77,163,255,.35)}
.btn.good{background:rgba(43,228,167,.14);border-color:rgba(43,228,167,.35)}
.btn.bad{background:rgba(255,77,109,.14);border-color:rgba(255,77,109,.35)}
.btn.ghost{background:rgba(0,0,0,.18)}
.btn.small{padding:10px 10px;border-radius:12px;font-size:12px}
.btnGroup{display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px}
.btnGroup2{display:grid;grid-template-columns:1fr 1fr;gap:10px}
/* trade strip */
.tradeStrip{
display:grid;
grid-template-columns: 1.1fr 1fr;
gap:12px;
}
.bigPrice{
display:grid;
grid-template-columns:1fr 1fr;
gap:10px;
}
.priceBox{
border-radius:16px;
padding:12px;
border:1px solid rgba(255,255,255,.10);
background:linear-gradient(180deg, rgba(0,0,0,.18), rgba(0,0,0,.10));
}
.priceBox .lab{color:var(--muted);font-size:12px;font-weight:900}
.priceBox .amt{font-size:20px;font-weight:950;margin-top:4px}
.priceBox.buy{border-color:rgba(77,163,255,.28)}
.priceBox.sell{border-color:rgba(255,200,87,.28)}
.priceBox.buy .amt{color:rgba(200,225,255,.98)}
.priceBox.sell .amt{color:rgba(255,230,180,.98)}
.stepper{
display:grid;
grid-template-columns: 54px 1fr 54px;
gap:10px;
align-items:center;
}
.stepBtn{
height:44px;
border-radius:14px;
border:1px solid rgba(255,255,255,.14);
background:rgba(255,255,255,.06);
color:var(--text);
font-weight:950;
cursor:pointer;
}
.stepBtn:hover{background:rgba(255,255,255,.10)}
.qtyRow{display:grid;grid-template-columns:1fr 1fr;gap:10px}
/* bars */
.bars{display:grid;grid-template-columns:1fr;gap:10px;margin-top:10px}
.barline{display:flex;align-items:center;gap:10px}
.barlabel{width:64px;color:var(--muted);font-size:12px;font-weight:900}
.barwrap{flex:1;height:12px;border-radius:999px;overflow:hidden;border:1px solid rgba(255,255,255,.10);background:rgba(255,255,255,.06)}
.barfill{height:100%}
.barvalue{width:44px;text-align:right;color:var(--muted);font-size:12px}
/* dials */
.dialGrid{display:grid;grid-template-columns: 1fr 1fr;gap:12px}
.dial{
display:grid;
grid-template-columns: 82px 1fr;
gap:12px;
align-items:center;
padding:10px;
border-radius:16px;
border:1px solid rgba(255,255,255,.10);
background:rgba(0,0,0,.16);
}
.knob{
width:76px;height:76px;border-radius:50%;
border:1px solid rgba(255,255,255,.12);
background:
radial-gradient(circle at 35% 30%, rgba(255,255,255,.20), rgba(255,255,255,.06) 45%, rgba(0,0,0,.25) 70%),
conic-gradient(from 225deg, rgba(77,163,255,.90) 0deg, rgba(77,163,255,.18) 0deg, rgba(255,255,255,.08) 270deg, rgba(255,255,255,.05) 360deg);
box-shadow: inset 0 0 0 2px rgba(0,0,0,.20);
position:relative;
}
.knob:after{
content:"";
position:absolute;
left:50%;top:50%;
width:3px;height:26px;
transform: translate(-50%,-90%) rotate(0deg);
transform-origin: 50% 85%;
background:rgba(255,255,255,.92);
border-radius:999px;
opacity:.95;
}
.dial .meta{display:flex;flex-direction:column;gap:6px}
.dial .name{font-size:12px;color:var(--muted);font-weight:900}
.dial .val{font-size:14px;font-weight:950}
.dial input[type="range"]{width:100%}
input[type="range"]{
-webkit-appearance:none;appearance:none;height:10px;border-radius:999px;
background:rgba(255,255,255,.08);
border:1px solid rgba(255,255,255,.10);
outline:none;
}
input[type="range"]::-webkit-slider-thumb{
-webkit-appearance:none;appearance:none;
width:22px;height:22px;border-radius:50%;
background:rgba(77,163,255,.95);
border:1px solid rgba(255,255,255,.28);
box-shadow: 0 8px 18px rgba(0,0,0,.35);
cursor:pointer;
}
/* chart */
#chart{
width:100%;height:260px;
border-radius:16px;
border:1px solid rgba(255,255,255,.10);
background:rgba(0,0,0,.16);
}
.legend{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:10px}
.legLeft{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.ldot{width:10px;height:10px;border-radius:50%;border:1px solid rgba(255,255,255,.25)}
.ldot.blue{background:var(--blue)}
.ldot.gold{background:var(--gold)}
.ldot.good{background:var(--good)}
.ldot.bad{background:var(--bad)}
.tiny{font-size:11px;color:var(--muted)}
.pill{
display:inline-flex;align-items:center;gap:8px;
padding:8px 10px;border-radius:999px;
border:1px solid rgba(255,255,255,.14);
background:rgba(0,0,0,.22);
font-size:12px;font-weight:900;
}
/* placement */
#playerCard{grid-column:1;grid-row:1}
#marketCard{grid-column:2;grid-row:1}
#rightCard{grid-column:3;grid-row:1}
#ledgerCard{grid-column:1 / 3;grid-row:2}
#chartCard{grid-column:3;grid-row:2}
/* scroll areas */
.scroll{overflow:auto;min-height:0}
.scroll::-webkit-scrollbar{width:10px}
.scroll::-webkit-scrollbar-thumb{background:rgba(255,255,255,.10);border-radius:999px;border:2px solid rgba(0,0,0,.25)}
.scroll::-webkit-scrollbar-track{background:rgba(0,0,0,.18);border-radius:999px}
</style>
</head>
<body>
<div id="app">
<canvas id="stars" width="1500" height="1500"></canvas>
<div class="shell">
<!-- TOP BAR -->
<div class="topbar">
<div class="brand">
<div class="t">POC 10 — Market Buy/Sell with Dynamic Pricing</div>
<div class="s">Loop: Buy low → travel → sell high → upgrade → repeat. Prices change over time and react to your trades.</div>
</div>
<div class="statusRow">
<div class="chip" id="chipLoc"><span class="dot"></span><span id="locName">—</span></div>
<div class="chip" id="chipState"><span class="dot"></span><span id="stateName">Docked</span></div>
<div class="chip"><span class="dot"></span><span>Credits</span> <span class="mono" id="credits">$0.00</span></div>
<div class="chip"><span class="dot"></span><span>Cargo</span> <span class="mono" id="cargoCap">0/0</span></div>
</div>
</div>
<!-- MAIN GRID -->
<div class="grid">
<!-- PLAYER -->
<div class="card" id="playerCard">
<div class="hd">
<div>
<div class="ttl">Player</div>
<div class="sub">Upgrades increase profit per minute</div>
</div>
<div class="right">
<span class="pill" id="feePill">Fee 2.0%</span>
</div>
</div>
<div class="body">
<div class="stack">
<div class="mini">
<div class="row">
<div>
<div class="k">Net worth (credits + cargo)</div>
<div class="v mono" id="netWorth">$0.00</div>
</div>
<div style="text-align:right">
<div class="k">Travel timer</div>
<div class="v mono" id="eta">Docked</div>
</div>
</div>
<div class="sep"></div>
<div class="hint">
Action order: choose a commodity → set quantity → buy/sell → travel if needed.
</div>
</div>
<div class="mini">
<div class="row" style="margin-bottom:10px">
<div>
<div class="ttl" style="font-size:13px">Travel</div>
<div class="sub" style="margin-top:2px">Different stations = different prices</div>
</div>
<div class="pill" id="intelPill">Intel L1</div>
</div>
<select class="select" id="destSelect"></select>
<div style="height:10px"></div>
<div class="btnGroup2">
<button class="btn good" id="travelBtn">Travel</button>
<button class="btn" id="dockBtn" disabled>Dock</button>
</div>
<div class="sep"></div>
<div class="hint" id="bestHint">Best sell hint: pick a commodity.</div>
<div class="hint mono" id="bestHint2"></div>
</div>
<div class="mini">
<div class="row" style="margin-bottom:10px">
<div>
<div class="ttl" style="font-size:13px">Upgrades</div>
<div class="sub" style="margin-top:2px">Spend profit to increase scale</div>
</div>
</div>
<div class="btnGroup">
<button class="btn primary" id="upCargo">+ Cargo</button>
<button class="btn primary" id="upEngine">+ Engine</button>
<button class="btn primary" id="upIntel">+ Intel</button>
</div>
<div class="sep"></div>
<div class="hint">
Cargo: carry more each trip. Engine: faster trips. Intel: better routing hints.
</div>
</div>
<div class="mini">
<div class="row" style="margin-bottom:8px">
<div class="ttl" style="font-size:13px">Event Log</div>
<div class="k mono" id="tickLabel">00000</div>
</div>
<div class="scroll" id="log" style="max-height:210px;font-size:12px;line-height:1.35;color:var(--muted)"></div>
</div>
</div>
</div>
</div>
<!-- MARKET (CENTER) -->
<div class="card" id="marketCard">
<div class="hd">
<div>
<div class="ttl">Market</div>
<div class="sub">Buy and sell at the current station</div>
</div>
<div class="right">
<span class="pill" id="ppTag">—</span>
</div>
</div>
<div class="body">
<div class="stack">
<div class="mini">
<div class="row" style="margin-bottom:10px">
<div style="flex:1">
<div class="k">Commodity</div>
<select class="select" id="commoditySelect"></select>
</div>
<div style="width:220px">
<div class="k">Quick quantity</div>
<div class="qtyRow">
<button class="btn small ghost" id="q1">1</button>
<button class="btn small ghost" id="q10">10</button>
</div>
</div>
</div>
<div class="tradeStrip">
<div class="bigPrice">
<div class="priceBox buy">
<div class="lab">Buy price (each)</div>
<div class="amt mono" id="buyPrice">$0.00</div>
</div>
<div class="priceBox sell">
<div class="lab">Sell price (each)</div>
<div class="amt mono" id="sellPrice">$0.00</div>
</div>
</div>
<div>
<div class="k">Trade quantity</div>
<div class="stepper" style="margin-top:6px">
<button class="stepBtn" id="qtyMinus">−</button>
<input class="input mono" id="qtyInput" value="1" inputmode="numeric" />
<button class="stepBtn" id="qtyPlus">+</button>
</div>
<div style="height:10px"></div>
<div class="btnGroup2">
<button class="btn primary" id="buyBtn">Buy</button>
<button class="btn bad" id="sellBtn">Sell</button>
</div>
<div style="height:10px"></div>
<div class="btnGroup2">
<button class="btn primary" id="buyMax">Buy Max</button>
<button class="btn bad" id="sellAll">Sell All</button>
</div>
</div>
</div>
<div class="bars">
<div class="barline">
<div class="barlabel">Supply</div>
<div class="barwrap"><div class="barfill" id="supplyBar"></div></div>
<div class="barvalue mono" id="supplyVal">—</div>
</div>
<div class="barline">
<div class="barlabel">Demand</div>
<div class="barwrap"><div class="barfill" id="demandBar"></div></div>
<div class="barvalue mono" id="demandVal">—</div>
</div>
</div>
<div class="sep"></div>
<div class="hint">
Your trades move the market: buying increases demand and reduces supply; selling does the reverse.
</div>
</div>
<div class="mini">
<div class="row" style="margin-bottom:10px">
<div>
<div class="ttl" style="font-size:13px">Profit Preview (sell everything here)</div>
<div class="sub" style="margin-top:2px">Based on your cost basis; fees included</div>
</div>
</div>
<table class="table">
<thead>
<tr><th>Line</th><th class="rightTxt">Value</th></tr>
</thead>
<tbody>
<tr><td>Held quantity</td><td class="rightTxt mono" id="heldQty">—</td></tr>
<tr><td>Average cost (each)</td><td class="rightTxt mono" id="avgCost">—</td></tr>
<tr><td>Gross sell value</td><td class="rightTxt mono" id="sellValue">—</td></tr>
<tr><td>Transaction fees</td><td class="rightTxt mono" id="feesEst">—</td></tr>
<tr><td><b>Net profit</b></td><td class="rightTxt mono" id="netIfSold">—</td></tr>
</tbody>
</table>
<div class="hint" style="margin-top:10px">
If you hold 0 units, this section stays blank. Buy first, then compare destinations.
</div>
</div>
</div>
</div>
</div>
<!-- RIGHT (DIALS + RULES) -->
<div class="card" id="rightCard">
<div class="hd">
<div>
<div class="ttl">Tuning</div>
<div class="sub">Adjust the economy and watch it react</div>
</div>
<div class="right">
<span class="pill" id="livePill">Live</span>
</div>
</div>
<div class="body">
<div class="stack">
<div class="mini">
<div class="ttl" style="font-size:13px;margin-bottom:10px">What this POC proves</div>
<div class="hint">
1) Location matters: prices differ by station.<br>
2) Time matters: prices drift while you travel.<br>
3) Actions matter: trades change supply and demand.<br>
4) Scale matters: profit funds upgrades that increase profit/min.
</div>
</div>
<div class="mini">
<div class="ttl" style="font-size:13px;margin-bottom:10px">Dials</div>
<div class="dialGrid">
<div class="dial" data-dial="fee">
<div class="knob" id="knobFee"></div>
<div class="meta">
<div class="name">Transaction Fee</div>
<div class="val mono" id="feeLabel">2.0%</div>
<input id="feeSlider" type="range" min="0" max="8" step="0.1" value="2" />
</div>
</div>
<div class="dial" data-dial="drift">
<div class="knob" id="knobDrift"></div>
<div class="meta">
<div class="name">Drift Rate</div>
<div class="val mono" id="driftLabel">0.040</div>
<input id="driftSlider" type="range" min="0.005" max="0.150" step="0.005" value="0.040" />
</div>
</div>
<div class="dial" data-dial="elas">
<div class="knob" id="knobElas"></div>
<div class="meta">
<div class="name">Elasticity</div>
<div class="val mono" id="elasLabel">0.90</div>
<input id="elasSlider" type="range" min="0.2" max="1.6" step="0.05" value="0.90" />
</div>
</div>
<div class="dial" data-dial="impact">
<div class="knob" id="knobImpact"></div>
<div class="meta">
<div class="name">Player Impact</div>
<div class="val mono" id="impactLabel">1.00</div>
<input id="impactSlider" type="range" min="0.2" max="2.5" step="0.1" value="1.0" />
</div>
</div>
</div>
<div class="sep"></div>
<div class="hint">
Drift pulls supply/demand toward baseline. Elasticity controls how strongly price responds. Player Impact scales how much your trades move the market.
</div>
</div>
<div class="mini">
<div class="row" style="margin-bottom:6px">
<div class="ttl" style="font-size:13px">Run Summary</div>
</div>
<div class="row">
<div>
<div class="k">Last trade profit</div>
<div class="v mono" id="lastProfit">—</div>
</div>
<div style="text-align:right">
<div class="k">Lifetime profit</div>
<div class="v mono" id="lifeProfit">—</div>
</div>
</div>
<div class="sep"></div>
<div class="row">
<div>
<div class="k">Profit per minute</div>
<div class="v mono" id="lifePPM">—</div>
</div>
<div style="text-align:right">
<div class="k">Trips</div>
<div class="v mono" id="tripCount">0</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- LEDGER (BOTTOM WIDE) -->
<div class="card" id="ledgerCard">
<div class="hd">
<div>
<div class="ttl">Cargo Ledger</div>
<div class="sub">Holdings, cost basis, and estimated sell value at this station</div>
</div>
<div class="right">
<span class="pill" id="chartPill">Local</span>
</div>
</div>
<div class="body scroll">
<table class="table" id="cargoTable">
<thead>
<tr>
<th>Commodity</th>
<th class="rightTxt">Qty</th>
<th class="rightTxt">Avg Cost</th>
<th class="rightTxt">Sell (ea)</th>
<th class="rightTxt">Net after fee (ea)</th>
</tr>
</thead>
<tbody></tbody>
</table>
<div style="height:12px"></div>
<div class="mini">
<div class="ttl" style="font-size:13px;margin-bottom:8px">Route Intelligence (selected commodity)</div>
<div class="row">
<div>
<div class="k">Selected commodity</div>
<div class="v" id="selName">—</div>
</div>
<div style="text-align:right">
<div class="k">Held</div>
<div class="v mono" id="selHeld">0</div>
</div>
</div>
<div class="sep"></div>
<div class="row">
<div>
<div class="k">Best sell destination</div>
<div class="v" id="bestDest">—</div>
</div>
<div style="text-align:right">
<div class="k">Distance</div>
<div class="v mono" id="bestDist">—</div>
</div>
</div>
<div class="row">
<div>
<div class="k">Estimated net margin</div>
<div class="v mono" id="bestMargin">—</div>
</div>
<div style="text-align:right">
<div class="k">Estimated profit/min</div>
<div class="v mono" id="bestPPM">—</div>
</div>
</div>
<div class="hint" id="bestExplain" style="margin-top:8px"></div>
</div>
</div>
</div>
<!-- CHART (BOTTOM RIGHT) -->
<div class="card" id="chartCard">
<div class="hd">
<div>
<div class="ttl">Price Chart</div>
<div class="sub">Last 120 ticks at this station (selected commodity)</div>
</div>
<div class="right">
<span class="pill mono" id="chartScale">—</span>
</div>
</div>
<div class="body">
<canvas id="chart" width="392" height="260"></canvas>
<div class="legend">
<div class="legLeft">
<span class="ldot blue"></span><span class="tiny">Buy</span>
<span class="ldot gold"></span><span class="tiny">Sell</span>
<span class="ldot good"></span><span class="tiny">You bought</span>
<span class="ldot bad"></span><span class="tiny">You sold</span>
</div>
<div class="tiny mono" id="locMini">—</div>
</div>
<div class="hint" style="margin-top:10px">
You should see your buy/sell markers shift supply/demand and move the line. If you can’t see that, the model is failing.
</div>
</div>
</div>
</div>
</div>
</div>
<script>
(() => {
// ---------- Helpers ----------
const clamp = (v,min,max)=>Math.max(min,Math.min(max,v));
const fmt = (n)=> (n>=0? "" : "-") + "$" + Math.abs(n).toFixed(2);
const fmt0 = (n)=> (n>=0? "" : "-") + "$" + Math.abs(n).toFixed(0);
const now = ()=>performance.now();
const el = (id)=>document.getElementById(id);
// ---------- Starfield ----------
const starC = el("stars");
const sctx = starC.getContext("2d");
const stars = [];
for(let i=0;i<900;i++){
stars.push({
x:Math.random()*1500,
y:Math.random()*1500,
r:Math.random()*1.2,
a:0.25+Math.random()*0.55,
tw:Math.random()*Math.PI*2,
sp:0.2+Math.random()*0.7
});
}
function drawStars(){
sctx.clearRect(0,0,1500,1500);
sctx.fillStyle="#000";
sctx.fillRect(0,0,1500,1500);
for(const st of stars){
st.tw += 0.01*st.sp;
const alpha = st.a*(0.65 + 0.35*Math.sin(st.tw));
sctx.fillStyle = `rgba(220,235,255,${alpha})`;
sctx.beginPath();
sctx.arc(st.x, st.y, st.r, 0, Math.PI*2);
sctx.fill();
}
}
// ---------- Economy Model ----------
const COMMS = [
{id:"ore", name:"Ore", base: 24, mass:1},
{id:"fuel", name:"Fuel", base: 18, mass:1},
{id:"food", name:"Food", base: 12, mass:1},
{id:"tech", name:"Tech", base: 55, mass:1}
];
const LOCS = [
{id:"aegis", name:"Aegis Station", dist:{aegis:0, cinder:2, verdant:3, helio:5}},
{id:"cinder", name:"Cinder Outpost", dist:{aegis:2, cinder:0, verdant:2, helio:4}},
{id:"verdant", name:"Verdant Port", dist:{aegis:3, cinder:2, verdant:0, helio:2}},
{id:"helio", name:"Helio Exchange", dist:{aegis:5, cinder:4, verdant:2, helio:0}},
];
const baseline = {
aegis: { ore:{s:65,d:40}, fuel:{s:45,d:60}, food:{s:55,d:55}, tech:{s:35,d:70} },
cinder: { ore:{s:78,d:35}, fuel:{s:30,d:80}, food:{s:40,d:65}, tech:{s:25,d:85} },
verdant: { ore:{s:35,d:70}, fuel:{s:60,d:45}, food:{s:85,d:25}, tech:{s:40,d:60} },
helio: { ore:{s:40,d:65}, fuel:{s:55,d:55}, food:{s:35,d:70}, tech:{s:75,d:35} },
};
const market = {};
for(const L of LOCS){
market[L.id] = {};
for(const C of COMMS){
const b = baseline[L.id][C.id];
market[L.id][C.id] = { supply:b.s, demand:b.d, histBuy:[], histSell:[], histEvents:[] };
}
}
// tunables
let feeRate = 0.02;
let driftRate = 0.040;
let elasticity = 0.90;
let impact = 1.00;
function priceAt(locId, commId){
const C = COMMS.find(x=>x.id===commId);
const st = market[locId][commId];
const s = clamp(st.supply, 1, 99);
const d = clamp(st.demand, 1, 99);
const ss = s/100, dd = d/100;
let mult = Math.pow(0.55 + 1.20*dd, elasticity) / Math.pow(0.55 + 1.20*ss, elasticity);
mult = clamp(mult, 0.55, 2.10);
const mid = C.base * mult;
return { buy: mid*1.04, sell: mid*0.96, mid };
}
function driftMarkets(){
for(const L of LOCS){
for(const C of COMMS){
const st = market[L.id][C.id];
const b = baseline[L.id][C.id];
st.supply += (b.s - st.supply) * driftRate;
st.demand += (b.d - st.demand) * driftRate;
st.supply += (Math.random()-0.5) * 0.18;
st.demand += (Math.random()-0.5) * 0.18;
st.supply = clamp(st.supply, 1, 99);
st.demand = clamp(st.demand, 1, 99);
}
}
}
// ---------- Player ----------
const player = {
loc:"aegis",
traveling:false,
dest:null,
travelEndTick:0,
credits: 500,
cargoCap: 30,
engineLvl: 1,
intelLvl: 1,
tripCount:0,
lifeProfit:0,
lastProfit:0,
startTime: now(),
cargo: { ore:{qty:0,avgCost:0}, fuel:{qty:0,avgCost:0}, food:{qty:0,avgCost:0}, tech:{qty:0,avgCost:0} },
};
function cargoUsed(){
let u=0;
for(const C of COMMS) u += player.cargo[C.id].qty * C.mass;
return u;
}
function cargoValueHere(){
let v=0;
for(const C of COMMS){
const q = player.cargo[C.id].qty;
if(q<=0) continue;
v += q * priceAt(player.loc, C.id).sell;
}
return v;
}
function netWorth(){ return player.credits + cargoValueHere(); }
// ---------- UI refs ----------
const locName = el("locName");
const stateName = el("stateName");
const creditsEl = el("credits");
const cargoCapEl = el("cargoCap");
const netWorthEl = el("netWorth");
const etaEl = el("eta");
const feePill = el("feePill");
const intelPill = el("intelPill");
const tickLabel = el("tickLabel");
const locMini = el("locMini");
const commoditySelect = el("commoditySelect");
const destSelect = el("destSelect");
const buyPriceEl = el("buyPrice");
const sellPriceEl = el("sellPrice");
const supplyBar = el("supplyBar");
const demandBar = el("demandBar");
const supplyVal = el("supplyVal");
const demandVal = el("demandVal");
const qtyInput = el("qtyInput");
const heldQtyEl = el("heldQty");
const avgCostEl = el("avgCost");
const sellValueEl = el("sellValue");
const feesEstEl = el("feesEst");
const netIfSoldEl = el("netIfSold");
const ppTag = el("ppTag");
const bestHint = el("bestHint");
const bestHint2 = el("bestHint2");
const selName = el("selName");
const selHeld = el("selHeld");
const bestDest = el("bestDest");
const bestDist = el("bestDist");
const bestMargin = el("bestMargin");
const bestPPM = el("bestPPM");
const bestExplain = el("bestExplain");
const lastProfitEl = el("lastProfit");
const lifeProfitEl = el("lifeProfit");
const lifePPMEl = el("lifePPM");
const tripCountEl = el("tripCount");
const logEl = el("log");
const travelBtn = el("travelBtn");
const dockBtn = el("dockBtn");
const upCargoBtn = el("upCargo");
const upEngineBtn = el("upEngine");
const upIntelBtn = el("upIntel");
// dials
const feeSlider = el("feeSlider");
const driftSlider = el("driftSlider");
const elasSlider = el("elasSlider");
const impactSlider = el("impactSlider");
const feeLabel = el("feeLabel");
const driftLabel = el("driftLabel");
const elasLabel = el("elasLabel");
const impactLabel = el("impactLabel");
const knobFee = el("knobFee");
const knobDrift = el("knobDrift");
const knobElas = el("knobElas");
const knobImpact = el("knobImpact");
// chart
const chart = el("chart");
const cctx = chart.getContext("2d");
const chartScale = el("chartScale");
const chartPill = el("chartPill");
// ---------- Populate selects ----------
for(const C of COMMS){
const opt = document.createElement("option");
opt.value = C.id;
opt.textContent = C.name;
commoditySelect.appendChild(opt);
}
function refreshDestSelect(){
destSelect.innerHTML = "";
for(const L of LOCS){
if(L.id===player.loc) continue;
const opt = document.createElement("option");
opt.value = L.id;
opt.textContent = L.name;
destSelect.appendChild(opt);
}
}
refreshDestSelect();
// ---------- Logging ----------
function pushLog(msg){
const t = new Date().toLocaleTimeString([], {hour:"2-digit", minute:"2-digit", second:"2-digit"});
const div = document.createElement("div");
div.textContent = `[${t}] ${msg}`;
logEl.prepend(div);
while(logEl.childNodes.length>80) logEl.removeChild(logEl.lastChild);
}
// ---------- Knob visuals ----------
function setKnob(knobEl, sliderEl){
const min = parseFloat(sliderEl.min), max = parseFloat(sliderEl.max), val = parseFloat(sliderEl.value);
const t = (val - min) / (max - min);
const sweep = 270;
const start = 225;
const fillDeg = Math.round(t * sweep);
knobEl.style.background =
`radial-gradient(circle at 35% 30%, rgba(255,255,255,.20), rgba(255,255,255,.06) 45%, rgba(0,0,0,.25) 70%),
conic-gradient(from ${start}deg, rgba(77,163,255,.92) 0deg, rgba(77,163,255,.18) ${fillDeg}deg, rgba(255,255,255,.08) ${fillDeg}deg, rgba(255,255,255,.05) ${sweep}deg, rgba(255,255,255,.05) 360deg)`;
const rot = (-135 + t*270);
knobEl.dataset.rot = rot;
}
const ptrStyle = document.createElement("style");
document.head.appendChild(ptrStyle);
function refreshPointers(){
const pairs = [
["#knobFee", knobFee.dataset.rot||0],
["#knobDrift", knobDrift.dataset.rot||0],
["#knobElas", knobElas.dataset.rot||0],
["#knobImpact", knobImpact.dataset.rot||0],
];
ptrStyle.textContent = pairs.map(([sel,rot]) =>
`${sel}:after{transform: translate(-50%,-90%) rotate(${rot}deg);}`
).join("\\n");
}
function updateAllKnobs(){
setKnob(knobFee, feeSlider);
setKnob(knobDrift, driftSlider);
setKnob(knobElas, elasSlider);
setKnob(knobImpact, impactSlider);
refreshPointers();
}
// ---------- Trades ----------
function canTrade(){ return !player.traveling; }
function parseQty(){
const raw = (qtyInput.value||"").trim();
const n = Math.floor(parseFloat(raw));
if(!Number.isFinite(n) || n<=0) return 1;
return clamp(n, 1, 999999);
}
function setQty(n){ qtyInput.value = String(clamp(Math.floor(n),1,999999)); }
function buy(commId, qty){
if(!canTrade()) return;
qty = Math.floor(qty);
if(qty<=0) return;
const C = COMMS.find(x=>x.id===commId);
const st = market[player.loc][commId];
const p = priceAt(player.loc, commId).buy;
const capLeft = player.cargoCap - cargoUsed();
const maxByCap = Math.floor(capLeft / C.mass);
const maxByMoney = Math.floor(player.credits / (p * (1+feeRate)));
const max = Math.max(0, Math.min(maxByCap, maxByMoney));
const q = Math.min(qty, max);
if(q<=0){ pushLog("Buy blocked: not enough credits or cargo capacity."); return; }
const cost = q * p;
const fee = cost * feeRate;
player.credits -= (cost + fee);
const led = player.cargo[commId];
const oldQty = led.qty;
const newQty = oldQty + q;
led.avgCost = newQty>0 ? ((led.avgCost*oldQty) + (p*q)) / newQty : 0;
led.qty = newQty;
const k = 0.65 * impact;
st.supply = clamp(st.supply - (q*k), 1, 99);
st.demand = clamp(st.demand + (q*k*0.85), 1, 99);
st.histEvents.push({t:tick, type:"buy"});
pushLog(`Bought ${q} ${C.name} @ ${fmt(p)} each (fee ${fmt(fee)}).`);
redrawAll();
}
function sell(commId, qty){
if(!canTrade()) return;
qty = Math.floor(qty);
if(qty<=0) return;
const C = COMMS.find(x=>x.id===commId);
const st = market[player.loc][commId];
const p = priceAt(player.loc, commId).sell;
const led = player.cargo[commId];
const q = Math.min(qty, led.qty);
if(q<=0){ pushLog(`Sell blocked: you hold 0 ${C.name}.`); return; }
const gross = q * p;
const fee = gross * feeRate;
const net = gross - fee;
const profit = (p - led.avgCost) * q - fee;
player.credits += net;
led.qty -= q;
if(led.qty<=0){ led.qty=0; led.avgCost=0; }
const k = 0.65 * impact;
st.supply = clamp(st.supply + (q*k), 1, 99);
st.demand = clamp(st.demand - (q*k*0.85), 1, 99);
st.histEvents.push({t:tick, type:"sell"});
player.lastProfit = profit;
player.lifeProfit += profit;
pushLog(`Sold ${q} ${C.name} @ ${fmt(p)} each (fee ${fmt(fee)}). Profit vs cost: ${fmt(profit)}.`);
redrawAll();
}
// ---------- Travel ----------
function travelTo(destId){
if(player.traveling) return;
if(destId===player.loc) return;
const cur = LOCS.find(x=>x.id===player.loc);
const dist = cur.dist[destId] || 3;
const baseTicksPerDist = 45;
const speedMult = 1 + (player.engineLvl-1)*0.22;
const travelTicks = Math.max(18, Math.round((dist * baseTicksPerDist) / speedMult));
player.traveling = true;
player.dest = destId;
player.travelEndTick = tick + travelTicks;
travelBtn.disabled = true;
dockBtn.disabled = false;
pushLog(`Travel started: ${LOCS.find(x=>x.id===player.loc).name} → ${LOCS.find(x=>x.id===destId).name} (ETA ${travelTicks} ticks).`);
redrawAll();
}
function dock(){
if(!player.traveling) return;
if(tick < player.travelEndTick) return;
player.traveling = false;
player.loc = player.dest;
player.dest = null;
player.travelEndTick = 0;
player.tripCount += 1;
travelBtn.disabled = false;
dockBtn.disabled = true;
refreshDestSelect();
pushLog(`Arrived and docked at ${LOCS.find(x=>x.id===player.loc).name}.`);
redrawAll();
}
// ---------- Upgrades ----------
function upgradeCargo(){
const lvl = Math.round((player.cargoCap - 30)/10) + 1;
const cost = 220 + (lvl-1)*180;
if(player.credits < cost){ pushLog(`Cargo upgrade blocked: need ${fmt0(cost)}.`); return; }
player.credits -= cost;
player.cargoCap += 10;
pushLog(`Upgrade purchased: +10 cargo capacity (cost ${fmt0(cost)}).`);
redrawAll();
}
function upgradeEngine(){
const lvl = player.engineLvl;
const cost = 260 + (lvl-1)*240;
if(player.credits < cost){ pushLog(`Engine upgrade blocked: need ${fmt0(cost)}.`); return; }
player.credits -= cost;
player.engineLvl += 1;
pushLog(`Upgrade purchased: Engine L${player.engineLvl} (cost ${fmt0(cost)}).`);
redrawAll();
}
function upgradeIntel(){
const lvl = player.intelLvl;
const cost = 280 + (lvl-1)*260;
if(player.credits < cost){ pushLog(`Intel upgrade blocked: need ${fmt0(cost)}.`); return; }
player.credits -= cost;
player.intelLvl = clamp(player.intelLvl+1, 1, 3);
pushLog(`Upgrade purchased: Intel L${player.intelLvl} (cost ${fmt0(cost)}).`);
redrawAll();
}
// ---------- History ----------
const HIST_LEN = 120;
function recordHistory(){
for(const L of LOCS){
for(const C of COMMS){
const st = market[L.id][C.id];
const p = priceAt(L.id, C.id);
st.histBuy.push(p.buy);
st.histSell.push(p.sell);
if(st.histBuy.length>HIST_LEN) st.histBuy.shift();
if(st.histSell.length>HIST_LEN) st.histSell.shift();
st.histEvents = st.histEvents.filter(e => (tick - e.t) <= (HIST_LEN-1));
}
}
}
// ---------- Best sell hint ----------
function calcBestSell(commId){
const held = player.cargo[commId].qty;
const basis = player.cargo[commId].avgCost;
const curLoc = player.loc;
const qty = held>0 ? held : 1;
const distances = LOCS
.map(L => ({id:L.id, d:(LOCS.find(x=>x.id===curLoc).dist[L.id]||99)}))
.filter(x=>x.id!==curLoc)
.sort((a,b)=>a.d-b.d);
let candidateIds = [];
if(player.intelLvl===1) candidateIds = distances.slice(0,1).map(x=>x.id);
else if(player.intelLvl===2) candidateIds = distances.slice(0, Math.max(1, distances.length-1)).map(x=>x.id);
else candidateIds = distances.map(x=>x.id);
let best = null;
for(const id of candidateIds){
const dist = LOCS.find(x=>x.id===curLoc).dist[id] || 3;
const pSell = priceAt(id, commId).sell;
const gross = qty * pSell;
const fee = gross * feeRate;
const net = gross - fee;
const pBuyHere = priceAt(curLoc, commId).buy;
const impliedCost = held>0 ? (qty*basis) : (qty*pBuyHere*(1+feeRate));
const margin = net - impliedCost;
const baseTicksPerDist = 45;
const speedMult = 1 + (player.engineLvl-1)*0.22;
const travelTicks = Math.max(18, Math.round((dist*baseTicksPerDist)/speedMult));
const minutes = travelTicks / 60;
const ppm = minutes>0 ? (margin / minutes) : margin;
if(!best || ppm > best.ppm){
best = {id, dist, margin, ppm, pSell, travelTicks};
}
}
return best;
}
// ---------- Chart ----------
function drawChart(){
const commId = commoditySelect.value;
const st = market[player.loc][commId];
const W = chart.width, H = chart.height;
cctx.clearRect(0,0,W,H);
cctx.fillStyle = "rgba(0,0,0,.16)";
cctx.fillRect(0,0,W,H);
cctx.strokeStyle = "rgba(255,255,255,.08)";
cctx.lineWidth = 1;
for(let i=0;i<=5;i++){
const y = Math.round((i/5)*H);
cctx.beginPath(); cctx.moveTo(0,y); cctx.lineTo(W,y); cctx.stroke();
}
for(let i=0;i<=6;i++){
const x = Math.round((i/6)*W);
cctx.beginPath(); cctx.moveTo(x,0); cctx.lineTo(x,H); cctx.stroke();
}
const buy = st.histBuy, sell = st.histSell;
if(buy.length<2){ chartScale.textContent = "—"; return; }
let lo=Infinity, hi=-Infinity;
for(let i=0;i<buy.length;i++){
lo = Math.min(lo, buy[i], sell[i]);
hi = Math.max(hi, buy[i], sell[i]);
}
const pad = (hi-lo)*0.12 + 0.5;
lo -= pad; hi += pad;
chartScale.textContent = `${fmt(lo)} → ${fmt(hi)}`;
const yOf = (v)=> H - ((v-lo)/(hi-lo))*H;
const xOf = (i)=> (i/(HIST_LEN-1))*W;
function drawLine(arr, color){
cctx.strokeStyle = color;
cctx.lineWidth = 2;
cctx.beginPath();
for(let i=0;i<arr.length;i++){
const x=xOf(i), y=yOf(arr[i]);
if(i===0) cctx.moveTo(x,y); else cctx.lineTo(x,y);
}
cctx.stroke();
}
drawLine(buy, "rgba(77,163,255,.95)");
drawLine(sell,"rgba(255,200,87,.92)");
for(const e of st.histEvents){
const age = tick - e.t;
const idx = Math.max(0, Math.min(HIST_LEN-1, (HIST_LEN-1)-age));
const x = xOf(idx);
const y = e.type==="buy" ? yOf(buy[idx]) : yOf(sell[idx]);
cctx.fillStyle = e.type==="buy" ? "rgba(43,228,167,.95)" : "rgba(255,77,109,.95)";
cctx.beginPath(); cctx.arc(x,y,3.5,0,Math.PI*2); cctx.fill();
}
}
// ---------- Render ----------
const cargoTbody = el("cargoTable").querySelector("tbody");
function redrawCargoTable(){
cargoTbody.innerHTML = "";
for(const C of COMMS){
const led = player.cargo[C.id];
const q = led.qty;
const pSell = priceAt(player.loc, C.id).sell;
const netAfterFeeEa = pSell * (1-feeRate);
const tr = document.createElement("tr");
tr.innerHTML = `
<td>${C.name}</td>
<td class="rightTxt mono">${q}</td>
<td class="rightTxt mono">${q>0 ? fmt(led.avgCost) : "—"}</td>
<td class="rightTxt mono">${fmt(pSell)}</td>
<td class="rightTxt mono">${fmt(netAfterFeeEa)}</td>
`;
cargoTbody.appendChild(tr);
}
}
function redrawMarketPanel(){
const commId = commoditySelect.value;
const st = market[player.loc][commId];
const p = priceAt(player.loc, commId);
buyPriceEl.textContent = fmt(p.buy);
sellPriceEl.textContent = fmt(p.sell);
const s = clamp(st.supply,1,99);
const d = clamp(st.demand,1,99);
supplyVal.textContent = s.toFixed(0);
demandVal.textContent = d.toFixed(0);
supplyBar.style.width = `${s}%`;
demandBar.style.width = `${d}%`;
supplyBar.style.background = `linear-gradient(90deg, rgba(77,163,255,.85), rgba(77,163,255,.25))`;
demandBar.style.background = `linear-gradient(90deg, rgba(255,200,87,.85), rgba(255,200,87,.22))`;
const held = player.cargo[commId].qty;
const basis = player.cargo[commId].avgCost;
const gross = held * p.sell;
const fees = gross * feeRate;
const net = gross - fees;
const basisTotal = held * basis;
const profit = net - basisTotal;
heldQtyEl.textContent = String(held);
avgCostEl.textContent = held>0 ? fmt(basis) : "—";
sellValueEl.textContent = held>0 ? fmt(gross) : "—";
feesEstEl.textContent = held>0 ? fmt(fees) : "—";
netIfSoldEl.textContent = held>0 ? fmt(profit) : "—";
if(held<=0){
ppTag.textContent = "No position";
ppTag.style.borderColor = "rgba(255,255,255,.14)";
} else if(profit>0){
ppTag.textContent = "Profitable here";
ppTag.style.borderColor = "rgba(43,228,167,.40)";
} else {
ppTag.textContent = "Unprofitable here";
ppTag.style.borderColor = "rgba(255,77,109,.40)";
}
}
function redrawTop(){
locName.textContent = LOCS.find(x=>x.id===player.loc).name;
stateName.textContent = player.traveling ? "Traveling" : "Docked";
creditsEl.textContent = fmt(player.credits);
cargoCapEl.textContent = `${cargoUsed()}/${player.cargoCap}`;
netWorthEl.textContent = fmt(netWorth());
feePill.textContent = `Fee ${(feeRate*100).toFixed(1)}%`;
intelPill.textContent = `Intel L${player.intelLvl}`;
chartPill.textContent = LOCS.find(x=>x.id===player.loc).name;
locMini.textContent = LOCS.find(x=>x.id===player.loc).name;
if(player.traveling){
etaEl.textContent = `${Math.max(0, player.travelEndTick - tick)} ticks`;
travelBtn.disabled = true;
dockBtn.disabled = (tick < player.travelEndTick);
destSelect.disabled = true;
} else {
etaEl.textContent = "Docked";
travelBtn.disabled = false;
dockBtn.disabled = true;
destSelect.disabled = false;
}
const cargoLvl = Math.round((player.cargoCap - 30)/10) + 1;
upCargoBtn.textContent = `+ Cargo (${fmt0(220 + (cargoLvl-1)*180)})`;
upEngineBtn.textContent = `+ Engine (${fmt0(260 + (player.engineLvl-1)*240)})`;
upIntelBtn.textContent = `+ Intel (${fmt0(280 + (player.intelLvl-1)*260)})`;
lastProfitEl.textContent = player.lastProfit===0 ? "—" : fmt(player.lastProfit);
lifeProfitEl.textContent = fmt(player.lifeProfit);
tripCountEl.textContent = String(player.tripCount);
const mins = (now() - player.startTime) / 60000;
const ppm = mins>0 ? player.lifeProfit / mins : 0;
lifePPMEl.textContent = fmt(ppm) + "/min";
}
function redrawHints(){
const commId = commoditySelect.value;
const C = COMMS.find(x=>x.id===commId);
const held = player.cargo[commId].qty;
selName.textContent = C.name;
selHeld.textContent = String(held);
const best = calcBestSell(commId);
if(!best){
bestHint.textContent = "Best sell hint: —";
bestHint2.textContent = "";
bestDest.textContent = "—";
bestDist.textContent = "—";
bestMargin.textContent = "—";
bestPPM.textContent = "—";
bestExplain.textContent = "";
return;
}
const destName = LOCS.find(x=>x.id===best.id).name;
bestDest.textContent = destName;
bestDist.textContent = `${best.dist} jumps`;
bestMargin.textContent = fmt(best.margin);
bestPPM.textContent = fmt(best.ppm) + "/min";
bestHint.textContent = `Best sell hint: ${destName} (${best.margin>=0?"+":""}${fmt(best.margin)})`;
bestHint2.textContent = `ETA ~${best.travelTicks} ticks • Uses Intel L${player.intelLvl} visibility.`;
bestExplain.textContent =
`Hint scope: ` +
(player.intelLvl===1 ? "nearest station only." : player.intelLvl===2 ? "most stations (excludes farthest)." : "all stations.");
}
function redrawAll(){
redrawTop();
redrawMarketPanel();
redrawCargoTable();
redrawHints();
drawChart();
updateAllKnobs();
}
// ---------- Tick loop ----------
let tick = 0;
function step(){
tick++;
driftMarkets();
recordHistory();
if(player.traveling && tick >= player.travelEndTick){
dock();
}
tickLabel.textContent = String(tick).padStart(5,"0");
redrawTop();
redrawMarketPanel();
redrawHints();
drawChart();
}
// ---------- Controls ----------
el("qtyMinus").addEventListener("click", ()=>setQty(parseQty()-1));
el("qtyPlus").addEventListener("click", ()=>setQty(parseQty()+1));
el("q1").addEventListener("click", ()=>setQty(1));
el("q10").addEventListener("click", ()=>setQty(10));
qtyInput.addEventListener("input", ()=>{
const n = parseQty();
qtyInput.value = String(n);
});
el("buyBtn").addEventListener("click", ()=>buy(commoditySelect.value, parseQty()));
el("sellBtn").addEventListener("click", ()=>sell(commoditySelect.value, parseQty()));
el("buyMax").addEventListener("click", ()=>{
const commId = commoditySelect.value;
const p = priceAt(player.loc, commId).buy;
const capLeft = player.cargoCap - cargoUsed();
const maxByCap = Math.floor(capLeft);
const maxByMoney = Math.floor(player.credits / (p*(1+feeRate)));
buy(commId, Math.max(0, Math.min(maxByCap, maxByMoney)));
});
el("sellAll").addEventListener("click", ()=>{
const commId = commoditySelect.value;
sell(commId, player.cargo[commId].qty);
});
commoditySelect.addEventListener("change", ()=>redrawAll());
travelBtn.addEventListener("click", ()=>travelTo(destSelect.value));
dockBtn.addEventListener("click", ()=>dock());
upCargoBtn.addEventListener("click", ()=>upgradeCargo());
upEngineBtn.addEventListener("click", ()=>upgradeEngine());
upIntelBtn.addEventListener("click", ()=>upgradeIntel());
feeSlider.addEventListener("input", ()=>{
feeRate = parseFloat(feeSlider.value)/100;
feeLabel.textContent = (feeRate*100).toFixed(1) + "%";
redrawAll();
});
driftSlider.addEventListener("input", ()=>{
driftRate = parseFloat(driftSlider.value);
driftLabel.textContent = driftRate.toFixed(3);
redrawAll();
});
elasSlider.addEventListener("input", ()=>{
elasticity = parseFloat(elasSlider.value);
elasLabel.textContent = elasticity.toFixed(2);
redrawAll();
});
impactSlider.addEventListener("input", ()=>{
impact = parseFloat(impactSlider.value);
impactLabel.textContent = impact.toFixed(2);
redrawAll();
});
// ---------- Init ----------
function initDialLabels(){
feeLabel.textContent = (feeRate*100).toFixed(1) + "%";
driftLabel.textContent = driftRate.toFixed(3);
elasLabel.textContent = elasticity.toFixed(2);
impactLabel.textContent = impact.toFixed(2);
}
initDialLabels();
// seed chart history so it isn’t empty
for(let i=0;i<30;i++){ driftMarkets(); recordHistory(); tick++; }
tick = 0;
pushLog("Ready. Step 1: buy a commodity. Step 2: check the best sell hint. Step 3: travel and sell. Step 4: upgrade.");
drawStars();
redrawAll();
setInterval(step, 250);
setInterval(drawStars, 80);
})();
</script>
</body>
</html>
'></iframe>
</div>