Implement short-lived stellar event handling and optimize batch processing; add tests for new functionality
This commit is contained in:
@@ -340,12 +340,15 @@ func _clamp_auto_resources(target: Dictionary) -> void:
|
||||
|
||||
# Each type collects production into consecutive lifespan_delta-wide creation windows.
|
||||
# Its evolution window is the creation window shifted forward by one lifespan.
|
||||
# Optional direct completions return the remaining object-year integral for
|
||||
# newborn production. NAN means the caller should integrate ordinary batches.
|
||||
func _append_batch(target: Dictionary, id: String, amount: float,
|
||||
created_from: float, created_to: float) -> void:
|
||||
created_from: float, created_to: float, completed: Dictionary = {},
|
||||
completed_before: float = -INF) -> float:
|
||||
if amount <= 0.0 or not entities.has(id) or entities[id].lifespan <= 0.0:
|
||||
return
|
||||
return NAN
|
||||
if created_to < created_from:
|
||||
return
|
||||
return NAN
|
||||
if not target.batches.has(id):
|
||||
target.batches[id] = []
|
||||
if not target.has("batch_origins"):
|
||||
@@ -354,13 +357,19 @@ func _append_batch(target: Dictionary, id: String, amount: float,
|
||||
var lifespan: float = CivilizationState.effective_span(self, target, id, Global.CivilizationRules)
|
||||
var batch_duration: float = lifespan / Global.batch_count if lifespan > 10.0 else lifespan
|
||||
if batch_duration <= 0.0:
|
||||
return
|
||||
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: float = target.batch_origins[id]
|
||||
var cursor: float = created_from
|
||||
var duration: float = created_to - created_from
|
||||
var remaining_amount: float = amount
|
||||
var completed_amount: float = 0.0
|
||||
var completed_from: float = INF
|
||||
var completed_to: float = -INF
|
||||
# Subtract virtual windows in their original order to preserve floating-point
|
||||
# rounding of lifetime production, rather than subtracting one large sum.
|
||||
var remaining_object_years: float = amount * maxf(0.0, completed_before - (created_from + created_to) * 0.5) if is_finite(completed_before) else NAN
|
||||
# Compute the index once. Recomputing floor from a rounded boundary can
|
||||
# select the preceding window forever (e.g. fractional origins at large ages).
|
||||
var window_index: float = floorf((cursor - origin) / batch_duration)
|
||||
@@ -377,29 +386,37 @@ func _append_batch(target: Dictionary, id: String, amount: float,
|
||||
var window_end: float = origin + (window_index + 1.0) * batch_duration
|
||||
if window_end <= cursor:
|
||||
push_error("Batch window exceeds floating-point time precision: " + id)
|
||||
return
|
||||
return remaining_object_years
|
||||
var part_end: float = minf(created_to, window_end)
|
||||
var part_amount: float = remaining_amount
|
||||
if duration > 0.0 and part_end < created_to:
|
||||
part_amount = floorf(amount * (part_end - created_from) / duration) - (amount - remaining_amount)
|
||||
var batch: Dictionary = {}
|
||||
for existing: Dictionary in batches_by_start.get(window_start, []):
|
||||
if existing.get("created_from", -INF) == window_start and existing.start_year == window_start + lifespan and existing.end_year == window_end + lifespan:
|
||||
batch = existing
|
||||
break
|
||||
if batch.is_empty():
|
||||
batch = {
|
||||
"created_from": window_start,
|
||||
"created_to": window_end,
|
||||
"start_year": window_start + lifespan,
|
||||
"end_year": window_end + lifespan,
|
||||
"amount": 0.0,
|
||||
}
|
||||
batches.append(batch)
|
||||
if not batches_by_start.has(window_start):
|
||||
batches_by_start[window_start] = []
|
||||
batches_by_start[window_start].append(batch)
|
||||
batch.amount += part_amount
|
||||
if 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 += part_amount
|
||||
completed_from = minf(completed_from, window_start + lifespan)
|
||||
completed_to = maxf(completed_to, window_end + lifespan)
|
||||
remaining_object_years -= part_amount * _expired_years(completed_before, window_start + lifespan, window_end + lifespan)
|
||||
else:
|
||||
var batch: Dictionary = {}
|
||||
for existing: Dictionary in batches_by_start.get(window_start, []):
|
||||
if existing.get("created_from", -INF) == window_start and existing.start_year == window_start + lifespan and existing.end_year == window_end + lifespan:
|
||||
batch = existing
|
||||
break
|
||||
if batch.is_empty():
|
||||
batch = {
|
||||
"created_from": window_start,
|
||||
"created_to": window_end,
|
||||
"start_year": window_start + lifespan,
|
||||
"end_year": window_end + lifespan,
|
||||
"amount": 0.0,
|
||||
}
|
||||
batches.append(batch)
|
||||
if not batches_by_start.has(window_start):
|
||||
batches_by_start[window_start] = []
|
||||
batches_by_start[window_start].append(batch)
|
||||
batch.amount += part_amount
|
||||
remaining_amount -= part_amount
|
||||
if duration == 0.0 or part_end >= created_to:
|
||||
break
|
||||
@@ -407,6 +424,10 @@ func _append_batch(target: Dictionary, id: String, amount: float,
|
||||
window_index += 1.0
|
||||
batches.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
||||
return a.created_from < b.created_from)
|
||||
if completed_amount > 0.0:
|
||||
if not completed.has(id): completed[id] = []
|
||||
completed[id].append({"amount": completed_amount, "from": completed_from, "to": completed_to})
|
||||
return remaining_object_years
|
||||
|
||||
|
||||
# Move the left boundary after each partial withdrawal, keeping a uniform density.
|
||||
@@ -517,6 +538,11 @@ func _evolution_products(target: Dictionary, entity: OwniverseEntity, count: flo
|
||||
target.evolution_remainders[entity.id] = maxf(0.0, expected - positive)
|
||||
_add_output_terms(outputs, str(entity.output_or.get("positive", "")), positive)
|
||||
_add_output_terms(outputs, str(entity.output_or.get("negative", "")), count - positive)
|
||||
# Entity.blast is authoritative even if imported output still contains
|
||||
# blast:N. Zero disables the event; never add both representations.
|
||||
outputs.erase("blast")
|
||||
if entity.blast > 0:
|
||||
outputs["blast"] = count * entity.blast
|
||||
return outputs
|
||||
|
||||
|
||||
@@ -576,11 +602,12 @@ func _create_planets(target: Dictionary, star_id: String, count: float,
|
||||
# Successors born and dying inside one long step must still emit their lifetime S.
|
||||
# Integrate the birth interval and subtract the newly added death-window integral.
|
||||
func _produce_newborn_lifetime(target: Dictionary, id: String, count: float,
|
||||
born_from: float, born_to: float, to_year: float, before_batches: Array) -> void:
|
||||
born_from: float, born_to: float, to_year: float, before_batches: Array,
|
||||
remaining_object_years: float = NAN) -> void:
|
||||
var products: Dictionary = _yearly_products(entities[id])
|
||||
if products.is_empty() or count <= 0.0:
|
||||
return
|
||||
var object_years: float = count * maxf(0.0, to_year - (born_from + born_to) * 0.5)
|
||||
var object_years: float = remaining_object_years if is_finite(remaining_object_years) else count * maxf(0.0, to_year - (born_from + born_to) * 0.5)
|
||||
var previous_amounts: Dictionary = {}
|
||||
for old: Dictionary in before_batches:
|
||||
var start: float = old.get("created_from", -INF)
|
||||
@@ -650,11 +677,27 @@ func _apply_blast(target: Dictionary, power: float, environment: Dictionary) ->
|
||||
|
||||
func _evolve_batches(target: Dictionary, to_year: float) -> void:
|
||||
# A long cycle may contain several generations; preserve actual birth intervals.
|
||||
while _evolve_batch_generation(target, to_year):
|
||||
var completed: Dictionary = {}
|
||||
while _evolve_batch_generation(target, to_year, completed):
|
||||
pass
|
||||
|
||||
|
||||
func _evolve_batch_generation(target: Dictionary, to_year: float) -> bool:
|
||||
func _uses_short_lived_fast_path(id: String) -> bool:
|
||||
# Only terminal stellar events with linear outputs and no living successors.
|
||||
var entity: OwniverseEntity = entities[id]
|
||||
if entity.event_group != "SNGRBB" or not entity.output_or.is_empty() or not entity.life_rule.is_empty():
|
||||
return false
|
||||
for successor in entity.output_and:
|
||||
if entities.has(successor) and entities[successor].lifespan > 0.0:
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func _evolve_batch_generation(target: Dictionary, to_year: float, completed: Dictionary = {}) -> bool:
|
||||
# Apply direct completions at the original next-generation boundary, so
|
||||
# binding transfers, the blast environment and capacity checks keep order.
|
||||
var completed_now: Dictionary = completed.duplicate(true)
|
||||
completed.clear()
|
||||
var quantities: Dictionary = _physical_amounts(target)
|
||||
var expired: Dictionary = {}
|
||||
var preview: Dictionary = {"batches": {}}
|
||||
@@ -665,6 +708,8 @@ func _evolve_batch_generation(target: Dictionary, to_year: float) -> bool:
|
||||
if not entities.has(id) or entities[id].destroy_level <= 0:
|
||||
continue
|
||||
var dying: float = 0.0
|
||||
for part: Dictionary in completed_now.get(id, []):
|
||||
dying += part.amount
|
||||
for part: Dictionary in _withdraw_batches(preview, id, to_year):
|
||||
dying += part.amount
|
||||
if quantities.get(id, 0.0) > 0.0:
|
||||
@@ -673,7 +718,7 @@ func _evolve_batch_generation(target: Dictionary, to_year: float) -> bool:
|
||||
# Withdraw all types before creating successors: iteration order cannot affect them.
|
||||
for id in target.batches.keys():
|
||||
if entities.has(id):
|
||||
expired[id] = _withdraw_batches(target, id, to_year)
|
||||
expired[id] = completed_now.get(id, []) + _withdraw_batches(target, id, to_year)
|
||||
var has_expired: bool = false
|
||||
for parts: Array in expired.values():
|
||||
if not parts.is_empty():
|
||||
@@ -758,8 +803,9 @@ func _evolve_batch_generation(target: Dictionary, to_year: float) -> bool:
|
||||
else:
|
||||
amount = _credit(target, successor, amount)
|
||||
var before_batches: Array = target.batches.get(successor, []).duplicate(true)
|
||||
_append_batch(target, successor, amount, part.from, part.to)
|
||||
_produce_newborn_lifetime(target, successor, amount, part.from, part.to, to_year, before_batches)
|
||||
var cutoff: float = to_year if _uses_short_lived_fast_path(successor) else -INF
|
||||
var integral: float = _append_batch(target, successor, amount, part.from, part.to, completed, cutoff)
|
||||
_produce_newborn_lifetime(target, successor, amount, part.from, part.to, to_year, before_batches, integral)
|
||||
new_owners[successor] = new_owners.get(successor, 0.0) + amount
|
||||
for owner_id in incoming:
|
||||
if not target.binds.has(owner_id):
|
||||
|
||||
Reference in New Issue
Block a user