Add dependency navigation, production throttle control, and locked card heights to prevent grid jumping
This commit is contained in:
parent
1212224c19
commit
0a9a022fbc
9 changed files with 298 additions and 18 deletions
|
|
@ -213,6 +213,9 @@ def get_initial_state() -> dict:
|
||||||
'lines': 1 if is_unlocked else 0,
|
'lines': 1 if is_unlocked else 0,
|
||||||
'manager': False,
|
'manager': False,
|
||||||
'logistics': 1 if is_unlocked else 0,
|
'logistics': 1 if is_unlocked else 0,
|
||||||
|
'logisticsEnabled': True,
|
||||||
|
'supplyDownstreamEnabled': True,
|
||||||
|
'productionTarget': 1.0,
|
||||||
'cycleProgress': 0.0,
|
'cycleProgress': 0.0,
|
||||||
'isBottlenecked': False,
|
'isBottlenecked': False,
|
||||||
'totalProduced': 0.0
|
'totalProduced': 0.0
|
||||||
|
|
@ -255,7 +258,12 @@ def process_offline_and_ticks(state: dict) -> dict:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
milestone_mult = get_unit_milestone_multiplier(lines)
|
milestone_mult = get_unit_milestone_multiplier(lines)
|
||||||
cycle_rate = (1.0 / p_cfg['baseCycleTime']) * milestone_mult * robotics_speed_mult
|
target_throttle = float(p_state.get('productionTarget', 1.0))
|
||||||
|
if target_throttle <= 0.0:
|
||||||
|
p_state['isBottlenecked'] = False
|
||||||
|
continue
|
||||||
|
|
||||||
|
cycle_rate = (1.0 / p_cfg['baseCycleTime']) * milestone_mult * robotics_speed_mult * target_throttle
|
||||||
|
|
||||||
inputs = p_cfg.get('inputs', [])
|
inputs = p_cfg.get('inputs', [])
|
||||||
if not inputs:
|
if not inputs:
|
||||||
|
|
@ -396,6 +404,17 @@ def apply_actions(state: dict, actions: list) -> dict:
|
||||||
current = state['products'][pid].get('supplyDownstreamEnabled', True) is not False
|
current = state['products'][pid].get('supplyDownstreamEnabled', True) is not False
|
||||||
state['products'][pid]['supplyDownstreamEnabled'] = not current
|
state['products'][pid]['supplyDownstreamEnabled'] = not current
|
||||||
|
|
||||||
|
elif atype == 'SET_PRODUCTION_TARGET':
|
||||||
|
pid = act.get('productId')
|
||||||
|
target = act.get('target', 1.0)
|
||||||
|
if pid in state.get('products', {}):
|
||||||
|
try:
|
||||||
|
target_val = float(target)
|
||||||
|
if 0.0 <= target_val <= 1.0:
|
||||||
|
state['products'][pid]['productionTarget'] = target_val
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
elif atype == 'TRIGGER_IPO':
|
elif atype == 'TRIGGER_IPO':
|
||||||
earned = calculate_shares_earned(state.get('lifetimeRevenue', 0.0))
|
earned = calculate_shares_earned(state.get('lifetimeRevenue', 0.0))
|
||||||
if earned > 0:
|
if earned > 0:
|
||||||
|
|
|
||||||
|
|
@ -125,11 +125,43 @@ def test_policy_toggles():
|
||||||
assert state['products']['flour']['totalProduced'] > 0 # Flour received wheat!
|
assert state['products']['flour']['totalProduced'] > 0 # Flour received wheat!
|
||||||
print("PASS: test_policy_toggles")
|
print("PASS: test_policy_toggles")
|
||||||
|
|
||||||
|
def test_production_target_throttle():
|
||||||
|
state = engine.get_initial_state()
|
||||||
|
state['products']['wheat']['logisticsEnabled'] = False # Don't sell to isolate production rate
|
||||||
|
|
||||||
|
# Set throttle to 0.5 (50%)
|
||||||
|
state = engine.apply_actions(state, [{'type': 'SET_PRODUCTION_TARGET', 'productId': 'wheat', 'target': 0.5}])
|
||||||
|
assert state['products']['wheat']['productionTarget'] == 0.5
|
||||||
|
|
||||||
|
state['lastTick'] = time.time() - 10.0
|
||||||
|
state = engine.process_offline_and_ticks(state)
|
||||||
|
produced_at_50 = state['products']['wheat']['totalProduced']
|
||||||
|
|
||||||
|
# Reset and test at 1.0 (100%)
|
||||||
|
state2 = engine.get_initial_state()
|
||||||
|
state2['products']['wheat']['logisticsEnabled'] = False
|
||||||
|
state2['lastTick'] = time.time() - 10.0
|
||||||
|
state2 = engine.process_offline_and_ticks(state2)
|
||||||
|
produced_at_100 = state2['products']['wheat']['totalProduced']
|
||||||
|
|
||||||
|
# 50% should produce roughly half (5 units vs 10 units over 10s at 1.0s base cycle)
|
||||||
|
assert produced_at_50 < produced_at_100
|
||||||
|
assert abs(produced_at_50 - (produced_at_100 * 0.5)) <= 1.0
|
||||||
|
|
||||||
|
# Set throttle to 0.0 (Paused / Off)
|
||||||
|
state = engine.apply_actions(state, [{'type': 'SET_PRODUCTION_TARGET', 'productId': 'wheat', 'target': 0.0}])
|
||||||
|
stock_before = state['products']['wheat']['stock']
|
||||||
|
state['lastTick'] = time.time() - 10.0
|
||||||
|
state = engine.process_offline_and_ticks(state)
|
||||||
|
assert state['products']['wheat']['stock'] == stock_before # Zero production when throttled to 0%
|
||||||
|
print(f"PASS: test_production_target_throttle (50%: {produced_at_50}, 100%: {produced_at_100})")
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
test_initial_state()
|
test_initial_state()
|
||||||
test_raw_production_and_logistics()
|
test_raw_production_and_logistics()
|
||||||
test_chained_dependency_and_bottleneck()
|
test_chained_dependency_and_bottleneck()
|
||||||
test_chained_microchips_and_boards()
|
test_chained_microchips_and_boards()
|
||||||
test_policy_toggles()
|
test_policy_toggles()
|
||||||
|
test_production_target_throttle()
|
||||||
test_ipo_and_shares()
|
test_ipo_and_shares()
|
||||||
print("\nALL BACKEND ENGINE TESTS PASSED SUCCESSFULLY!")
|
print("\nALL BACKEND ENGINE TESTS PASSED SUCCESSFULLY!")
|
||||||
|
|
|
||||||
|
|
@ -57,8 +57,9 @@
|
||||||
|
|
||||||
.production-grid {
|
.production-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(390px, 1fr));
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.facility-card {
|
.facility-card {
|
||||||
|
|
@ -70,8 +71,8 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
min-height: 420px;
|
min-height: 440px;
|
||||||
gap: 12px;
|
gap: 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: border-color 0.2s, box-shadow 0.2s;
|
transition: border-color 0.2s, box-shadow 0.2s;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
@ -85,7 +86,29 @@
|
||||||
|
|
||||||
.facility-card.selected {
|
.facility-card.selected {
|
||||||
border-color: var(--primary-blue);
|
border-color: var(--primary-blue);
|
||||||
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.15);
|
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card Navigation Highlight Pulse */
|
||||||
|
.facility-card.card-highlight-pulse {
|
||||||
|
animation: cardPulseAnim 1.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes cardPulseAnim {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(37, 99, 235, 0.7);
|
||||||
|
border-color: var(--primary-blue);
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
box-shadow: 0 0 0 8px rgba(37, 99, 235, 0);
|
||||||
|
border-color: var(--primary-blue);
|
||||||
|
transform: scale(1.01);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(37, 99, 235, 0);
|
||||||
|
transform: scale(1.0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.facility-card.locked-card {
|
.facility-card.locked-card {
|
||||||
|
|
@ -228,7 +251,9 @@
|
||||||
background: var(--bg-surface-sub);
|
background: var(--bg-surface-sub);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
min-height: 84px;
|
height: 86px;
|
||||||
|
min-height: 86px;
|
||||||
|
max-height: 86px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -259,7 +284,7 @@
|
||||||
color: var(--amber-dark);
|
color: var(--amber-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Inputs Strip */
|
/* Inputs Strip - Standardized fixed-height container to eliminate grid shifts */
|
||||||
.inputs-strip {
|
.inputs-strip {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -268,30 +293,55 @@
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
border: 1px dashed var(--border-color);
|
border: 1px dashed var(--border-color);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
|
height: 72px;
|
||||||
|
min-height: 72px;
|
||||||
|
max-height: 72px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputs-strip.raw-placeholder {
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background: #fafaf9;
|
||||||
|
border-style: dotted;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-style: italic;
|
||||||
|
text-align: center;
|
||||||
|
padding: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputs-label {
|
.inputs-label {
|
||||||
font-size: 0.68rem;
|
font-size: 0.68rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputs-chips {
|
.inputs-chips {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 44px;
|
||||||
|
scrollbar-width: thin;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-chip {
|
.input-chip {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
font-size: 0.72rem;
|
font-size: 0.70rem;
|
||||||
background: var(--bg-surface);
|
background: var(--bg-surface);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-chip.low-stock {
|
.input-chip.low-stock {
|
||||||
|
|
@ -355,6 +405,68 @@
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Facility Throttle Control Bar */
|
||||||
|
.facility-throttle-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
padding: 3px 6px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.throttle-label {
|
||||||
|
font-size: 0.68rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
margin-right: 2px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.throttle-pills {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-throttle-pill {
|
||||||
|
flex: 1;
|
||||||
|
background: var(--bg-surface);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px 0;
|
||||||
|
font-size: 0.70rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
|
transition: all 0.15s;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-throttle-pill:hover {
|
||||||
|
border-color: var(--border-subtle);
|
||||||
|
background: var(--bg-surface-sub);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-throttle-pill.active {
|
||||||
|
background: var(--primary-blue);
|
||||||
|
border-color: var(--primary-blue);
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-throttle-pill.active.pill-off {
|
||||||
|
background: #ef4444;
|
||||||
|
border-color: #dc2626;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Action Buttons */
|
/* Action Buttons */
|
||||||
.facility-actions-row {
|
.facility-actions-row {
|
||||||
|
|
|
||||||
|
|
@ -144,10 +144,45 @@ class ProductionApp {
|
||||||
this.mountActiveView();
|
this.mountActiveView();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'SELECT_PRODUCT':
|
case 'SELECT_PRODUCT': {
|
||||||
state.selectedProductId = action.productId;
|
state.selectedProductId = action.productId;
|
||||||
|
const targetP = getProduct(action.productId);
|
||||||
|
|
||||||
|
// If user selected a dependency or unit from inspector, ensure we navigate / go to that facility card
|
||||||
|
let needsViewMount = false;
|
||||||
|
if (state.activeTab !== 'production') {
|
||||||
|
state.activeTab = 'production';
|
||||||
|
document.querySelectorAll('.nav-btn, .nav-tab').forEach(b => {
|
||||||
|
b.classList.toggle('active', b.dataset.tab === 'production');
|
||||||
|
});
|
||||||
|
needsViewMount = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetP && state.selectedSector !== 'all' && state.selectedSector !== targetP.sector) {
|
||||||
|
state.selectedSector = targetP.sector;
|
||||||
|
needsViewMount = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needsViewMount) {
|
||||||
|
this.mountActiveView();
|
||||||
|
}
|
||||||
this.mountInspectorPane();
|
this.mountInspectorPane();
|
||||||
|
|
||||||
|
// Smooth scroll to the facility card and highlight it
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const cardEl = document.getElementById('card-' + action.productId);
|
||||||
|
if (cardEl) {
|
||||||
|
cardEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
cardEl.classList.remove('card-highlight-pulse');
|
||||||
|
void cardEl.offsetWidth; // Force CSS reflow to re-trigger animation
|
||||||
|
cardEl.classList.add('card-highlight-pulse');
|
||||||
|
setTimeout(() => {
|
||||||
|
cardEl.classList.remove('card-highlight-pulse');
|
||||||
|
}, 1600);
|
||||||
|
}
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'UNLOCK_PRODUCT': {
|
case 'UNLOCK_PRODUCT': {
|
||||||
const p = getProduct(action.productId);
|
const p = getProduct(action.productId);
|
||||||
|
|
@ -241,6 +276,21 @@ class ProductionApp {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'SET_PRODUCTION_TARGET': {
|
||||||
|
const p = getProduct(action.productId);
|
||||||
|
const pState = state.products[action.productId];
|
||||||
|
if (p && pState) {
|
||||||
|
const target = Math.max(0, Math.min(1.0, action.target));
|
||||||
|
pState.productionTarget = target;
|
||||||
|
const pct = Math.round(target * 100);
|
||||||
|
this.showToast(`${p.name} Production Target set to ${pct}%`, 'info');
|
||||||
|
this.sync.queueAction(action);
|
||||||
|
this.updateActiveView();
|
||||||
|
this.updateInspectorPane();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'MANUAL_CYCLE': {
|
case 'MANUAL_CYCLE': {
|
||||||
const p = getProduct(action.productId);
|
const p = getProduct(action.productId);
|
||||||
const pState = state.products[action.productId];
|
const pState = state.products[action.productId];
|
||||||
|
|
|
||||||
|
|
@ -44,9 +44,16 @@ export class SimulationEngine {
|
||||||
const lines = pState.lines || 0;
|
const lines = pState.lines || 0;
|
||||||
if (lines <= 0) continue;
|
if (lines <= 0) continue;
|
||||||
|
|
||||||
|
// Production target / throttle (1.0 = 100%, 0.75 = 75%, 0.5 = 50%, 0.25 = 25%, 0.0 = 0% / paused)
|
||||||
|
const targetThrottle = pState.productionTarget !== undefined ? pState.productionTarget : 1.0;
|
||||||
|
if (targetThrottle <= 0.0) {
|
||||||
|
pState.isBottlenecked = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Compute facility speed
|
// Compute facility speed
|
||||||
const milestoneMult = getUnitMilestoneMultiplier(lines);
|
const milestoneMult = getUnitMilestoneMultiplier(lines);
|
||||||
const cycleRate = (1.0 / p.baseCycleTime) * milestoneMult * roboticsSpeedMult;
|
const cycleRate = (1.0 / p.baseCycleTime) * milestoneMult * roboticsSpeedMult * targetThrottle;
|
||||||
|
|
||||||
// Handle Raw Extraction vs Manufacturing Inputs
|
// Handle Raw Extraction vs Manufacturing Inputs
|
||||||
const isRaw = !p.inputs || p.inputs.length === 0;
|
const isRaw = !p.inputs || p.inputs.length === 0;
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ export function getInitialState() {
|
||||||
logistics: p.unlockedDefault ? 1 : 0,
|
logistics: p.unlockedDefault ? 1 : 0,
|
||||||
logisticsEnabled: true,
|
logisticsEnabled: true,
|
||||||
supplyDownstreamEnabled: true,
|
supplyDownstreamEnabled: true,
|
||||||
|
productionTarget: 1.0,
|
||||||
cycleProgress: 0.0,
|
cycleProgress: 0.0,
|
||||||
isBottlenecked: false,
|
isBottlenecked: false,
|
||||||
totalProduced: 0.0
|
totalProduced: 0.0
|
||||||
|
|
|
||||||
|
|
@ -67,10 +67,27 @@ export function renderFacilityCardHtml(p, pState, state, buyMode) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
} else {
|
||||||
|
inputsHtml = `
|
||||||
|
<div class="inputs-strip raw-placeholder">
|
||||||
|
<span>Natural Raw Resource • Direct extraction from deposits (No upstream inputs needed)</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const currentThrottle = pState.productionTarget !== undefined ? pState.productionTarget : 1.0;
|
||||||
|
const throttleLevels = [
|
||||||
|
{ val: 1.0, label: '100%' },
|
||||||
|
{ val: 0.75, label: '75%' },
|
||||||
|
{ val: 0.50, label: '50%' },
|
||||||
|
{ val: 0.25, label: '25%' },
|
||||||
|
{ val: 0.0, label: '0% (Off)' }
|
||||||
|
];
|
||||||
|
|
||||||
let statusBadge = '';
|
let statusBadge = '';
|
||||||
if (pState.isBottlenecked) {
|
if (currentThrottle <= 0.0) {
|
||||||
|
statusBadge = `<span class="status-badge badge-idle" id="badge-${p.id}"><iconify-icon icon="lucide:pause-circle"></iconify-icon> Throttled (0%)</span>`;
|
||||||
|
} else if (pState.isBottlenecked) {
|
||||||
statusBadge = `<span class="status-badge badge-warning" id="badge-${p.id}"><iconify-icon icon="lucide:alert-triangle"></iconify-icon> Waiting for Inputs</span>`;
|
statusBadge = `<span class="status-badge badge-warning" id="badge-${p.id}"><iconify-icon icon="lucide:alert-triangle"></iconify-icon> Waiting for Inputs</span>`;
|
||||||
} else if (pState.lines > 0) {
|
} else if (pState.lines > 0) {
|
||||||
statusBadge = `<span class="status-badge badge-success" id="badge-${p.id}"><iconify-icon icon="lucide:check-circle"></iconify-icon> Operating (${milestoneMult}x Speed)</span>`;
|
statusBadge = `<span class="status-badge badge-success" id="badge-${p.id}"><iconify-icon icon="lucide:check-circle"></iconify-icon> Operating (${milestoneMult}x Speed)</span>`;
|
||||||
|
|
@ -137,6 +154,22 @@ export function renderFacilityCardHtml(p, pState, state, buyMode) {
|
||||||
|
|
||||||
${inputsHtml}
|
${inputsHtml}
|
||||||
|
|
||||||
|
<!-- Facility Throttle Level Selector (100%, 75%, 50%, 25%, 0%) -->
|
||||||
|
<div class="facility-throttle-bar">
|
||||||
|
<span class="throttle-label">Throttle:</span>
|
||||||
|
<div class="throttle-pills">
|
||||||
|
${throttleLevels.map(lvl => {
|
||||||
|
const isMatch = Math.abs(currentThrottle - lvl.val) < 0.01;
|
||||||
|
const offClass = lvl.val === 0.0 ? 'pill-off' : '';
|
||||||
|
return `
|
||||||
|
<button class="btn-throttle-pill ${isMatch ? `active ${offClass}` : ''}" data-product="${p.id}" data-target="${lvl.val}">
|
||||||
|
${lvl.label}
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
}).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Facility Supply & Distribution Policies -->
|
<!-- Facility Supply & Distribution Policies -->
|
||||||
<div class="facility-policy-bar">
|
<div class="facility-policy-bar">
|
||||||
<span class="policy-label">Policy:</span>
|
<span class="policy-label">Policy:</span>
|
||||||
|
|
@ -296,18 +329,35 @@ export function updateFacilityCardDom(p, pState, state, buyMode) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Throttle buttons DOM state
|
||||||
|
const currentThrottle = pState.productionTarget !== undefined ? pState.productionTarget : 1.0;
|
||||||
|
const cardEl = document.getElementById(`card-${p.id}`);
|
||||||
|
if (cardEl) {
|
||||||
|
cardEl.querySelectorAll('.btn-throttle-pill').forEach(btn => {
|
||||||
|
const btnVal = parseFloat(btn.dataset.target);
|
||||||
|
const isMatch = Math.abs(currentThrottle - btnVal) < 0.01;
|
||||||
|
const offClass = btnVal === 0.0 ? 'pill-off' : '';
|
||||||
|
btn.className = `btn-throttle-pill ${isMatch ? `active ${offClass}` : ''}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Status Badge
|
// Status Badge
|
||||||
const elBadge = document.getElementById(`badge-${p.id}`);
|
const elBadge = document.getElementById(`badge-${p.id}`);
|
||||||
if (elBadge) {
|
if (elBadge) {
|
||||||
const isBottlenecked = pState.isBottlenecked;
|
if (currentThrottle <= 0.0) {
|
||||||
const hasWarning = elBadge.classList.contains('badge-warning');
|
elBadge.className = 'status-badge badge-idle';
|
||||||
if (isBottlenecked && !hasWarning) {
|
elBadge.innerHTML = `<iconify-icon icon="lucide:pause-circle"></iconify-icon> Throttled (0%)`;
|
||||||
|
} else if (pState.isBottlenecked) {
|
||||||
elBadge.className = 'status-badge badge-warning';
|
elBadge.className = 'status-badge badge-warning';
|
||||||
elBadge.innerHTML = `<iconify-icon icon="lucide:alert-triangle"></iconify-icon> Waiting for Inputs`;
|
elBadge.innerHTML = `<iconify-icon icon="lucide:alert-triangle"></iconify-icon> Waiting for Inputs`;
|
||||||
} else if (!isBottlenecked && hasWarning) {
|
} else if (pState.lines > 0) {
|
||||||
const milestoneMult = getUnitMilestoneMultiplier(pState.lines);
|
const milestoneMult = getUnitMilestoneMultiplier(pState.lines);
|
||||||
|
const pctStr = currentThrottle < 1.0 ? ` (${Math.round(currentThrottle * 100)}%)` : '';
|
||||||
elBadge.className = 'status-badge badge-success';
|
elBadge.className = 'status-badge badge-success';
|
||||||
elBadge.innerHTML = `<iconify-icon icon="lucide:check-circle"></iconify-icon> Operating (${milestoneMult}x Speed)`;
|
elBadge.innerHTML = `<iconify-icon icon="lucide:check-circle"></iconify-icon> Operating (${milestoneMult}x Speed${pctStr})`;
|
||||||
|
} else {
|
||||||
|
elBadge.className = 'status-badge badge-idle';
|
||||||
|
elBadge.innerHTML = `Idle (0 Lines)`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,10 +32,11 @@ export function getProductOutputRates(p, pState, state) {
|
||||||
const milestoneMult = getUnitMilestoneMultiplier(pState.lines);
|
const milestoneMult = getUnitMilestoneMultiplier(pState.lines);
|
||||||
const roboticsMult = state.upgrades?.automated_robotics ? 2.0 : 1.0;
|
const roboticsMult = state.upgrades?.automated_robotics ? 2.0 : 1.0;
|
||||||
const baseCycleTime = Math.max(0.1, p.baseCycleTime / (milestoneMult * roboticsMult));
|
const baseCycleTime = Math.max(0.1, p.baseCycleTime / (milestoneMult * roboticsMult));
|
||||||
const isBottlenecked = !!pState.isBottlenecked;
|
const targetThrottle = pState.productionTarget !== undefined ? pState.productionTarget : 1.0;
|
||||||
|
const isBottlenecked = !!pState.isBottlenecked || targetThrottle <= 0;
|
||||||
|
|
||||||
// Output velocity: units per second
|
// Output velocity: units per second
|
||||||
const cyclesPerSec = (1 / baseCycleTime);
|
const cyclesPerSec = (1 / baseCycleTime) * targetThrottle;
|
||||||
const prodRate = isBottlenecked ? 0 : cyclesPerSec * p.outputAmount * pState.lines;
|
const prodRate = isBottlenecked ? 0 : cyclesPerSec * p.outputAmount * pState.lines;
|
||||||
|
|
||||||
// Logistics revenue rate: items sold passively per second * price
|
// Logistics revenue rate: items sold passively per second * price
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,14 @@ export function mountProductionFloor(container, state, onAction, engine) {
|
||||||
container.querySelectorAll('.btn-toggle-downstream').forEach(btn => {
|
container.querySelectorAll('.btn-toggle-downstream').forEach(btn => {
|
||||||
btn.onclick = () => onAction({ type: 'TOGGLE_DOWNSTREAM_SUPPLY', productId: btn.dataset.product });
|
btn.onclick = () => onAction({ type: 'TOGGLE_DOWNSTREAM_SUPPLY', productId: btn.dataset.product });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
container.querySelectorAll('.btn-throttle-pill').forEach(btn => {
|
||||||
|
btn.onclick = () => onAction({
|
||||||
|
type: 'SET_PRODUCTION_TARGET',
|
||||||
|
productId: btn.dataset.product,
|
||||||
|
target: parseFloat(btn.dataset.target)
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue