195 lines
12 KiB
GDScript
195 lines
12 KiB
GDScript
extends "res://world/owniverse/simulation_module.gd"
|
|
const E = preload("res://world/owniverse/math/exact.gd")
|
|
const CivilizationState = preload("res://world/owniverse/civilization_state.gd")
|
|
const SimulationTime = preload("res://world/owniverse/simulation_time.gd")
|
|
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
|
|
|
|
func scale_batches(target: Dictionary, id: String, factor: Variant) -> void:
|
|
var batches: Array = target.batches.get(id, [])
|
|
if batches.is_empty():
|
|
return
|
|
# The common case for short-lived stellar events is a single cohort. Avoid
|
|
# allocating the shares array and sorting it when no redistribution is
|
|
# needed.
|
|
if batches.size() == 1:
|
|
var only: Dictionary = batches[0]
|
|
var expected: Variant = E.multiply(only.amount, E.maximum(0, factor))
|
|
only.amount = E.round_value(expected)
|
|
only.withdraw_remainder = E.minimum(only.get("withdraw_remainder", 0), only.amount)
|
|
return
|
|
var total: Variant = 0
|
|
var assigned: Variant = 0
|
|
var shares: Array = []
|
|
for batch : Dictionary in batches:
|
|
var expected: Variant = E.multiply(batch.amount, E.maximum(0, factor))
|
|
total = E.add(total, expected)
|
|
batch.amount = E.floor_value(expected)
|
|
assigned = E.add(assigned, batch.amount)
|
|
shares.append({"batch": batch, "rest": E.subtract(expected, batch.amount)})
|
|
shares.sort_custom(func ( a : Dictionary , b : Dictionary ) -> bool : return E.gt(a.rest, b.rest))
|
|
for share : Dictionary in shares:
|
|
if E.lt(assigned, E.round_value(total)):
|
|
share.batch.amount = E.add(share.batch.amount, 1)
|
|
assigned = E.add(assigned, 1)
|
|
share.batch.withdraw_remainder = E.minimum(share.batch.get("withdraw_remainder", 0), share.batch.amount)
|
|
|
|
|
|
func append_batch(target: Dictionary, id: String, amount: Variant, created_from: Variant, created_to: Variant, completed: Dictionary = { }, completed_before: Variant = - INF) -> Variant:
|
|
if ((E.le(amount, 0) or (not world.catalog.entities.has(id))) or E.le(world.catalog.entities[id].lifespan, 0)):
|
|
return NAN
|
|
if E.lt(created_to, created_from):
|
|
return NAN
|
|
if (not target.batches.has(id)):
|
|
target.batches[id] = []
|
|
if (not target.has("batch_origins")):
|
|
target.batch_origins = {}
|
|
var batches: Array = target.batches[id]
|
|
var lifespan: Variant = CivilizationState.effective_span(world, target, id, Global.CivilizationRules)
|
|
if E.eq(world.catalog.entities[id].group_as, "Star"):
|
|
lifespan = E.divide(lifespan, WorldParameters.effective(target, "Bur"))
|
|
var batch_duration: Variant = (E.divide(lifespan, Global.batch_count) if E.gt(lifespan, 10) else lifespan)
|
|
if E.le(batch_duration, 0):
|
|
return NAN
|
|
if (not target.batch_origins.has(id)):
|
|
target.batch_origins[id] = (batches[0].get("created_from", created_from) if (not batches.is_empty()) else created_from)
|
|
var origin: Variant = target.batch_origins[id]
|
|
var cursor: Variant = created_from
|
|
var duration: Variant = E.subtract(created_to, created_from)
|
|
var remaining_amount: Variant = amount
|
|
var completed_amount: Variant = 0
|
|
var completed_from: Variant = INF
|
|
var completed_to: Variant = E.neg(INF)
|
|
# Subtract each completed window's lifetime production exactly.
|
|
var remaining_object_years: Variant = (E.multiply(amount, E.maximum(0, E.subtract(completed_before, E.multiply((E.add(created_from, created_to)), E.of("0.5"))))) if E.finite(completed_before) else NAN)
|
|
# Advance an exact window index rather than repeatedly finding a boundary.
|
|
var window_index: Variant = E.floor_value(E.divide((E.subtract(cursor, origin)), batch_duration))
|
|
if E.le(E.add(origin, E.multiply((E.add(window_index, 1)), batch_duration)), cursor):
|
|
window_index = E.add(window_index, 1)
|
|
var batches_by_start: Dictionary = {}
|
|
var skip_empty_expired: bool = E.gt(duration, 0) and E.finite(completed_before) and E.eq(amount, E.floor_value(amount))
|
|
for existing : Dictionary in batches:
|
|
var start: Variant = existing.get("created_from", E.neg(INF))
|
|
if (not batches_by_start.has(str(start))):
|
|
batches_by_start[str(start)] = []
|
|
batches_by_start[str(start)].append(existing)
|
|
while true:
|
|
# Whole allocation changes only when its cumulative quota reaches the
|
|
# next integer. Skip expired empty windows without changing the span
|
|
# passed to the next generation or the lifetime production integral.
|
|
if skip_empty_expired:
|
|
var threshold = E.add(created_from, E.mul_div(duration, E.add(E.subtract(amount, remaining_amount), 1), amount))
|
|
var next_nonempty = E.subtract(E.ceil_value(E.divide(E.subtract(threshold, origin), batch_duration)), 1)
|
|
var expired_end = E.floor_value(E.divide(E.subtract(E.subtract(completed_before, lifespan), origin), batch_duration))
|
|
var skip_to = E.minimum(next_nonempty, expired_end)
|
|
if E.gt(skip_to, window_index):
|
|
completed_from = E.minimum(completed_from, E.add(E.add(origin, E.multiply(window_index, batch_duration)), lifespan))
|
|
completed_to = E.maximum(completed_to, E.add(E.add(origin, E.multiply(skip_to, batch_duration)), lifespan))
|
|
window_index = skip_to
|
|
cursor = E.add(origin, E.multiply(window_index, batch_duration))
|
|
var window_start: Variant = E.add(origin, E.multiply(window_index, batch_duration))
|
|
var window_end: Variant = E.add(origin, E.multiply((E.add(window_index, 1)), batch_duration))
|
|
if E.le(window_end, cursor):
|
|
push_error("Batch window does not advance: " + id)
|
|
return remaining_object_years
|
|
var part_end: Variant = E.minimum(created_to, window_end)
|
|
var part_amount: Variant = remaining_amount
|
|
if (E.gt(duration, 0) and E.lt(part_end, created_to)):
|
|
part_amount = E.subtract(E.floor_value(E.mul_div(amount, (E.subtract(part_end, created_from)), duration)), (E.subtract(amount, remaining_amount)))
|
|
if E.le(E.add(window_end, lifespan), completed_before):
|
|
# Retain the original integer allocation and lifetime integral, but
|
|
# do not allocate thousands of already-dead batch dictionaries.
|
|
completed_amount = E.add(completed_amount, part_amount)
|
|
completed_from = E.minimum(completed_from, E.add(window_start, lifespan))
|
|
completed_to = E.maximum(completed_to, E.add(window_end, lifespan))
|
|
remaining_object_years = E.subtract(remaining_object_years, E.multiply(part_amount, expired_years(completed_before, E.add(window_start, lifespan), E.add(window_end, lifespan))))
|
|
else:
|
|
var batch: Dictionary = {}
|
|
for existing : Dictionary in batches_by_start.get(str(window_start), []):
|
|
if ((E.eq(existing.get("created_from", E.neg(INF)), window_start) and E.eq(existing.start_year, E.add(window_start, lifespan))) and E.eq(existing.end_year, E.add(window_end, lifespan))):
|
|
batch = existing
|
|
break
|
|
if batch.is_empty():
|
|
batch = {"created_from": window_start, "created_to": window_end, "start_year": E.add(window_start, lifespan), "end_year": E.add(window_end, lifespan), "amount": 0}
|
|
batches.append(batch)
|
|
if (not batches_by_start.has(str(window_start))):
|
|
batches_by_start[str(window_start)] = []
|
|
batches_by_start[str(window_start)].append(batch)
|
|
batch.amount = E.add(batch.amount, part_amount)
|
|
remaining_amount = E.subtract(remaining_amount, part_amount)
|
|
if (E.eq(duration, 0) or E.ge(part_end, created_to)):
|
|
break
|
|
cursor = window_end
|
|
window_index = E.add(window_index, 1)
|
|
batches.sort_custom(func ( a : Dictionary , b : Dictionary ) -> bool : return E.lt(a.created_from, b.created_from))
|
|
if E.gt(completed_amount, 0):
|
|
if (not completed.has(id)): completed[id] = []
|
|
completed[id].append({"amount": completed_amount, "from": completed_from, "to": completed_to})
|
|
return remaining_object_years
|
|
|
|
|
|
func withdraw_batches(target: Dictionary, id: String, to_year: Variant) -> Array:
|
|
var expired: Array = []
|
|
var remaining: Array = []
|
|
for batch : Dictionary in target.batches.get(id, []):
|
|
var start: Variant = E.maximum(batch.start_year, batch.get("processed_until", batch.start_year))
|
|
var end: Variant = batch.end_year
|
|
var amount: Variant = batch.amount
|
|
if E.le(amount, 0):
|
|
continue
|
|
if (E.lt(to_year, start) or ((E.eq(to_year, start) and E.gt(end, start)))):
|
|
remaining.append(batch)
|
|
continue
|
|
var consumed: Variant = amount
|
|
var until: Variant = E.minimum(to_year, end)
|
|
if E.lt(to_year, end):
|
|
var carry: Variant = batch.get("withdraw_remainder", 0)
|
|
var expected: Variant = E.add(carry, E.mul_div((E.subtract(amount, carry)), (E.subtract(to_year, start)), (E.subtract(end, start))))
|
|
consumed = E.floor_value(expected)
|
|
batch.withdraw_remainder = E.subtract(expected, consumed)
|
|
batch.amount = E.maximum(0, E.subtract(amount, consumed))
|
|
batch.processed_until = to_year
|
|
remaining.append(batch)
|
|
if E.gt(consumed, 0):
|
|
expired.append({"amount": consumed, "from": start, "to": until})
|
|
target.batches[id] = remaining
|
|
return expired
|
|
|
|
|
|
func object_years(source: Dictionary, id: String, quantity: Variant, from_year: Variant, to_year: Variant) -> Variant:
|
|
var result: Variant = E.multiply(quantity, SimulationTime.elapsed_years(from_year, to_year))
|
|
for batch : Dictionary in source.batches.get(id, []):
|
|
var start: Variant = E.maximum(batch.start_year, batch.get("processed_until", batch.start_year))
|
|
var end: Variant = batch.end_year
|
|
result = E.subtract(result, E.multiply(batch.amount, (E.subtract(expired_years(to_year, start, end), expired_years(from_year, start, end)))))
|
|
return E.maximum(0, result)
|
|
|
|
|
|
func expired_years(year: Variant, start: Variant, end: Variant) -> Variant:
|
|
if E.le(year, start):
|
|
return 0
|
|
if E.le(end, start):
|
|
return E.subtract(year, start)
|
|
if E.ge(year, end):
|
|
return E.add(E.subtract(year, end), E.multiply((E.subtract(end, start)), E.of("0.5")))
|
|
return E.mul_div((E.subtract(year, start)), (E.subtract(year, start)), (E.multiply(2, (E.subtract(end, start)))))
|
|
|
|
|
|
func consume_from_batches(entity: OwniverseEntity, current_year: Variant) -> Variant:
|
|
var total: Variant = 0
|
|
var physical: Dictionary = world.ledger.physical_amounts(world.state)
|
|
for part : Dictionary in withdraw_batches(world.state, entity.id, current_year):
|
|
total = E.add(total, part.amount)
|
|
var fraction: Variant = (E.clamp_value(E.divide(total, physical[entity.id]), 0, 1) if E.gt(physical.get(entity.id, 0), 0) else 0)
|
|
world.state.amount[entity.id] = E.multiply(world.state.amount.get(entity.id, 0), (E.subtract(1, fraction)))
|
|
for owner_id in world.state.binds:
|
|
if world.state.binds[owner_id].has(entity.id):
|
|
world.state.binds[owner_id][entity.id] = E.multiply(world.state.binds[owner_id][entity.id], E.subtract(1, fraction))
|
|
world.ledger.round_object_allocations(world.state)
|
|
world.ledger.refresh_groups(world.state)
|
|
world.presentation.refresh(world.state, world.catalog)
|
|
return total
|
|
|
|
|
|
func add_entity_to_batch(entity_id: String, amount: Variant, current_year: Variant) -> void:
|
|
append_batch(world.state, entity_id, amount, current_year, current_year)
|