242 lines
15 KiB
GDScript
242 lines
15 KiB
GDScript
extends RefCounted
|
|
const REWARDS := preload("res://scripts/planets/planet_rewards.gd")
|
|
const PROFILES := preload("res://scripts/planets/planet_profiles.gd")
|
|
const UI := preload("res://scripts/planets/expedition_ui.gd")
|
|
const KINDS := ["trader", "barter", "collector", "blind_cache", "scan_cache", "safe_bet", "prototype", "rescue", "evacuation", "service"]
|
|
const TITLES := {
|
|
"trader":"Wędrowny handlarz", "barter":"Wymiana towarów", "collector":"Kolekcjoner minerałów",
|
|
"blind_cache":"Kontener w ciemno", "scan_cache":"Płatny skan skrytki", "safe_bet":"Pewna wypłata albo ryzyko",
|
|
"prototype":"Test prototypu", "rescue":"Akcja ratunkowa", "evacuation":"Ewakuacja etapami", "service":"Mobilny serwis"
|
|
}
|
|
const STORIES := {
|
|
"trader":"Przy zakurzonym transporterze miga przenośny nadajnik. Właściciel przedstawia się jako niezależny dostawca, choć oznaczenia na skrzyniach należą do kilku różnych firm. Twierdzi, że kończy interesy na tej planecie i woli sprzedać zapasy taniej, niż zabierać je z powrotem na orbitę.",
|
|
"barter":"Obok niewielkiej instalacji wydobywczej stoi łazik załadowany po same osie. Jego operator od tygodni wydobywa ten sam minerał, ale zabrakło mu materiałów do naprawy urządzeń. Proponuje wymianę części swojego zapasu na surowce z twojej ładowni. Przesyła dokładny wykaz towarów.",
|
|
"collector":"Sygnał prowadzi do samotnego badacza pochylonego nad analizatorem próbek. Na widok twojego spisu ładunku nagle przerywa rozmowę. Jednego z przewożonych przez ciebie surowców potrzebuje do ukończenia badań. Ma środki z kończącego się grantu i jest gotów zapłacić więcej niż zwykły skup.",
|
|
"blind_cache":"Złomiarz wyciągnął z wraku zamknięty kontener. Plomba wygląda na nienaruszoną, ale tabliczka ładunkowa została wypalona podczas wejścia w atmosferę. Sprzedawca nie zamierza tracić czasu na otwieranie zabezpieczeń. Podaje cenę za całość i zapewnia, że poprzedni kupujący trafił świetnie. O wcześniejszych klientach mówi już mniej chętnie.",
|
|
"scan_cache":"W ruinach starego magazynu ktoś odnalazł pancerną skrytkę. Stojąca obok operatorka ma skaner zdolny zajrzeć przez jej osłonę, lecz urządzenie zużywa drogie ogniwa. Za niewielką opłatą sprawdzi, co może znajdować się w środku. Otwarcie zamka będzie kosztować osobno. Uprzedza, że skorodowana obudowa zakłóca odczyt.",
|
|
"safe_bet":"Zarządca opuszczanego punktu przeładunkowego sprzedaje ostatnie zapasy. Za podaną kwotę możesz zabrać przygotowany zestaw części albo jeden z zaplombowanych pakietów odzyskanych z transportu. Otwarty zestaw jest skromny, ale sprawdzony. W zamkniętych pakietach zdarzały się wartościowe podzespoły — oraz materiały, których nikt nie chciał kupić.",
|
|
"prototype":"Inżynierka rozłożyła stanowisko pomiarowe daleko od zabudowań. Potrzebuje lądownika do krótkiego testu nowego układu zasilania. Zapewnia podstawową zapłatę niezależnie od wyniku i dodatkową premię za poprawny przebieg próby. Gdy pytasz o ryzyko, bez słowa przesyła zapis poprzedniego testu. Widać na nim nagły skok napięcia.",
|
|
"rescue":"Słaby sygnał alarmowy dobiega z przewróconego łazika. Jego kierowca żyje, lecz nie może wydostać się z kabiny. Do uniesienia wraku potrzebny jest prowizoryczny podnośnik zbudowany z twoich części. Czujniki ostrzegają, że uszkodzona bateria pojazdu jest niestabilna. Uwięziony obiecuje zapłatę, jeśli pomożesz mu wrócić do obozu.",
|
|
"evacuation":"Nad małym obozem zbiera się burza, a jego transport ewakuacyjny stoi z rozebranym silnikiem. Pierwsza grupa ludzi czeka już przy awaryjnym schronieniu. Dalej pozostali pracownicy zabezpieczający sprzęt i ranni, których trudniej przenieść. Koordynator może zapłacić za każdy ukończony etap pomocy, ale z każdą minutą warunki będą gorsze.",
|
|
"service":"Na skraju polany stoi warsztat na szerokich gąsienicach. Mechanik odbiera sygnał z twojego lądownika i po chwili przesyła listę uszkodzeń. Ma części wystarczające na jedną szybką naprawę. Podaje cenę oraz dokładny zakres prac, po czym uruchamia spawarkę i czeka na odpowiedź."
|
|
}
|
|
|
|
static func prepare(event: Dictionary, captain: Resource, progress: Resource) -> void:
|
|
if event.get("prepared", false) or not KINDS.has(event.get("kind", "")):
|
|
return
|
|
var rng := RandomNumberGenerator.new()
|
|
rng.seed = int(event.get("seed", progress.visit_seed ^ 0x45564e54))
|
|
event.stage = 0
|
|
event.scanned = false
|
|
event.level = maxi(1, GameState.system_level)
|
|
event.reward_profile = progress.active_profile.duplicate(true)
|
|
var scale := int(event.level)
|
|
var count := rng.randi_range(3, 6)
|
|
var missile_price: int = captain.discounted_price(ShipInventory.MISSILE_BUY_BASE_COST*scale)
|
|
event.missile_count = count
|
|
event.missile_normal_price = missile_price*count
|
|
event.missile_price = maxi(1, floori(missile_price*count*REWARDS.missile_price_ratio(event.reward_profile)))
|
|
event.module = _module(rng, scale, event.reward_profile)
|
|
event.prize = _mystery(rng, scale, event.reward_profile)
|
|
event.rolls = [rng.randf(), rng.randf(), rng.randf()]
|
|
event.damage_rolls = [rng.randi_range(0, 8), rng.randi_range(0, 8), rng.randi_range(0, 8)]
|
|
event.collect_key = PROFILES.MATERIALS[rng.randi_range(0, PROFILES.MATERIALS.size()-1)]
|
|
event.prepared = true
|
|
|
|
static func _module(rng: RandomNumberGenerator, level: int, profile: Dictionary) -> ItemInstance:
|
|
return REWARDS.generate_module(rng,profile,level)
|
|
|
|
static func roll_module_bonus(rng: RandomNumberGenerator, profile: Dictionary) -> int:
|
|
return REWARDS.roll_module_bonus(rng,profile)
|
|
|
|
static func _mystery(rng: RandomNumberGenerator, level: int, profile: Dictionary) -> Dictionary:
|
|
var roll := rng.randf()
|
|
if roll < 0.5:
|
|
return REWARDS.scale_materials({"parts":rng.randi_range(2,8)},profile)
|
|
if roll < 0.85:
|
|
return REWARDS.scale_materials({str(PROFILES.MATERIALS[rng.randi_range(0,PROFILES.MATERIALS.size()-1)]):rng.randi_range(6,15)},profile)
|
|
if roll < 0.95:
|
|
return {"missile":rng.randi_range(2,4)}
|
|
return {"module":_module(rng,level,profile)}
|
|
|
|
static func _option(id: String, label: String, cost: Dictionary = {}, reward: Dictionary = {}) -> Dictionary:
|
|
return {"id":id, "label":label, "cost":cost, "reward":reward}
|
|
|
|
static func chance(event: Dictionary, captain: Resource, progress: Resource) -> float:
|
|
var stage := int(event.get("stage",0)) if event.kind == "evacuation" else 0
|
|
var value := PROFILES.success_chance(progress.active_profile,captain,stage)
|
|
return clampf(value - (0.10 if event.kind == "prototype" else 0.0),0.0,1.0)
|
|
|
|
static func damage_range(event: Dictionary, progress: Resource) -> Vector2i:
|
|
var stage := int(event.get("stage",0)) if event.kind == "evacuation" else 0
|
|
var lower := (12 if event.kind == "prototype" else 8) + stage*4 + roundi(float(progress.active_profile.difficulty)*10)
|
|
return Vector2i(lower,lower+8)
|
|
|
|
static func options(event: Dictionary, captain: Resource, progress: Resource) -> Array[Dictionary]:
|
|
prepare(event,captain,progress)
|
|
var found: Array[Dictionary] = []
|
|
if not event.get("prepared",false) or event.get("complete",false):
|
|
return found
|
|
var scale := int(event.level)
|
|
match str(event.kind):
|
|
"trader":
|
|
found.append(_option("missiles","Kup %d rakiet · sklep: %d kr." % [event.missile_count,event.missile_normal_price],{"credits":event.missile_price},{"missile":event.missile_count}))
|
|
found.append(_option("materials","Kup zestaw surowców",{"credits":captain.discounted_price(70*scale)},{"copper":8,"silicon":8}))
|
|
var item: ItemInstance = event.module
|
|
var offer := _option("module","Kup: %s · poz. %d" % [item.definition.display_name,item.item_level],{"credits":captain.discounted_price(140*scale)},{"module":item})
|
|
offer.tooltip = item.get_tooltip_text()
|
|
found.append(offer)
|
|
"barter":
|
|
found.append(_option("minerals","Wymień surowce",{"iron":12,"copper":4},{"nickel":8}))
|
|
var ammo_cost := REWARDS.missile_barter_cost(event.reward_profile)
|
|
found.append(_option("missiles","Wymień surowce na 3 rakiety",{"iron":ammo_cost,"sulfur":ammo_cost,"uranium":ammo_cost},{"missile":3}))
|
|
"collector":
|
|
found.append(_option("sell","Sprzedaj próbki",{str(event.collect_key):6},{"credits":120*scale}))
|
|
"blind_cache":
|
|
found.append(_option("open","Kup kontener · zawartość nieznana",{"credits":captain.discounted_price(100*scale)}))
|
|
"scan_cache":
|
|
if not event.scanned:
|
|
found.append(_option("scan","Zleć skan skrytki",{"credits":captain.discounted_price(15*scale)}))
|
|
else:
|
|
found.append(_option("open","Otwórz przeskanowaną skrytkę",{"credits":captain.discounted_price(75*scale)}))
|
|
"safe_bet":
|
|
var price: int = captain.discounted_price(80*scale)
|
|
found.append(_option("safe","Weź gwarantowany zestaw",{"credits":price},{"parts":6}))
|
|
found.append(_option("gamble","Wybierz tajemniczy pakiet",{"credits":price}))
|
|
"prototype":
|
|
found.append(_option("test","Uruchom prototyp · podstawa: %d kr." % (35*scale),{},{"credits":90*scale,"module":event.module}))
|
|
"rescue":
|
|
found.append(_option("rescue","Zbuduj podnośnik i pomóż kierowcy",{"parts":6},{"credits":140*scale,"parts":8}))
|
|
"evacuation":
|
|
var stage := int(event.stage)
|
|
found.append(_option("help","Etap %d/3 · %s" % [stage+1,["Zabezpiecz schronienie","Ewakuuj pracowników","Przenieś rannych"][stage]],{"parts":2+stage},{"credits":[40,70,110][stage]*scale,"parts":2+stage*2}))
|
|
if stage > 0:
|
|
found.append(_option("stop","Zakończ pomoc i zachowaj dotychczasową zapłatę"))
|
|
"service":
|
|
var amount := mini(25,ceili(100.0-progress.lander_hp))
|
|
if amount > 0:
|
|
found.append(_option("repair","Napraw lądownik: +%d HP" % amount,{"credits":captain.discounted_price(amount*4*scale)}))
|
|
for option in found:
|
|
option.reward = REWARDS.scale_materials(option.reward,event.reward_profile)
|
|
return found
|
|
|
|
static func stock(captain: Resource, progress: Resource, key: String) -> int:
|
|
match key:
|
|
"credits": return int(captain.credits)+int(progress.cargo_credits)
|
|
"parts": return floori(captain.parts)+int(progress.cargo_parts)
|
|
return int(captain.minerals.get(key,0))+int(progress.cargo.get(key,0))
|
|
|
|
static func can_pay(captain: Resource, progress: Resource, cost: Dictionary) -> bool:
|
|
for key in cost:
|
|
if stock(captain,progress,key) < int(cost[key]):
|
|
return false
|
|
return true
|
|
|
|
static func _pay(captain: Resource, progress: Resource, cost: Dictionary) -> void:
|
|
for key in cost:
|
|
var amount := int(cost[key])
|
|
if key == "credits":
|
|
var from_cargo := mini(amount,int(progress.cargo_credits))
|
|
progress.cargo_credits -= from_cargo
|
|
captain.credits -= amount-from_cargo
|
|
elif key == "parts":
|
|
var from_cargo := mini(amount,int(progress.cargo_parts))
|
|
progress.cargo_parts -= from_cargo
|
|
captain.parts -= amount-from_cargo
|
|
else:
|
|
var from_cargo := mini(amount,int(progress.cargo.get(key,0)))
|
|
progress.cargo[key] = int(progress.cargo.get(key,0))-from_cargo
|
|
if int(progress.cargo[key]) == 0:
|
|
progress.cargo.erase(key)
|
|
captain.minerals[key] = int(captain.minerals.get(key,0))-(amount-from_cargo)
|
|
|
|
static func _grant(progress: Resource, reward: Dictionary) -> void:
|
|
for key in reward:
|
|
match key:
|
|
"credits": progress.cargo_credits += int(reward[key])
|
|
"parts": progress.cargo_parts += int(reward[key])
|
|
"missile": progress.cargo_missiles += int(reward[key])
|
|
"module": progress.cargo_modules.append(reward[key])
|
|
_: progress.add_material(key,int(reward[key]))
|
|
|
|
static func reward_text(reward: Dictionary) -> String:
|
|
var materials := reward.duplicate()
|
|
materials.erase("module")
|
|
materials.erase("credits")
|
|
var text := UI.material_text(materials)
|
|
if reward.has("credits"):
|
|
text += (" · " if not text.is_empty() else "") + "%d kr." % int(reward.credits)
|
|
if reward.has("module"):
|
|
var item: ItemInstance = reward.module
|
|
text += ("\n" if not text.is_empty() else "") + "%s · poziom %d" % [item.definition.display_name,item.item_level]
|
|
return text
|
|
|
|
static func scan_text(event: Dictionary) -> String:
|
|
if not event.get("scanned",false):
|
|
return "Skan ujawni rodzaj zawartości, ale nie jej ilość ani stan. Otwarcie wymaga osobnej opłaty."
|
|
var prize: Dictionary = event.prize
|
|
if prize.has("module"):
|
|
return "ODCZYT: podzespół statku. Poziomu i parametrów nie udało się ustalić."
|
|
if prize.has("missile"):
|
|
return "ODCZYT: amunicja rakietowa. Liczba sztuk nieznana."
|
|
if prize.has("parts"):
|
|
return "ODCZYT: części zamienne. Ilość nieznana."
|
|
return "ODCZYT: próbki mineralne. Dokładny skład i ilość nieznane."
|
|
|
|
static func resolve(event: Dictionary, action: String, captain: Resource, progress: Resource) -> Dictionary:
|
|
if not progress.active or progress.lander_hp <= 0 or event.get("complete",false):
|
|
return {"success":false,"reason":"To spotkanie jest już niedostępne."}
|
|
var choices := options(event,captain,progress)
|
|
var selected: Dictionary = {}
|
|
for option in choices:
|
|
if str(option.id) == action:
|
|
selected = option
|
|
break
|
|
if selected.is_empty():
|
|
return {"success":false,"reason":"Ta decyzja jest niedostępna."}
|
|
if not can_pay(captain,progress,selected.cost):
|
|
return {"success":false,"reason":"Brakuje zasobów. Koszt: "+reward_text(selected.cost)}
|
|
if event.kind in ["prototype","rescue","evacuation"] and action != "stop" and chance(event,captain,progress) <= 0.0:
|
|
return {"success":false,"blocked":true,"reason":"Ten etap ma 0% szans. Obniż zagrożenie odpowiednim wyposażeniem."}
|
|
_pay(captain,progress,selected.cost)
|
|
var reward: Dictionary = selected.reward.duplicate()
|
|
var success := true
|
|
var damage := 0
|
|
var message := ""
|
|
if event.kind == "scan_cache" and action == "scan":
|
|
event.scanned = true
|
|
message = "Skan zakończony. Możesz opłacić otwarcie lub wrócić później."
|
|
elif event.kind in ["blind_cache","scan_cache"] or (event.kind == "safe_bet" and action == "gamble"):
|
|
reward = event.prize.duplicate()
|
|
event.complete = true
|
|
elif event.kind in ["prototype","rescue","evacuation"] and action != "stop":
|
|
var stage := int(event.stage)
|
|
success = float(event.rolls[stage]) < chance(event,captain,progress)
|
|
if not success:
|
|
var limits := damage_range(event,progress)
|
|
damage = limits.x+int(event.damage_rolls[stage])
|
|
progress.damage(damage)
|
|
reward.clear()
|
|
message = "NIEPOWODZENIE · utrata %d HP. Wcześniejszy łup pozostaje w ładowni." % damage
|
|
if event.kind == "prototype":
|
|
reward.credits = int(reward.get("credits",0))+35*int(event.level)
|
|
if success and event.kind == "evacuation":
|
|
event.stage = stage+1
|
|
event.complete = int(event.stage) >= 3
|
|
else:
|
|
event.complete = true
|
|
else:
|
|
event.complete = true
|
|
if action == "repair":
|
|
var restored := minf(25,100.0-progress.lander_hp)
|
|
progress.lander_hp = minf(100,progress.lander_hp+restored)
|
|
message = "Naprawiono lądownik: +%d HP." % ceili(restored)
|
|
elif action == "stop":
|
|
message = "Zakończono pomoc. Dotychczasowa zapłata pozostaje w ładowni wyprawy."
|
|
_grant(progress,reward)
|
|
if not reward.is_empty():
|
|
message += ("\n" if not message.is_empty() else "") + "Otrzymano: "+reward_text(reward)
|
|
captain.emit_changed()
|
|
progress.emit_changed()
|
|
return {"success":success,"message":message,"damage":damage,"reward":reward}
|