Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
210 changes: 210 additions & 0 deletions src/energy_manager.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
use crate::ship_nft::{DataKey as ShipDataKey, ShipNft};
use soroban_sdk::{contracterror, contracttype, symbol_short, Env};

// ─── Storage Keys ─────────────────────────────────────────────────────────

#[derive(Clone)]
#[contracttype]
pub enum EnergyKey {
/// Per-ship energy balance: `EnergyBalance(ship_id)`.
EnergyBalance(u64),
/// Global base recharge efficiency rate (u32, percentage).
BaseRechargeRate,
/// Per-ship blueprint-derived efficiency bonus: `EfficiencyBonus(ship_id)`.
EfficiencyBonus(u64),
}

// ─── Custom Errors ────────────────────────────────────────────────────────

#[contracterror]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[repr(u32)]
pub enum EnergyError {
InsufficientEnergy = 1,
ShipNotFound = 2,
InvalidAmount = 3,
Overflow = 4,
}

// ─── Constants ────────────────────────────────────────────────────────────

/// Default recharge efficiency: 50%.
const DEFAULT_RECHARGE_RATE: u32 = 50;

/// Maximum energy cap to prevent overflow.
const MAX_ENERGY: u32 = u32::MAX;

// ─── Internal Helpers ─────────────────────────────────────────────────────

/// Verify that a ship exists in persistent storage.
fn require_ship_exists(env: &Env, ship_id: u64) -> Result<(), EnergyError> {
let _ship: ShipNft = env
.storage()
.persistent()
.get(&ShipDataKey::Ship(ship_id))
.ok_or(EnergyError::ShipNotFound)?;
Ok(())
}

// ─── Public API (called by NebulaNomadContract in lib.rs) ─────────────────

/// Initialize energy for a ship with a given starting balance.
///
/// The ship must already exist (minted via `ship_nft`). Sets the initial
/// energy balance and emits an initialization event.
pub fn initialize_energy(env: &Env, ship_id: u64, initial_energy: u32) -> Result<(), EnergyError> {
require_ship_exists(env, ship_id)?;

env.storage()
.persistent()
.set(&EnergyKey::EnergyBalance(ship_id), &initial_energy);

env.events().publish(
(symbol_short!("energy"), symbol_short!("init")),
(ship_id, initial_energy),
);

Ok(())
}

/// Consume energy from a ship's balance for actions like scans and harvests.
///
/// Validates that the amount is non-zero and that the ship has sufficient
/// energy. Returns the remaining balance after deduction.
pub fn consume_energy(env: &Env, ship_id: u64, amount: u32) -> Result<u32, EnergyError> {
if amount == 0 {
return Err(EnergyError::InvalidAmount);
}

require_ship_exists(env, ship_id)?;

let balance: u32 = env
.storage()
.persistent()
.get(&EnergyKey::EnergyBalance(ship_id))
.unwrap_or(0);

if balance < amount {
return Err(EnergyError::InsufficientEnergy);
}

// Safe: underflow impossible due to the check above.
let new_balance = balance - amount;

env.storage()
.persistent()
.set(&EnergyKey::EnergyBalance(ship_id), &new_balance);

env.events().publish(
(symbol_short!("energy"), symbol_short!("consumed")),
(ship_id, amount, new_balance),
);

Ok(new_balance)
}

/// Convert resources into ship energy using the effective recharge rate.
///
/// The effective rate is `base_rate + per-ship bonus`, capped at 100%.
/// Energy gained is computed in i128 space to prevent intermediate overflow,
/// then clamped to u32::MAX via saturating_add.
pub fn recharge_energy(
env: &Env,
ship_id: u64,
resource_amount: i128,
) -> Result<u32, EnergyError> {
if resource_amount <= 0 {
return Err(EnergyError::InvalidAmount);
}

require_ship_exists(env, ship_id)?;

let base_rate: u32 = env
.storage()
.instance()
.get(&EnergyKey::BaseRechargeRate)
.unwrap_or(DEFAULT_RECHARGE_RATE);

let bonus: u32 = env
.storage()
.persistent()
.get(&EnergyKey::EfficiencyBonus(ship_id))
.unwrap_or(0);

// Cap effective rate at 100%.
let effective_rate: u32 = {
let sum = base_rate.saturating_add(bonus);
if sum > 100 { 100 } else { sum }
};

// Compute in i128 to avoid intermediate overflow.
let gained_i128 = resource_amount * (effective_rate as i128) / 100;

// Clamp to u32 range.
let energy_gained: u32 = if gained_i128 > MAX_ENERGY as i128 {
MAX_ENERGY
} else {
gained_i128 as u32
};

let balance: u32 = env
.storage()
.persistent()
.get(&EnergyKey::EnergyBalance(ship_id))
.unwrap_or(0);

let new_balance = balance.saturating_add(energy_gained);

env.storage()
.persistent()
.set(&EnergyKey::EnergyBalance(ship_id), &new_balance);

env.events().publish(
(symbol_short!("energy"), symbol_short!("rechargd")),
(ship_id, energy_gained, new_balance),
);

Ok(new_balance)
}

/// Read the current energy balance for a ship (view function).
pub fn get_energy(env: &Env, ship_id: u64) -> u32 {
env.storage()
.persistent()
.get(&EnergyKey::EnergyBalance(ship_id))
.unwrap_or(0)
}

/// Set the global base recharge efficiency rate (admin function).
pub fn set_base_recharge_rate(env: &Env, rate: u32) {
env.storage()
.instance()
.set(&EnergyKey::BaseRechargeRate, &rate);
}

/// Apply a blueprint-derived efficiency bonus to a specific ship.
///
/// This enables dynamic upgrade paths: craft a blueprint, then apply it
/// to boost a ship's recharge efficiency permanently.
pub fn apply_efficiency_bonus(env: &Env, ship_id: u64, bonus: u32) -> Result<(), EnergyError> {
require_ship_exists(env, ship_id)?;

env.storage()
.persistent()
.set(&EnergyKey::EfficiencyBonus(ship_id), &bonus);

env.events().publish(
(symbol_short!("energy"), symbol_short!("upgrade")),
(ship_id, bonus),
);

Ok(())
}

/// Read the current base recharge rate (view function).
pub fn get_recharge_rate(env: &Env) -> u32 {
env.storage()
.instance()
.get(&EnergyKey::BaseRechargeRate)
.unwrap_or(DEFAULT_RECHARGE_RATE)
}
2 changes: 1 addition & 1 deletion src/governance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ pub fn create_proposal(env: Env, creator: Address, description: String, param_ch
env.storage().instance().set(&symbol_short!("next_gid"), &(proposal_id + 1));

env.events().publish(
(symbol_short!("gov"), symbol_short!("proposal_created")),
(symbol_short!("gov"), symbol_short!("prop_new")),
(proposal_id, creator),
);

Expand Down
4 changes: 2 additions & 2 deletions src/indexer_callbacks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ pub fn register_indexer_callback(env: Env, caller: Address, callback_id: Symbol)
env.storage().persistent().set(&(symbol_short!("idx_cb"), callback_id.clone()), &callback);

env.events().publish(
(symbol_short!("indexer"), symbol_short!("registered")),
(symbol_short!("indexer"), symbol_short!("registrd")),
(callback_id,),
);

Expand All @@ -39,7 +39,7 @@ pub fn trigger_indexer_event(env: Env, event_type: Symbol, payload: BytesN<256>)
// Every call triggers an event that horizon indexers can filter and aggregate.

env.events().publish(
(symbol_short!("indexer_ev"), event_type),
(symbol_short!("idxer_ev"), event_type),
(payload,),
);

Expand Down
48 changes: 47 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#![no_std]

use soroban_sdk::{contract, contractimpl, Address, Bytes, BytesN, Env, Symbol, Vec};
use soroban_sdk::{contract, contractimpl, Address, Bytes, BytesN, Env, String, Symbol, Vec};

mod blueprint_factory;
mod nebula_explorer;
Expand All @@ -16,6 +16,7 @@ mod difficulty_scaler;
mod randomness_oracle;
mod treasure_vault;

mod energy_manager;
mod yield_farming;
mod governance;
mod theme_customizer;
Expand All @@ -35,6 +36,7 @@ pub use referral_system::{Referral, ReferralError};
pub use player_profile::{PlayerProfile, ProfileError, ProgressUpdate};
pub use session_manager::{Session, SessionError};
pub use ship_registry::Ship;
pub use energy_manager::EnergyError;

pub use dex_integration::{cancel_listing, harvest_and_list};
pub use difficulty_scaler::{
Expand Down Expand Up @@ -424,4 +426,48 @@ impl NebulaNomadContract {
) -> Result<(), indexer_callbacks::IndexerError> {
indexer_callbacks::trigger_indexer_event(env, event_type, payload)
}

// ─── Energy Management ───────────────────────────────────────────────────

/// Initialize energy balance for a ship with a starting value.
pub fn initialize_energy(
env: Env,
ship_id: u64,
initial_energy: u32,
) -> Result<(), EnergyError> {
energy_manager::initialize_energy(&env, ship_id, initial_energy)
}

/// Consume energy from a ship for scans, harvests, and other actions.
pub fn consume_energy(env: Env, ship_id: u64, amount: u32) -> Result<u32, EnergyError> {
energy_manager::consume_energy(&env, ship_id, amount)
}

/// Recharge a ship's energy by converting resources at the effective rate.
pub fn recharge_energy(
env: Env,
ship_id: u64,
resource_amount: i128,
) -> Result<u32, EnergyError> {
energy_manager::recharge_energy(&env, ship_id, resource_amount)
}

/// Read the current energy balance for a ship.
pub fn get_energy(env: Env, ship_id: u64) -> u32 {
energy_manager::get_energy(&env, ship_id)
}

/// Set the global base recharge efficiency rate.
pub fn set_base_recharge_rate(env: Env, rate: u32) {
energy_manager::set_base_recharge_rate(&env, rate)
}

/// Apply a blueprint-derived efficiency bonus to a ship's recharge rate.
pub fn apply_efficiency_bonus(
env: Env,
ship_id: u64,
bonus: u32,
) -> Result<(), EnergyError> {
energy_manager::apply_efficiency_bonus(&env, ship_id, bonus)
}
}
4 changes: 2 additions & 2 deletions src/theme_customizer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ pub fn generate_theme_preview(env: Env, theme_id: Symbol) -> Result<ThemePreview
s if s == symbol_short!("nebula2") => Ok(ThemePreview {
name: symbol_short!("Void"),
colors: Vec::from_array(&env, [symbol_short!("000000"), symbol_short!("444444")]),
particles: symbol_short!("DarkMatter"),
particles: symbol_short!("DrkMattr"),
}),
s if s == symbol_short!("nebula3") => Ok(ThemePreview {
name: symbol_short!("Nova"),
Expand All @@ -52,7 +52,7 @@ pub fn generate_theme_preview(env: Env, theme_id: Symbol) -> Result<ThemePreview
s if s == symbol_short!("nebula7") => Ok(ThemePreview {
name: symbol_short!("BlackHole"),
colors: Vec::from_array(&env, [symbol_short!("000000"), symbol_short!("111111")]),
particles: symbol_short!("Singularity"),
particles: symbol_short!("Singlrty"),
}),
s if s == symbol_short!("nebula8") => Ok(ThemePreview {
name: symbol_short!("Aurora"),
Expand Down
Loading
Loading