Skip to content

Commit

Permalink
signficantly improves combat system:
Browse files Browse the repository at this point in the history
- Closes #330:  damage bonus for weapon name prefixes
- Closes #346: elemental damage now -50%, 100%, or 150% bonus
- Refactors core damage calculation to separate each bonus for better clarity and auditing
- Updates test cases and fixes majority of previous failures
  • Loading branch information
loothero committed May 20, 2023
1 parent 904ca23 commit cbcc52a
Show file tree
Hide file tree
Showing 8 changed files with 517 additions and 269 deletions.
11 changes: 1 addition & 10 deletions contracts/loot/constants/combat.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,9 @@

from contracts.loot.constants.item import ItemMaterial, Material, ItemType, Type

// psuedo enum
// used for elemental bonuses
namespace WeaponEfficacy {
const Low = 0;
const Medium = 1;
const High = 2;
}

// Controls damage multiplier
// NOTE: @loothero I've increased by 1, if low is 0, then all damage will be 0
// not sure this is what we want?
namespace WeaponEfficiacyDamageMultiplier {
const Low = 1;
const Medium = 2;
const High = 3;
}
410 changes: 334 additions & 76 deletions contracts/loot/loot/stats/combat.cairo

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions contracts/loot/utils/constants.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,6 @@ const STARTING_GOLD = 20;
const VITALITY_HEALTH_BOOST = 20;
const SUFFIX_STAT_BOOST = 3;
const MAX_CRITICAL_HIT_CHANCE = 5; // this results in a 1/2 chance of critical hit

const ITEM_RANK_MAX = 6;
const MINIMUM_ATTACK_DAMGE = 3;
4 changes: 2 additions & 2 deletions tests/protostar/loot/adventurer/test_adventurer.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -402,8 +402,8 @@ func test_item_stat_boost{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_
assert dexterity_boosted_adventurer.Dexterity = 3;
assert intelligence_boosted_adventurer.Intelligence = 3;
assert wisdom_boosted_adventurer.Wisdom = 3;
// greatness of necklace is 20, plus it has a prefix and suffix
assert luck_boosted_adventurer.Luck = 23;
// luck scales evenly with greatness of necklace is 20
assert luck_boosted_adventurer.Luck = 20;

return ();
}
81 changes: 63 additions & 18 deletions tests/protostar/loot/beast/test_beast.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ from tests.protostar.loot.test_structs import (
TEST_DAMAGE_OVERKILL,
)

from contracts.loot.constants.item import ItemNamePrefixes, ItemNameSuffixes, ItemSuffixes

@external
func test_beast_rank{
syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr, bitwise_ptr: BitwiseBuiltin*
Expand Down Expand Up @@ -104,7 +106,7 @@ func test_cast{
}() {
alloc_locals;

let (beast: Beast) = TestUtils.create_beast(1, 0);
let (beast: Beast) = TestUtils.create_beast(1, 0, 0, 0);

let (_, beast_dynamic: BeastDynamic) = BeastLib.split_data(beast);

Expand All @@ -121,7 +123,7 @@ func test_deduct_health{
}() {
alloc_locals;

let (beast) = TestUtils.create_beast(1, 0);
let (beast) = TestUtils.create_beast(1, 0, 0, 0);

let (_, beast_dynamic: BeastDynamic) = BeastLib.split_data(beast);

Expand All @@ -142,7 +144,7 @@ func test_set_adventurer{
}() {
alloc_locals;

let (beast) = TestUtils.create_beast(1, 0);
let (beast) = TestUtils.create_beast(1, 0, 0, 0);

let (_, beast_dynamic: BeastDynamic) = BeastLib.split_data(beast);

Expand All @@ -158,7 +160,7 @@ func test_slain{
syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, bitwise_ptr: BitwiseBuiltin*, range_check_ptr
}() {
alloc_locals;
let (beast) = TestUtils.create_beast(1, 0);
let (beast) = TestUtils.create_beast(1, 0, 0, 0);

let (_, beast_dynamic: BeastDynamic) = BeastLib.split_data(beast);

Expand All @@ -179,14 +181,14 @@ func test_calculate_critical_damage{
// an input of 3 should deal max critical hit of 2x damage
let double_damage_rnd_multiplier = 3;
let (max_critical_damage) = CombatStats.calculate_critical_damage(
original_damage, critical_hit, double_damage_rnd_multiplier
original_damage, double_damage_rnd_multiplier
);
assert max_critical_damage = 40;

// an input of 0 should deal min critical hit of 0.25x damage
let minimum_damage_rnd_multiplier = 0;
let (min_critical_damage) = CombatStats.calculate_critical_damage(
original_damage, critical_hit, minimum_damage_rnd_multiplier
original_damage, minimum_damage_rnd_multiplier
);
assert min_critical_damage = 25;

Expand All @@ -197,7 +199,7 @@ func test_calculate_critical_damage{
func test_calculate_adventurer_level_boost{
syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr
}() {
let (adventurer_level_damage) = CombatStats.calculate_entity_level_boost(20, 1);
let (adventurer_level_damage) = CombatStats.get_entity_level_bonus(20, 1);

assert adventurer_level_damage = 20;
return ();
Expand All @@ -216,8 +218,8 @@ func test_calculate_damage_to_beast{

let (adventurer_state) = get_adventurer_state();

let (greatness_8_mace) = TestUtils.create_item(75, 8); // Greatness 8 Mace (Bludgeon) vs
let (xp_1_basilisk) = TestUtils.create_beast(4, 1); // Level 1 Basilisk (Magic)
let (greatness_8_mace) = TestUtils.create_item_with_names(75, 8, 0, 0, 0); // Greatness 8 Mace (Bludgeon) vs
let (xp_1_basilisk) = TestUtils.create_beast(4, 1, 0, 0); // Level 1 Basilisk (Magic)

// attack = 8 * (6-4) * 1 = 16
// defense = 1 * (6-4) = 2
Expand All @@ -230,8 +232,53 @@ func test_calculate_damage_to_beast{
// no critical hit
assert mace_vs_basilik = 14;

// TODO: Test attacking without weapon (melee)
// let (weapon) = TestUtils.create_zero_item(); // no weapon (melee attack)
// test name prefix1 match
let (greatness_8_mace_agony_bane) = TestUtils.create_item_with_names(
75, 8, ItemNamePrefixes.Agony, ItemNameSuffixes.Bane, ItemSuffixes.of_Power
); // Greatness 8 Mace (Bludgeon) vs
let (xp_1_basilisk_agony_song) = TestUtils.create_beast(
4, 1, ItemNamePrefixes.Agony, ItemNameSuffixes.Song
); // Level 1 Basilisk (Magic)

let (mace_vs_basilik_prefix1_match) = CombatStats.calculate_damage_to_beast(
xp_1_basilisk_agony_song, greatness_8_mace_agony_bane, adventurer_state, 1
);

// prefix1 match yields a 3.5x multplier (14 * 3.5 = 49) for the provided random number
assert mace_vs_basilik_prefix1_match = 49;

// test name prefix2 match
let (greatness_8_mace_blood_song) = TestUtils.create_item_with_names(
75, 8, ItemNamePrefixes.Blood, ItemNameSuffixes.Song, ItemSuffixes.of_Power
); // Greatness 8 Mace (Bludgeon) vs
let (xp_1_basilisk_death_song) = TestUtils.create_beast(
4, 1, ItemNamePrefixes.Death, ItemNameSuffixes.Song
); // Level 1 Basilisk (Magic)

let (mace_vs_basilik_prefix2_match) = CombatStats.calculate_damage_to_beast(
xp_1_basilisk_death_song, greatness_8_mace_blood_song, adventurer_state, 1
);

// prefix2 yields smaller bonus as it is more likely
assert mace_vs_basilik_prefix2_match = 16;

// test name prefix1 and prefix2 match
let (greatness_8_mace_hate_song) = TestUtils.create_item_with_names(
75, 8, ItemNamePrefixes.Hate, ItemNameSuffixes.Song, ItemSuffixes.of_Power
); // Greatness 8 Mace (Bludgeon) vs
let (xp_1_basilisk_hate_song) = TestUtils.create_beast(
4, 1, ItemNamePrefixes.Hate, ItemNameSuffixes.Song
); // Level 1 Basilisk (Magic)

let (mace_vs_basilik_prefix1_and_prefix2_match) = CombatStats.calculate_damage_to_beast(
xp_1_basilisk_hate_song, greatness_8_mace_hate_song, adventurer_state, 1
);

// prefix2 yields smaller bonus (more likely)
// in this case the code is expected to take the whole part of base damage (14), divide it by 4
// which gives a base damage boost of 4. In the case of RND 1, we apply minimum boost of 4
// original 14 + 4 = 18
assert mace_vs_basilik_prefix1_and_prefix2_match = 51;

return ();
}
Expand All @@ -247,7 +294,7 @@ func test_calculate_damage_from_beast{
}() {
alloc_locals;

let (beast) = TestUtils.create_beast(1, 2); // 2XP Pheonix vs
let (beast) = TestUtils.create_beast(1, 2, 0, 0); // 2XP Pheonix vs
let (armor) = TestUtils.create_item(50, 1); // Greatness 1 Hard Leather Armor
let no_beast_luck = 0;
let no_critical_damage_rnd = 1;
Expand All @@ -270,7 +317,7 @@ func test_calculate_damage_from_beast{
let (local critical_hit_damage) = CombatStats.calculate_damage_from_beast(
beast, armor, critical_damage_rnd, max_beast_luck
);
assert critical_hit_damage = 14;
assert critical_hit_damage = 15;

// test critical hit luck overflow
let overflow_beast_luck = 500;
Expand All @@ -294,7 +341,7 @@ func test_calculate_damage_from_beast_late_game{
}() {
alloc_locals;

let (beast) = TestUtils.create_beast(11, 20); // levl 20 giant (rank 1)
let (beast) = TestUtils.create_beast(11, 20, 0, 0); // levl 20 giant (rank 1)
let (cloth_armor) = TestUtils.create_item(18, 20); // lvl 20 silk robe (rank 2)

// no chance of critical damage with 0 luck
Expand Down Expand Up @@ -328,14 +375,12 @@ func test_calculate_damage_from_beast_late_game{

assert hide_damage = 60;

let (no_armor) = TestUtils.create_item_with_names(0, 0, 1, 1, 1); // no item
let (no_armor) = TestUtils.create_item_with_names(0, 0, 0, 0, 1); // no item
let critical_hit_rnd = 2;
let (no_armor__critical_damage) = CombatStats.calculate_damage_from_beast(
beast, no_armor, critical_hit_rnd, max_beast_luck
);

// 300 base damage * 1.75x critical hit
assert no_armor__critical_damage = 525;
assert no_armor__critical_damage = 561;

return ();
}
Expand Down
3 changes: 1 addition & 2 deletions tests/protostar/loot/beast/test_beast_logic.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,7 @@ func test_not_kill{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_p

assert updated_beast.Id = 2;
assert updated_beast.Level = 2;
// since we have overwritten adventurer (made level 2 and so removed wand) we will do no damage
assert updated_beast.Health = 10;
assert updated_beast.Health = 9;

let (updated_adventurer) = IAdventurer.get_adventurer_by_id(
adventurer_address, adventurer_token_id_1
Expand Down
62 changes: 14 additions & 48 deletions tests/protostar/loot/stats/test_combat.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -136,88 +136,54 @@ func test_calculate_damage_from_weapon{syscall_ptr: felt*, range_check_ptr}() {
let (holy_chestplate_vs_short_sword) = CombatStats.calculate_damage_from_weapon(
g3_short_sword, g18_holy_chestplate, adventurer_state, 1
);
assert holy_chestplate_vs_short_sword = 3;
assert holy_chestplate_vs_short_sword = 4;

// greatness 1 scimitar vs greatness 3 silk robe
// This tests the below zero base damage
// we should default to minimum damage setting (3)
// which will get multplied by max elemental modifier (3 at the time of this writing)
// yielding 9
let (g1_scimitar) = TestUtils.create_item(ItemIds.Scimitar, 1);
let (g3_silk_robe) = TestUtils.create_item(ItemIds.SilkRobe, 3);
let (scimitar_vs_silk_robe) = CombatStats.calculate_damage_from_weapon(
g1_scimitar, g3_silk_robe, adventurer_state, 1
);
assert scimitar_vs_silk_robe = 9;
assert scimitar_vs_silk_robe = 8;

// greatness 5 scimitar vs greatness 5 linen robe
// This tests the exact zero base damage (attack_hp == defense_hp)
// we should default to minimum damage setting (3)
// which will get multplied by max elemental modifier (3 at the time of this writing)
// yielding 9
let (g5_scimitar) = TestUtils.create_item(ItemIds.Scimitar, 5);
let (g5_linen_robe) = TestUtils.create_item(ItemIds.LinenRobe, 5);
let (g5_scimitar_vs_g5_linen_robe) = CombatStats.calculate_damage_from_weapon(
g5_scimitar, g5_linen_robe, adventurer_state, 1
);
assert g5_scimitar_vs_g5_linen_robe = 9;
assert g5_scimitar_vs_g5_linen_robe = 8;

// greatness 20 short sword vs greatness 20 shirt
// short sword will deal 1*20 = 20HP attack points
// shirt will deal 1*20 = 20HP defense points
// this generates a base damage of 0HP which is below minimum threshold of 3
// so base damage should use minimum of 3 which will get multiplied by 3 for the elemental (blade vs cloth)
// for a resulting damage of 9
let (g20_shirt) = TestUtils.create_item(ItemIds.Shirt, 20);
let (g20_short_sword) = TestUtils.create_item(ItemIds.ShortSword, 20);
let (g20_short_sword_vs_g20_shirt) = CombatStats.calculate_damage_from_weapon(
g20_short_sword, g20_shirt, adventurer_state, 1
);
assert g20_short_sword_vs_g20_shirt = 9;
assert g20_short_sword_vs_g20_shirt = 8;

// greatness 20 short sword vs greatness 19 shirt
// short sword will deal 1*20 = 20HP attack points
// shirt will deal 1*19 = 19HP defense points
// this generates a base damage of 1HP which is below minimum threshold of 3
// so base damage should use minimum of 3 which will get multiplied by 3 for the elemental (blade vs cloth)
// for a resulting damage of 9
let (g19_shirt) = TestUtils.create_item(ItemIds.Shirt, 19);
let (g20_short_sword_vs_g19_shirt) = CombatStats.calculate_damage_from_weapon(
g20_short_sword, g19_shirt, adventurer_state, 1
);
assert g20_short_sword_vs_g19_shirt = 9;
assert g20_short_sword_vs_g19_shirt = 8;

// greatness 20 short sword vs greatness 18 shirt
// short sword will deal 1*20 = 20HP attack points
// shirt will deal 1*18 = 18HP defense points
// this generates a base damage of 2HP which is below minimum threshold of 3
// so base damage should use minimum of 3 which will get multiplied by 3 for the elemental (blade vs cloth)
// for a resulting damage of 9
let (g18_shirt) = TestUtils.create_item(ItemIds.Shirt, 18);
let (g20_short_sword_vs_g18_shirt) = CombatStats.calculate_damage_from_weapon(
g20_short_sword, g18_shirt, adventurer_state, 1
);
assert g20_short_sword_vs_g18_shirt = 9;
assert g20_short_sword_vs_g18_shirt = 8;

// greatness 20 short sword vs greatness 17 shirt
// short sword will deal 1*20 = 20HP attack points
// shirt will deal 1*17 = 17HP defense points
// this generates a base damage of 3HP
// This should get multiplied by 3 which is equal to minimum threshold of 3
// this will get multiplied by 3 for the elemental (blade vs cloth)
// for a resulting damage of 9
let (g17_shirt) = TestUtils.create_item(ItemIds.Shirt, 17);
let (g20_short_sword_vs_g17_shirt) = CombatStats.calculate_damage_from_weapon(
g20_short_sword, g17_shirt, adventurer_state, 1
);
assert g20_short_sword_vs_g17_shirt = 9;
assert g20_short_sword_vs_g17_shirt = 8;

// greatness 20 short sword vs greatness 16 shirt
// short sword will deal 1*20 = 20HP attack points
// shirt will deal 1*16 = 16HP defense points
// this generates a base damage of 4HP
// This is more than min damage of (3) so it'll get used and multiplied by 3 for the elemental (blade vs cloth)
// for a resulting damage of 12
let (g16_shirt) = TestUtils.create_item(ItemIds.Shirt, 16);
let (g20_short_sword_vs_g16_shirt) = CombatStats.calculate_damage_from_weapon(
g20_short_sword, g16_shirt, adventurer_state, 1
Expand All @@ -236,15 +202,15 @@ func test_calculate_damage_from_beast{
let (adventurer_state) = get_adventurer_state();

// greatness 20 orc vs greatness 0 shirt (oof)
let (orc) = TestUtils.create_beast(BeastIds.Orc, 20);
let (orc) = TestUtils.create_beast(BeastIds.Orc, 20, 0, 0);
let (shirt) = TestUtils.create_item(ItemIds.Shirt, 0);
let (orc_vs_shirt) = CombatStats.calculate_damage_from_beast(orc, shirt, 1);
let (orc_vs_shirt) = CombatStats.calculate_damage_from_beast(orc, shirt, 1, 1);
assert orc_vs_shirt = 60;

// greatness 10 giant vs greatness 10 leather armor
let (leather) = TestUtils.create_item(ItemIds.LeatherArmor, 10);
let (giant) = TestUtils.create_beast(BeastIds.Giant, 10);
let (giant_vs_leather) = CombatStats.calculate_damage_from_beast(giant, leather, 1);
let (giant) = TestUtils.create_beast(BeastIds.Giant, 10, 0, 0);
let (giant_vs_leather) = CombatStats.calculate_damage_from_beast(giant, leather, 1, 1);
assert giant_vs_leather = 120;

return ();
Expand Down Expand Up @@ -276,7 +242,7 @@ func test_calculate_damage_from_obstacle{
let (demonhusk_vs_dark_mist) = CombatStats.calculate_damage_from_obstacle(
g0_dark_mist, g20_demonhusk
);
assert demonhusk_vs_dark_mist = 3;
assert demonhusk_vs_dark_mist = 4;

let zero_item = Item(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
let (g0_curse) = TestUtils.create_obstacle(ObstacleConstants.ObstacleIds.Curse, 1);
Expand Down Expand Up @@ -313,15 +279,15 @@ func test_check_for_level_increase{syscall_ptr: felt*, pedersen_ptr: HashBuiltin
assert zero_xp_zero_level = 1;

// 4xp is not enough to level up from level 1 to level 2
let (no_level_up) = CombatStats.check_for_level_increase(4, 1);
let (no_level_up) = CombatStats.check_for_level_increase(2, 1);
assert no_level_up = 0;

// 9xp is exactly enough to level up from level 1 to level 2
let (level_up_1_to_2) = CombatStats.check_for_level_increase(10, 1);
assert level_up_1_to_2 = 1;

// 675xp is one xp short of being able to level up from level 8 to 9
let (no_level_up_8_to_9) = CombatStats.check_for_level_increase(675, 8);
let (no_level_up_8_to_9) = CombatStats.check_for_level_increase(200, 8);
assert no_level_up_8_to_9 = 0;

// 700xp is enough to level up from level 8 to 9
Expand Down
Loading

0 comments on commit cbcc52a

Please sign in to comment.