feat(progression): add unit milestones, multi-tiered trophies, buy multipliers and offshore perk shop

This commit is contained in:
Christian 2026-10-15 18:17:21 +02:00
parent e34189ea44
commit 81cf7f2129
5 changed files with 1216 additions and 210 deletions

View file

@ -1,7 +1,6 @@
import math import math
import time import time
# Drug Catalog mirroring config.js
# Drug Catalog mirroring config.js # Drug Catalog mirroring config.js
DRUG_CONFIGS = { DRUG_CONFIGS = {
'weed': {'rawCost': 10, 'sellPrice': 16, 'produceRate': 1.0, 'sellRate': 1.0, 'supplyRate': 1.0, 'supplierCost': 75, 'workerCost': 100, 'dealerCost': 150, 'unlockCost': 0}, 'weed': {'rawCost': 10, 'sellPrice': 16, 'produceRate': 1.0, 'sellRate': 1.0, 'supplyRate': 1.0, 'supplierCost': 75, 'workerCost': 100, 'dealerCost': 150, 'unlockCost': 0},
@ -28,23 +27,70 @@ SKILL_COSTS = {
'smug_1': 2000, 'smug_2': 50000, 'smug_3': 1000000 'smug_1': 2000, 'smug_2': 50000, 'smug_3': 1000000
} }
UNIT_MILESTONES = [1, 5, 10, 25, 50, 100, 200, 300, 400, 500, 750, 1000, 1500, 2000]
OFFSHORE_PERK_COSTS = {
'swiss_vault': [1, 3, 8, 20, 50],
'auto_contracts': [2, 5, 12, 30],
'corrupt_judiciary': [2, 6, 15, 40],
'bulk_discount': [5, 15, 35, 80],
'compound_interest': [4, 10, 25, 60, 150]
}
ACHIEVEMENT_TIER_REQS = {
'dirty_empire': [10000, 250000, 5000000, 100000000, 1000000000],
'master_synthesizer': [100, 1000, 15000, 200000, 2500000],
'street_monopoly': [100, 1500, 25000, 300000, 3000000],
'supply_baron': [250, 3000, 50000, 500000],
'vault_hoarder': [50000, 500000, 10000000, 250000000],
'syndicate_army': [25, 100, 500, 2500],
'purity_legend': [5, 25, 100, 500],
'shadow_cartel': [3, 10, 30],
'swiss_kingpin': [1, 3, 10, 25],
'formula_expansion': [3, 6, 8, 10]
}
def clean_round(val: float) -> float: def clean_round(val: float) -> float:
"""Rounds float gracefully to 1 decimal place without floating point anomalies.""" """Rounds float gracefully to 1 decimal place without floating point anomalies."""
if math.isnan(val) or math.isinf(val) or val < 0: if math.isnan(val) or math.isinf(val) or val < 0:
return 0.0 return 0.0
return round(float(val), 1) return round(float(val), 1)
def get_unit_milestone_multiplier(count: int) -> float:
"""Calculates 2x multiplier for each milestone threshold achieved."""
reached = sum(1 for m in UNIT_MILESTONES if count >= m)
return float(math.pow(2.0, reached))
def get_all_tier_milestone_multiplier(state: dict, unit_key: str) -> float:
"""Calculates 2x global set multiplier when all unlocked drugs reach a milestone."""
unlocked = [d for d in state.get('drugs', {}).values() if d.get('unlocked')]
if not unlocked:
return 1.0
min_count = min(d.get(unit_key, 0) for d in unlocked)
reached = sum(1 for m in UNIT_MILESTONES if min_count >= m)
return float(math.pow(2.0, reached))
def get_initial_state() -> dict: def get_initial_state() -> dict:
state = { state = {
'cash': 25.0, 'cash': 25.0,
'cleanBonds': 0, 'cleanBonds': 0,
'totalCleanBonds': 0,
'heat': 0.0, 'heat': 0.0,
'totalEarned': 0.0, 'totalEarned': 0.0,
'lastTick': time.time(), 'stats': {
'drugs': {}, 'totalProduced': 0.0,
'totalSold': 0.0,
'totalSupplied': 0.0,
'purityProcs': 0,
'raidsSurvived': 0,
'prestigeCount': 0
},
'achievements': {}, # { ach_id: claimed_tier_int }
'offshorePerks': {}, # { perk_id: level_int }
'skills': {s: False for s in SKILL_COSTS}, 'skills': {s: False for s in SKILL_COSTS},
'achievements': {}, 'synthesis': {r: 0 for r in SYNTHESIS_RECIPES},
'synthesis': {r: 0 for r in SYNTHESIS_RECIPES} 'drugs': {},
'lastTick': time.time()
} }
for drug_id, cfg in DRUG_CONFIGS.items(): for drug_id, cfg in DRUG_CONFIGS.items():
state['drugs'][drug_id] = { state['drugs'][drug_id] = {
@ -58,26 +104,100 @@ def get_initial_state() -> dict:
return state return state
def get_multipliers(state: dict): def get_multipliers(state: dict):
bonds = state.get('cleanBonds', 0) # Total lifetime bonds provide permanent base boost
total_bonds = max(state.get('totalCleanBonds', 0), state.get('cleanBonds', 0))
skills = state.get('skills', {}) skills = state.get('skills', {})
achievements = state.get('achievements', {}) ach = state.get('achievements', {})
perks = state.get('offshorePerks', {})
prod_m = 1.0 + (bonds * 0.03) # Compound interest perk raises bond sale value
compound_lvl = perks.get('compound_interest', 0)
bond_sell_rate = 0.04 + (compound_lvl * 0.02)
bond_prod_rate = 0.03
prod_m = 1.0 + (total_bonds * bond_prod_rate)
if skills.get('chem_1'): prod_m += 0.15 if skills.get('chem_1'): prod_m += 0.15
if achievements.get('millionaire'): prod_m += 0.20
sell_m = 1.0 + (bonds * 0.04) # Achievement bonuses for production
synth_tier = ach.get('master_synthesizer', 0)
prod_m += [0.0, 0.05, 0.10, 0.15, 0.25, 0.50][min(5, synth_tier)]
formula_tier = ach.get('formula_expansion', 0)
prod_m += [0.0, 0.05, 0.10, 0.15, 0.30][min(4, formula_tier)]
# Sales multiplier
sell_m = 1.0 + (total_bonds * bond_sell_rate)
if skills.get('king_2'): sell_m += 0.25 if skills.get('king_2'): sell_m += 0.25
if achievements.get('first_hit'): sell_m += 0.05 empire_tier = ach.get('dirty_empire', 0)
if achievements.get('cartel_tier'): sell_m += 0.15 sell_m += [0.0, 0.05, 0.10, 0.15, 0.25, 0.50][min(5, empire_tier)]
# Raw material cost multiplier
raw_m = 0.85 if skills.get('chem_2') else 1.0 raw_m = 0.85 if skills.get('chem_2') else 1.0
vault_tier = ach.get('vault_hoarder', 0)
raw_discount = [0.0, 0.05, 0.10, 0.15, 0.25][min(4, vault_tier)]
raw_m = max(0.1, raw_m - raw_discount)
# Dealer speed multiplier
dealer_m = 1.0 dealer_m = 1.0
if skills.get('king_1'): dealer_m += 0.20 if skills.get('king_1'): dealer_m += 0.20
if skills.get('king_3'): dealer_m += 0.50 if skills.get('king_3'): dealer_m += 0.50
street_tier = ach.get('street_monopoly', 0)
dealer_m += [0.0, 0.05, 0.10, 0.15, 0.25, 0.50][min(5, street_tier)]
return prod_m, sell_m, raw_m, dealer_m # Supplier speed multiplier
supply_tier = ach.get('supply_baron', 0)
supplier_m = 1.0 + [0.0, 0.05, 0.10, 0.15, 0.25][min(4, supply_tier)]
# Syndicate Army all speed multiplier
army_tier = ach.get('syndicate_army', 0)
all_unit_m = 1.0 + [0.0, 0.05, 0.10, 0.15, 0.25][min(4, army_tier)]
dealer_m *= all_unit_m
supplier_m *= all_unit_m
worker_m = all_unit_m
return prod_m, sell_m, raw_m, dealer_m, supplier_m, worker_m
def get_current_stat_value(state: dict, stat_key: str) -> float:
"""Helper to query current progress for an achievement category."""
if stat_key == 'totalEarned':
return float(state.get('totalEarned', 0.0))
elif stat_key == 'cash':
return float(state.get('cash', 0.0))
elif stat_key == 'totalPersonnel':
total = 0
for d in state.get('drugs', {}).values():
total += d.get('suppliers', 0) + d.get('workers', 0) + d.get('dealers', 0)
return float(total)
elif stat_key == 'unlockedCount':
return float(sum(1 for d in state.get('drugs', {}).values() if d.get('unlocked')))
else:
return float(state.get('stats', {}).get(stat_key, 0.0))
def validate_achievement_claim(state: dict, ach_id: str, tier: int) -> bool:
reqs = ACHIEVEMENT_TIER_REQS.get(ach_id)
if not reqs or tier < 1 or tier > len(reqs):
return False
current_claimed = state.get('achievements', {}).get(ach_id, 0)
if tier != current_claimed + 1:
return False
req_val = reqs[tier - 1]
stat_key_map = {
'dirty_empire': 'totalEarned',
'master_synthesizer': 'totalProduced',
'street_monopoly': 'totalSold',
'supply_baron': 'totalSupplied',
'vault_hoarder': 'cash',
'syndicate_army': 'totalPersonnel',
'purity_legend': 'purityProcs',
'shadow_cartel': 'raidsSurvived',
'swiss_kingpin': 'prestigeCount',
'formula_expansion': 'unlockedCount'
}
stat_key = stat_key_map.get(ach_id, 'totalEarned')
current_val = get_current_stat_value(state, stat_key)
return current_val >= req_val
def process_offline_and_ticks(state: dict) -> dict: def process_offline_and_ticks(state: dict) -> dict:
now = time.time() now = time.time()
@ -88,7 +208,14 @@ def process_offline_and_ticks(state: dict) -> dict:
if delta <= 0: if delta <= 0:
return sanitize_state(state) return sanitize_state(state)
prod_m, sell_m, raw_m, dealer_m = get_multipliers(state) prod_m, sell_m, raw_m, dealer_m, supplier_m, worker_m = get_multipliers(state)
# All-tier milestone multipliers
all_tier_sup = get_all_tier_milestone_multiplier(state, 'suppliers')
all_tier_work = get_all_tier_milestone_multiplier(state, 'workers')
all_tier_deal = get_all_tier_milestone_multiplier(state, 'dealers')
stats = state.setdefault('stats', {})
for drug_id, data in state.get('drugs', {}).items(): for drug_id, data in state.get('drugs', {}).items():
cfg = DRUG_CONFIGS.get(drug_id) cfg = DRUG_CONFIGS.get(drug_id)
@ -98,54 +225,48 @@ def process_offline_and_ticks(state: dict) -> dict:
# Suppliers automatic raw cultivation / procurement # Suppliers automatic raw cultivation / procurement
suppliers = data.get('suppliers', 0) suppliers = data.get('suppliers', 0)
if suppliers > 0: if suppliers > 0:
supply_speed = suppliers * cfg.get('supplyRate', 1.0) * prod_m unit_milestone_m = get_unit_milestone_multiplier(suppliers)
data['raw'] = data.get('raw', 0.0) + (supply_speed * delta) supply_speed = suppliers * cfg.get('supplyRate', 1.0) * prod_m * supplier_m * unit_milestone_m * all_tier_sup
added_raw = supply_speed * delta
data['raw'] = data.get('raw', 0.0) + added_raw
stats['totalSupplied'] = stats.get('totalSupplied', 0.0) + added_raw
# Workers production (Refining raw -> product) # Workers production (Refining raw -> product)
workers = data.get('workers', 0) workers = data.get('workers', 0)
raw = data.get('raw', 0.0) raw = data.get('raw', 0.0)
if workers > 0 and raw > 0: if workers > 0 and raw > 0:
potential_prod = workers * cfg['produceRate'] * prod_m * delta unit_milestone_m = get_unit_milestone_multiplier(workers)
potential_prod = workers * cfg['produceRate'] * prod_m * worker_m * unit_milestone_m * all_tier_work * delta
actual_prod = min(raw, potential_prod) actual_prod = min(raw, potential_prod)
data['raw'] = max(0.0, raw - actual_prod) data['raw'] = max(0.0, raw - actual_prod)
data['product'] = data.get('product', 0.0) + actual_prod data['product'] = data.get('product', 0.0) + actual_prod
stats['totalProduced'] = stats.get('totalProduced', 0.0) + actual_prod
# Dealers selling (Product -> Cash) # Dealers selling (Product -> Cash)
dealers = data.get('dealers', 0) dealers = data.get('dealers', 0)
product = data.get('product', 0.0) product = data.get('product', 0.0)
if dealers > 0 and product > 0: if dealers > 0 and product > 0:
potential_sell = dealers * cfg['sellRate'] * dealer_m * delta unit_milestone_m = get_unit_milestone_multiplier(dealers)
potential_sell = dealers * cfg['sellRate'] * dealer_m * unit_milestone_m * all_tier_deal * delta
actual_sell = min(product, potential_sell) actual_sell = min(product, potential_sell)
data['product'] = max(0.0, product - actual_sell) data['product'] = max(0.0, product - actual_sell)
rev = actual_sell * cfg['sellPrice'] * sell_m rev = actual_sell * cfg['sellPrice'] * sell_m
state['cash'] = state.get('cash', 0.0) + rev state['cash'] = state.get('cash', 0.0) + rev
state['totalEarned'] = state.get('totalEarned', 0.0) + rev state['totalEarned'] = state.get('totalEarned', 0.0) + rev
stats['totalSold'] = stats.get('totalSold', 0.0) + actual_sell
# Heat decay # Heat decay
state['heat'] = max(0.0, state.get('heat', 0.0) - (0.5 * delta)) state['heat'] = max(0.0, state.get('heat', 0.0) - (0.5 * delta))
check_server_achievements(state)
return sanitize_state(state) return sanitize_state(state)
def check_server_achievements(state: dict):
ach = state.setdefault('achievements', {})
if state.get('totalEarned', 0) > 0:
ach['first_hit'] = True
if state.get('drugs', {}).get('weed', {}).get('workers', 0) >= 10:
ach['grower_op'] = True
if state.get('drugs', {}).get('meth', {}).get('unlocked'):
ach['blue_sky'] = True
if state.get('drugs', {}).get('cocaine', {}).get('unlocked'):
ach['cartel_tier'] = True
if state.get('cash', 0) >= 1000000:
ach['millionaire'] = True
if state.get('cleanBonds', 0) >= 1:
ach['laundered'] = True
def apply_actions(state: dict, actions: list) -> dict: def apply_actions(state: dict, actions: list) -> dict:
"""Applies and authorizes player actions sequentially against server authority.""" """Applies and authorizes player actions sequentially against server authority."""
prod_m, sell_m, raw_m, dealer_m = get_multipliers(state) prod_m, sell_m, raw_m, dealer_m, supplier_m, worker_m = get_multipliers(state)
# Unit price scaling factor (1.15 down to 1.11 via bulk_discount perk)
discount_lvl = state.get('offshorePerks', {}).get('bulk_discount', 0)
scaling_r = max(1.10, 1.15 - (discount_lvl * 0.01))
for act in actions: for act in actions:
action_type = act.get('type') action_type = act.get('type')
@ -159,6 +280,7 @@ def apply_actions(state: dict, actions: list) -> dict:
if state['cash'] >= total_cost: if state['cash'] >= total_cost:
state['cash'] -= total_cost state['cash'] -= total_cost
state['drugs'][drug_id]['raw'] = state['drugs'][drug_id].get('raw', 0.0) + amount state['drugs'][drug_id]['raw'] = state['drugs'][drug_id].get('raw', 0.0) + amount
state.setdefault('stats', {})['totalSupplied'] = state['stats'].get('totalSupplied', 0.0) + amount
elif action_type == 'manufacture' and drug_id in DRUG_CONFIGS: elif action_type == 'manufacture' and drug_id in DRUG_CONFIGS:
data = state['drugs'][drug_id] data = state['drugs'][drug_id]
@ -168,10 +290,16 @@ def apply_actions(state: dict, actions: list) -> dict:
data['raw'] -= actual data['raw'] -= actual
yield_count = actual yield_count = actual
if state.get('skills', {}).get('chem_3'): if state.get('skills', {}).get('chem_3'):
# Purity proc bonus # 10% chance on client for 3x yield
yield_count *= 1.3 yield_count *= 1.3
state.setdefault('stats', {})['purityProcs'] = state['stats'].get('purityProcs', 0) + 1
data['product'] = data.get('product', 0.0) + yield_count data['product'] = data.get('product', 0.0) + yield_count
state['heat'] = min(100.0, state.get('heat', 0.0) + (0.5 * actual)) state.setdefault('stats', {})['totalProduced'] = state['stats'].get('totalProduced', 0.0) + yield_count
# Swiss vault secrecy perk reduces heat generation
vault_lvl = state.get('offshorePerks', {}).get('swiss_vault', 0)
heat_rise = max(0.2, (0.5 * actual) * (1.0 - vault_lvl * 0.12))
state['heat'] = min(100.0, state.get('heat', 0.0) + heat_rise)
elif action_type == 'sell_manual' and drug_id in DRUG_CONFIGS: elif action_type == 'sell_manual' and drug_id in DRUG_CONFIGS:
data = state['drugs'][drug_id] data = state['drugs'][drug_id]
@ -183,7 +311,11 @@ def apply_actions(state: dict, actions: list) -> dict:
profit = actual * cfg['sellPrice'] * sell_m profit = actual * cfg['sellPrice'] * sell_m
state['cash'] += profit state['cash'] += profit
state['totalEarned'] += profit state['totalEarned'] += profit
state['heat'] = min(100.0, state.get('heat', 0.0) + (0.8 * actual)) state.setdefault('stats', {})['totalSold'] = state['stats'].get('totalSold', 0.0) + actual
vault_lvl = state.get('offshorePerks', {}).get('swiss_vault', 0)
heat_rise = max(0.3, (0.8 * actual) * (1.0 - vault_lvl * 0.12))
state['heat'] = min(100.0, state.get('heat', 0.0) + heat_rise)
elif action_type == 'hire_supplier' and drug_id in DRUG_CONFIGS: elif action_type == 'hire_supplier' and drug_id in DRUG_CONFIGS:
cfg = DRUG_CONFIGS[drug_id] cfg = DRUG_CONFIGS[drug_id]
@ -191,7 +323,7 @@ def apply_actions(state: dict, actions: list) -> dict:
base_cost = cfg.get('supplierCost', 75) base_cost = cfg.get('supplierCost', 75)
for _ in range(hire_qty): for _ in range(hire_qty):
count = state['drugs'][drug_id].get('suppliers', 0) count = state['drugs'][drug_id].get('suppliers', 0)
cost = math.floor(base_cost * math.pow(1.15, count)) cost = math.floor(base_cost * math.pow(scaling_r, count))
if state['cash'] >= cost: if state['cash'] >= cost:
state['cash'] -= cost state['cash'] -= cost
state['drugs'][drug_id]['suppliers'] = count + 1 state['drugs'][drug_id]['suppliers'] = count + 1
@ -204,7 +336,7 @@ def apply_actions(state: dict, actions: list) -> dict:
base_cost = cfg['workerCost'] base_cost = cfg['workerCost']
for _ in range(hire_qty): for _ in range(hire_qty):
count = state['drugs'][drug_id].get('workers', 0) count = state['drugs'][drug_id].get('workers', 0)
cost = math.floor(base_cost * math.pow(1.15, count)) cost = math.floor(base_cost * math.pow(scaling_r, count))
if state['cash'] >= cost: if state['cash'] >= cost:
state['cash'] -= cost state['cash'] -= cost
state['drugs'][drug_id]['workers'] = count + 1 state['drugs'][drug_id]['workers'] = count + 1
@ -217,7 +349,7 @@ def apply_actions(state: dict, actions: list) -> dict:
base_cost = cfg['dealerCost'] base_cost = cfg['dealerCost']
for _ in range(hire_qty): for _ in range(hire_qty):
count = state['drugs'][drug_id].get('dealers', 0) count = state['drugs'][drug_id].get('dealers', 0)
cost = math.floor(base_cost * math.pow(1.15, count)) cost = math.floor(base_cost * math.pow(scaling_r, count))
if state['cash'] >= cost: if state['cash'] >= cost:
state['cash'] -= cost state['cash'] -= cost
state['drugs'][drug_id]['dealers'] = count + 1 state['drugs'][drug_id]['dealers'] = count + 1
@ -230,6 +362,12 @@ def apply_actions(state: dict, actions: list) -> dict:
if state['cash'] >= cost and not state['drugs'][drug_id].get('unlocked'): if state['cash'] >= cost and not state['drugs'][drug_id].get('unlocked'):
state['cash'] -= cost state['cash'] -= cost
state['drugs'][drug_id]['unlocked'] = True state['drugs'][drug_id]['unlocked'] = True
# Procurement pipeline perk gives free starter units!
starter_units = state.get('offshorePerks', {}).get('auto_contracts', 0)
if starter_units > 0:
state['drugs'][drug_id]['suppliers'] = state['drugs'][drug_id].get('suppliers', 0) + starter_units
state['drugs'][drug_id]['workers'] = state['drugs'][drug_id].get('workers', 0) + starter_units
elif action_type == 'blend_recipe': elif action_type == 'blend_recipe':
recipe_id = act.get('recipe_id') recipe_id = act.get('recipe_id')
@ -255,28 +393,72 @@ def apply_actions(state: dict, actions: list) -> dict:
state['cash'] -= cost state['cash'] -= cost
state.setdefault('skills', {})[skill_id] = True state.setdefault('skills', {})[skill_id] = True
elif action_type == 'claim_achievement':
ach_id = act.get('ach_id')
tier = int(act.get('tier', 1))
if validate_achievement_claim(state, ach_id, tier):
state.setdefault('achievements', {})[ach_id] = tier
elif action_type == 'buy_offshore_perk':
perk_id = act.get('perk_id')
if perk_id in OFFSHORE_PERK_COSTS:
costs = OFFSHORE_PERK_COSTS[perk_id]
current_lvl = state.setdefault('offshorePerks', {}).get(perk_id, 0)
if current_lvl < len(costs):
cost = costs[current_lvl]
if state.get('cleanBonds', 0) >= cost:
state['cleanBonds'] -= cost
state['offshorePerks'][perk_id] = current_lvl + 1
elif action_type == 'bribe_police': elif action_type == 'bribe_police':
bribe_cost = max(100.0, state['cash'] * 0.15) base_bribe = max(100.0, state['cash'] * 0.15)
judiciary_lvl = state.get('offshorePerks', {}).get('corrupt_judiciary', 0)
bribe_cost = max(25.0, base_bribe * (1.0 - judiciary_lvl * 0.20))
if state['cash'] >= bribe_cost: if state['cash'] >= bribe_cost:
state['cash'] -= bribe_cost state['cash'] -= bribe_cost
state['heat'] = 0.0 state['heat'] = 0.0
state.setdefault('stats', {})['raidsSurvived'] = state['stats'].get('raidsSurvived', 0) + 1
elif action_type == 'flush_stash': elif action_type == 'flush_stash':
state['heat'] = 20.0 state['heat'] = 20.0
state.setdefault('stats', {})['raidsSurvived'] = state['stats'].get('raidsSurvived', 0) + 1
elif action_type == 'breakout_raid':
success = bool(act.get('success', False))
if success:
state['heat'] = 0.0
state.setdefault('stats', {})['raidsSurvived'] = state['stats'].get('raidsSurvived', 0) + 1
elif action_type == 'launder_prestige': elif action_type == 'launder_prestige':
bonds_to_earn = math.floor(math.sqrt(max(0.0, state.get('totalEarned', 0.0)) / 25000.0)) bonds_to_earn = math.floor(math.sqrt(max(0.0, state.get('totalEarned', 0.0)) / 25000.0))
gain = max(0, bonds_to_earn - state.get('cleanBonds', 0)) gain = max(0, bonds_to_earn - state.get('totalCleanBonds', 0))
if gain >= 1: if gain >= 1:
state['cleanBonds'] += gain state['totalCleanBonds'] = state.get('totalCleanBonds', 0) + gain
state['cash'] = 50.0 state['cleanBonds'] = state.get('cleanBonds', 0) + gain
stats = state.setdefault('stats', {})
stats['prestigeCount'] = stats.get('prestigeCount', 0) + 1
# Calculate start cash with bond and swiss kingpin bonuses
start_cash = 50.0 + (state['totalCleanBonds'] * 25.0)
swiss_tier = state.get('achievements', {}).get('swiss_kingpin', 0)
if swiss_tier >= 1: start_cash += 250.0
if swiss_tier >= 2: start_cash += 2500.0
if swiss_tier >= 3: start_cash += 25000.0
if swiss_tier >= 4: start_cash += 250000.0
state['cash'] = start_cash
state['heat'] = 0.0 state['heat'] = 0.0
# Starter units from auto_contracts perk
free_starter = state.get('offshorePerks', {}).get('auto_contracts', 0)
for d_id, d_data in state['drugs'].items(): for d_id, d_data in state['drugs'].items():
d_data['unlocked'] = (d_id == 'weed') d_data['unlocked'] = (d_id == 'weed')
d_data['raw'] = 0.0 d_data['raw'] = 0.0
d_data['product'] = 0.0 d_data['product'] = 0.0
d_data['suppliers'] = 0 d_data['suppliers'] = free_starter if d_data['unlocked'] else 0
d_data['workers'] = 0 d_data['workers'] = free_starter if d_data['unlocked'] else 0
d_data['dealers'] = 0 d_data['dealers'] = 0
return sanitize_state(state) return sanitize_state(state)
@ -286,7 +468,17 @@ def sanitize_state(state: dict) -> dict:
state['cash'] = clean_round(state.get('cash', 0.0)) state['cash'] = clean_round(state.get('cash', 0.0))
state['heat'] = clean_round(min(100.0, max(0.0, state.get('heat', 0.0)))) state['heat'] = clean_round(min(100.0, max(0.0, state.get('heat', 0.0))))
state['totalEarned'] = clean_round(state.get('totalEarned', 0.0)) state['totalEarned'] = clean_round(state.get('totalEarned', 0.0))
state['cleanBonds'] = int(state.get('cleanBonds', 0)) state['cleanBonds'] = max(0, int(state.get('cleanBonds', 0)))
state['totalCleanBonds'] = max(state['cleanBonds'], int(state.get('totalCleanBonds', 0)))
stats = state.setdefault('stats', {})
for k in ['totalProduced', 'totalSold', 'totalSupplied']:
stats[k] = clean_round(stats.get(k, 0.0))
for k in ['purityProcs', 'raidsSurvived', 'prestigeCount']:
stats[k] = int(stats.get(k, 0))
state.setdefault('achievements', {})
state.setdefault('offshorePerks', {})
for d_id, data in state.get('drugs', {}).items(): for d_id, data in state.get('drugs', {}).items():
data['raw'] = clean_round(data.get('raw', 0.0)) data['raw'] = clean_round(data.get('raw', 0.0))

View file

@ -258,6 +258,215 @@ html, body {
font-weight: 700; font-weight: 700;
} }
/* Buy Multiplier Selector Bar */
#buy-multiplier-bar {
background: #090c0f;
border-top: 1px solid rgba(36, 45, 54, 0.5);
padding: 6px 14px;
display: flex;
justify-content: space-between;
align-items: center;
}
.buy-multi-label {
font-family: var(--font-mono);
font-size: 0.65rem;
color: var(--text-secondary);
font-weight: 700;
letter-spacing: 0.5px;
}
.buy-multi-group {
display: flex;
gap: 4px;
}
.btn-multi-pill {
background: #141b22;
color: var(--text-secondary);
border: 1px solid #283442;
border-radius: 6px;
padding: 4px 10px;
font-family: var(--font-mono);
font-size: 0.7rem;
font-weight: 700;
cursor: pointer;
transition: all 0.15s;
}
.btn-multi-pill:hover {
color: var(--text-primary);
border-color: var(--border-accent);
}
.btn-multi-pill.active {
background: #252814;
border-color: var(--brand-gold);
color: var(--brand-gold);
box-shadow: 0 0 8px rgba(234, 179, 8, 0.25);
}
/* Nav Badge for unclaimed Trophies */
.nav-badge {
position: absolute;
top: 4px;
right: 22%;
background: var(--neon-red);
color: #fff;
font-size: 0.6rem;
font-weight: 800;
padding: 1px 5px;
border-radius: 10px;
min-width: 16px;
text-align: center;
box-shadow: 0 0 8px rgba(239, 68, 68, 0.8);
animation: pulse 1.8s infinite;
}
/* Milestone Indicator */
.milestone-badge {
display: inline-flex;
align-items: center;
gap: 3px;
font-family: var(--font-mono);
font-size: 0.65rem;
background: #121d26;
border: 1px solid #233546;
border-radius: 4px;
padding: 1px 6px;
color: var(--neon-cyan);
}
/* Trophy Cabinet & Achievements */
.trophy-card {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 12px 14px;
display: flex;
flex-direction: column;
gap: 8px;
position: relative;
overflow: hidden;
}
.trophy-card.completed {
border-color: #22c55e;
background: #0d1a14;
}
.trophy-card.claimable {
border-color: var(--brand-gold);
box-shadow: 0 0 12px rgba(234, 179, 8, 0.3);
animation: glow-gold 2s infinite alternate;
}
@keyframes glow-gold {
from { box-shadow: 0 0 6px rgba(234, 179, 8, 0.2); }
to { box-shadow: 0 0 16px rgba(234, 179, 8, 0.5); }
}
.trophy-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.trophy-stars {
color: var(--brand-gold);
font-size: 0.85rem;
letter-spacing: 2px;
}
.trophy-progress-track {
width: 100%;
height: 6px;
background: #161e27;
border-radius: 3px;
overflow: hidden;
}
.trophy-progress-fill {
height: 100%;
background: linear-gradient(90deg, #06b6d4, #22c55e);
transition: width 0.3s ease;
}
.btn-claim-reward {
background: linear-gradient(135deg, #eab308 0%, #ca8a04 100%);
color: #000;
font-weight: 800;
font-family: var(--font-mono);
border: none;
border-radius: 8px;
padding: 8px 12px;
cursor: pointer;
text-align: center;
box-shadow: 0 0 12px rgba(234, 179, 8, 0.6);
transition: transform 0.1s, filter 0.15s;
}
.btn-claim-reward:active {
transform: scale(0.96);
}
/* Swiss Offshore Perk Shop */
.perk-grid {
display: grid;
grid-template-columns: 1fr;
gap: 10px;
margin-top: 10px;
}
.perk-card {
background: #0d1217;
border: 1px solid #1f2a36;
border-radius: 10px;
padding: 12px;
display: flex;
flex-direction: column;
gap: 8px;
}
.perk-card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.perk-lvl-badge {
font-family: var(--font-mono);
font-size: 0.7rem;
font-weight: bold;
color: var(--neon-cyan);
background: #101c26;
padding: 2px 8px;
border-radius: 4px;
border: 1px solid #1a3246;
}
.btn-perk-buy {
background: #122332;
border: 1px solid #234562;
color: var(--neon-cyan);
padding: 8px;
border-radius: 6px;
font-family: var(--font-mono);
font-weight: 700;
font-size: 0.75rem;
cursor: pointer;
transition: background 0.15s;
}
.btn-perk-buy:hover:not(:disabled) {
background: #18334a;
}
.btn-perk-buy:disabled {
opacity: 0.4;
cursor: not-allowed;
}
/* Main Viewport Content */ /* Main Viewport Content */
#main-viewport { #main-viewport {
flex: 1; flex: 1;

View file

@ -71,6 +71,18 @@
<!-- Dynamically populated chips of all owned products --> <!-- Dynamically populated chips of all owned products -->
</div> </div>
</div> </div>
<!-- Buy Multiplier Selector Bar (1x, 10x, 100x, Next Milestone, Max) -->
<div id="buy-multiplier-bar">
<span class="buy-multi-label">BUY QTY:</span>
<div class="buy-multi-group">
<button class="btn-multi-pill active" data-mode="1">1x</button>
<button class="btn-multi-pill" data-mode="10">10x</button>
<button class="btn-multi-pill" data-mode="100">100x</button>
<button class="btn-multi-pill" data-mode="next">Next</button>
<button class="btn-multi-pill" data-mode="max">Max</button>
</div>
</div>
</header> </header>
<!-- Toast Notifications Container --> <!-- Toast Notifications Container -->
@ -91,6 +103,11 @@
<iconify-icon icon="game-icons:cash"></iconify-icon> <iconify-icon icon="game-icons:cash"></iconify-icon>
<span>Streets</span> <span>Streets</span>
</button> </button>
<button class="nav-tab" data-tab="trophies" style="position: relative;">
<iconify-icon icon="game-icons:trophy-cup"></iconify-icon>
<span>Trophies</span>
<span id="trophy-badge" class="nav-badge" style="display: none;">0</span>
</button>
<button class="nav-tab" data-tab="tree"> <button class="nav-tab" data-tab="tree">
<iconify-icon icon="game-icons:spider-web"></iconify-icon> <iconify-icon icon="game-icons:spider-web"></iconify-icon>
<span>Network</span> <span>Network</span>

View file

@ -341,12 +341,213 @@ const SKILL_TREE = [
} }
]; ];
// Achievements with permanent passive bonuses // --- UNIT MILESTONES (1, 5, 10, 25, 50, 100...) ---
const ACHIEVEMENTS = [ const UNIT_MILESTONES = [1, 5, 10, 25, 50, 100, 200, 300, 400, 500, 750, 1000, 1500, 2000];
{ id: 'first_hit', name: 'First Batch', desc: 'Sell your first gram of product.', unlocked: false, reward: '+5% global sell value' },
{ id: 'grower_op', name: 'Green Thumb', desc: 'Hire 10 Weed Growers.', unlocked: false, reward: '+10% weed production' }, // --- SWISS OFFSHORE PERKS (Permanent upgrades bought with Clean Bonds) ---
{ id: 'blue_sky', name: 'Say My Name', desc: 'Unlock Methamphetamine.', unlocked: false, reward: '-10% meth raw cost' }, const OFFSHORE_PERKS = [
{ id: 'cartel_tier', name: 'El Patrón', desc: 'Unlock Cocaine.', unlocked: false, reward: '+15% all sales' }, {
{ id: 'millionaire', name: 'Seven Figures', desc: 'Amass $1,000,000 cash.', unlocked: false, reward: '+20% worker speed' }, id: 'swiss_vault',
{ id: 'laundered', name: 'Clean Hands', desc: 'Launder your first $1,000,000 offshore.', unlocked: false, reward: '+5% clean bond multiplier' } name: 'Swiss Vault Secrecy',
icon: 'game-icons:safe',
desc: 'Heavy suppression on police surveillance. Heat rises slower during all operations.',
costs: [1, 3, 8, 20, 50],
bonusPerLevel: 0.12,
formatBonus: (lvl) => `-${lvl * 12}% Heat Rise`
},
{
id: 'auto_contracts',
name: 'Procurement Pipeline',
icon: 'game-icons:conveyor-belt',
desc: 'Unlocking drug formulas grants free starter suppliers & workers immediately.',
costs: [2, 5, 12, 30],
bonusPerLevel: 1,
formatBonus: (lvl) => `+${lvl} Free Starter Suppliers & Workers`
},
{
id: 'corrupt_judiciary',
name: 'Corrupt Judiciary',
icon: 'game-icons:judge-wig',
desc: 'Discounts on police raid bribes and lighter consequences during blockades.',
costs: [2, 6, 15, 40],
bonusPerLevel: 0.20,
formatBonus: (lvl) => `-${lvl * 20}% Raid Bribe Cost`
},
{
id: 'bulk_discount',
name: 'Wholesale Black Market',
icon: 'game-icons:price-tag',
desc: 'Reduces unit price scaling factor from 1.15 down towards 1.11.',
costs: [5, 15, 35, 80],
bonusPerLevel: 0.01,
formatBonus: (lvl) => `Scaling: ${(1.15 - lvl * 0.01).toFixed(2)}x (was 1.15x)`
},
{
id: 'compound_interest',
name: 'Compound Bond Yield',
icon: 'game-icons:gold-bar',
desc: 'Each Clean Bond yields an extra +2% global sales multiplier.',
costs: [4, 10, 25, 60, 150],
bonusPerLevel: 0.02,
formatBonus: (lvl) => `+${(4 + lvl * 2)}% Global Sales / Bond`
}
]; ];
// --- MULTI-TIERED TROPHIES & ACHIEVEMENTS ---
const ACHIEVEMENT_TIERS = [
{
id: 'dirty_empire',
name: 'Dirty Empire',
category: 'finance',
icon: 'game-icons:money-stack',
desc: 'Accumulate career revenue.',
statKey: 'totalEarned',
formatValue: (v) => `$${fmtNum(v)}`,
tiers: [
{ req: 10000, name: 'Street Hustler', rewardDesc: '+5% Global Sales Value', bonusType: 'sell_multi', bonusVal: 0.05 },
{ req: 250000, name: 'Trap Lord', rewardDesc: '+10% Global Sales Value', bonusType: 'sell_multi', bonusVal: 0.10 },
{ req: 5000000, name: 'Syndicate Boss', rewardDesc: '+15% Global Sales Value', bonusType: 'sell_multi', bonusVal: 0.15 },
{ req: 100000000, name: 'Kingpin of Kings', rewardDesc: '+25% Global Sales Value', bonusType: 'sell_multi', bonusVal: 0.25 },
{ req: 1000000000, name: 'El Puro Supremo', rewardDesc: '+50% Global Sales Value', bonusType: 'sell_multi', bonusVal: 0.50 }
]
},
{
id: 'master_synthesizer',
name: 'Master Synthesizer',
category: 'production',
icon: 'game-icons:flask',
desc: 'Synthesize product in active labs.',
statKey: 'totalProduced',
formatValue: (v) => `${fmtNum(v)}g`,
tiers: [
{ req: 100, name: 'Test Tube Cook', rewardDesc: '+5% Production Speed', bonusType: 'prod_multi', bonusVal: 0.05 },
{ req: 1000, name: 'Lab Technician', rewardDesc: '+10% Production Speed', bonusType: 'prod_multi', bonusVal: 0.10 },
{ req: 15000, name: 'Chief Chemist', rewardDesc: '+15% Production Speed', bonusType: 'prod_multi', bonusVal: 0.15 },
{ req: 200000, name: 'Industrial Alchemist', rewardDesc: '+25% Production Speed', bonusType: 'prod_multi', bonusVal: 0.25 },
{ req: 2500000, name: 'Synthesis God', rewardDesc: '+50% Production Speed', bonusType: 'prod_multi', bonusVal: 0.50 }
]
},
{
id: 'street_monopoly',
name: 'Corner Network',
category: 'distribution',
icon: 'game-icons:hand-truck',
desc: 'Distribute grams on the streets.',
statKey: 'totalSold',
formatValue: (v) => `${fmtNum(v)}g`,
tiers: [
{ req: 100, name: 'Corner Pitcher', rewardDesc: '+5% Dealer Throughput', bonusType: 'dealer_speed', bonusVal: 0.05 },
{ req: 1500, name: 'Neighborhood Hub', rewardDesc: '+10% Dealer Throughput', bonusType: 'dealer_speed', bonusVal: 0.10 },
{ req: 25000, name: 'Metropolitan Monopoly', rewardDesc: '+15% Dealer Throughput', bonusType: 'dealer_speed', bonusVal: 0.15 },
{ req: 300000, name: 'Continental Route', rewardDesc: '+25% Dealer Throughput', bonusType: 'dealer_speed', bonusVal: 0.25 },
{ req: 3000000, name: 'Global Cartel Distribution', rewardDesc: '+50% Dealer Throughput', bonusType: 'dealer_speed', bonusVal: 0.50 }
]
},
{
id: 'supply_baron',
name: 'Supply Chain Baron',
category: 'logistics',
icon: 'game-icons:sprout',
desc: 'Harvest & cultivate raw materials via suppliers.',
statKey: 'totalSupplied',
formatValue: (v) => `${fmtNum(v)} raw`,
tiers: [
{ req: 250, name: 'Seedling Sprout', rewardDesc: '+5% Supplier Speed', bonusType: 'supplier_speed', bonusVal: 0.05 },
{ req: 3000, name: 'Greenhouse Fleet', rewardDesc: '+10% Supplier Speed', bonusType: 'supplier_speed', bonusVal: 0.10 },
{ req: 50000, name: 'Agricultural Consortium', rewardDesc: '+15% Supplier Speed', bonusType: 'supplier_speed', bonusVal: 0.15 },
{ req: 500000, name: 'Precursor Monopoly', rewardDesc: '+25% Supplier Speed', bonusType: 'supplier_speed', bonusVal: 0.25 }
]
},
{
id: 'vault_hoarder',
name: 'Liquid Vault',
category: 'finance',
icon: 'game-icons:vault',
desc: 'Hold liquid cash simultaneously on hand.',
statKey: 'cash',
formatValue: (v) => `$${fmtNum(v)}`,
tiers: [
{ req: 50000, name: 'Shoebox Stash', rewardDesc: '-5% Raw Material Costs', bonusType: 'raw_discount', bonusVal: 0.05 },
{ req: 500000, name: 'Duffel Bag Stack', rewardDesc: '-10% Raw Material Costs', bonusType: 'raw_discount', bonusVal: 0.10 },
{ req: 10000000, name: 'Private Bank Vault', rewardDesc: '-15% Raw Material Costs', bonusType: 'raw_discount', bonusVal: 0.15 },
{ req: 250000000, name: 'Central Bank Infiltrator', rewardDesc: '-25% Raw Material Costs', bonusType: 'raw_discount', bonusVal: 0.25 }
]
},
{
id: 'syndicate_army',
name: 'Syndicate Army',
category: 'workforce',
icon: 'game-icons:crowd-dive',
desc: 'Hire total workforce (Suppliers + Workers + Dealers).',
statKey: 'totalPersonnel',
formatValue: (v) => `${fmtInt(v)} staff`,
tiers: [
{ req: 25, name: 'Crew of 25', rewardDesc: '+5% All Unit Speeds', bonusType: 'all_speed', bonusVal: 0.05 },
{ req: 100, name: 'Centurion 100', rewardDesc: '+10% All Unit Speeds', bonusType: 'all_speed', bonusVal: 0.10 },
{ req: 500, name: 'Battalion 500', rewardDesc: '+15% All Unit Speeds', bonusType: 'all_speed', bonusVal: 0.15 },
{ req: 2500, name: 'Legion 2,500', rewardDesc: '+25% All Unit Speeds', bonusType: 'all_speed', bonusVal: 0.25 }
]
},
{
id: 'purity_legend',
name: '99.1% Crystallization',
category: 'chemistry',
icon: 'game-icons:crystal-growth',
desc: 'Trigger rare 3x Purity Syntheses.',
statKey: 'purityProcs',
formatValue: (v) => `${fmtInt(v)} procs`,
tiers: [
{ req: 5, name: 'Blue Crystal Spark', rewardDesc: '+2% Extra 3x Proc Chance', bonusType: 'proc_chance', bonusVal: 0.02 },
{ req: 25, name: 'Glass Master', rewardDesc: '+4% Extra 3x Proc Chance', bonusType: 'proc_chance', bonusVal: 0.04 },
{ req: 100, name: 'Cook of Legend', rewardDesc: '+8% Extra 3x Proc Chance', bonusType: 'proc_chance', bonusVal: 0.08 },
{ req: 500, name: 'Absolute 100% Purity', rewardDesc: '+15% Extra 3x Proc Chance', bonusType: 'proc_chance', bonusVal: 0.15 }
]
},
{
id: 'shadow_cartel',
name: 'Heat Evasion',
category: 'stealth',
icon: 'game-icons:police-officer-head',
desc: 'Survive raids by bribe or breakout.',
statKey: 'raidsSurvived',
formatValue: (v) => `${fmtInt(v)} survived`,
tiers: [
{ req: 3, name: 'Friendly Sheriff', rewardDesc: '-10% Police Heat Generation', bonusType: 'heat_reduction', bonusVal: 0.10 },
{ req: 10, name: 'Bought Precinct', rewardDesc: '-20% Police Heat Generation', bonusType: 'heat_reduction', bonusVal: 0.20 },
{ req: 30, name: 'Immunity Pass', rewardDesc: '-35% Police Heat Generation', bonusType: 'heat_reduction', bonusVal: 0.35 }
]
},
{
id: 'swiss_kingpin',
name: 'Swiss Laundromat',
category: 'prestige',
icon: 'game-icons:tied-scroll',
desc: 'Perform Offshore Prestige Laundering.',
statKey: 'prestigeCount',
formatValue: (v) => `${fmtInt(v)} resets`,
tiers: [
{ req: 1, name: 'First Swiss Wire', rewardDesc: '+$250 Starting Cash on Reset', bonusType: 'prestige_start_cash', bonusVal: 250 },
{ req: 3, name: 'Geneva Account', rewardDesc: '+$2,500 Starting Cash on Reset', bonusType: 'prestige_start_cash', bonusVal: 2500 },
{ req: 10, name: 'Zurich Private Vault', rewardDesc: '+$25,000 Starting Cash on Reset', bonusType: 'prestige_start_cash', bonusVal: 25000 },
{ req: 25, name: 'Shadow World Bank', rewardDesc: '+$250,000 Starting Cash on Reset', bonusType: 'prestige_start_cash', bonusVal: 250000 }
]
},
{
id: 'formula_expansion',
name: 'Cartel Formulary',
category: 'discovery',
icon: 'game-icons:dna1',
desc: 'Unlock classified substance formulas.',
statKey: 'unlockedCount',
formatValue: (v) => `${fmtInt(v)} / 10 formulas`,
tiers: [
{ req: 3, name: '3 Formulas Mastered', rewardDesc: '+5% All Production', bonusType: 'prod_multi', bonusVal: 0.05 },
{ req: 6, name: '6 Formulas Mastered', rewardDesc: '+10% All Production', bonusType: 'prod_multi', bonusVal: 0.10 },
{ req: 8, name: '8 Formulas Mastered', rewardDesc: '+15% All Production', bonusType: 'prod_multi', bonusVal: 0.15 },
{ req: 10, name: 'Apex Spectrum (All 10)', rewardDesc: '+30% All Production', bonusType: 'prod_multi', bonusVal: 0.30 }
]
}
];
// Backwards-compatibility alias
const ACHIEVEMENTS = ACHIEVEMENT_TIERS;

File diff suppressed because it is too large Load diff