feat(progression): add unit milestones, multi-tiered trophies, buy multipliers and offshore perk shop
This commit is contained in:
parent
e34189ea44
commit
81cf7f2129
5 changed files with 1216 additions and 210 deletions
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
Loading…
Add table
Reference in a new issue