Skip to content

Conversation

@truthixify
Copy link
Contributor

@truthixify truthixify commented Sep 8, 2025

Closes #150

Summary by CodeRabbit

  • New Features
    • Dynamic upgrade costs that respond to market conditions and item rarity.
    • Success rates scale with gear and player level for upgrade attempts.
    • Market activity is tracked and influences upgrade behavior over time.
    • Upgrade events show exact materials consumed per attempt.
    • Combat rebalance: damage now uses a gear-centric model (rarity, upgrade level, type, player level), unified melee handling, legacy XP bonus removed, and diminishing returns at high levels.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 8, 2025

Warning

Rate limit exceeded

@truthixify has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 2 minutes and 46 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between d7fe6d6 and b072819.

📒 Files selected for processing (2)
  • src/systems/gear.cairo (6 hunks)
  • src/systems/player.cairo (5 hunks)

Walkthrough

Adds ItemRarity and two singleton dojo models (MarketConditions, MarketActivity); refactors gear upgrades to compute dynamic costs and success rates using rarity and market state while tracking activity; converts player damage to a gear‑centric, level/rarity-aware calculation with new helpers; fixes Option clone semantics.

Changes

Cohort / File(s) Summary
Models & clones
src/models/gear.cairo
Added pub enum ItemRarity (Common, Uncommon, Rare, Epic, Legendary) and two dojo singleton models: MarketConditions and MarketActivity (derive Drop, Copy, Serde). Replaced placeholder clones with proper clones for Option<UpgradeCost>, Option<UpgradeInfo>, Option<GearStatsCalculated>, and Option<Array<(u256,u256)>>.
Gear upgrade system
src/systems/gear.cairo
Replaced static upgrade-cost/success logic with market-aware flows: read MarketConditions, compute calculate_dynamic_upgrade_cost(...), compute calculate_upgrade_success_rate(...) (uses ItemRarity), validate & consume dynamic materials, increment MarketActivity, emit updated events, and added helpers: get_item_rarity, update_market_conditions, get_recent_market_activity, world_default. Exports/imports updated to include new public types.
Player damage system
src/systems/player.cairo
Reworked damage computation to use gear-centric helpers: calculate_balanced_damage, calculate_gear_damage, get_item_rarity, get_base_damage_for_type, calculate_level_damage, apply_diminishing_returns, calculate_xp_requirement, calculate_level_from_xp. Imports expanded to use GearType, GearLevelStats, ItemRarity, and parse_id. Replaced hardcoded scaling with level/rarity/gear-aware formulas.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor P as Player
    participant G as GearSystem (contract)
    participant W as WorldStorage/Models
    participant M as MarketActivity/Conditions

    P->>G: upgrade_gear(gear, materials)
    G->>W: read MarketConditions (id=0)
    G->>G: get_item_rarity(gear.asset_id)
    G->>G: calculate_dynamic_upgrade_cost(gear, MarketConditions)
    G->>G: calculate_upgrade_success_rate(gear, player.level)
    G->>G: validate materials vs dynamic cost
    alt success
        G->>G: pseudo_random < success_rate
        G->>W: write upgraded gear
        G->>W: write MarketActivity (increment)
        G-->>P: emit UpgradeSuccess(materials_consumed = dynamic_cost)
    else failure
        G->>W: write MarketActivity (increment)
        G-->>P: emit UpgradeFailed(materials_consumed = dynamic_cost)
    end
    opt periodic
        G->>G: update_market_conditions()
        G->>W: write MarketConditions
    end
Loading
sequenceDiagram
    autonumber
    actor P as Player
    participant PS as PlayerSystem (contract)
    participant G as Gear helpers/data

    P->>PS: perform_attack()
    PS->>PS: calculate_level_damage(player.level)
    PS->>G: calculate_gear_damage(gear)
    G->>G: get_item_rarity(gear.asset_id)
    G->>G: get_base_damage_for_type(parse_id(gear.asset_id).type)
    PS->>PS: apply faction multiplier & diminishing returns
    PS-->>P: final_damage
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • KevinMB0220

Pre-merge checks (2 passed, 3 warnings)

❌ Failed Checks (3 warnings)
Check Name Status Explanation Resolution
Linked Issues Check ⚠️ Warning While the pull request implements objectives for balanced damage, item rarity, dynamic upgrade costs, success rates, progression formulas, and market controls, it omits the recommended balance testing and reporting functions (e.g., test_game_balance and BalanceReport), leaving one key objective unmet. Add the missing balance testing and reporting functions to measure and report on average damage, upgrade success rates, economic health, and progression speed as outlined in the linked issue.
Out of Scope Changes Check ⚠️ Warning The pull request includes refactoring of clone implementations for various Option-wrapped types in models/gear.cairo, which is unrelated to the game balance and economy design objectives defined in issue #150. Consider removing or relocating the clone-refactoring changes into a separate pull request to keep this PR focused on the balance and economy features.
Description Check ⚠️ Warning The current description only includes “Closes #150” and lacks any contextual details, a summary of changes, or rationale, making it difficult for reviewers to quickly understand what the PR delivers. Please expand the description to include a concise summary of the changes, their motivation, and how they address the linked issue, and consider adding a pull request template to the repository to ensure comprehensive descriptions in the future.
✅ Passed Checks (2 passed)
Check Name Status Explanation
Title Check ✅ Passed The title succinctly captures the primary change by referencing the addition of game balance and economic design, which aligns directly with the pull request’s main objectives and clearly communicates the high-level intent.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.

Poem

In carrot fields where markets hum,
I hop through rarities overcome.
Costs that stretch and chances bend,
I nibble upgrades, counts ascend.
A rabbit cheers — balance will mend. 🥕🐇

✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/systems/player.cairo (1)

531-560: Weapon damage path ignores level scaling and diminishing returns.

Melee uses calculate_balanced_damage; weapons do not, diverging from the stated balance goals.

Apply:

-        fn calculate_weapon_damage(
-            self: @ContractState, player: Player, items: Span<u256>, faction_stats: FactionStats,
-        ) -> u256 {
-            let world = self.world_default();
-            let mut total_damage = 0;
-            let mut item_index = 0;
-            loop {
-                if item_index >= items.len() {
-                    break;
-                }
-                let item_id = *items.at(item_index);
-                let item: Gear = world.read_model(item_id);
-                if !self.can_deal_damage(item.clone()) || !player.is_available(item.id) {
-                    item_index += 1;
-                    continue;
-                }
-                let weapon_damage = self.calculate_gear_damage(item);
-                total_damage += weapon_damage;
-                item_index += 1;
-            };
-            let faction_damage = (total_damage * faction_stats.damage_multiplier) / 100;
-            let rank_multiplier = 100 + (player.rank.into() * 5);
-            let final_damage = (faction_damage * rank_multiplier) / 100;
-            final_damage
-        }
+        fn calculate_weapon_damage(
+            self: @ContractState, player: Player, items: Span<u256>, faction_stats: FactionStats,
+        ) -> u256 {
+            let world = self.world_default();
+            let mut total_gear_damage = 0;
+            let mut i = 0;
+            loop {
+                if i >= items.len() { break; }
+                let item_id = *items.at(i);
+                let item: Gear = world.read_model(item_id);
+                if self.can_deal_damage(item.clone()) && player.is_available(item.id) {
+                    total_gear_damage += self.calculate_gear_damage(item);
+                }
+                i += 1;
+            };
+            self.calculate_final_damage(player, total_gear_damage, faction_stats)
+        }

Add this helper (outside changed ranges):

fn calculate_final_damage(
    self: @ContractState,
    player: Player,
    total_gear_damage: u256,
    faction_stats: FactionStats,
) -> u256 {
    let level_damage = calculate_level_damage(player.level);
    let faction_damage = (level_damage + total_gear_damage) * faction_stats.damage_multiplier / 100;
    let rank_multiplier = 100 + (player.rank.into() * 5);
    let prelim = (faction_damage * rank_multiplier) / 100;
    apply_diminishing_returns(prelim, player.level)
}
src/systems/gear.cairo (1)

103-117: Guard against zero-amount transfers.

If any final_amount is 0, we still attempt ERC1155 transfers. Filter zeros when building costs (see suggested change in calculate_dynamic_upgrade_cost).

🧹 Nitpick comments (5)
src/models/gear.cairo (1)

454-462: Optional: encode rarity multipliers explicitly.

Consider attaching explicit numeric multipliers (e.g., as a fn rarity_multiplier(ItemRarity)->u256) to avoid duplicating match arms across modules.

src/systems/player.cairo (2)

848-863: Remove unused variable.

world is unused.

-            let world = self.world_default();

865-879: Duplicate rarity logic across modules.

get_item_rarity exists here and in GearActions; centralize in a helper to avoid drift.

src/systems/gear.cairo (2)

695-717: Clamp is good; also handle uninitialized (zero) multiplier explicitly.

If cost_multiplier remains 0 from an old state, force it to 100 before applying drift.

-            if recent_activity > target_activity * 120 / 100 {
+            if market.cost_multiplier == 0 {
+                market.cost_multiplier = 100;
+            } else if recent_activity > target_activity * 120 / 100 {
                 market.cost_multiplier = market.cost_multiplier * 105 / 100;

648-662: Duplicate rarity heuristic.

This function duplicates the same thresholds present in player module. Prefer a single helper.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ac2dcc2 and 45b69a2.

📒 Files selected for processing (3)
  • src/models/gear.cairo (1 hunks)
  • src/systems/gear.cairo (5 hunks)
  • src/systems/player.cairo (5 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build-and-test
🔇 Additional comments (1)
src/systems/gear.cairo (1)

132-132: Verify RNG threshold semantics
Please share the Cairo 1 source for origami_random::dice::Dice::roll() (or DiceTrait::roll) so we can confirm whether it returns values in [0, sides) or [1, sides] and adjust the comparison at src/systems/gear.cairo:132 accordingly.

Comment on lines 471 to 479
#[dojo::model]
#[derive(Drop, Copy, Serde)]
pub struct MarketActivity {
#[key]
pub id: u8, // Always 0 for singleton
pub activity_count: u256, // Number of upgrade attempts
pub last_reset_timestamp: u64
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Seed MarketActivity.last_reset_timestamp to avoid immediate resets.

On first read it’s 0, so get_recent_market_activity always resets. Seed it during init as shown in the previous diff.

🤖 Prompt for AI Agents
In src/models/gear.cairo around lines 471 to 479,
MarketActivity.last_reset_timestamp is left at 0 on initialization causing
get_recent_market_activity to immediately treat it as expired; update the
initialization path (where MarketActivity is created/seeded) to set
last_reset_timestamp to the current block timestamp (or now) instead of 0 so the
first read does not trigger an immediate reset; ensure the init code obtains the
proper u64 timestamp and assigns it when inserting the singleton MarketActivity.

Comment on lines 119 to 123
// Increment market activity counter
let mut market_activity: MarketActivity = world.read_model(0);
market_activity.activity_count += 1;
world.write_model(@market_activity);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Close the feedback loop: update market after each attempt.

Incrementing activity without recalculating conditions delays price adaptation.

             market_activity.activity_count += 1;
             world.write_model(@market_activity);
+            self.update_market_conditions();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Increment market activity counter
let mut market_activity: MarketActivity = world.read_model(0);
market_activity.activity_count += 1;
world.write_model(@market_activity);
// Increment market activity counter
let mut market_activity: MarketActivity = world.read_model(0);
market_activity.activity_count += 1;
world.write_model(@market_activity);
self.update_market_conditions();
🤖 Prompt for AI Agents
In src/systems/gear.cairo around lines 119 to 123, the code only increments
market_activity and writes it back, but doesn't recalculate or persist updated
market conditions after each attempt; read the current market state immediately
before incrementing to avoid staleness, run the market-recalculation/update
routine (e.g., call the existing update_market/update_market_conditions function
or recalc_prices using the new activity_count), and then write the fully updated
market model back to storage so price/condition changes take effect immediately
after each attempt.

Comment on lines 512 to 529
fn calculate_melee_damage(
self: @ContractState, player: Player, faction_stats: FactionStats,
) -> u256 {
// Base weapon damage from player stats
let base_damage = 10 + (player.level / 100); // Simple Level scaling

// Apply faction damage multiplier
let faction_damage = (base_damage * faction_stats.damage_multiplier) / 100;

// Factor in player rank/level
let rank_multiplier = 100 + (player.rank.into() * 5); // 5% per rank
let final_damage = (faction_damage * rank_multiplier) / 100;

final_damage
let default_gear = Gear { // Create a default gear for melee
id: 0,
item_type: 0x1.try_into().unwrap(),
asset_id: 0,
variation_ref: 0,
total_count: 1,
in_action: true,
upgrade_level: 0,
owner: player.id,
max_upgrade_level: 1,
min_xp_needed: 0,
spawned: true,
};
self.calculate_balanced_damage(player, default_gear, faction_stats)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Type mismatch: item_type expects felt252, but a GearType is provided.

This won’t type-check.

Apply:

-                item_type: 0x1.try_into().unwrap(),
+                item_type: GearType::Weapon.into(),

Also add the import near the other uses:

// add near other uses
use core::traits::Into;
🤖 Prompt for AI Agents
In src/systems/player.cairo around lines 512 to 529, the Gear literal sets
item_type using a GearType value which causes a type mismatch because item_type
expects felt252; change the assignment to convert the GearType into felt252
(e.g. replace the direct GearType literal with a conversion call like .into() on
the GearType value) and add the import use core::traits::Into; near the other
imports so the conversion compiles.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
src/models/gear.cairo (2)

463-470: Zero-cost hazard: MarketConditions.cost_multiplier defaults to 0 if the singleton isn’t seeded.

Unseeded models will serialize as zeros, making upgrade costs zero if the math multiplies by cost_multiplier. Seed a neutral multiplier in dojo_init and document the unit (bps vs 1e18 vs percent). This was flagged earlier; still critical to fix before merge.

Recommended: standardize on basis points (10_000 = 100%). Add shared constants here and seed accordingly in systems init.

Add shared constants (in this file):

// Place near other top-level items
const MARKET_SINGLETON_ID: u8 = 0;
const COST_MULTIPLIER_SCALE_BPS: u256 = 10_000; // 100.00%
const COST_MULTIPLIER_MIN_BPS: u256 = 5_000;    // 50.00%
const COST_MULTIPLIER_MAX_BPS: u256 = 20_000;   // 200.00%

Seed in src/systems/gear.cairo (dojo_init):

@@
 fn dojo_init(ref self: ContractState) {
     let mut world = self.world_default();
     self._assert_admin();
     self._initialize_gear_assets(ref world);
+    use starknet::get_block_timestamp;
+    use crate::models::gear::{
+        MarketConditions, MarketActivity, MARKET_SINGLETON_ID, COST_MULTIPLIER_SCALE_BPS
+    };
+    // Seed market models with sane defaults
+    world.write_model(@MarketConditions {
+        id: MARKET_SINGLETON_ID,
+        cost_multiplier: COST_MULTIPLIER_SCALE_BPS, // 1.00x
+    });
+    world.write_model(@MarketActivity {
+        id: MARKET_SINGLETON_ID,
+        activity_count: 0,
+        last_reset_timestamp: get_block_timestamp(),
+    });
 }

Verification helper:

#!/bin/bash
# 1) Confirm any seed exists
rg -nP -C2 'write_model\(\s*@MarketConditions|write_model\(\s*@MarketActivity'
# 2) Inspect cost math to infer unit (bps/percent/1e18)
rg -nP -C3 'cost_multiplier' src | sed -n '1,200p'

471-479: Seed last_reset_timestamp on MarketActivity to avoid immediate “first-read reset.”

Leaving it at 0 causes the reset logic to trigger instantly. Initialize with block timestamp during deploy/init (see previous comment’s diff).

🧹 Nitpick comments (2)
src/models/gear.cairo (2)

499-506: Nit: variable name typo in clone impl (“geat_stats”).

Spelling nit; doesn’t affect behavior but harms readability.

-            Option::Some(geat_stats) => Option::Some(geat_stats.clone()),
+            Option::Some(gear_stats) => Option::Some(gear_stats.clone()),

454-461: Derive Default & Introspect on ItemRarity and mark Common as default — add Introspect, Default, and #[default] on Common; stable Felt252 conversion impls remain optional since no existing code relies on ordinal values.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 45b69a2 and d7fe6d6.

📒 Files selected for processing (1)
  • src/models/gear.cairo (1 hunks)

@KevinMB0220 KevinMB0220 self-requested a review September 10, 2025 02:12
@KevinMB0220 KevinMB0220 merged commit a598d74 into SunsetLabs-Game:main Sep 10, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Missing Game Balance and Economy Design

2 participants