Implement mounted monster behavior and animations that run separately from the main monster itself. Added Goblin Boar Rider AI and Goblin Bow (while on Boar Rider) AI. Added spawn of submonster on death of main mounted monster. Release Build 9199.

pull/57/head
sigonasr2 7 months ago
parent c83f84f29c
commit 5c7e5a3ab3
  1. 2
      Adventures in Lestoria/Adventures in Lestoria.vcxproj
  2. 3
      Adventures in Lestoria/AdventuresInLestoria.cpp
  3. 2
      Adventures in Lestoria/AdventuresInLestoria.h
  4. 10
      Adventures in Lestoria/Animation.cpp
  5. 34
      Adventures in Lestoria/Goblin_Boar_Rider.cpp
  6. 26
      Adventures in Lestoria/Monster.cpp
  7. 18
      Adventures in Lestoria/Monster.h
  8. 2
      Adventures in Lestoria/RUN_STRATEGY.cpp
  9. 2
      Adventures in Lestoria/Version.h
  10. 14
      Adventures in Lestoria/assets/Campaigns/2_1.tmx
  11. 51
      Adventures in Lestoria/assets/config/MonsterStrategies.txt
  12. 8
      Adventures in Lestoria/assets/config/Monsters.txt
  13. BIN
      Adventures in Lestoria/assets/gamepack.pak
  14. 4
      Adventures in Lestoria/olcUTIL_DataFile.h
  15. BIN
      x64/Release/Adventures in Lestoria.exe

@ -712,7 +712,7 @@
</SubType>
</ClCompile>
<ClCompile Include="GameState.cpp" />
<ClCompile Include="MountMonster.cpp">
<ClCompile Include="Goblin_Boar_Rider.cpp">
<SubType>
</SubType>
</ClCompile>

@ -3016,11 +3016,12 @@ void AiL::InitializeLevels(){
Test::RunMapTests();
}
void AiL::SpawnMonster(vf2d pos,MonsterData&data,bool upperLevel,bool isBossSpawn){
Monster&AiL::SpawnMonster(vf2d pos,MonsterData&data,bool upperLevel,bool isBossSpawn){
monstersToBeSpawned.push_back(Monster(pos,data,upperLevel,isBossSpawn));
if(isBossSpawn){
totalBossEncounterMobs++;
}
return monstersToBeSpawned.back();
}
void AiL::DrawPie(vf2d center,float radius,float degreesCut,Pixel col){

@ -277,7 +277,7 @@ public:
void RenderTile(vi2d pos,TilesheetData tileSheet,int tileSheetIndex,vi2d tileSheetPos);
void RenderTile(TileRenderData&tileSheet,Pixel col);
bool IsReflectiveTile(TilesheetData tileSheet,int tileID);
void SpawnMonster(vf2d pos,MonsterData&data,bool upperLevel=false,bool isBossSpawn=false); //Queues a monster for spawning on the next frame.
Monster&SpawnMonster(vf2d pos,MonsterData&data,bool upperLevel=false,bool isBossSpawn=false); //Queues a monster for spawning on the next frame.
void DrawPie(vf2d center,float radius,float degreesCut,Pixel col);
void DrawSquarePie(vf2d center,float radius,float degreesCut,Pixel col);
void RenderCooldowns();

@ -253,21 +253,19 @@ void sig::Animation::InitializeAnimations(){
Animate2D::FrameSequence goblin_bow_attack_n,goblin_bow_attack_e,goblin_bow_attack_s,goblin_bow_attack_w;
//Idle sequences for the mounted boar bow goblin.
for(int animationIndex=0;animationIndex<4;animationIndex++){
Animate2D::FrameSequence mountAnimation;
Animate2D::FrameSequence mountAnimation{0.6f};
for(int i=0;i<2;i++){
mountAnimation.AddFrame(Animate2D::Frame{&GFX["monsters/commercial_assets/Goblin (Bow)_foreground.png"],{{i*32,animationIndex*32},{32,32}}});
}
ANIMATION_DATA[std::format("GOBLIN_BOW_MOUNTED_{}",animationIndex)]=mountAnimation;
animationIndex++;
}
//Shooting sequences for the mounted boar bow goblin.
for(int animationIndex=0;animationIndex<4;animationIndex++){
Animate2D::FrameSequence mountShootAnimation;
for(int animationIndex=0;animationIndex<4;animationIndex++){
Animate2D::FrameSequence mountShootAnimation{0.06f,Animate2D::Style::OneShot};
for(int i=0;i<4;i++){
mountShootAnimation.AddFrame(Animate2D::Frame{&GFX["monsters/commercial_assets/Goblin (Bow)_foreground.png"],{{i*32,index*32+4*32},{32,32}}});
mountShootAnimation.AddFrame(Animate2D::Frame{&GFX["monsters/commercial_assets/Goblin (Bow)_foreground.png"],{{i*32,animationIndex*32+4*32},{32,32}}});
}
ANIMATION_DATA[std::format("GOBLIN_BOW_ATTACK_{}",animationIndex)]=mountShootAnimation;
animationIndex++;
}
for(auto&dat:GFX){

@ -39,8 +39,11 @@ All rights reserved.
#include "DEFINES.h"
#include "Monster.h"
#include "MonsterStrategyHelpers.h"
#include "BulletTypes.h"
INCLUDE_ANIMATION_DATA
INCLUDE_BULLET_LIST
INCLUDE_game
using A=Attribute;
@ -48,10 +51,39 @@ void Monster::STRATEGY::GOBLIN_BOAR_RIDER(Monster&m,float fElapsedTime,std::stri
//We have access to GOBLIN_BOW_MOUNTED_X and GOBLIN_BOW_ATTACK_X
if(!m.B(A::INITIALIZED_MOUNTED_MONSTER)){
m.B(A::INITIALIZED_MOUNTED_MONSTER)=true;
m.F(A::PERCEPTION_LEVEL)=ConfigFloat("Starting Perception Level");
m.internal_mounted_animState=Animate2D::AnimationState{};
m.mounted_animation=Animate2D::Animation<std::string>{};
for(const std::string&animation:Config("Imposed Monster Animations").GetValues()){
for(bool firstAnimation=true;const std::string&animation:Config("Imposed Monster Animations").GetValues()){
m.mounted_animation.value().AddState(animation,ANIMATION_DATA.at(animation));
if(firstAnimation)m.mounted_animation.value().ChangeState(m.internal_mounted_animState.value(),animation);
firstAnimation=false;
}
m.mountedSprOffset=ConfigVec("Imposed Monster Offset");
m.deathData.push_back(DeathSpawnInfo{ConfigString("Spawned Monster"),1U});
}
BOAR(m,fElapsedTime,"Boar");
m.F(A::ATTACK_COOLDOWN)+=fElapsedTime;
if(m.F(A::SHOOT_TIMER)>0.f){
m.F(A::SHOOT_TIMER)-=fElapsedTime;
if(m.F(A::SHOOT_TIMER)<=0.f){
geom2d::line pointTowardsPlayer(m.GetPos(),game->GetPlayer()->GetPos());
vf2d extendedLine=pointTowardsPlayer.upoint(1.1f);
CreateBullet(Arrow)(m.GetPos(),extendedLine,pointTowardsPlayer.vector().norm()*ConfigFloat("Arrow Spd"),"goblin_arrow.png",ConfigFloat("Arrow Hitbox Radius"),m.GetAttack(),m.OnUpperLevel())EndBullet;
Arrow&arrow=static_cast<Arrow&>(*BULLET_LIST.back());
arrow.PointToBestTargetPath(m.F(A::PERCEPTION_LEVEL));
m.F(A::ATTACK_COOLDOWN)=0.f;
m.F(A::PERCEPTION_LEVEL)=std::min(ConfigFloat("Maximum Perception Level"),m.F(A::PERCEPTION_LEVEL)+ConfigFloat("Perception Level Increase"));
m.mounted_animation.value().ChangeState(m.internal_mounted_animState.value(),std::format("GOBLIN_BOW_MOUNTED_{}",int(m.GetFacingDirection())));
}
}else
if(m.F(A::ATTACK_COOLDOWN)>=ConfigFloat("Attack Reload Time")){
m.F(A::SHOOT_TIMER)=ConfigFloat("Attack Windup Time");
m.mounted_animation.value().ChangeState(m.internal_mounted_animState.value(),std::format("GOBLIN_BOW_ATTACK_{}",int(m.GetFacingDirection())));
}
}

@ -399,7 +399,7 @@ void Monster::Draw()const{
if(overlaySprite.length()!=0){
game->view.DrawPartialRotatedDecal(GetPos()-vf2d{0,GetZ()},GFX[overlaySprite].Decal(),spriteRot,GetFrame().GetSourceRect().size/2,GetFrame().GetSourceRect().pos,GetFrame().GetSourceRect().size,vf2d(GetSizeMult()*(!HasFourWaySprites()&&GetFacingDirection()==Direction::WEST?-1:1),GetSizeMult()),{blendCol.r,blendCol.g,blendCol.b,overlaySpriteTransparency});
}
if(HasMountedMonster())game->view.DrawPartialRotatedDecal(GetPos()-vf2d{0,GetZ()},GetMountedFrame().GetSourceImage()->Decal(),spriteRot,GetMountedFrame().GetSourceRect().size/2,GetMountedFrame().GetSourceRect().pos,GetMountedFrame().GetSourceRect().size,vf2d(GetSizeMult()*(!HasFourWaySprites()&&GetFacingDirection()==Direction::EAST?-1:1),GetSizeMult()),blendCol);
if(HasMountedMonster())game->view.DrawPartialRotatedDecal(GetPos()-vf2d{0,GetZ()}+mountedSprOffset,GetMountedFrame().value().GetSourceImage()->Decal(),spriteRot,GetMountedFrame().value().GetSourceRect().size/2,GetMountedFrame().value().GetSourceRect().pos,GetMountedFrame().value().GetSourceRect().size,vf2d(GetSizeMult()*(!HasFourWaySprites()&&GetFacingDirection()==Direction::EAST?-1:1),GetSizeMult()),blendCol);
std::vector<Buff>shieldBuffs=GetBuffs(BARRIER_DAMAGE_REDUCTION);
if(shieldBuffs.size()>0){
@ -768,6 +768,15 @@ std::map<ItemInfo*,uint16_t>Monster::SpawnDrops(){
void Monster::OnDeath(){
animation.ChangeState(internal_animState,GetDeathAnimationName());
if(HasMountedMonster()){
for(DeathSpawnInfo&deathInfo:deathData){
deathInfo.Spawn(GetPos(),OnUpperLevel());
}
mounted_animation={};
internal_mounted_animState={};
}
if(isBoss){
game->ReduceBossEncounterMobCount();
if(game->BossEncounterMobCount()==0){
@ -948,10 +957,21 @@ const bool Monster::HasFourWaySprites()const{
const bool Monster::HasMountedMonster()const{
if(internal_mounted_animState.has_value()^mounted_animation.has_value())ERR("WARNING! The internal mounted animation state and the mounted animation variables are not matching! They should both either be on or both be off! THIS SHOULD NOT BE HAPPENING!");
return true;
return internal_mounted_animState.has_value()&&mounted_animation.has_value();
}
const std::optional<const Animate2D::Frame&>Monster::GetMountedFrame()const{
const std::optional<const Animate2D::Frame>Monster::GetMountedFrame()const{
if(!HasMountedMonster())return {};
else return mounted_animation.value().GetFrame(internal_mounted_animState.value());
}
DeathSpawnInfo::DeathSpawnInfo(const std::string_view monsterName,const uint8_t spawnAmt,const vf2d spawnOffset)
:monsterSpawnName(monsterName),spawnAmt(spawnAmt),spawnLocOffset(spawnOffset){
if(!MONSTER_DATA.count(std::string(monsterName)))ERR(std::format("WARNING! Monster {} specified in DeathSpawnInfo does not exist! Please provide a proper monster name.",monsterName));
}
void DeathSpawnInfo::Spawn(const vf2d monsterDeathPos,const bool onUpperLevel){
for(uint8_t i=0;i<spawnAmt;i++){
game->SpawnMonster(monsterDeathPos+spawnLocOffset,MONSTER_DATA.at(monsterSpawnName),onUpperLevel).iframe_timer=0.25f;
}
}

@ -51,7 +51,6 @@ All rights reserved.
INCLUDE_ITEM_DATA
INCLUDE_MONSTER_DATA
INCLUDE_game
struct DamageNumber;
class AiL;
@ -65,21 +64,15 @@ class DeathSpawnInfo{
uint8_t spawnAmt;
vf2d spawnLocOffset;
public:
DeathSpawnInfo(const std::string_view monsterName,const uint8_t spawnAmt,const vf2d spawnOffset={})
:monsterSpawnName(monsterName),spawnAmt(spawnAmt),spawnLocOffset(spawnOffset){
if(!MONSTER_DATA.count(std::string(monsterName)))ERR(std::format("WARNING! Monster {} specified in DeathSpawnInfo does not exist! Please provide a proper monster name.",monsterName));
}
void Spawn(const vf2d monsterDeathPos,const bool onUpperLevel){
for(uint8_t i=0;i<spawnAmt;i++){
game->SpawnMonster(monsterDeathPos+spawnLocOffset,MONSTER_DATA.at(monsterSpawnName),onUpperLevel);
}
}
DeathSpawnInfo(const std::string_view monsterName,const uint8_t spawnAmt,const vf2d spawnOffset={});
void Spawn(const vf2d monsterDeathPos,const bool onUpperLevel);
};
class Monster:IAttributable{
friend struct STRATEGY;
friend class AiL;
friend class InventoryCreator;
friend class DeathSpawnInfo;
public:
Monster()=delete;
Monster(vf2d pos,MonsterData data,bool upperLevel=false,bool bossMob=false);
@ -92,7 +85,7 @@ public:
//Obtains the size multiplier (from 0.f-1.f).
float GetSizeMult()const;
Animate2D::Frame GetFrame()const;
const std::optional<const Animate2D::Frame&>GetMountedFrame()const;
const std::optional<const Animate2D::Frame>GetMountedFrame()const;
bool Update(float fElapsedTime);
//Returns true when damage is actually dealt. Provide whether or not the attack is on the upper level or not. Monsters must be on the same level to get hit by it. (there is a death check and level check here.)
//If you need to hurt multiple enemies try AiL::HurtEnemies()
@ -236,6 +229,7 @@ private:
float prevFacingDirectionAngle=0.f; //Keeps track of the angle of the previous target to ensure four-way sprite facing changes don't happen too early or spastically.
float lastFacingDirectionChange=0.f; //How much time has passed since the last facing direction change. Used to ensure another facing direction change doesn't happen too quickly. Probably allowing one every quarter second is good enough.
std::vector<DeathSpawnInfo>deathData; //Data that contains further actions and information when this monster dies. Mostly used to spawn sub-monsters from a defeated monster.
vf2d mountedSprOffset{};
private:
struct STRATEGY{
static int _GetInt(Monster&m,std::string param,std::string strategy,int index=0);
@ -245,7 +239,7 @@ private:
static double _GetPixels(Monster&m,std::string param,std::string strategy,int index=0);
static vf2d _GetVec(Monster&m,std::string param,std::string strategy,int index=0);
static const std::string&_GetString(Monster&m,std::string param,std::string strategy,int index=0);
static datafile _Get(Monster&m,std::string param,std::string strategy);
static const datafile&_Get(Monster&m,std::string param,std::string strategy);
static void RUN_STRATEGY(Monster&m,float fElapsedTime);
static void RUN_TOWARDS(Monster&m,float fElapsedTime,std::string strategy);
static void SHOOT_AFAR(Monster&m,float fElapsedTime,std::string strategy);

@ -101,7 +101,7 @@ vf2d Monster::STRATEGY::_GetVec(Monster&m,std::string param,std::string strategy
return {DATA["MonsterStrategy"][strategy].GetProperty(param).GetReal(index),DATA["MonsterStrategy"][strategy].GetProperty(param).GetReal(index+1)};
}
}
datafile Monster::STRATEGY::_Get(Monster&m,std::string param,std::string strategy){
const datafile&Monster::STRATEGY::_Get(Monster&m,std::string param,std::string strategy){
if(m.IsNPC()&&DATA["NPCs"][m.name].HasProperty(param)){
return DATA["NPCs"][m.name].GetProperty(param);
}else

@ -39,7 +39,7 @@ All rights reserved.
#define VERSION_MAJOR 1
#define VERSION_MINOR 2
#define VERSION_PATCH 0
#define VERSION_BUILD 9180
#define VERSION_BUILD 9199
#define stringify(a) stringify_(a)
#define stringify_(a) #a

@ -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="238" height="369" tilewidth="24" tileheight="24" infinite="0" nextlayerid="7" nextobjectid="33">
<map version="1.10" tiledversion="1.10.2" class="Map" orientation="orthogonal" renderorder="right-down" width="238" height="369" tilewidth="24" tileheight="24" infinite="0" nextlayerid="7" nextobjectid="34">
<properties>
<property name="Backdrop" propertytype="Backdrop" value="mountain_day"/>
<property name="Background Music" propertytype="BGM" value="foresty1_1"/>
@ -1882,17 +1882,7 @@
<ellipse/>
</object>
<object id="28" name="Player Spawn" type="PlayerSpawnLocation" x="5112" y="8064" width="24" height="24"/>
<object id="30" template="../maps/Monsters/Goblin (Dagger).tx" x="4466" y="8088">
<properties>
<property name="spawner" type="object" value="15"/>
</properties>
</object>
<object id="31" template="../maps/Monsters/Goblin (Dagger).tx" x="4785" y="7901">
<properties>
<property name="spawner" type="object" value="15"/>
</properties>
</object>
<object id="32" template="../maps/Monsters/Goblin (Dagger).tx" x="4565" y="7976">
<object id="33" template="../maps/Monsters/Goblin Boar Rider.tx" x="4511" y="8032">
<properties>
<property name="spawner" type="object" value="15"/>
</properties>

@ -617,8 +617,57 @@ MonsterStrategy
{
# Which monster spawns on death of the boar.
Spawned Monster = Goblin (Bow)
Imposed Monster Spritesheet = Goblin (Bow)_foreground.png
Imposed Monster Offset = 0,-10
Imposed Monster Animations = GOBLIN_BOW_MOUNTED_0,GOBLIN_BOW_MOUNTED_1,GOBLIN_BOW_MOUNTED_2,GOBLIN_BOW_MOUNTED_3,GOBLIN_BOW_ATTACK_0,GOBLIN_BOW_ATTACK_1,GOBLIN_BOW_ATTACK_2,GOBLIN_BOW_ATTACK_3,
###################
# Goblin Bow Stuff
###################
Attack Reload Time = 2.0s
# How long it takes to prepare the attack once an attack is queued.
Attack Windup Time = 1.0s
Arrow Spd = 350
Arrow Hitbox Radius = 8
# The perception level indicates how accurate the bow user's shots become over time. Perception can be between 0-90. A perception level of 90 should never miss. This doesn't necessarily mean lower numbers will miss, just that it doesn't auto-correct for error as much.
Starting Perception Level = 0
# Every shot taken, the bow user's perception level will increase by this amount.
Perception Level Increase = 2.5
Maximum Perception Level = 45
#######################
# END Goblin Bow Stuff
#######################
###################
### Boar Stuff
###################
Closein Range = 700
Backpedal Range = 400
# Number of times the boar scratches the ground before charging.
# The amount of time this takes is also dependent on the animation speed (extra animation 0)
Ground Scratch Count = 2
Charge Movespeed = 130%
Charge Distance = 900
# Amount of time to wait after charging before returning to Move Phase.
Charge Recovery Time = 0.3s
Backpedal Movespeed = 50%
Charge Knockback Amount = 140
###################
### End Boar Stuff
###################
}
}

@ -652,7 +652,7 @@ Monsters
XP = 21
Strategy = Run Towards
Strategy = Goblin Boar Rider
#Size of each animation frame
SheetFrameSize = 32,32
@ -666,9 +666,9 @@ Monsters
# 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 = 1, 0.6, Repeat
JUMP = 1, 0.2, Repeat
SHOOT = 1, 0.2, OneShot
DEATH = 1, 0.15, OneShot
WALK = 4, 0.2, Repeat
SCRATCH = 5, 0.1, Repeat
DEATH = 4, 0.15, OneShot
}
Hurt Sound = Monster Hurt

@ -157,12 +157,12 @@ namespace olc::utils
return m_vContent.size();
}
inline const std::vector<std::string>&GetValues()
inline const std::vector<std::string>&GetValues()const
{
return m_vContent;
}
inline const std::unordered_map<std::string,size_t>&GetKeys(){
inline const std::unordered_map<std::string,size_t>&GetKeys()const{
return m_mapObjects;
}

Loading…
Cancel
Save