diff --git a/autoload/global.gd b/autoload/global.gd index 64d9f55..ef1c12a 100644 --- a/autoload/global.gd +++ b/autoload/global.gd @@ -1,6 +1,6 @@ extends Node -const SPEED_LIMIT: int = 4 +const SPEED_LIMIT: int = 5 var Settings: Dictionary = { "filename": "save-0001", diff --git a/stage/stage.gd b/stage/stage.gd index 44b3bc1..e05d5f3 100644 --- a/stage/stage.gd +++ b/stage/stage.gd @@ -315,12 +315,22 @@ func _change_speed(delta: int) -> void: func _on_topmenu_button_pressed(ButtonName) -> void: - print("Debug: ", owniverse.state) + print("Debug → ") + #, owniverse.state) + for id in owniverse.state.batches: + var s = owniverse.state.batches[id].size() + if s != 0: + print(id, " → ", s) + #, owniverse.state.batches[id]) + if id == 'HG': + print(owniverse.state.batches[id]) + #var s: float = 1E15 #var z = 1.0 #for i in range(30): #print(i, " | ", z, " | ", " | ", s - z) #z *= 10 + print("-------") func _on_unlock_and_save_timeout() -> void: @@ -356,7 +366,7 @@ func save_data() -> void: game_data["state"] = owniverse.state SaveManager.save_data("owniverse", "qwerty", game_data) - print("saved ", game_data.version, "\n\n\n") + #print("saved ", game_data.version, "\n\n\n") func init_enitities() -> Dictionary: diff --git a/world/owniverse/owniverse.gd b/world/owniverse/owniverse.gd index 5c4085b..1c8fa7e 100644 --- a/world/owniverse/owniverse.gd +++ b/world/owniverse/owniverse.gd @@ -29,8 +29,11 @@ var state: Dictionary = { 'speed_in_year': 1, 'energy': 1E14, 'batches': {}, + 'batch_origins': {}, 'binds': {}, 'amount': {}, + 'distribution': {}, + 'auto_resources': {}, 'stats': { 'total_amount': {}, } @@ -44,36 +47,617 @@ var speed: int: return state.speed set(value): state.speed = clampi(value, 1, Global.SPEED_LIMIT) - state.speed_in_years = pow(10, state.speed - 1) - print(state.speed, " | ", state.speed_in_years) + state.speed_in_year = pow(10.0, state.speed - 1) -func evolute(delta: float): - - var new_time: Dictionary = _add_time_delta(delta) - # produce and evolute - - state.time = new_time +func evolute(delta: float) -> void: + if delta <= 0.0 or not is_finite(delta): + return - #new_owniverse.state = state - #new_owniverse.stats = stats - #new_owniverse.entities = entities - - #.locked_entities = locked_entities - - + var previous: Dictionary = state + var next: Dictionary = previous.duplicate_deep() + next.speed = clampi(int(previous.speed), 1, Global.SPEED_LIMIT) + next.speed_in_year = pow(10.0, next.speed - 1) + var datetime: float = previous.time.datetime + delta * next.speed_in_year + var from_year: float = floorf(previous.time.year) + var to_year: float = floorf(datetime) + if to_year > from_year: + var quantities: Dictionary = _physical_amounts(previous) + var distribution: Dictionary = _normalize_distribution(quantities) + next.distribution = distribution + + _produce_for_period(previous, next, quantities, distribution, from_year, to_year) + _evolve_batches(next, to_year) + _refresh_groups(next) + _clamp_auto_resources(next) + next.time = {"datetime": datetime, "year": to_year} + + # Publish only after every phase has finished; previous stays untouched. + new_state = next + state = next + for id in next.amount: + if next.amount[id] != previous.amount.get(id, 0.0): + items_to_update[id] = next.amount[id] func _add_time_delta(delta: float) -> Dictionary: - var new_datetime = state.time.datetime + delta * state.speed_in_year - # - #new_owniverse.state.year = floorf(new_owniverse.state.datetime) - var new_time:= { - 'datetime': new_datetime, - 'year': floorf(new_datetime), - } - #print(new_time) - return new_time + var new_datetime: float = state.time.datetime + delta * pow(10.0, speed - 1) + return {"datetime": new_datetime, "year": floorf(new_datetime)} + + +func _is_group(id: String) -> bool: + if entity_groups.has(id): + return true + for entity: OwniverseEntity in entities.values(): + if entity.group_as == id: + return true + return false + + +func _physical_amounts(source: Dictionary) -> Dictionary: + var amounts: Dictionary = {} + for id in entities: + if not _is_group(id): + amounts[id] = maxf(0.0, source.amount.get(id, 0.0)) + for bindings: Dictionary in source.binds.values(): + for id in bindings: + if amounts.has(id): + amounts[id] += maxf(0.0, bindings[id]) + return amounts + + +func _members(id: String) -> Array: + if not _is_group(id): + return [id] + var members: Array = [] + for key in entities: + if entities[key].group_as == id: + members.append(key) + return members + + +func _available(source: Dictionary, id: String) -> float: + var amount: float = 0.0 + for member in _members(id): + amount += maxf(0.0, source.amount.get(member, 0.0)) + return amount + + +# Resolve group references to concrete types, proportional to free quantities. +func _expand_requirements(source: Dictionary, requirements: Dictionary) -> Dictionary: + var expanded: Dictionary = {} + for id in requirements: + var weights: Dictionary = {} + for member in _members(id): + weights[member] = source.amount.get(member, 0.0) + weights = _normalize_distribution(weights) + for member in weights: + expanded[member] = expanded.get(member, 0.0) + maxf(0.0, requirements[id]) * weights[member] + return expanded + + +func _take_available(target: Dictionary, id: String, amount: float) -> Dictionary: + var quantities: Dictionary = {} + for member in _members(id): + quantities[member] = target.amount.get(member, 0.0) + var distribution: Dictionary = _normalize_distribution(quantities) + var taken: Dictionary = {} + var total: float = minf(amount, _available(target, id)) + var discrete: bool = not distribution.is_empty() + for member in distribution: + discrete = discrete and _is_discrete(member) + if discrete: + total = floorf(total) + var shares: Array = [] + var assigned: float = 0.0 + for member in distribution: + var expected: float = total * distribution[member] + taken[member] = floorf(expected) if discrete else expected + assigned += taken[member] + shares.append({"id": member, "rest": expected - taken[member]}) + if discrete: + shares.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: + return a.rest > b.rest if a.rest != b.rest else a.id < b.id) + for share: Dictionary in shares: + if assigned >= total: + break + taken[share.id] += 1.0 + assigned += 1.0 + for member in taken: + target.amount[member] = maxf(0.0, target.amount.get(member, 0.0) - taken[member]) + return taken + + +func _scale_batches(target: Dictionary, id: String, factor: float) -> void: + var batches: Array = target.batches.get(id, []) + var total: float = 0.0 + var assigned: float = 0.0 + var shares: Array = [] + for batch: Dictionary in batches: + var expected: float = batch.amount * maxf(0.0, factor) + total += expected + batch.amount = floorf(expected) + assigned += batch.amount + shares.append({"batch": batch, "rest": expected - batch.amount}) + shares.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: + return a.rest > b.rest) + for share: Dictionary in shares: + if assigned < roundf(total): + share.batch.amount += 1.0 + assigned += 1.0 + share.batch.withdraw_remainder = minf(share.batch.get("withdraw_remainder", 0.0), share.batch.amount) + + +func _is_discrete(_id: String) -> bool: + return true + + +func _credit(target: Dictionary, id: String, amount: float) -> float: + if amount <= 0.0: + return 0.0 + if _is_discrete(id): + if not target.has("production_remainders"): + target.production_remainders = {} + var expected: float = amount + target.production_remainders.get(id, 0.0) + amount = floorf(expected) + target.production_remainders[id] = expected - amount + target.amount[id] = target.amount.get(id, 0.0) + amount + target.stats.total_amount[id] = target.stats.total_amount.get(id, 0.0) + amount + if entities.has(id) and entities[id].group_as != "": + var group: String = entities[id].group_as + target.stats.total_amount[group] = target.stats.total_amount.get(group, 0.0) + amount + return amount + + +# Allocate whole objects between free stock and owners without changing totals. +func _round_object_allocations(target: Dictionary) -> void: + var physical: Dictionary = _physical_amounts(target) + for id in physical: + if not _is_discrete(id): + continue + var slots: Array = [{"owner": "", "value": target.amount.get(id, 0.0)}] + for owner in target.binds: + if target.binds[owner].has(id): + slots.append({"owner": owner, "value": target.binds[owner][id]}) + var assigned: float = 0.0 + for slot: Dictionary in slots: + slot.whole = floorf(slot.value) + slot.rest = slot.value - slot.whole + assigned += slot.whole + slots.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: + return a.rest > b.rest if a.rest != b.rest else a.owner < b.owner) + var left: float = roundf(physical[id]) - assigned + for slot: Dictionary in slots: + if left > 0.0: + slot.whole += 1.0 + left -= 1.0 + if slot.owner == "": + target.amount[id] = slot.whole + else: + target.binds[slot.owner][id] = slot.whole + + +func _refresh_groups(target: Dictionary) -> void: + var groups: Dictionary = {} + for id in entities: + var group: String = entities[id].group_as + if group != "": + groups[group] = groups.get(group, 0.0) + target.amount.get(id, 0.0) + for group in groups: + target.amount[group] = groups[group] + + +func _clamp_auto_resources(target: Dictionary) -> void: + for id in target.get("auto_resources", {}): + target.auto_resources[id] = minf(target.auto_resources[id], _available(target, id)) + + +# Each type collects production into consecutive lifespan_delta-wide creation windows. +# Its evolution window is the creation window shifted forward by one lifespan. +func _append_batch(target: Dictionary, id: String, amount: float, + created_from: float, created_to: float) -> void: + if amount <= 0.0 or not entities.has(id) or entities[id].lifespan <= 0.0: + return + if created_to < created_from: + return + 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: float = entities[id].lifespan + var batch_duration: float = entities[id].lifespan_delta + if batch_duration <= 0.0: + return + 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 + while true: + var window_index: float = floorf((cursor - origin) / batch_duration) + var window_start: float = origin + window_index * batch_duration + var window_end: float = origin + (window_index + 1.0) * batch_duration + 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: + if existing.get("created_from", -INF) == window_start: + 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) + batch.amount += part_amount + remaining_amount -= part_amount + if duration == 0.0 or part_end >= created_to: + break + cursor = window_end + batches.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: + return a.created_from < b.created_from) + + +# Move the left boundary after each partial withdrawal, keeping a uniform density. +func _withdraw_batches(target: Dictionary, id: String, to_year: float) -> Array: + var expired: Array = [] + var remaining: Array = [] + for batch: Dictionary in target.batches.get(id, []): + var start: float = maxf(batch.start_year, batch.get("processed_until", batch.start_year)) + var end: float = batch.end_year + var amount: float = batch.amount + if amount <= 0.0: + continue + if to_year < start or (to_year == start and end > start): + remaining.append(batch) + continue + var consumed: float = amount + var until: float = minf(to_year, end) + if to_year < end: + var carry: float = batch.get("withdraw_remainder", 0.0) + var expected: float = carry + (amount - carry) * (to_year - start) / (end - start) + consumed = floorf(expected) + batch.withdraw_remainder = expected - consumed + batch.amount = maxf(0.0, amount - consumed) + batch.processed_until = to_year + remaining.append(batch) + if consumed > 0.0: + expired.append({"amount": consumed, "from": start, "to": until}) + target.batches[id] = remaining + return expired + + +func _evolution_outputs(entity: OwniverseEntity) -> Dictionary: + if entity.output_or.is_empty(): + return entity.output_and.duplicate(true) + # Expected quantities keep evolution independent of frame rate and RNG order. + var chance: float = clampf(entity.output_or.get("chance", 0.0), 0.0, 1.0) + var outputs: Dictionary = {} + for branch in ["negative", "positive"]: + var weight: float = chance if branch == "positive" else 1.0 - chance + for term in str(entity.output_or.get(branch, "")).split("&"): + var parts: PackedStringArray = term.split(":") + var id: String = parts[0] + var amount: float = float(parts[1]) if parts.size() == 2 else 1.0 + if id != "" and weight > 0.0: + outputs[id] = outputs.get(id, 0.0) + amount * weight + return outputs + + +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): + pass + + +func _evolve_batch_generation(target: Dictionary, to_year: float) -> bool: + var quantities: Dictionary = _physical_amounts(target) + var expired: Dictionary = {} + # 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) + var has_expired: bool = false + for parts: Array in expired.values(): + if not parts.is_empty(): + has_expired = true + break + if not has_expired: + return false + var fractions: Dictionary = {} + var outputs: Dictionary = {} + for id in expired: + var total: float = 0.0 + for part: Dictionary in expired[id]: + total += part.amount + fractions[id] = clampf(total / quantities[id], 0.0, 1.0) if quantities.get(id, 0.0) > 0.0 else 0.0 + outputs[id] = _evolution_outputs(entities[id]) + + # Use a frozen binding table so simultaneous transitions cannot cascade by type order. + var bindings: Dictionary = target.binds.duplicate(true) + var incoming: Dictionary = {} + for owner in bindings: + var owner_survival: float = 1.0 - fractions.get(owner, 0.0) + target.binds[owner] = {} + for id in bindings[owner]: + var held: float = bindings[owner][id] + var released: float = held * (1.0 - owner_survival) + target.amount[id] = target.amount.get(id, 0.0) + released + var fraction: float = fractions.get(id, 0.0) + var remaining: float = held * owner_survival * (1.0 - fraction) + target.binds[owner][id] = target.binds[owner].get(id, 0.0) + remaining + for successor in outputs.get(id, {}): + var transferred: float = held * owner_survival * fraction * maxf(0.0, outputs[id][successor]) + if not incoming.has(owner): + incoming[owner] = {} + incoming[owner][successor] = incoming[owner].get(successor, 0.0) + transferred + + for id in fractions: + target.amount[id] = maxf(0.0, target.amount.get(id, 0.0) * (1.0 - fractions[id])) + + var new_owners: Dictionary = {} + for id in expired: + for part: Dictionary in expired[id]: + for successor in outputs[id]: + var amount: float = part.amount * maxf(0.0, outputs[id][successor]) + amount = _credit(target, successor, amount) + _append_batch(target, successor, amount, part.from, part.to) + new_owners[successor] = new_owners.get(successor, 0.0) + amount + for owner in incoming: + for id in incoming[owner]: + var transferred: float = minf(incoming[owner][id], target.amount.get(id, 0.0)) + target.binds[owner][id] = target.binds[owner].get(id, 0.0) + transferred + target.amount[id] = maxf(0.0, target.amount.get(id, 0.0) - transferred) + _bind_new_owners(target, new_owners) + _round_object_allocations(target) + return true + + +func _bind_new_owners(target: Dictionary, amounts: Dictionary) -> void: + var requests: Dictionary = {} + for owner in amounts: + if not entities.has(owner): + continue + for id in entities[owner].bind: + var quantity: float = amounts[owner] * maxf(0.0, entities[owner].bind[id]) + var weights: Dictionary = {} + for member in _members(id): + weights[member] = target.amount.get(member, 0.0) + weights = _normalize_distribution(weights) + for member in weights: + if not requests.has(member): + requests[member] = {} + requests[member][owner] = requests[member].get(owner, 0.0) + quantity * weights[member] + for id in requests: + var requested: float = 0.0 + for quantity in requests[id].values(): + requested += quantity + var available: float = _available(target, id) + var factor: float = minf(1.0, available / requested) if requested > 0.0 else 0.0 + for owner in requests[id]: + var quantity: float = requests[id][owner] * factor + if not target.binds.has(owner): + target.binds[owner] = {} + target.binds[owner][id] = target.binds[owner].get(id, 0.0) + quantity + target.amount[id] = maxf(0.0, target.amount.get(id, 0.0) - quantity) + + +# Integrate survival over the period, including partial batch expiry. +func _object_years(source: Dictionary, id: String, quantity: float, + from_year: float, to_year: float) -> float: + var result: float = quantity * (to_year - from_year) + for batch: Dictionary in source.batches.get(id, []): + var start: float = maxf(batch.start_year, batch.get("processed_until", batch.start_year)) + var end: float = batch.end_year + result -= batch.amount * (_expired_years(to_year, start, end) - _expired_years(from_year, start, end)) + return maxf(0.0, result) + + +func _expired_years(year: float, start: float, end: float) -> float: + if year <= start: + return 0.0 + if end <= start: + return year - start + if year >= end: + return year - end + (end - start) * 0.5 + return (year - start) * (year - start) / (2.0 * (end - start)) + + +# Largest-remainder allocation: whole float values, bounded by each request. +func _allocate_whole(requests: Dictionary, limit: float) -> Dictionary: + var total: float = 0.0 + for amount in requests.values(): + total += floorf(maxf(0.0, amount)) + var available: float = floorf(minf(maxf(0.0, limit), total)) + var result: Dictionary = {} + var order: Array = [] + var assigned: float = 0.0 + for id in requests: + var expected: float = floorf(maxf(0.0, requests[id])) * available / total if total > 0.0 else 0.0 + result[id] = floorf(expected) + assigned += result[id] + order.append({"id": id, "rest": expected - result[id]}) + order.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: + return a.rest > b.rest if a.rest != b.rest else a.id < b.id) + for item: Dictionary in order: + if assigned >= available: + break + if result[item.id] < floorf(requests[item.id]): + result[item.id] += 1.0 + assigned += 1.0 + return result + + +# The entity currently exposes type/amount fields; life_product can contain +# several resources, each specified as total lifetime output. +func _yearly_products(entity: OwniverseEntity) -> Dictionary: + var products: Dictionary = {} + if not entity.life_product.is_empty() and entity.lifespan > 0.0: + for resource in entity.life_product: + products[resource] = maxf(0.0, entity.life_product[resource]) / entity.lifespan + elif entity.product_per_year_type != "": + products[entity.product_per_year_type] = maxf(0.0, entity.product_per_year_amount) + return products + + +func _produce_for_period(previous: Dictionary, target: Dictionary, quantities: Dictionary, + distribution: Dictionary, from_year: float, to_year: float) -> void: + var basic: Dictionary = {} + var automatic: Dictionary = {} + for id in quantities: + var entity: OwniverseEntity = entities[id] + var object_years: float = _object_years(previous, id, quantities[id], from_year, to_year) + var auto_share: float = clampf(entity.auto, 0.0, 1.0) + var products: Dictionary = _yearly_products(entity) + for product in products: + var amount: float = object_years * products[product] + basic[product] = basic.get(product, 0.0) + amount * (1.0 - auto_share) + automatic[product] = automatic.get(product, 0.0) + amount * auto_share + # Basic and automatic output each retain their unproduced remainder. + if not target.has("resource_remainders"): + target.resource_remainders = {"basic": {}, "automatic": {}} + var pending: Dictionary = target.resource_remainders + var requests: Dictionary = {} + for id in basic: + pending.basic[id] = pending.basic.get(id, 0.0) + basic[id] + pending.automatic[id] = pending.automatic.get(id, 0.0) + automatic[id] + var totals: Dictionary = {} + for channel in ["basic", "automatic"]: + for id in pending[channel]: + totals[id] = maxf(0.0, pending.basic.get(id, 0.0) + pending.automatic.get(id, 0.0)) + for id in totals: + if id == "S" or id == "H": + requests[id] = floorf(totals[id]) + var funded: Dictionary = _allocate_whole(requests, floorf(maxf(0.0, previous.energy))) + automatic = {} + target.energy = previous.energy + for id in totals: + var amount: float = floorf(totals[id]) + if id == "S" or id == "H": + amount = funded.get(id, 0.0) + target.energy -= amount + # Split the actual whole output, retaining the allocation error for next time. + var auto_ratio: float = clampf(pending.automatic.get(id, 0.0) / totals[id], 0.0, 1.0) if totals[id] > 0.0 else 0.0 + var auto_amount: float = floorf(amount * auto_ratio) + pending.automatic[id] = pending.automatic.get(id, 0.0) - auto_amount + pending.basic[id] = pending.basic.get(id, 0.0) - (amount - auto_amount) + automatic[id] = auto_amount + amount = _credit(target, id, amount) + _append_batch(target, id, amount, from_year, to_year) + + # Keep unused automatic resources across cycles; they are part of free amounts. + var budget: Dictionary = previous.get("auto_resources", {}).duplicate(true) + for id in automatic: + budget[id] = budget.get(id, 0.0) + automatic[id] + for id in budget: + budget[id] = minf(maxf(0.0, budget[id]), _available(target, id)) + target.auto_resources = budget + + # One common multiplier preserves the initial count ratios, even under scarcity. + var candidates: Dictionary = {} + for id in distribution: + var entity: OwniverseEntity = entities[id] + if entity.group_as != "Star": + continue + var has_cost: bool = false + for resource in entity.cost: + if entity.cost[resource] > 0.0: + has_cost = true + if has_cost: + candidates[id] = distribution[id] + candidates = _normalize_distribution(candidates) + if candidates.is_empty(): + return + var costs: Dictionary = {} + var needs: Dictionary = {} + for id in candidates: + if _auto_capacity(target, entities[id], budget) <= 0.0: + continue + for resource in entities[id].cost: + var cost: float = maxf(0.0, entities[id].cost[resource]) * candidates[id] + costs[resource] = costs.get(resource, 0.0) + cost + needs[resource] = needs.get(resource, 0.0) + cost + for resource in entities[id].bind: + needs[resource] = needs.get(resource, 0.0) + maxf(0.0, entities[id].bind[resource]) * candidates[id] + var scale: float = INF + for resource in costs: + if costs[resource] > 0.0: + scale = minf(scale, budget.get(resource, 0.0) / costs[resource]) + for resource in needs: + if needs[resource] > 0.0: + scale = minf(scale, _available(target, resource) / needs[resource]) + var concrete_needs: Dictionary = _expand_requirements(target, needs) + for resource in concrete_needs: + if concrete_needs[resource] > 0.0: + scale = minf(scale, _available(target, resource) / concrete_needs[resource]) + if not is_finite(scale) or scale <= 0.0: + return + # Rare types are served first; signed count deficits compensate later cycles. + if not target.has("auto_star_deficits"): + target.auto_star_deficits = {} + var deficits: Dictionary = target.auto_star_deficits + var order: Array = candidates.keys() + order.sort_custom(func(a: String, b: String) -> bool: + return candidates[a] < candidates[b] if candidates[a] != candidates[b] else a < b) + var created: Dictionary = {} + var total_created: float = 0.0 + for id in order: + var entity: OwniverseEntity = entities[id] + var step: float = _production_step(entity) + if not is_finite(step): + continue + var quota: float = maxf(0.0, candidates[id] * scale + deficits.get(id, 0.0)) + var desired: float = floorf(quota / step) * step + var actual: float = minf(desired, _auto_capacity(target, entity, budget)) + if actual <= 0.0: + continue + for resource in entity.cost: + var cost: float = roundf(maxf(0.0, entity.cost[resource]) * actual) + budget[resource] = maxf(0.0, budget.get(resource, 0.0) - cost) + var before: Dictionary = _physical_amounts(target) + var taken: Dictionary = _take_available(target, resource, cost) + for consumed_id in taken: + var factor: float = 1.0 - taken[consumed_id] / before[consumed_id] if before.get(consumed_id, 0.0) > 0.0 else 0.0 + _scale_batches(target, consumed_id, factor) + _bind_new_owners(target, {id: actual}) + _round_object_allocations(target) + created[id] = actual + total_created += actual + for id in candidates: + # Credit only actual production, so retained resources aren't counted twice. + deficits[id] = deficits.get(id, 0.0) + candidates[id] * total_created - created.get(id, 0.0) + for id in created: + _credit(target, id, created[id]) + _append_batch(target, id, created[id], from_year, to_year) + + +func _auto_capacity(source: Dictionary, entity: OwniverseEntity, budget: Dictionary) -> float: + var capacity: float = INF + var needs: Dictionary = {} + for resource in entity.cost: + var cost: float = maxf(0.0, entity.cost[resource]) + needs[resource] = cost + if cost > 0.0: + capacity = minf(capacity, floorf(budget.get(resource, 0.0) / cost)) + for resource in entity.bind: + needs[resource] = needs.get(resource, 0.0) + maxf(0.0, entity.bind[resource]) + for resource in needs: + if needs[resource] > 0.0: + capacity = minf(capacity, floorf(_available(source, resource) / needs[resource])) + var concrete: Dictionary = _expand_requirements(source, needs) + for resource in concrete: + if concrete[resource] > 0.0: + capacity = minf(capacity, floorf(_available(source, resource) / concrete[resource])) + var step: float = _production_step(entity) + return floorf(capacity / step) * step if is_finite(capacity) and is_finite(step) else 0.0 # ---------- @@ -96,7 +680,7 @@ func produce(item_id: String, desired_amount: float) -> float: if desired_amount <= 0: return 0.0 - var max_producible = calculate_max_producible(entity, desired_amount) + var max_producible = floorf(calculate_max_producible(entity, desired_amount)) if max_producible <= 0: return 0.0 @@ -106,6 +690,7 @@ func produce(item_id: String, desired_amount: float) -> float: # Add produced entities _process_enitity(item_id, max_producible) + add_entity_to_batch(item_id, max_producible, state.time.datetime) return max_producible @@ -119,14 +704,13 @@ func produce_hs(entity_id: String, value: float) -> float: return 0.0 var to_produce: float = floorf(value) - to_produce = min(to_produce, state.energy) + to_produce = minf(to_produce, floorf(state.energy)) if to_produce <= 0: return 0.0 state.energy -= to_produce _process_enitity(entity_id, to_produce) - state.stats.total_amount[entity_id] = state.stats.total_amount.get(entity_id, 0.0) + to_produce return to_produce @@ -135,58 +719,76 @@ func produce_hs(entity_id: String, value: float) -> float: ## @param entity - entity to produce ## @param desired_amount - desired quantity ## @return maximum producible amount considering all constraints +# Find the smallest whole batch whose cost is whole for every resource. +# Decimal costs are represented as integer fractions; combine their denominators. +func _production_step(entity: OwniverseEntity) -> float: + var step: float = 1.0 + for value in entity.cost.values(): + var cost: float = maxf(0.0, value) + if cost == 0.0: + continue + var decimal: String = String.num(cost, 12) + var point: int = decimal.find(".") + var digits: int = decimal.length() - point - 1 if point >= 0 else 0 + var denominator: float = pow(10.0, digits) + var numerator: float = roundf(cost * denominator) + if numerator == 0.0: + return INF + denominator /= _whole_gcd(numerator, denominator) + step = step / _whole_gcd(step, denominator) * denominator + return step + + +func _whole_gcd(a: float, b: float) -> float: + while b > 0.0: + var remainder: float = fmod(a, b) + a = b + b = remainder + return a + + func calculate_max_producible(entity: OwniverseEntity, desired_amount: float = 1.0E150) -> float: - var max_producible: float = desired_amount - - # Check cost constraints - resources consumed during production - for cost_item in entity.cost: - if entity.cost[cost_item] == 0: - continue - var available = state.amount.get(cost_item, 0.0) - var max_from_cost = floorf(available / entity.cost[cost_item]) - max_producible = min(max_producible, max_from_cost) - - # Check bind constraints - resources locked by produced entity - for bind_item in entity.bind: - if entity.bind[bind_item] == 0: - continue - var available = state.amount.get(bind_item, 0.0) - var max_from_bind = floorf(available / entity.bind[bind_item]) - max_producible = min(max_producible, max_from_bind) - - return max_producible + var requirements: Dictionary = {} + for resource in entity.cost: + requirements[resource] = maxf(0.0, entity.cost[resource]) + for resource in entity.bind: + requirements[resource] = requirements.get(resource, 0.0) + maxf(0.0, entity.bind[resource]) + var available: float = maxf(0.0, desired_amount) + for resource in requirements: + if requirements[resource] > 0.0: + available = minf(available, floorf(_available(state, resource) / requirements[resource])) + var concrete: Dictionary = _expand_requirements(state, requirements) + for resource in concrete: + if concrete[resource] > 0.0: + available = minf(available, floorf(_available(state, resource) / concrete[resource])) + var step: float = _production_step(entity) + return floorf(available / step) * step if is_finite(step) else 0.0 -## Applies cost (consumes resources) -## @param entity - entity being produced -## @param amount - amount to produce +## Consume resources without erasing cumulative production statistics. func apply_costs(entity: OwniverseEntity, amount: float) -> void: - for cost_item in entity.cost: - if entity.cost[cost_item] == 0: - continue - var cost_amount = amount * entity.cost[cost_item] - _process_enitity(cost_item, -cost_amount) + var costs: Dictionary = {} + for resource in entity.cost: + costs[resource] = roundf(maxf(0.0, entity.cost[resource]) * amount) + for resource in costs: + var quantities: Dictionary = _physical_amounts(state) + var taken: Dictionary = _take_available(state, resource, costs[resource]) + for id in taken: + var factor: float = 1.0 - taken[id] / quantities[id] if quantities.get(id, 0.0) > 0.0 else 0.0 + _scale_batches(state, id, factor) + items_to_update[id] = state.amount[id] + _refresh_groups(state) + _clamp_auto_resources(state) -## Applies bind (locks resources) -## @param item_id - id of entity being produced -## @param entity - entity being produced -## @param amount - amount to produce -func apply_binds(item_id: String, entity: OwniverseEntity, amount: float) -> void: - for bind_item in entity.bind: - if entity.bind[bind_item] == 0: - continue - var bind_amount = amount * entity.bind[bind_item] - - # Initialize binds dictionary for this entity if needed - if not state.binds.has(item_id): - state.binds[item_id] = {} - - # Record bound amount - state.binds[item_id][bind_item] = bind_amount - - # Decrease available amount (lock it) - _process_enitity(bind_item, -bind_amount) +## Keep concrete bound types, so their ages and transitions remain traceable. +func apply_binds(item_id: String, _entity: OwniverseEntity, amount: float) -> void: + _bind_new_owners(state, {item_id: amount}) + _round_object_allocations(state) + _refresh_groups(state) + _clamp_auto_resources(state) + for id in state.amount: + items_to_update[id] = state.amount[id] ## Computes a normalized distribution of dictionary values (sum equals 1.0) @@ -196,7 +798,7 @@ func _normalize_distribution(values: Dictionary) -> Dictionary: var normalized_distribution: Dictionary = {} var distribution_total: float = 0 for id in values: - if values[id] != 0: + if values[id] > 0: distribution_total += values[id] normalized_distribution[id] = values[id] for id in normalized_distribution: @@ -207,71 +809,26 @@ func _normalize_distribution(values: Dictionary) -> Dictionary: #region Batches -## Consumes entities from batches based on current year -## Iterates through batches and withdraws amount based on time period: -## - If current_year >= end_year: consume entire batch -## - If current_year in [start_year, end_year): consume proportional amount -## @param entity - entity to consume from -## @param current_year - current year in universe -## @return total amount consumed from all applicable batches +## Withdraws only the previously unprocessed part of each evolution interval. func consume_from_batches(entity: OwniverseEntity, current_year: float) -> float: - var batches = state.batches[entity.id] - var total_consumed: float = 0.0 - - # Iterate backwards to safely remove completed batches - state.amount[entity.id] = 0 - for i in range(batches.size() - 1, -1, -1): - var batch = batches[i] - - # Skip batches that haven't started yet - if current_year < batch["start_year"]: - state.amount[entity.id] += batch["amount"] - continue - - # If year is past batch end, consume entire batch - if current_year >= batch["end_year"]: - total_consumed += batch["amount"] - batches.remove_at(i) - else: - # Consume proportional amount based on elapsed time - var batch_duration = batch["end_year"] - batch["start_year"] - var time_elapsed = current_year - batch["start_year"] - var proportion = time_elapsed / batch_duration - var consumed_amount = batch["amount"] * proportion - - total_consumed += consumed_amount - batch["amount"] -= consumed_amount - state.amount[entity.id] += batch["amount"] - - return total_consumed + var total: float = 0.0 + var physical: Dictionary = _physical_amounts(state) + for part: Dictionary in _withdraw_batches(state, entity.id, current_year): + total += part.amount + var fraction: float = clampf(total / physical[entity.id], 0.0, 1.0) if physical.get(entity.id, 0.0) > 0.0 else 0.0 + state.amount[entity.id] = state.amount.get(entity.id, 0.0) * (1.0 - fraction) + for owner in state.binds: + if state.binds[owner].has(entity.id): + state.binds[owner][entity.id] *= 1.0 - fraction + _round_object_allocations(state) + _refresh_groups(state) + items_to_update[entity.id] = state.amount[entity.id] + return total -## Adds entity amount to appropriate batch based on current year -## state.batches[entity_id] contains list of batches with intervals and amounts -## Each batch: {start_year, end_year, amount} -## @param entity_id - id of entity to add -## @param amount - quantity of entity being added -## @param current_year - current year in universe +## Add manually created objects to the lifespan_delta window containing current_year. func add_entity_to_batch(entity_id: String, amount: float, current_year: float) -> void: - var batches = state.batches[entity_id] - - # Check if we can add to last batch - var last_batch: Dictionary - - if batches.size() > 0: - last_batch = batches[-1] - - if batches.size() == 0 or current_year >= last_batch["end_year"]: - # Create new batch based on current_year - last_batch = { - "start_year": current_year, - "end_year": current_year + entities[entity_id].lifespan_delta, - "amount": amount - } - batches.append(last_batch) - else: - # Add to existing batch - last_batch["amount"] += amount + _append_batch(state, entity_id, amount, current_year, current_year) #endregion @@ -318,7 +875,7 @@ func _process_enitity(entity_id: String, amount: float) -> void: # Update state amount state.amount[entity_id] = state.amount.get(entity_id, 0.0) + amount - state.stats.total_amount[entity_id] += amount + state.stats.total_amount[entity_id] = state.stats.total_amount.get(entity_id, 0.0) + maxf(0.0, amount) # Update items to display items_to_update[entity_id] = state.amount[entity_id] @@ -328,7 +885,7 @@ func _process_enitity(entity_id: String, amount: float) -> void: if entity.group_as != "": state.amount[entity.group_as] = state.amount.get(entity.group_as, 0.0) + amount items_to_update[entity.group_as] = state.amount[entity.group_as] - state.stats.total_amount[entity.group_as] += amount + state.stats.total_amount[entity.group_as] = state.stats.total_amount.get(entity.group_as, 0.0) + maxf(0.0, amount) diff --git a/world/owniverse/state.gd b/world/owniverse/state.gd index 5f46175..63e9e50 100644 --- a/world/owniverse/state.gd +++ b/world/owniverse/state.gd @@ -71,9 +71,10 @@ func add(entity: OwniverseEntity, year: float, amount: float): } _batch_array.append(_batch) - print('---------') - print(batches) - + #print('---------') + #for id in batches: + #print(id) + # func process(from_year: float, to_year: float) -> void: # производим