Refactored player buff rendering code. Added Freeze Ground Slowdown oscillation color effect. Implemented Speed Up Spell. Changed MonsterAbility lambda to return success or failure spell cast state. Skeleton Lightnng Mage monster setup. Release Build 12939
All checks were successful
Emscripten Build / Build_and_Deploy_Web_Build (push) Successful in 8m8s

This commit is contained in:
AMay 2026-03-06 15:37:33 -06:00
parent 451c248f7d
commit 19746432d4
28 changed files with 209 additions and 60 deletions

View File

@ -1089,25 +1089,20 @@ void AiL::RenderWorld(float fElapsedTime){
pos=player->teleportStartPosition.lerp(player->teleportTarget,("Wizard.Right Click Ability.AnimationTime"_F-player->teleportAnimationTimer)/"Wizard.Right Click Ability.AnimationTime"_F);
}
const std::vector<Buff>attackBuffs{player->GetStatBuffs({"Attack","Attack %"})};
const std::vector<Buff>movespeedBuffs{player->GetBuffs(BuffType::SPEEDBOOST)};
const std::vector<Buff>adrenalineRushBuffs{player->GetBuffs(BuffType::ADRENALINE_RUSH)};
const std::vector<Buff>damageReductionBuffs{player->GetBuffs(BuffType::DAMAGE_REDUCTION)};
const std::vector<Buff>inkSlowdownDebuff{player->GetBuffs(BuffType::INK_SLOWDOWN)};
const std::vector<Buff>curseDebuff{player->GetBuffs(BuffType::PIRATE_GHOST_CAPTAIN_CURSE_DOT)};
const std::vector<Buff>hastenBuff{player->GetBuffs(BuffType::HASTEN)};
const bool displayCoinSymbol{player->GetBuffs(BuffType::PIRATE_GHOST_CAPTAIN_CURSE_COIN).size()>0};
const bool displayCoinSymbol{player->HasBuff(BuffType::PIRATE_GHOST_CAPTAIN_CURSE_COIN)};
Pixel playerCol{WHITE};
if(attackBuffs.size()>0)playerCol={255,uint8_t(255*abs(sin(1.4f*attackBuffs[0].totalTimeAlive))),uint8_t(255*abs(sin(1.4f*attackBuffs[0].totalTimeAlive)))};
else if(adrenalineRushBuffs.size()>0)playerCol={uint8_t(255*abs(sin(6.f*adrenalineRushBuffs[0].totalTimeAlive))),255,uint8_t(255*abs(sin(6.f*adrenalineRushBuffs[0].totalTimeAlive)))};
else if(movespeedBuffs.size()>0)playerCol={uint8_t(255*abs(sin(2.f*movespeedBuffs[0].totalTimeAlive))),255,uint8_t(255*abs(sin(2.f*movespeedBuffs[0].totalTimeAlive)))};
else if(inkSlowdownDebuff.size()>0)playerCol={uint8_t(255*abs(sin(2.f*inkSlowdownDebuff[0].totalTimeAlive))),uint8_t(255*abs(sin(2.f*inkSlowdownDebuff[0].totalTimeAlive))),uint8_t(255*abs(sin(2.f*inkSlowdownDebuff[0].totalTimeAlive)))};
else if(curseDebuff.size()>0)playerCol={uint8_t(128*abs(sin(2.f*GetRunTime()))+127),uint8_t(128*abs(sin(2.f*GetRunTime()))),uint8_t(128*abs(sin(2.f*GetRunTime()))+127)};
else if(hastenBuff.size()>0)playerCol={uint8_t(abs(sin(0.4f*GetRunTime()))+127),uint8_t(0),uint8_t(0)};
else if(auto&buff{player->GetBuff(BuffType::ADRENALINE_RUSH)};buff)playerCol={uint8_t(255*abs(sin(6.f*(*buff).totalTimeAlive))),255,uint8_t(255*abs(sin(2.f*(*buff).totalTimeAlive)))};
else if(auto&buff{player->GetBuff(BuffType::INK_SLOWDOWN)};buff)playerCol={uint8_t(255*abs(sin(2.f*(*buff).totalTimeAlive))),255,uint8_t(255*abs(sin(6.f*(*buff).totalTimeAlive)))};
else if(auto&buff{player->GetBuff(BuffType::FREEZEGROUND_SLOWDOWN)};buff)playerCol={uint8_t(192*abs(sin(3.f*(*buff).totalTimeAlive))),uint8_t(192*abs(sin(3.f*(*buff).totalTimeAlive))),uint8_t(255*abs(sin(3.f*(*buff).totalTimeAlive)))};
else if(auto&buff{player->GetBuff(BuffType::SPEEDBOOST)};buff)playerCol={uint8_t(255*abs(sin(2.f*(*buff).totalTimeAlive))),uint8_t(255*abs(sin(2.f*(*buff).totalTimeAlive))),uint8_t(255*abs(sin(2.f*(*buff).totalTimeAlive)))};
else if(player->HasBuff(BuffType::PIRATE_GHOST_CAPTAIN_CURSE_DOT))playerCol={uint8_t(128*abs(sin(2.f*GetRunTime()))+127),uint8_t(128*abs(sin(2.f*GetRunTime()))),uint8_t(128*abs(sin(2.f*GetRunTime()))+127)};
else if(player->HasBuff(BuffType::HASTEN))playerCol={uint8_t(abs(sin(0.4f*GetRunTime()))+127),uint8_t(0),uint8_t(0)};
if(player->HasIframes())playerCol.a*=0.62f;
if(damageReductionBuffs.size()>0)view.DrawPartialSquishedRotatedDecal(pos+vf2d{0,-player->GetZ()*(std::signbit(scale.y)?-1:1)},player->playerOutline.Decal(),player->GetSpinAngle(),{12,12},animationFrame.get().GetSourceRect().pos,animationFrame.get().GetSourceRect().size,playerScale*scale+0.1f,{1.f,player->ySquishFactor},{210,210,210,uint8_t(util::lerp(0,255,abs(sin((PI*GetRunTime())/1.25f))))});
if(player->HasBuff(BuffType::DAMAGE_REDUCTION))view.DrawPartialSquishedRotatedDecal(pos+vf2d{0,-player->GetZ()*(std::signbit(scale.y)?-1:1)},player->playerOutline.Decal(),player->GetSpinAngle(),{12,12},animationFrame.get().GetSourceRect().pos,animationFrame.get().GetSourceRect().size,playerScale*scale+0.1f,{1.f,player->ySquishFactor},{210,210,210,uint8_t(util::lerp(0,255,abs(sin((PI*GetRunTime())/1.25f))))});
view.DrawPartialSquishedRotatedDecal(pos+vf2d{0,-player->GetZ()*(std::signbit(scale.y)?-1:1)},animationFrame.get().GetSourceImage()->Decal(),player->GetSpinAngle(),{12,12},animationFrame.get().GetSourceRect().pos,animationFrame.get().GetSourceRect().size,playerScale*scale,{1.f,player->ySquishFactor},playerCol);

View File

@ -45,8 +45,8 @@ INCLUDE_game
Blizzard::Blizzard(const vf2d pos,const float radius, const float lifetime,const int damage,const float tickRate,const std::pair<MinScale,MaxScale>snowSizeRange,const float emitterFreq,const bool upperLevel,const FriendlyType friendly)
:damage(damage),tickRate(tickRate),tickTimer(tickRate),radius(radius),friendly(friendly),blizzardSFX("Blizzard",pos),Effect(pos,lifetime,"aiming_target.png",upperLevel,(radius*2)*vf2d{1.f,1.f}/24.f,0.5f,{},{199,247,239,128},0.f,0.f,true)
,snow(std::make_unique<BlizzardSnowEmitter>(pos,"circle.png",radius,snowSizeRange,emitterFreq,lifetime,upperLevel)){
for(int i:std::ranges::iota_view(0,100))snow->Emit();
,snow(std::make_unique<BlizzardSnowEmitter>(pos,"circle.png",radius*1.25f,snowSizeRange,emitterFreq,lifetime,upperLevel)){
for(int i:std::ranges::iota_view(0,200))snow->Emit();
}
bool Blizzard::Update(float fElapsedTime){
if(!IsDead()){

View File

@ -70,6 +70,8 @@ enum BuffType{
PIRATE_GHOST_CAPTAIN_CURSE_COIN, //A coin icon appears above the player's head.
PIRATE_GHOST_CAPTAIN_CURSE_DOT, //The same as above, but now is a damage over time as well.
HASTEN, //Any unit under this effect gets X% Attack,Movement speed, and Animation speed
SPEED_UP_SPELL_BUFF,
FREEZEGROUND_SLOWDOWN,
};
enum class BuffRestorationType{
ONE_OFF, //This is used as a hack fix for the RestoreDuringCast Item script since they require us to restore 1 tick immediately. Over time buffs do not apply a tick immediately.

View File

@ -38,6 +38,7 @@ All rights reserved.
#include "Entity.h"
#include "Player.h"
#include "MonsterStrategyHelpers.h"
#define is(type) std::holds_alternative<type>(this->entity)
#define get(type) std::get<type>(this->entity)
@ -77,4 +78,27 @@ bool Entity::Hurt(int damage,bool onUpperLevel,float z,HurtFlag::HurtFlag hurtFl
std::optional<Buff>Entity::EditBuff(BuffType buff){
CallClassFunc(EditBuff(buff));
}
const bool Entity::IsSkeletonLightningMage()const{
if(is(Player*)){return false;}
else if(is(Monster*)){return DATA.HasProperty(std::format("MonsterStrategy.{}.Is Skeleton Lightning Mage?",get(Monster*)->GetStrategyName()))&&Monster::STRATEGY::_GetBool(*get(Monster*),"Is Skeleton Lightning Mage?",get(Monster*)->GetStrategyName());}
ERR("Entity is not a valid type! THIS SHOULD NOT BE HAPPENING!");
std::unreachable();
};
const bool Entity::IsBoss()const{
if(is(Player*)){return false;}
else if(is(Monster*)){return get(Monster*)->isBoss;}
ERR("Entity is not a valid type! THIS SHOULD NOT BE HAPPENING!");
std::unreachable();
};
const std::vector<Buff>Entity::GetBuffs(BuffType buff)const{
CallClassFunc(GetBuffs(buff));
}
const bool Entity::OnUpperLevel()const{
CallClassFunc(OnUpperLevel());
}

View File

@ -60,6 +60,10 @@ public:
const float GetDistanceFrom(vf2d target)const;
bool Hurt(int damage,bool onUpperLevel,float z,HurtFlag::HurtFlag hurtFlags=HurtFlag::NONE);
std::optional<Buff>EditBuff(BuffType buff);
const bool IsSkeletonLightningMage()const;
const bool IsBoss()const;
const std::vector<Buff>GetBuffs(BuffType buff)const;
const bool OnUpperLevel()const;
private:
const std::variant<Monster*,Player*>entity;
inline bool operator==(const Entity&rhs){return entity==rhs.entity;}

View File

@ -15,7 +15,7 @@ bool FreezeGround::Update(float fElapsedTime){
tickTimer+=fElapsedTime;
while(tickTimer>=settings.slowdownFrequency){
for(Entity&target:game->GetTargetsInRange(pos,settings.radius/100.f*24,OnUpperLevel(),GetZ(),friendly?HurtType::MONSTER:HurtType::PLAYER)){
Buff&slowdownBuff{target.GetOrAddBuff(BuffType::SLOWDOWN,{settings.slowdownFrequency,0.f})};
Buff&slowdownBuff{target.GetOrAddBuff(BuffType::FREEZEGROUND_SLOWDOWN,{settings.slowdownFrequency,0.f})};
slowdownBuff.monsterBuffCallbackFunc=[settings=settings](std::weak_ptr<Monster>m,Buff&buff){
int stacks=round(buff.intensity/settings.slowdownPerStack*100.f);
@ -38,7 +38,7 @@ bool FreezeGround::Update(float fElapsedTime){
std::ranges::for_each(entitiesThatHaveLeft,[&slowdownPerStack=settings.slowdownPerStack](auto&pair){
auto&[key,entity]=pair;
if(auto buffCheck{entity.EditBuff(BuffType::SLOWDOWN)};buffCheck){
if(auto buffCheck{entity.EditBuff(BuffType::FREEZEGROUND_SLOWDOWN)};buffCheck){
Buff&slowdownBuff{*buffCheck};
int stacks=round(slowdownBuff.intensity/slowdownPerStack*100.f);
if(stacks>1)stacks--;

View File

@ -64,7 +64,6 @@ INCLUDE_DAMAGENUMBER_LIST
INCLUDE_game
INCLUDE_BULLET_LIST
INCLUDE_DATA
INCLUDE_GFX
INCLUDE_SPAWNER_CONTROLLER
INCLUDE_SPAWNER_LIST
@ -111,16 +110,10 @@ float Monster::GetMoveSpdMult()const{
float moveSpdPct=stats.A_Read("Move Spd %")/100.f;
float mod_moveSpd=moveSpdPct;
for(const Buff&b:GetBuffs(SLOWDOWN)){
mod_moveSpd-=moveSpdPct*b.intensity;
for(const Buff&debuff:GetBuffs({SLOWDOWN,SELF_INFLICTED_SLOWDOWN,BLOCK_SLOWDOWN,INK_SLOWDOWN,FREEZEGROUND_SLOWDOWN})){
mod_moveSpd-=moveSpdPct*debuff.intensity;
}
for(const Buff&b:GetBuffs(SELF_INFLICTED_SLOWDOWN)){
mod_moveSpd-=moveSpdPct*b.intensity;
}
for(const Buff&b:GetBuffs(LOCKON_SPEEDBOOST)){
mod_moveSpd+=moveSpdPct*b.intensity;
}
for(const Buff&b:GetBuffs(SPEEDBOOST)){
for(const Buff&b:GetBuffs({LOCKON_SPEEDBOOST,SPEEDBOOST,SPEED_UP_SPELL_BUFF,ADRENALINE_RUSH})){
mod_moveSpd+=moveSpdPct*b.intensity;
}
return mod_moveSpd;
@ -534,7 +527,6 @@ void Monster::Draw()const{
if(GetBuffs(BuffType::GLOW_PURPLE).size()>0)glowPurpleBuff=GetBuffs(BuffType::GLOW_PURPLE)[0];
const auto HasBuff=[&](const BuffType buff){return GetBuffs(buff).size()>0;};
//The lerpCutAmount is how much to divide the initial color by, which is used as the lerp oscillation amount. 0.5 means half the color is always active, and the other half linearly oscillates. 0.1 would mean 90% of the color is normal and 10% of the color oscillates.
const auto GetBuffBlendCol=[&](const BuffType buff,const float oscillationTime_s,const Pixel blendCol,const float lerpCutAmount=0.5f){return Pixel{uint8_t(blendCol.r*(1-lerpCutAmount)+blendCol.r*lerpCutAmount*abs(sin(oscillationTime_s*PI*GetBuffs(buff)[0].totalTimeAlive))),uint8_t(blendCol.g*(1-lerpCutAmount)+blendCol.g*lerpCutAmount*abs(sin(oscillationTime_s*PI*GetBuffs(buff)[0].totalTimeAlive))),uint8_t(blendCol.b*(1-lerpCutAmount)+blendCol.b*lerpCutAmount*abs(sin(oscillationTime_s*PI*GetBuffs(buff)[0].totalTimeAlive)))};};
@ -544,6 +536,7 @@ void Monster::Draw()const{
else if(HasBuff(BuffType::COLOR_MOD))blendCol=GetBuffBlendCol(BuffType::COLOR_MOD,1.4f,PixelRaw(GetBuffs(BuffType::COLOR_MOD)[0].intensity),1.f);
else if(HasBuff(BuffType::SLOWDOWN))blendCol=GetBuffBlendCol(BuffType::SLOWDOWN,1.4f,{255,255,128},0.5f);
else if(HasBuff(BuffType::HASTEN))blendCol=GetBuffBlendCol(BuffType::HASTEN,0.4f,{128,0,0},1.f);
else if(HasBuff(BuffType::SPEED_UP_SPELL_BUFF))blendCol=GetBuffBlendCol(BuffType::SPEED_UP_SPELL_BUFF,0.2f,{192,255,192},0.6f);
else if(glowPurpleBuff.has_value())blendCol=Pixel{uint8_t(255*abs(sin(1.4*glowPurpleBuff.value().lifetime))),uint8_t(255*abs(sin(1.4*glowPurpleBuff.value().lifetime))),uint8_t(128+127*abs(sin(1.4*glowPurpleBuff.value().lifetime)))};
const vf2d hitTimerOffset=vf2d{sin(20*PI*lastHitTimer+randomFrameOffset),0.f}*2.f*GetSizeMult();
@ -1803,9 +1796,9 @@ void Monster::CastAbility(const MonsterAbilityData&data,const vf2d pos){
}
void Monster::PerformSpell(const CastInfo&castData){
MonsterAbility::SPELLS.at(castData.name)(castData.name,castData.castPos,*this,GetStrategyName());
const MonsterAbility::SpellSucceededResult result{MonsterAbility::SPELLS.at(castData.name)(castData.name,castData.castPos,*this,GetStrategyName())};
PerformIdleAnimation();
mp-=castData.mpCost;
if(result== MonsterAbility::SpellSucceededResult::SPELL_COMPLETED)mp-=castData.mpCost;
}
void Monster::RunStrategy(const MonsterStrategy&strategy){

View File

@ -51,6 +51,7 @@ All rights reserved.
#include "HurtDamageInfo.h"
#include "Oscillator.h"
#include"MonsterStrategyHelpers.h"
#include"Entity.h"
INCLUDE_ITEM_DATA
INCLUDE_MONSTER_DATA
@ -66,6 +67,8 @@ namespace MonsterTests{
class MonsterTest;
};
class Entity;
class DeathSpawnInfo{
std::string monsterSpawnName;
uint8_t spawnAmt;
@ -81,6 +84,7 @@ class Monster:public IAttributable{
friend class DeathSpawnInfo;
friend class MonsterTests::MonsterTest;
friend struct MonsterData;
friend const bool Entity::IsBoss()const;
public:
enum MonsterStrategy{
RUN_TOWARDS,
@ -131,6 +135,7 @@ public:
static std::string ERR;
static int _GetInt(Monster&m,const std::string&param,const std::string&strategy,int index=0);
static float _GetFloat(Monster&m,const std::string&param,const std::string&strategy,int index=0);
static bool _GetBool(Monster&m,const std::string&param,const std::string&strategy,int index=0);
static Pixel _GetPixel(Monster&m,const std::string&param,const std::string&strategy,int index=0);
//Converts unit distances to pixels. (Every 100 units = 24 pixels)
static float _GetPixels(Monster&m,const std::string&param,const std::string&strategy,int index=0);

View File

@ -3,20 +3,24 @@
#include"MonsterStrategyHelpers.h"
#include"BulletTypes.h"
#include "util.h"
#include<algorithm>
INCLUDE_game
std::unordered_map<std::string_view,std::function<void(SpellName,CastPos,CasterMonster&,const StrategyName&)>>MonsterAbility::SPELLS{
{"Meteor",[](SpellName spellName,CastPos pos,CasterMonster&m,const StrategyName&strategy)->void{
std::unordered_map<std::string_view,std::function<MonsterAbility::SpellSucceededResult(SpellName,CastPos,CasterMonster&,const StrategyName&)>>MonsterAbility::SPELLS{
{"Meteor",[](SpellName spellName,CastPos pos,CasterMonster&m,const StrategyName&strategy)->SpellSucceededResult{
game->AddEffect(std::make_unique<Meteor>(pos,3.f,m.OnUpperLevel(),Meteor::SKELETON_MAGE,std::pair<MeteorDamage,PulsatingFireDamage>{int(m.GetAttack()*ConfigFloat("Meteor Damage Mult")),int(m.GetAttack()*ConfigFloat("Fire Ring Damage Mult"))},"Wizard.Ability 3.MeteorFadeoutTime"_F));
return SpellSucceededResult::SPELL_COMPLETED;
}},
{"Fire Bolt",[](SpellName spellName,CastPos pos,CasterMonster&m,const StrategyName&strategy)->void{
{"Fire Bolt",[](SpellName spellName,CastPos pos,CasterMonster&m,const StrategyName&strategy)->SpellSucceededResult{
CreateBullet(FireBolt)(m.GetPos(),util::pointTo(m.GetPos(),game->GetPlayer()->GetPos())*ConfigFloat("Fire Bolt Bullet Speed"),"Wizard.Ability 1.Radius"_F/100*12,m.GetAttack()*ConfigFloat("Fire Bolt Damage Mult"),m.OnUpperLevel())EndBullet;
return SpellSucceededResult::SPELL_COMPLETED;
}},
{"Blizzard",[](SpellName spellName,CastPos pos,CasterMonster&m,const StrategyName&strategy)->void{
game->AddEffect(std::make_unique<Blizzard>(pos,ConfigPixels("Blizzard Radius"),ConfigFloat("Blizzard Duration"),m.GetAttack()*ConfigFloat("Blizzard Tick Damage Mult"),ConfigFloat("Blizzard Tick Rate"),std::pair<MinScale,MaxScale>{1.f,2.f},0.1f,m.OnUpperLevel(),NON_FRIENDLY));
{"Blizzard",[](SpellName spellName,CastPos pos,CasterMonster&m,const StrategyName&strategy)->SpellSucceededResult{
game->AddEffect(std::make_unique<Blizzard>(pos,ConfigPixels("Blizzard Radius"),ConfigFloat("Blizzard Duration"),m.GetAttack()*ConfigFloat("Blizzard Tick Damage Mult"),ConfigFloat("Blizzard Tick Rate"),std::pair<MinScale,MaxScale>{1.f,2.f},0.05f,m.OnUpperLevel(),NON_FRIENDLY));
return SpellSucceededResult::SPELL_COMPLETED;
}},
{"Freeze Ground",[](SpellName spellName,CastPos pos,CasterMonster&m,const StrategyName&strategy)->void{
{"Freeze Ground",[](SpellName spellName,CastPos pos,CasterMonster&m,const StrategyName&strategy)->SpellSucceededResult{
const FreezeGround::FreezeGroundSettings settings{
.radius=ConfigFloat("Freeze Ground Radius"),
.slowdownPerStack=ConfigFloat("Freeze Ground Slowdown Per Stack"),
@ -29,5 +33,26 @@ std::unordered_map<std::string_view,std::function<void(SpellName,CastPos,CasterM
.lifetime=ConfigFloat("Freeze Ground Lifetime"),
};
game->AddEffect(std::make_unique<FreezeGround>(pos,ConfigFloat("Freeze Ground Lifetime"),m.GetAttack(),settings,m.OnUpperLevel(),NON_FRIENDLY),true);
return SpellSucceededResult::SPELL_COMPLETED;
}},
{"Speed Up",[](SpellName spellName,CastPos pos,CasterMonster&m,const StrategyName&strategy)->SpellSucceededResult{
if(const auto&entitiesInRange{game->GetTargetsInRange(m.GetPos(),ConfigPixels("Speed Up Radius"),m.OnUpperLevel(),m.GetZ(),HurtType::MONSTER)|std::views::filter([](const Entity&entity){return !entity.IsSkeletonLightningMage()&&!entity.IsBoss();})|std::ranges::to<std::vector>()};entitiesInRange.size()>0){
std::vector<Entity>randomEntityChosenList;
std::ranges::sample(entitiesInRange,std::back_inserter(randomEntityChosenList),1,util::GetRNG());
if(randomEntityChosenList.size()!=1)ERR(std::format("WARNING! Expected only 1 element! ({} inside container)",randomEntityChosenList.size()));
Entity&chosenEntity{randomEntityChosenList.at(0)};
Buff&speedBoostBuff{chosenEntity.GetOrAddBuff(BuffType::SPEED_UP_SPELL_BUFF,{ConfigFloat("Speed Up Boost Duration"),ConfigFloat("Speed Up Boost Amount")/100.f})};
speedBoostBuff.lifetime=speedBoostBuff.originalDuration;
for(int i:std::ranges::iota_view(0,30)){
vf2d polarRandomCoord{util::random_range(0,24),util::degToRad(util::random(360))};
game->AddEffect(std::make_unique<Effect>(chosenEntity.GetPos()+polarRandomCoord.cart(),util::random_range(0,3),"circle.png",chosenEntity.OnUpperLevel(),util::random_range(0.5f,1.75f)*vf2d{1,1},1.f,vf2d{0.f,-util::random_range(3.f,10.f)},PixelLerp({255,255,255,uint8_t(util::random_range(128,255))},{0,255,0,uint8_t(util::random_range(128,255))},util::random(1)),util::random(2*PI),0.f,false));
}
return SpellSucceededResult::SPELL_COMPLETED;
}
return SpellSucceededResult::SPELL_FAILED;
}},
{"Thunderorb",[](SpellName spellName,CastPos pos,CasterMonster&m,const StrategyName&strategy)->SpellSucceededResult{
//game->AddEffect(std::make_unique<Blizzard>(pos,ConfigPixels("Blizzard Radius"),ConfigFloat("Blizzard Duration"),m.GetAttack()*ConfigFloat("Blizzard Tick Damage Mult"),ConfigFloat("Blizzard Tick Rate"),std::pair<MinScale,MaxScale>{1.f,2.f},0.05f,m.OnUpperLevel(),NON_FRIENDLY));
return SpellSucceededResult::SPELL_FAILED;
}},
};

View File

@ -15,5 +15,9 @@ using CasterMonster=Monster;
class MonsterAbility{
public:
static std::unordered_map<std::string_view,std::function<void(SpellName,CastPos,CasterMonster&,const StrategyName&)>>SPELLS;
enum class SpellSucceededResult:bool{
SPELL_COMPLETED=true,
SPELL_FAILED=false,
};
static std::unordered_map<std::string_view,std::function<SpellSucceededResult(SpellName,CastPos,CasterMonster&,const StrategyName&)>>SPELLS;
};

View File

@ -43,6 +43,7 @@ using StrategyName=std::string;
#pragma once
#define ConfigInt(param) Monster::STRATEGY::_GetInt(m,param,strategy)
#define ConfigFloat(param) Monster::STRATEGY::_GetFloat(m,param,strategy)
#define ConfigBool(param) Monster::STRATEGY::_GetBool(m,param,strategy)
#define ConfigPixel(param) Monster::STRATEGY::_GetPixel(m,param,strategy)
//Converts unit distances to pixels. (Every 100 units = 24 pixels)
#define ConfigPixels(param) Monster::STRATEGY::_GetPixels(m,param,strategy)

View File

@ -313,27 +313,15 @@ const int Player::GetDefense()const{
float Player::GetMoveSpdMult(){
float moveSpdPct=GetEquipStat("Move Spd %")/100.f;
float mod_moveSpd=moveSpdPct;
for(const Buff&b:GetBuffs(BuffType::SLOWDOWN)){
mod_moveSpd-=moveSpdPct*b.intensity;
for(const Buff&debuff:GetBuffs({SLOWDOWN,SELF_INFLICTED_SLOWDOWN,BLOCK_SLOWDOWN,INK_SLOWDOWN,FREEZEGROUND_SLOWDOWN})){
mod_moveSpd-=moveSpdPct*debuff.intensity;
}
for(const Buff&b:GetBuffs(BuffType::BLOCK_SLOWDOWN)){
mod_moveSpd-=moveSpdPct*b.intensity;
}
for(const Buff&b:GetBuffs(BuffType::INK_SLOWDOWN)){
mod_moveSpd-=moveSpdPct*b.intensity;
}
for(const Buff&b:GetBuffs(LOCKON_SPEEDBOOST)){
mod_moveSpd+=moveSpdPct*b.intensity;
}
for(const Buff&b:GetBuffs(SPEEDBOOST)){
for(const Buff&b:GetBuffs({LOCKON_SPEEDBOOST,SPEEDBOOST,SPEED_UP_SPELL_BUFF,ADRENALINE_RUSH})){
mod_moveSpd+=moveSpdPct*b.intensity;
}
for(const Buff&b:GetStatBuffs({"Move Spd %"})){
mod_moveSpd+=moveSpdPct*b.intensity;
}
for(const Buff&b:GetBuffs(BuffType::ADRENALINE_RUSH)){
mod_moveSpd+=moveSpdPct*"Thief.Ability 3.Movement Speed Increase"_F/100.f;
}
return mod_moveSpd;
}
@ -1187,6 +1175,10 @@ const std::vector<Buff>Player::GetBuffs(BuffType buff)const{
return buffList|std::views::filter([&buff](const Buff&b){return b.type==buff;})|std::ranges::to<std::vector<Buff>>();
}
const std::vector<Buff>Player::GetBuffs(std::vector<BuffType>buffs)const{
return buffList|std::views::filter([&buffs](const Buff&b){return std::find(buffs.begin(),buffs.end(),b.type)!=buffs.end();})|std::ranges::to<std::vector>();
}
void Player::RemoveBuff(BuffType buff){
for(auto it=buffList.begin();it!=buffList.end();++it){
Buff&b=*it;
@ -2357,4 +2349,4 @@ const float Player::GetHastePct()const{
const float Player::GetDistanceFrom(vf2d target)const{
return geom2d::line<float>(GetPos(),target).length();
}
}

View File

@ -180,6 +180,7 @@ public:
Buff&AddBuff(BuffType type,BuffRestorationType restorationType,BuffOverTimeType::BuffOverTimeType overTimeType,float duration,float intensity,float timeBetweenTicks,Buff::PlayerBuffExpireCallbackFunction expireCallbackFunc);
const bool HasBuff(BuffType buff)const;
const std::vector<Buff>GetBuffs(BuffType buff)const;
const std::vector<Buff>GetBuffs(std::vector<BuffType>buffs)const;
const std::vector<Buff>GetStatBuffs(const std::vector<std::string>&attr)const;
const std::optional<Buff>GetBuff(BuffType buff)const;

View File

@ -121,6 +121,20 @@ float Monster::STRATEGY::_GetFloat(Monster&m,const std::string&param,const std::
return{};
}
}
bool Monster::STRATEGY::_GetBool(Monster&m,const std::string&param,const std::string&strategy,int index){
if(m.IsNPC()&&DATA["NPCs"][m.name].HasProperty(param)){
return float(DATA["NPCs"][m.name].GetProperty(param).GetBool(index));
}else
if(!m.IsNPC()&&DATA["Monsters"][m.name].HasProperty(param)){
return float(DATA["Monsters"][m.name].GetProperty(param).GetBool(index));
}else
if(DATA["MonsterStrategy"][strategy].HasProperty(param)){
return float(DATA["MonsterStrategy"][strategy].GetProperty(param).GetBool(index));
}else{
ERR(std::format("Monster {} trying to read non-existent Float Property {}[{}] for Strategy {}. THIS SHOULD NOT BE HAPPENING!",m.GetName(),param,index,strategy))
return{};
}
}
const std::string&Monster::STRATEGY::_GetString(Monster&m,const std::string&param,const std::string&strategy,int index){
if(m.IsNPC()&&DATA["NPCs"][m.name].HasProperty(param)){
return DATA["NPCs"][m.name].GetProperty(param).GetString(index);

View File

@ -39,7 +39,7 @@ All rights reserved.
#define VERSION_MAJOR 1
#define VERSION_MINOR 3
#define VERSION_PATCH 0
#define VERSION_BUILD 12914
#define VERSION_BUILD 12939
#define stringify(a) stringify_(a)
#define stringify_(a) #a

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.10.2" class="Map" orientation="orthogonal" renderorder="right-down" width="315" height="238" tilewidth="24" tileheight="24" infinite="0" nextlayerid="7" nextobjectid="17">
<map version="1.10" tiledversion="1.10.2" class="Map" orientation="orthogonal" renderorder="right-down" width="315" height="238" tilewidth="24" tileheight="24" infinite="0" nextlayerid="7" nextobjectid="23">
<properties>
<property name="Background Music" propertytype="BGM" value="undead_swamp"/>
<property name="Level Type" type="int" propertytype="LevelType" value="0"/>
@ -990,7 +990,27 @@
<object id="6" name="Monster Spawn Zone" type="SpawnGroup" x="438" y="4770" width="216" height="246">
<ellipse/>
</object>
<object id="16" template="../maps/Monsters/Skeleton Frost Mage.tx" x="612" y="4794">
<object id="17" template="../maps/Monsters/Skeleton Lightning Mage.tx" x="606" y="4788">
<properties>
<property name="spawner" type="object" value="6"/>
</properties>
</object>
<object id="18" template="../maps/Monsters/Crab.tx" x="618" y="4842">
<properties>
<property name="spawner" type="object" value="6"/>
</properties>
</object>
<object id="20" template="../maps/Monsters/Skeleton Lightning Mage.tx" x="570" y="4794">
<properties>
<property name="spawner" type="object" value="6"/>
</properties>
</object>
<object id="21" template="../maps/Monsters/Skeleton Frost Mage.tx" x="606" y="4806">
<properties>
<property name="spawner" type="object" value="6"/>
</properties>
</object>
<object id="22" template="../maps/Monsters/Skeleton Frost Mage.tx" x="600" y="4830">
<properties>
<property name="spawner" type="object" value="6"/>
</properties>

View File

@ -1456,5 +1456,11 @@ MonsterStrategy
Freeze Ground Stacks Removed Immediately On Leave = 1
Freeze Ground Stack Removal Rate = 1s
Freeze Ground Lifetime = 10s
Speed Up Radius = 400
Speed Up Boost Amount = 20%
Speed Up Boost Duration = 20s
Is Skeleton Lightning Mage? = false
}
}

View File

@ -2240,7 +2240,7 @@ Monsters
{
#Ability Name = Mana cost, % chance to cast (every second), Cast Time (Instant = 0.0s), [Variable representing the cast radius]
Blizzard = 60mp, 60%, 1.5sec, MonsterStrategy.Skeleton Mage.Blizzard Radius
# Freeze Ground = 30mp, 20%, 0.0sec
Freeze Ground = 30mp, 20%, 0.0sec
}
Strategy = Skeleton Mage
@ -2267,6 +2267,58 @@ Monsters
Death Sound = Slime Dead
Walk Sound = Slime Walk
# Drop Item Name, Drop Percentage(0-100%), Drop Min Quantity, Drop Max Quantity
#DROP[0] = Broken Bow,30%,1,1
}
Skeleton Lightning Mage
{
Health = 600
Attack = 44
CollisionDmg = 44
MoveSpd = 80%
Size = 90%
XP = 49
MP = 0
MP Recovery = 5mp/s
Abilities
{
#Ability Name = Mana cost, % chance to cast (every second), Cast Time (Instant = 0.0s), [Variable representing the cast radius]
Speed Up = 20mp, 15%, 0.0sec
Thunderorb = 40mp, 25%, 0.0sec
}
Strategy = Skeleton Mage
# Override Property from Strategy
Is Skeleton Lightning Mage? = true
#Size of each animation frame
SheetFrameSize = 32,32
# Setting this to true means every four rows indicates one animation, the ordering of the directions is: NORTH, EAST, SOUTH, WEST
4-Way Spritesheet = False
Animations
{
# Frame Count, Frame Speed (s), Frame Cycling (Repeat,OneShot,PingPong,Reverse,ReverseOneShot)
# Animations must be defined in the same order as they are in their sprite sheets
# The First Four animations must represent a standing, walking, attack, and death animation. Their names are up to the creator.
IDLE = 2, 0.6, Repeat
WALK = 3, 0.2, Repeat
ATTACK = 4, 0.1, Repeat
DEATH = 4, 0.15, OneShot
CASTING = 4, 0.1, Repeat
}
Hurt Sound = Monster Hurt
Death Sound = Slime Dead
Walk Sound = Slime Walk
# Drop Item Name, Drop Percentage(0-100%), Drop Min Quantity, Drop Max Quantity
#DROP[0] = Broken Bow,30%,1,1
}

View File

@ -39,7 +39,7 @@ CAMPAIGN_3_6 = 2026-01-21 20:30:58.4008008
CAMPAIGN_3_7 = 2026-01-21 20:30:58.4023071
CAMPAIGN_3_8 = 2026-01-21 20:30:58.4049027
CAMPAIGN_3_B1 = 2026-01-21 20:30:58.4336641
CAMPAIGN_4_1 = 2026-02-26 21:44:13.5188507
CAMPAIGN_4_1 = 2026-03-06 21:16:02.6682120
CAMPAIGN_4_2 = 2026-02-26 21:44:13.5210572
CAMPAIGN_4_4 = 2026-02-26 21:44:13.5270745
CAMPAIGN_4_6 = 2026-02-26 21:44:13.5342554

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<template>
<tileset firstgid="1" source="../Monsters.tsx"/>
<object name="Skeleton Frost Mage" type="Monster" gid="40" width="48" height="48"/>
<object name="Skeleton Frost Mage" type="Monster" gid="40" width="43.2" height="43.2"/>
</template>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<template>
<tileset firstgid="1" source="../Monsters.tsx"/>
<object name="Skeleton Lightning Mage" type="Monster" gid="41" width="43.2" height="43.2"/>
</template>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -1760,7 +1760,7 @@ namespace olc
{
if (id != -1)
{
renderer->DeleteTexture(id);
renderer->DeleteTexture(id);
id = -1;
}
}

View File

@ -196,6 +196,7 @@ namespace olc::utils
// Checks if a property exists - useful to avoid creating properties
// via reading them, though non-essential
// Access a datafile via a convenient name - "root.node.something.property"
inline bool HasProperty(const std::string& sName)
{
size_t x = sName.find_first_of('.');

View File

@ -53,6 +53,10 @@ int util::random(){
return distrib(rng);
}
std::mt19937&util::GetRNG(){
return rng;
}
const float util::random_range(const float min,const float max){
return random(max-min)+min;
}

View File

@ -50,6 +50,7 @@ namespace olc::util{
float random(float range);
//Returns a random float value min(inclusive) to max(exclusive).
const float random_range(const float min,const float max);
std::mt19937&GetRNG();
//Returns 0-32767 (as an int).
int random();
//Returns a normalized vector pointing from posFrom towards posTo.