Skip to content

Commit

Permalink
Merge pull request #765 from dolio/topic/hydra
Browse files Browse the repository at this point in the history
Beef up the hydra
  • Loading branch information
doug1234 authored Feb 29, 2024
2 parents 23a33f0 + 8c4f8a5 commit f0dccc3
Show file tree
Hide file tree
Showing 8 changed files with 178 additions and 5 deletions.
4 changes: 4 additions & 0 deletions TemplePlus/ability_fixes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,10 @@ int __cdecl AbilityConditionFixes::CombatReflexesAooReset(DispatcherCallbackArg
const auto dexScore = objects.StatLevelGet(args.objHndCaller, Stat::stat_dexterity);
auto extraAoos = objects.GetModFromStatLevel(dexScore);
extraAoos = std::max(extraAoos, 0);

// Enable hydra combat reflexes special case.
auto heads = d20Sys.D20QueryPython(args.objHndCaller, "Hydra Heads");
if (heads > 0) extraAoos = heads;
numAoosRem += extraAoos;
}

Expand Down
9 changes: 6 additions & 3 deletions TemplePlus/action_sequence.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3416,11 +3416,14 @@ int ActionSequenceSystem::ActionCostFullAttack(D20Actn* d20, TurnBasedStatus* tb
{
acp->chargeAfterPicker = 0;
acp->moveDistCost = 0;
acp->hourglassCost = 4;

auto cheap = d20Sys.D20QueryPython(d20->d20APerformer, "Full Attack As Standard");
acp->hourglassCost = cheap == 1 ? 2 : 4;

int flags = d20->d20Caf;
if (d20->d20Caf & D20CAF_FREE_ACTION || !combat->isCombatActive() )
acp->hourglassCost = 0;
if (tbStat->attackModeCode >= tbStat->baseAttackNumCode && tbStat->hourglassState >= 4 && !tbStat->numBonusAttacks)
if (tbStat->attackModeCode >= tbStat->baseAttackNumCode && tbStat->hourglassState >= 2 && !tbStat->numBonusAttacks)
{
FullAttackCostCalculate(d20, tbStat, (int*)&tbStat->baseAttackNumCode, (int*) &tbStat->numBonusAttacks,
(int*)&tbStat->numAttacks,(int*) &tbStat->attackModeCode);
Expand Down Expand Up @@ -4356,4 +4359,4 @@ ostream & operator<<(ostream & str, ActionErrorCode aec)
str << fmt::format("Unknown Action Error Code [{}]", i);
}
return str;
}
}
22 changes: 22 additions & 0 deletions TemplePlus/condition.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ class GenericCallbacks
static int __cdecl D20ModCountdownHandler(DispatcherCallbackArgs args);
static int __cdecl D20ModCountdownEndHandler(DispatcherCallbackArgs args);

static int __cdecl FastHealingOnBeginRound(DispatcherCallbackArgs args);

static int __cdecl MonsterRegenerationOnDamage(DispatcherCallbackArgs args);

Expand Down Expand Up @@ -582,6 +583,7 @@ class ConditionFunctionReplacement : public TempleFix {
spCallbacks.oldD20ModsSpellsSpellBonus = replaceFunction(0x100C4440, spCallbacks.D20ModsSpellsSpellBonus);

replaceFunction<int(DispatcherCallbackArgs)>(0x100F72E0, genericCallbacks.MonsterRegenerationOnDamage);
replaceFunction<int(DispatcherCallbackArgs)>(0x100F7AA0, genericCallbacks.FastHealingOnBeginRound);

replaceFunction(0x100FCF50, ManyShotAttack);
replaceFunction(0x100FCEB0, ManyShotMenu);
Expand Down Expand Up @@ -1638,6 +1640,26 @@ int GenericCallbacks::MonsterRegenerationOnDamage(DispatcherCallbackArgs args){
return 0;
}

int GenericCallbacks::FastHealingOnBeginRound(DispatcherCallbackArgs args)
{
auto *dispIo = dispatch.DispIoCheckIoType6(args.dispIO);
auto critter = args.objHndCaller;

if (critterSys.IsDeadNullDestroyed(critter)) return 0;
if (critterSys.GetHpDamage(critter) <= 0 && critterSys.GetSubdualDamage(critter) <= 0)
return 0;

auto rounds = dispIo->data1;

if (rounds <= 0) return 0;

auto heal = args.GetCondArg(0) * rounds;
damage.FastHeal(critter, heal);
histSys.CreateRollHistoryLineFromMesfile(57, critter, objHndl::null);

return 0;
}

int GenericCallbacks::PreferOneHandedWieldRadialMenu(DispatcherCallbackArgs args)
{
auto shield = inventory.ItemWornAt(args.objHndCaller, EquipSlot::Shield);
Expand Down
45 changes: 43 additions & 2 deletions TemplePlus/d20.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ class D20ActionCallbacks {

// Action Cost
static ActionErrorCode ActionCostCastSpell(D20Actn* d20a, TurnBasedStatus *tbStat, ActionCostPacket *acp);
static ActionErrorCode ActionCostCharge(D20Actn *d20a, TurnBasedStatus *tbStat, ActionCostPacket *acp);
static ActionErrorCode ActionCostFullRound(D20Actn* d20a, TurnBasedStatus *tbStat, ActionCostPacket *acp);
static ActionErrorCode ActionCostFullAttack(D20Actn* d20a, TurnBasedStatus* tbStat, ActionCostPacket* acp);
static ActionErrorCode ActionCostPartialCharge(D20Actn* d20a, TurnBasedStatus* tbStat, ActionCostPacket* acp);
Expand Down Expand Up @@ -500,6 +501,7 @@ void LegacyD20System::NewD20ActionsInit()
d20Type = D20A_CHARGE;
d20Defs[d20Type].performFunc = d20Callbacks.PerformCharge;
d20Defs[d20Type].actionFrameFunc = d20Callbacks.ActionFrameCharge;
d20Defs[d20Type].actionCost = d20Callbacks.ActionCostCharge;

d20Type = D20A_COUP_DE_GRACE;
d20Defs[d20Type].actionFrameFunc = d20Callbacks.ActionFrameCoupDeGrace;
Expand Down Expand Up @@ -3989,11 +3991,14 @@ ActionErrorCode D20ActionCallbacks::TurnBasedStatusCheckPython(D20Actn* d20a, Tu
ActionErrorCode D20ActionCallbacks::ActionCostFullAttack(D20Actn * d20a, TurnBasedStatus * tbStat, ActionCostPacket * acp){
acp->chargeAfterPicker = 0;
acp->moveDistCost = 0;
acp->hourglassCost = 4;

auto cheap = d20Sys.D20QueryPython(d20a->d20APerformer, "Full Attack As Standard");
acp->hourglassCost = cheap == 1 ? 2 : 4;

//int flags = d20a->d20Caf;
if ( (d20a->d20Caf & D20CAF_FREE_ACTION ) || !combatSys.isCombatActive())
acp->hourglassCost = 0;
if (tbStat->attackModeCode >= tbStat->baseAttackNumCode && tbStat->hourglassState >= 4 && !tbStat->numBonusAttacks){
if (tbStat->attackModeCode >= tbStat->baseAttackNumCode && tbStat->hourglassState >= 2 && !tbStat->numBonusAttacks){
actSeqSys.FullAttackCostCalculate(d20a, tbStat, (int*)&tbStat->baseAttackNumCode, (int*)&tbStat->numBonusAttacks,
(int*)&tbStat->numAttacks, (int*)&tbStat->attackModeCode);
tbStat->surplusMoveDistance = 0;
Expand Down Expand Up @@ -4052,6 +4057,42 @@ ActionErrorCode D20ActionCallbacks::ActionCostStandardAttack(D20Actn* d20a, Turn
return AEC_OK;
}

ActionErrorCode D20ActionCallbacks::ActionCostCharge(D20Actn *d20a, TurnBasedStatus *tbStat, ActionCostPacket *acp)
{
acp->hourglassCost = 0;
acp->chargeAfterPicker = 0;
acp->moveDistCost = 0;

auto inCombat = combatSys.isCombatActive();
bool notFree = !(d20a->d20Caf & D20CAF_FREE_ACTION);

if (notFree && inCombat) {
if (d20Sys.D20QueryPython(d20a->d20APerformer, "Full Attack On Charge")) {
acp->chargeAfterPicker = 1;

actSeqSys.FullAttackCostCalculate(
d20a, tbStat,
(int*)&tbStat->baseAttackNumCode,
(int*)&tbStat->numBonusAttacks,
(int*)&tbStat->numAttacks,
(int*)&tbStat->attackModeCode);

tbStat->tbsFlags |= TBSF_FullAttack;
} else {
tbStat->numAttacks = 0;
tbStat->baseAttackNumCode = 0;
tbStat->attackModeCode = 0;
tbStat->numBonusAttacks = 0;
}

tbStat->tbsFlags |= TBSF_Movement;

auto timeLeft = tbStat->hourglassState;

acp->hourglassCost = timeLeft > 2 ? timeLeft : 4;
}
return AEC_OK;
}

ActionErrorCode D20ActionCallbacks::ActionCostMoveAction(D20Actn *d20, TurnBasedStatus *tbStat, ActionCostPacket *acp)
{
Expand Down
38 changes: 38 additions & 0 deletions TemplePlus/damage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -814,6 +814,44 @@ void Damage::DamageCritterPython(objHndl attacker, objHndl tgt, DispIoDamage& ev
Py_DECREF(result);
}

// The logic for Fast Healing. This is a fixed amount of healing that
// happens at a regular interval. The amount applies to subdual first,
// then to lethal damage.
void Damage::FastHeal(objHndl critter, int amount) {
if (!critter || amount <= 0) return;

int totHeal = 0;

auto subdual = critterSys.GetSubdualDamage(critter);
// TODO: check for unhealable damage. Could be environmental for
// subdual, and cursed/infernal wounds for lethal, if either is
// implemented.
if (subdual > 0) {
auto sHeal = std::min(amount, subdual);
critterSys.SetSubdualDamage(critter, subdual - sHeal);

auto text = fmt::format("{} {}", sHeal, combatSys.GetCombatMesLine(33));
floatSys.floatMesLine(critter, 2, FloatLineColor::LightBlue, text.c_str());

amount -= sHeal;
totHeal += sHeal;
}

auto lethal = critterSys.GetHpDamage(critter);
if (amount > 0 && lethal > 0) {
auto heal = std::min(amount, lethal);
critterSys.SetHpDamage(critter, lethal - heal);

auto text = fmt::format("{} {}", heal, combatSys.GetCombatMesLine(32));
floatSys.floatMesLine(critter, 2, FloatLineColor::LightBlue, text.c_str());

totHeal += heal;
}

if (totHeal > 0)
d20Sys.d20SendSignal(critter, DK_SIG_HP_Changed, totHeal, 0);
}

void Damage::Heal(objHndl target, objHndl healer, const Dice& dice, D20ActionType actionType) {
int healingBonus = 0;
if (healer){
Expand Down
2 changes: 2 additions & 0 deletions TemplePlus/damage.h
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ class Damage {
void DamageCritter(objHndl attacker, objHndl tgt, DispIoDamage & evtObjDam);


void FastHeal(objHndl critter, int amount);

void Heal(objHndl target, objHndl healer, const Dice &dice, D20ActionType actionType);

void HealSpell(objHndl target, objHndl healer, const Dice &dice, D20ActionType actionType, int spellId);
Expand Down
1 change: 1 addition & 0 deletions tpdatasrc/co8fixes/rules/protos_override.tab
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
14263 obj_t_npc 110 14263 14263 size_huge mat_flesh 9 1110 50 OCF_MONSTER OCF_NO_FLEE OCF_MUTE 31 10 31 16 16 20 align_chaotic_evil CHAOS EVIL 5110 14263 15 2 2d8+10 Claw 20 2 1d6+5 Claw 18 1 1d8+5 Bite 18 ONF_EXTRAPLANAR ONF_NO_EQUIP 0 13 8 8 8 19 12d8 mc_type_outsider mc_subtype_chaotic mc_subtype_demon mc_subtype_evil mc_subtype_extraplanar Monster DR Holy 10 Monster Spell Resistance 21 Monster Energy Immunity Electricity Monster Energy Resistance Acid 10 Monster Energy Resistance Cold 10 Monster Energy Resistance Fire 10 Monster Confusion Immunity Monster Poison Immunity Bluff 30 Concentration 30 Diplomacy 8 Intimidate 32 Listen 44 Move Silently 36 Search 30 Sense Motive 30 Spellcraft 30 Spot 44 Cleave Power Attack Great Cleave Persuasive 309 0 0 0 0 2 0 0 0 0 'Chaos Hammer' domain_special 5 'Chaos Hammer' domain_special 5 'Chaos Hammer' domain_special 5 'Dispel Magic' domain_special 5 'Confusion' domain_special 4 'Hold Monster' domain_special 5 'Mirror Image' domain_special 2 Glabrezu (332)
14286 obj_t_npc 110 14286 14286 size_large mat_flesh 9 1123 45 160 155 OCF_MONSTER OCF_NO_FLEE OCF_MUTE 35 25 31 24 24 26 align_chaotic_evil 5100 14286 10 2 1d10+7 Slam 31 ONF_EXTRAPLANAR 0 20 12 12 12 19 20d8 mc_type_outsider mc_subtype_chaotic mc_subtype_demon mc_subtype_evil mc_subtype_extraplanar Monster DR Cold-Holy 15 Monster Spell Resistance 28 Monster Salamander Monster Energy Immunity Fire Monster Poison Immunity 0 0 Monster Energy Immunity Electricity Monster Energy Resistance Cold 10 Monster Poison Immunity Monster Subdual Immunity Monster Special Fade Out Monster Energy Resistance Acid 10 Bluff 46 Concentration 46 Diplomacy 46 Hide 38 Listen 62 Move Silently 46 Search 46 Sense Motive 46 Spot 62 Cleave Improved Initiative Power Attack Weapon Focus (Longsword) 309 0 0 0 0 2 0 0 0 0 'Dispel Magic' domain_special 5 'Dispel Magic' domain_special 5 'Dispel Magic' domain_special 5 'Dispel Magic' domain_special 5 'Fear' domain_special 4 'Slay Living' domain_special 5 'Suggestion' domain_special 5 Balor (321)
14325 obj_t_npc 95 14325 14325 size_medium 10 mat_flesh 13 100 25 453 13 11 12 9 9 9 race_human male align_lawful_good St. Cuthbert 6850 Brown Shorthair (m/f) mc_type_humanoid mc_subtype_human Fighter 1 Listen 2 Search 2 Tumble 2 Spot 2 Weapon Focus (Longsword) Alertness Improved Initiative 195 0 0 0 0 195 0 0 0 0 195 0 0 0 0 2 0 0 0 0 195 0 0 0 0
14343 obj_t_npc 130 14343 14343 size_large mat_flesh 7 1145 45 63 OCF_MONSTER OCF_NO_FLEE 17 12 20 2 10 9 align_true_neutral 4540 14343 10 5 1d10+3 Bite 6 ONF_KOS ONF_NO_EQUIP 4 4 4 1 6 5d10 mc_type_magical_beast Monster Fast Healing 15 Monster Stable Monster Hydra 5 0 Listen 10 Spot 10 feat combat reflexes Toughness Iron Will 6 0 0 0 0 2 0 0 0 0 Boar
14350 obj_t_npc 50 14350 14350 size_medium mat_flesh 8 1109 OCF_MONSTER OCF_MUTE OCF_NO_FLEE 20 16 21 6 9 6 male align_chaotic_neutral 4250 14350 5 2 1d6+5 Claw 9 1 1d6+2 Bite 4 ONF_KOS ONF_NO_EQUIP 4 2 5 2 5 6d8 mc_type_giant Monster Regeneration 5 Fire Acid Rend Listen 8 Spot 10 Alertness Iron Will Track 6 0 0 0 0 2 0 0 0 0
14352 obj_t_npc 200 14352 14352 size_large mat_flesh 5 1.5 1006 45 OCF_ANIMAL OCF_NO_FLEE 17 15 15 6 14 10 male align_neutral_evil 3730 5 1 1d6+4 Bite 7 ONF_NO_EQUIP 2 4 4 1 2 4d10 mc_type_magical_beast Monster Stable Tripping Bite Spot 4 Listen 4 Hide 4 MOve Silently 8 Survival 8 Alertness Track 2 0 0 0 0
14358 obj_t_npc 130 14358 14358 size_large mat_flesh 9 1123 45 160 165 OCF_MONSTER OCF_NO_FLEE 35 25 31 24 26 26 align_chaotic_evil 3580 14358 15 2 1d10+7 Gore 31 ONF_EXTRAPLANAR ONF_BOSS_MONSTER 20 12 12 12 20 20d4+80 mc_type_outsider mc_subtype_chaotic mc_subtype_demon mc_subtype_evil mc_subtype_extraplanar Monster DR Cold-Holy 15 Monster Spell Resistance 28 Monster Salamander Monster Energy Immunity Fire Monster Energy Resistance Acid 10 Monster Energy Immunity Electricity Monster Energy Resistance Cold 10 Monster Poison Immunity Monster Subdual Immunity Monster Special Fade Out Bluff 46 Concentration 46 Diplomacy 58 Hide 26 Listen 62 Move Silently 26 Search 46 Sense Motive 46 Spot 62 Cleave Improved Initiative Power Attack Weapon Focus (Longsword) 202 0 0 0 0 202 0 0 0 0 2 0 0 0 0 202 0 0 0 0 'Glabrezu Summon Quasits (INTERNAL)' domain_special 1 'Suggestion' domain_special 5 'Suggestion' domain_special 5 'Fear' domain_special 4 'Slay Living' domain_special 5 'Slay Living' domain_special 5 'Slay Living' domain_special 5 'Dispel Magic' domain_special 5 'Dispel Magic' domain_special 5 'Dispel Magic' domain_special 5 'Dispel Magic' domain_special 5 Balor (321)
Expand Down
62 changes: 62 additions & 0 deletions tpdatasrc/tpgamefiles/scr/tpModifiers/monster_hydra.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from templeplus.pymod import PythonModifier
from toee import *
import tpdp

def HeadCount(hydra, args, evt_obj):
evt_obj.return_val = args.get_arg(2)

return 0

def Yes(hydra, args, evt_obj):
evt_obj.return_val = 1

return 0

def Setup(hydra, args, evt_obj):
# set current heads to initial head count
args.set_arg(2, args.get_arg(0))

return 0

def GrowHeads(hydra, args, evt_obj):
base = args.get_arg(0)
heads = args.get_arg(2)

args.set_arg(2, min(base*2, heads + 2))

return 0

# 0: initial heads
# 1: reserved
# 2: current heads
# 3: extra
# 4: extra
# 5: extra
hydra = PythonModifier('Monster Hydra', 6, 1)
hydra.AddHook(ET_OnConditionAdd, EK_NONE, Setup, ())
hydra.AddHook(ET_OnConditionAddFromD20StatusInit, EK_NONE, Setup, ())
hydra.AddHook(ET_OnD20PythonQuery, 'Hydra Heads', HeadCount, ())
hydra.AddHook(ET_OnD20PythonQuery, 'Full Attack As Standard', Yes, ())
hydra.AddHook(ET_OnD20PythonQuery, 'Full Attack On Charge', Yes, ())
hydra.AddHook(ET_OnD20PythonSignal, 'Hydra Grow Heads', GrowHeads, ())
hydra.AddHook(ET_OnGetCritterNaturalAttacksNum, EK_NONE, HeadCount, ())

def SeverCountdown(hydra, args, evt_obj):
count = args.get_arg(0)

if (count <= 0):
hydra.d20_send_signal('Hydra Grow Heads')
args.condition_remove()
else:
args.set_arg(0, count-1)

return 0

# condition for tracking a hydra regrowing a severed head.
# currently there's no mechanism for severing a head

# 0: counter
# 1: regrowing
# 2: extra
severed = PythonModifier('Hydra Head Severed', 3, 0)
severed.AddHook(ET_OnBeginRound, EK_NONE, SeverCountdown, ())

0 comments on commit f0dccc3

Please sign in to comment.