Skip to content

Commit

Permalink
Finished First Level Spells (#35)
Browse files Browse the repository at this point in the history
  • Loading branch information
kupka authored Feb 11, 2025
1 parent 5c673c1 commit d6f7192
Show file tree
Hide file tree
Showing 54 changed files with 1,220 additions and 544 deletions.
18 changes: 12 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,18 @@ This example uses a very outdated version of libsrd5, an update will probably ar

libsrd5 Copyright 2021-2025, Thomas Kupka

## Current Status

The latest milestone finally contains **all** the monsters and their attacks from the SRD, though not all attack effects are fully implemented.
However, a ton already works, such as poison and grappling effects.

Next milestone will concentrate on implementing the spell effects, then I will work on the character classes.
## Current Status (Version 0.3.1)

| Topic | Status | Remark |
------------------------|-------------------|--------------------------|
| General <br> Functionality | 🟩🟩🟩🟩🟩🟩🟩🟩⬛⬛ | This includes rolling dice, <br> casting spells, attacking etc. |
| Character <br> Classes | 🟩🟩🟩🟩⬛⬛⬛⬛⬛⬛ | Work to some degree, <br> but needs a lot of detailling. |
| Character <br> Races | 🟩🟩🟩🟩🟩⬛⬛⬛⬛⬛ | All races available, <br> but pretty bare functionality. |
| Monsters | 🟩🟩🟩🟩🟩🟩🟩🟩🟩⬛ | All Monsters are available, <br> most are fully functional! |
| Spells | 🟩🟩🟩⬛⬛⬛⬛⬛⬛⬛ | All Spells are available, <br> Cantrips and 1st Level <br> fully implemented.<br> **Current focus of work**. |
| Feats | 🟩⬛⬛⬛⬛⬛⬛⬛⬛⬛ | Only very basic <br> functionality available. |
| Items | 🟩🟩🟩🟩🟩🟩⬛⬛⬛⬛ | Weapons, Armors (incl. magic) <br> are available. <br> Unique artifacts missing. |
| Descriptions | 🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩 | Done! Everything mentioned<br> in the SRD5 is available<br> in the library. |

## Acknowledgements

Expand Down
26 changes: 13 additions & 13 deletions libsrd5.tests/AttackEffectTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ private static Monster createPansyMonster() {
Monsters.Type.BEAST, Monsters.ID.GOAT, Alignment.LAWFUL_EVIL, 2, 1, 1, 1, 1, 1, 1, "1d6+10000", 40, 16,
new Attack[] { }, new Attack[] { }, Size.MEDIUM
);
pansyMonster.AddEffects(Effect.FAIL_STRENGTH_CHECK, Effect.FAIL_DEXERITY_CHECK, Effect.FAIL_CONSTITUTION_CHECK);
pansyMonster.AddEffect(Effect.FAIL_STRENGTH_CHECK, Effect.FAIL_DEXERITY_CHECK, Effect.FAIL_CONSTITUTION_CHECK);
return pansyMonster;
}

Expand All @@ -38,7 +38,7 @@ private static Monster createPansyMonsterThatDies() {
Monsters.Type.BEAST, Monsters.ID.GOAT, Alignment.LAWFUL_EVIL, 2, 1, 1, 1, 1, 1, 1, "1d1", 40, 16,
new Attack[] { }, new Attack[] { }, Size.MEDIUM
);
pansyMonster.AddEffects(Effect.FAIL_STRENGTH_CHECK, Effect.FAIL_DEXERITY_CHECK, Effect.FAIL_CONSTITUTION_CHECK);
pansyMonster.AddEffect(Effect.FAIL_STRENGTH_CHECK, Effect.FAIL_DEXERITY_CHECK, Effect.FAIL_CONSTITUTION_CHECK);
return pansyMonster;
}

Expand Down Expand Up @@ -79,20 +79,20 @@ private void attackEffectTest(AttackEffect effect, bool guaranteedLethal = false
Assert.True(pansyMonsterThatDies.Dead);
}
for (int j = 0; j < 20; j++) {
uberMonster.TakeDamage(randomDamageType(), Die.D4.Value);
uberMonster.TakeDamage(effect, randomDamageType(), Die.D4.Value);
uberMonster.EscapeFromGrapple();
uberMonster.OnStartOfTurn();
uberMonster.OnEndOfTurn();
averageMonster.TakeDamage(randomDamageType(), Die.D4.Value);
averageMonster.TakeDamage(effect, randomDamageType(), Die.D4.Value);
averageMonster.EscapeFromGrapple();
averageMonster.OnStartOfTurn();
averageMonster.OnEndOfTurn();
pansyMonster.TakeDamage(randomDamageType(), Die.D4.Value);
pansyMonster.TakeDamage(effect, randomDamageType(), Die.D4.Value);
pansyMonster.EscapeFromGrapple();
pansyMonster.OnStartOfTurn();
pansyMonster.OnEndOfTurn();
foreach (Combattant combattant in allCombattantTypes) {
combattant.TakeDamage(randomDamageType(), Die.D4.Value);
combattant.TakeDamage(effect, randomDamageType(), Die.D4.Value);
combattant.EscapeFromGrapple();
combattant.OnStartOfTurn();
combattant.OnEndOfTurn();
Expand Down Expand Up @@ -460,13 +460,13 @@ public void CouatlBiteEffectTest() {
Combattant target3 = createPansyMonster();
Attacks.CouatlBiteEffect.Invoke(Monsters.Aboleth, target1);
target1.RemoveEffect(Effect.COUATL_POISON);
target1.TakeDamage(DamageType.TRUE_DAMAGE, 1);
target1.TakeDamage(this, DamageType.TRUE_DAMAGE, 1);
Attacks.CouatlBiteEffect.Invoke(Monsters.Aboleth, target2);
target2.RemoveEffect(Effect.COUATL_POISON);
target2.TakeDamage(DamageType.TRUE_DAMAGE, 1);
target2.TakeDamage(this, DamageType.TRUE_DAMAGE, 1);
Attacks.CouatlBiteEffect.Invoke(Monsters.Aboleth, target3);
target3.RemoveEffect(Effect.COUATL_POISON);
target3.TakeDamage(DamageType.TRUE_DAMAGE, 1);
target3.TakeDamage(this, DamageType.TRUE_DAMAGE, 1);
Assert.False(target1.HasCondition(ConditionType.UNCONSCIOUS) && target2.HasCondition(ConditionType.UNCONSCIOUS) && target3.HasCondition(ConditionType.UNCONSCIOUS));
}

Expand Down Expand Up @@ -545,9 +545,9 @@ public void DrowPoisonTest() {
target1.RemoveEffect(Effect.DROW_POISON);
target2.RemoveEffect(Effect.DROW_POISON);
target3.RemoveEffect(Effect.DROW_POISON);
target1.TakeDamage(DamageType.TRUE_DAMAGE, 1);
target2.TakeDamage(DamageType.TRUE_DAMAGE, 1);
target3.TakeDamage(DamageType.TRUE_DAMAGE, 1);
target1.TakeDamage(this, DamageType.TRUE_DAMAGE, 1);
target2.TakeDamage(this, DamageType.TRUE_DAMAGE, 1);
target3.TakeDamage(this, DamageType.TRUE_DAMAGE, 1);
}

[Fact]
Expand Down Expand Up @@ -579,7 +579,7 @@ public void GhoulClawsTest() {
halfling.AddLevel(CharacterClasses.Barbarian);
highelf.AddLevel(CharacterClasses.Barbarian);
halfelf.AddLevel(CharacterClasses.Wizard);
for (int i = 0; i < 10; i++)
for (int i = 0; i < 100; i++)
Attacks.GhoulClawsEffect.Invoke(Monsters.Ghoul, halfling);
Attacks.GhoulClawsEffect.Invoke(Monsters.Ghoul, highelf);
Attacks.GhoulClawsEffect.Invoke(Monsters.Ghoul, halfelf);
Expand Down
32 changes: 32 additions & 0 deletions libsrd5.tests/BattlegroundTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,32 @@ public void SpecialConditionTest() {
Assert.False(ground.RangedAttackAction(ogre));
}

[Fact]
public void SpecialConditionTest2() {
ConditionType[] conditions = new ConditionType[] {
ConditionType.PRONE,
ConditionType.RESTRAINED,
ConditionType.STUNNED,
ConditionType.PARALYZED,
ConditionType.UNCONSCIOUS,
ConditionType.BLINDED,
ConditionType.PETRIFIED
};
foreach (ConditionType condition in conditions) {
Monster ogre = Monsters.Ogre;
Monster goblin = Monsters.Goblin;
Battleground2D ground = new Battleground2D(5, 5);
ground.AddCombattant(ogre, 1, 1);
ground.AddCombattant(goblin, 1, 2);
goblin.AddCondition(condition);
while (ground.CurrentCombattant != ogre) {
ground.NextPhase();
}
ground.NextPhase();
Assert.True(ground.MeleeAttackAction(goblin));
}
}

[Fact]
public void CastMagicMissileTest() {
Battleground2D ground = new Battleground2D(30, 30);
Expand Down Expand Up @@ -423,6 +449,12 @@ public void RangedTest() {
Assert.False(battle.RangedAttackAction(bandit));
else
Assert.False(battle.RangedAttackAction(ogre));
Assert.Throws<Srd5ArgumentException>(
delegate () {
battle.NextPhase();
battle.RangedAttackAction(battle.CurrentCombattant);
}
);
while (bandit.HitPoints > 0) {
while (battle.NextPhase() != TurnPhase.ACTION) ;
Assert.Throws<Srd5ArgumentException>(delegate { battle.RangedAttackAction(null); });
Expand Down
25 changes: 14 additions & 11 deletions libsrd5.tests/CharacterSheetTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ public void EquipArmorTest() {
Assert.Equal(14, sheet.ArmorClass);
sheet.Equip(chainShirt);
Assert.Equal(15, sheet.ArmorClass);
Assert.Throws<Srd5Exception>(delegate () {
sheet.ArmorClass = 20;
});
}

[Fact]
Expand Down Expand Up @@ -158,22 +161,22 @@ public void MagicWeaponTest() {
Weapon club = Weapons.Club;
sheet.Equip(club);
Assert.Equal(7, sheet.AttackProficiency);
Assert.Equal("1d4+4", sheet.MeleeAttacks[0].Damage.Dices.ToString());
Assert.Equal("1d4+4", sheet.MeleeAttacks[0].Damage.Dice.ToString());
Weapon clubPlus1 = Weapons.CreatePlus1Weapon(Weapons.Club);
sheet.Unequip(club);
sheet.Equip(clubPlus1);
Assert.Equal(8, sheet.AttackProficiency);
Assert.Equal("1d4+5", sheet.MeleeAttacks[0].Damage.Dices.ToString());
Assert.Equal("1d4+5", sheet.MeleeAttacks[0].Damage.Dice.ToString());
Weapon clubPlus2 = Weapons.CreatePlus2Weapon(Weapons.Club);
sheet.Unequip(clubPlus1);
sheet.Equip(clubPlus2);
Assert.Equal(9, sheet.AttackProficiency);
Assert.Equal("1d4+6", sheet.MeleeAttacks[0].Damage.Dices.ToString());
Assert.Equal("1d4+6", sheet.MeleeAttacks[0].Damage.Dice.ToString());
Weapon clubPlus3 = Weapons.CreatePlus3Weapon(Weapons.Club);
sheet.Unequip(clubPlus2);
sheet.Equip(clubPlus3);
Assert.Equal(10, sheet.AttackProficiency);
Assert.Equal("1d4+7", sheet.MeleeAttacks[0].Damage.Dices.ToString());
Assert.Equal("1d4+7", sheet.MeleeAttacks[0].Damage.Dice.ToString());
}

[Fact]
Expand Down Expand Up @@ -334,8 +337,8 @@ public void UnarmedTest() {
Assert.Empty(sheet.RangedAttacks);
Attack unarmed = sheet.MeleeAttacks[0];
Assert.Equal(4, unarmed.AttackBonus);
Assert.Equal("1d1+2", unarmed.Damage.Dices.ToString());
Assert.Equal(3, unarmed.Damage.Dices.Roll());
Assert.Equal("1d1+2", unarmed.Damage.Dice.ToString());
Assert.Equal(3, unarmed.Damage.Dice.Roll());
}

[Fact]
Expand Down Expand Up @@ -386,13 +389,13 @@ public void VersatileTest() {
CharacterSheet sheet = new CharacterSheet(Race.HALFLING);
Weapon quarterstaff = Weapons.Quarterstaff;
sheet.Equip(quarterstaff);
Assert.Equal(8, sheet.MeleeAttacks[0].Damage.Dices.Die);
Assert.Equal(8, sheet.MeleeAttacks[0].Damage.Dice.Die);
int ac = sheet.ArmorClass;
Shield shield = Shields.Shield;
Assert.Equal(quarterstaff, sheet.Inventory.MainHand);
sheet.Equip(shield);
Assert.Equal(ac + shield.AC, sheet.ArmorClass);
Assert.Equal(6, sheet.MeleeAttacks[0].Damage.Dices.Die);
Assert.Equal(6, sheet.MeleeAttacks[0].Damage.Dice.Die);
}

[Fact]
Expand Down Expand Up @@ -473,7 +476,7 @@ public void BagTest() {
public void LongRestTest() {
CharacterSheet sheet = new CharacterSheet(Race.HALFLING);
sheet.AddLevel(CharacterClasses.Barbarian);
sheet.TakeDamage(DamageType.SLASHING, 2);
sheet.TakeDamage(this, DamageType.SLASHING, 2);
Assert.True(sheet.HitPointsMax > sheet.HitPoints);
sheet.LongRest();
Assert.Equal(sheet.HitPointsMax, sheet.HitPoints);
Expand Down Expand Up @@ -538,7 +541,7 @@ public void HitPointMaxiumModifierTest() {
public void InstantDeathTest() {
CharacterSheet hero = new CharacterSheet(Race.HALF_ELF);
hero.AddLevel(CharacterClasses.Wizard);
hero.TakeDamage(DamageType.TRUE_DAMAGE, 100);
hero.TakeDamage(this, DamageType.TRUE_DAMAGE, 100);
Assert.True(hero.Dead);
}

Expand All @@ -549,7 +552,7 @@ public void DeathSavesTest() {
for (int i = 0; i < 100; i++) {
CharacterSheet hero = new CharacterSheet(Race.HALF_ELF);
hero.AddLevel(CharacterClasses.Wizard);
hero.TakeDamage(DamageType.TRUE_DAMAGE, hero.HitPointsMax);
hero.TakeDamage(this, DamageType.TRUE_DAMAGE, hero.HitPointsMax);
Assert.True(hero.HasEffect(Effect.FIGHTING_DEATH));
for (int j = 0; j < 6; j++) {
hero.OnStartOfTurn();
Expand Down
24 changes: 17 additions & 7 deletions libsrd5.tests/CombattantTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,22 @@ public void TakeDamageTest() {
ogre.AddEffect(Effect.RESISTANCE_LIGHTNING);
int hp = ogre.HitPoints;
Damage cold = new Damage(DamageType.COLD, "1d6+5");
ogre.TakeDamage(cold.Type, cold.Dices.RollCritical());
ogre.TakeDamage(ogre, cold.Type, cold.Dice.RollCritical());
Assert.InRange<int>(ogre.HitPoints, hp - 34, hp - 14);
hp = ogre.HitPoints;
Damage acid = new Damage(DamageType.ACID, "1d4+3");
ogre.TakeDamage(acid.Type, acid.Dices.Roll());
ogre.TakeDamage(ogre, acid.Type, acid.Dice.Roll());
Assert.Equal(hp, ogre.HitPoints);
Damage lightning = new Damage(DamageType.LIGHTNING, "1d12");
ogre.TakeDamage(lightning.Type, lightning.Dices.Roll());
ogre.TakeDamage(ogre, lightning.Type, lightning.Dice.Roll());
Assert.InRange<int>(ogre.HitPoints, hp - 6, hp);
Assert.Throws<Srd5ArgumentException>(delegate {
ogre.TakeDamage(DamageType.ACID, -100);
ogre.TakeDamage(this, DamageType.ACID, -100);
});
Assert.Throws<Srd5ArgumentException>(delegate {
ogre.HealDamage(-100);
});
ogre.TakeDamage(DamageType.PIERCING, ogre.HitPoints);
ogre.TakeDamage(this, DamageType.PIERCING, ogre.HitPoints);
Assert.True(ogre.Dead);
}

Expand All @@ -37,7 +37,7 @@ public void TakeDamageTestWithDCHalves() {
Combattant ogre = Monsters.Ogre;
int tookFullDamage = 0, tookHalfDamage = 0;
for (int i = 0; i < 10; i++) {
int damageTaken = ogre.TakeDamage(DamageType.TRUE_DAMAGE, 2, Spells.DCEffect.HALVES_DAMAGE, 10, AbilityType.DEXTERITY);
int damageTaken = ogre.TakeDamage(this, DamageType.TRUE_DAMAGE, 2, Spells.DCEffect.HALVES_DAMAGE, 10, AbilityType.DEXTERITY, out _);
if (damageTaken == 2) {
tookFullDamage++;
} else if (damageTaken == 1) {
Expand All @@ -52,7 +52,7 @@ public void TakeDamageTestWithDCNullify() {
Combattant ogre = Monsters.Ogre;
int tookFullDamage = 0, tookNoDamage = 0;
for (int i = 0; i < 10; i++) {
int damageTaken = ogre.TakeDamage(DamageType.TRUE_DAMAGE, 2, Spells.DCEffect.NULLIFIES_DAMAGE, 10, AbilityType.DEXTERITY);
int damageTaken = ogre.TakeDamage(this, DamageType.TRUE_DAMAGE, "2d1", Spells.DCEffect.NULLIFIES_DAMAGE, 10, AbilityType.DEXTERITY, out _);
if (damageTaken == 2) {
tookFullDamage++;
} else if (damageTaken == 0) {
Expand Down Expand Up @@ -105,6 +105,16 @@ public void CriticalDCTest() {
Assert.True(criticalFail && criticalSuccess);
}

[Fact]
public void DCEffectTest() {
Combattant ogre = Monsters.Ogre;
ogre.AddEffect(Effect.SPELL_BANE, Effect.SPELL_BLESS, Effect.SPELL_DIVINE_FAVOR);
ogre.DC(this, 20, AbilityType.STRENGTH, out _);
for (int i = 0; i < 10; i++) {
ogre.Attack(Attacks.OgreGreatclub, Monsters.Rat, 5);
}
}

[Fact]
public void DCTest() {
Combattant orc = Monsters.Orc;
Expand Down
2 changes: 1 addition & 1 deletion libsrd5.tests/DiceTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ public void DiceRolledEventTest() {
}

[Fact]
public void DicesConstructorTest() {
public void DiceConstructorTest() {
Assert.Equal("2d6+3", new Dice(2, 6, 3).ToString());
Assert.Equal("1d12", new Dice(1, 12, 0).ToString());
Assert.Equal("3d8-2", new Dice(3, 8, -2).ToString());
Expand Down
4 changes: 2 additions & 2 deletions libsrd5.tests/EffectsTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,12 @@ public void EnumerateEffectsTest() {
Assert.True(bandit.HasEffect(effect));
bandit.OnStartOfTurn();
bandit.OnEndOfTurn();
bandit.OnDamageTaken();
bandit.OnDamageTaken(effect, new Damage(DamageType.TRUE_DAMAGE, 1));
bandit.RemoveEffect(effect);
Assert.False(bandit.HasEffect(effect));
bandit.OnStartOfTurn();
bandit.OnEndOfTurn();
bandit.OnDamageTaken();
bandit.OnDamageTaken(effect, new Damage(DamageType.TRUE_DAMAGE, 1));
}
}

Expand Down
6 changes: 3 additions & 3 deletions libsrd5.tests/ItemTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public void UnEqualTest() {
Assert.False(club.Equals(dagger));
Assert.False(club.Equals(null));
Assert.False(club.Equals("not an item"));
Assert.False(club.IsThisA(null));
Assert.False(club.Is(null));
Assert.False(club.Equals(Armors.HideArmor));
}

Expand All @@ -38,7 +38,7 @@ public void ThingsUnEqualTest() {
Weapon club1 = Weapons.Club;
Weapon club2 = Weapons.Club;
Assert.False(club1.Equals(club2));
Assert.True(club1.IsThisA(club2));
Assert.True(club1.Is(club2));
}

[Fact]
Expand Down Expand Up @@ -76,7 +76,7 @@ public void HealingPotionTest() {
Consumable[] potions = new Consumable[] { Potions.PotionOfHealing, Potions.PotionOfGreaterHealing,
Potions.PotionOfSuperiorHealing, Potions.PotionOfSupremeHealing };
foreach (Consumable potion in potions) {
hero.TakeDamage(DamageType.SLASHING, hero.HitPointsMax);
hero.TakeDamage(this, DamageType.SLASHING, hero.HitPointsMax);
Assert.True(hero.HasEffect(Effect.FIGHTING_DEATH));
hero.Consume(potion);
Assert.False(hero.HasEffect(Effect.FIGHTING_DEATH));
Expand Down
2 changes: 1 addition & 1 deletion libsrd5.tests/MonsterTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public void AllMonsterTest() {
[Fact]
public void AutoFailDCTest() {
Monster orc = Monsters.Orc;
orc.AddEffects(Effect.FAIL_DEXERITY_CHECK, Effect.FAIL_STRENGTH_CHECK);
orc.AddEffect(Effect.FAIL_DEXERITY_CHECK, Effect.FAIL_STRENGTH_CHECK);
Assert.False(orc.DC(Spells.ID.HOLD_PERSON, 2, AbilityType.STRENGTH, true));
Assert.False(orc.DC(Weapons.Blowgun, 2, AbilityType.DEXTERITY));
}
Expand Down
Loading

0 comments on commit d6f7192

Please sign in to comment.