diff options
Diffstat (limited to 'source/game/StarHumanoid.cpp')
-rw-r--r-- | source/game/StarHumanoid.cpp | 1245 |
1 files changed, 1245 insertions, 0 deletions
diff --git a/source/game/StarHumanoid.cpp b/source/game/StarHumanoid.cpp new file mode 100644 index 0000000..acbed89 --- /dev/null +++ b/source/game/StarHumanoid.cpp @@ -0,0 +1,1245 @@ +#include "StarHumanoid.hpp" +#include "StarRoot.hpp" +#include "StarJsonExtra.hpp" +#include "StarDataStreamExtra.hpp" +#include "StarArmors.hpp" +#include "StarParticleDatabase.hpp" +#include "StarAssets.hpp" +#include "StarSpeciesDatabase.hpp" +#include "StarDanceDatabase.hpp" + +namespace Star { + +extern EnumMap<HumanoidEmote> const HumanoidEmoteNames{ + {HumanoidEmote::Idle, "Idle"}, + {HumanoidEmote::Blabbering, "Blabbering"}, + {HumanoidEmote::Shouting, "Shouting"}, + {HumanoidEmote::Happy, "Happy"}, + {HumanoidEmote::Sad, "Sad"}, + {HumanoidEmote::NEUTRAL, "NEUTRAL"}, + {HumanoidEmote::Laugh, "Laugh"}, + {HumanoidEmote::Annoyed, "Annoyed"}, + {HumanoidEmote::Oh, "Oh"}, + {HumanoidEmote::OOOH, "OOOH"}, + {HumanoidEmote::Blink, "Blink"}, + {HumanoidEmote::Wink, "Wink"}, + {HumanoidEmote::Eat, "Eat"}, + {HumanoidEmote::Sleep, "Sleep"} +}; + +Personality parsePersonality(Json const& config) { + return Personality{config.getString(0), config.getString(1), jsonToVec2F(config.get(2)), jsonToVec2F(config.get(3))}; +} + +HumanoidIdentity::HumanoidIdentity(Json config) { + if (config.isNull()) + config = JsonObject(); + + name = config.getString("name", "Humanoid"); + species = config.getString("species", "human"); + gender = GenderNames.getLeft(config.getString("gender", "male")); + hairGroup = config.getString("hairGroup", "hair"); + hairType = config.getString("hairType", "male1"); + hairDirectives = config.getString("hairDirectives", ""); + bodyDirectives = config.getString("bodyDirectives", ""); + emoteDirectives = config.getString("emoteDirectives", bodyDirectives); + facialHairGroup = config.getString("facialHairGroup", ""); + facialHairType = config.getString("facialHairType", ""); + facialHairDirectives = config.getString("facialHairDirectives", ""); + facialMaskGroup = config.getString("facialMaskGroup", ""); + facialMaskType = config.getString("facialMaskType", ""); + facialMaskDirectives = config.getString("facialMaskDirectives", ""); + + personality.idle = config.getString("personalityIdle", "idle.1"); + personality.armIdle = config.getString("personalityArmIdle", "idle.1"); + personality.headOffset = jsonToVec2F(config.get("personalityHeadOffset", JsonArray{0, 0})); + personality.armOffset = jsonToVec2F(config.get("personalityArmOffset", JsonArray{0, 0})); + + color = jsonToColor(config.get("color", "white")).toRgba(); + + imagePath = config.optString("imagePath"); +} + +Json HumanoidIdentity::toJson() const { + auto result = JsonObject{ + {"name", name}, + {"species", species}, + {"gender", GenderNames.getRight(gender)}, + {"hairGroup", hairGroup}, + {"hairType", hairType}, + {"hairDirectives", hairDirectives}, + {"bodyDirectives", bodyDirectives}, + {"emoteDirectives", emoteDirectives}, + {"facialHairGroup", facialHairGroup}, + {"facialHairType", facialHairType}, + {"facialHairDirectives", facialHairDirectives}, + {"facialMaskGroup", facialMaskGroup}, + {"facialMaskType", facialMaskType}, + {"facialMaskDirectives", facialMaskDirectives}, + {"personalityIdle", personality.idle}, + {"personalityArmIdle", personality.armIdle}, + {"personalityHeadOffset", jsonFromVec2F(personality.headOffset)}, + {"personalityArmOffset", jsonFromVec2F(personality.armOffset)}, + {"color", jsonFromColor(Color::rgba(color))} + }; + if (imagePath) + result["imagePath"] = *imagePath; + return result; +} + +DataStream& operator>>(DataStream& ds, HumanoidIdentity& identity) { + ds.read(identity.name); + ds.read(identity.species); + ds.read(identity.gender); + ds.read(identity.hairGroup); + ds.read(identity.hairType); + ds.read(identity.hairDirectives); + ds.read(identity.bodyDirectives); + ds.read(identity.emoteDirectives); + ds.read(identity.facialHairGroup); + ds.read(identity.facialHairType); + ds.read(identity.facialHairDirectives); + ds.read(identity.facialMaskGroup); + ds.read(identity.facialMaskType); + ds.read(identity.facialMaskDirectives); + ds.read(identity.personality.idle); + ds.read(identity.personality.armIdle); + ds.read(identity.personality.headOffset); + ds.read(identity.personality.armOffset); + ds.read(identity.color); + ds.read(identity.imagePath); + + return ds; +} + +DataStream& operator<<(DataStream& ds, HumanoidIdentity const& identity) { + ds.write(identity.name); + ds.write(identity.species); + ds.write(identity.gender); + ds.write(identity.hairGroup); + ds.write(identity.hairType); + ds.write(identity.hairDirectives); + ds.write(identity.bodyDirectives); + ds.write(identity.emoteDirectives); + ds.write(identity.facialHairGroup); + ds.write(identity.facialHairType); + ds.write(identity.facialHairDirectives); + ds.write(identity.facialMaskGroup); + ds.write(identity.facialMaskType); + ds.write(identity.facialMaskDirectives); + ds.write(identity.personality.idle); + ds.write(identity.personality.armIdle); + ds.write(identity.personality.headOffset); + ds.write(identity.personality.armOffset); + ds.write(identity.color); + ds.write(identity.imagePath); + + return ds; +} + +Humanoid::HumanoidTiming::HumanoidTiming(Json config) { + if (config.type() != Json::Type::Object) { + auto assets = Root::singleton().assets(); + config = assets->json("/humanoid.config:humanoidTiming"); + } + + if (config.contains("stateCycle")) + stateCycle = jsonToArrayF<STATESIZE>(config.get("stateCycle")); + if (config.contains("stateFrames")) + stateFrames = jsonToArrayU<STATESIZE>(config.get("stateFrames")); + + if (config.contains("emoteCycle")) + emoteCycle = jsonToArrayF<EmoteSize>(config.get("emoteCycle")); + if (config.contains("emoteFrames")) + emoteFrames = jsonToArrayU<EmoteSize>(config.get("emoteFrames")); +} + +bool Humanoid::HumanoidTiming::cyclicState(State state) { + switch (state) { + case State::Walk: + case State::Run: + case State::Swim: + return true; + default: + return false; + } +} + +bool Humanoid::HumanoidTiming::cyclicEmoteState(HumanoidEmote state) { + switch (state) { + case HumanoidEmote::Blabbering: + case HumanoidEmote::Shouting: + case HumanoidEmote::Sad: + case HumanoidEmote::Laugh: + case HumanoidEmote::Eat: + case HumanoidEmote::Sleep: + return true; + default: + return false; + } +} + +int Humanoid::HumanoidTiming::stateSeq(float timer, State state) const { + return genericSeq(timer, stateCycle[state], stateFrames[state], cyclicState(state)); +} + +int Humanoid::HumanoidTiming::emoteStateSeq(float timer, HumanoidEmote state) const { + return genericSeq(timer, emoteCycle[(size_t)state], emoteFrames[(size_t)state], cyclicEmoteState(state)); +} + +int Humanoid::HumanoidTiming::danceSeq(float timer, DancePtr dance) const { + return genericSeq(timer, dance->cycle, dance->steps.size(), dance->cyclic) - 1; +} + +int Humanoid::HumanoidTiming::genericSeq(float timer, float cycle, unsigned frames, bool cyclic) const { + if (cyclic) { + timer = fmod(timer, cycle); + } + return clamp<int>(timer * frames / cycle, 0, frames - 1) + 1; +} + +EnumMap<Humanoid::State> const Humanoid::StateNames{ + {Humanoid::State::Idle, "idle"}, + {Humanoid::State::Walk, "walk"}, + {Humanoid::State::Run, "run"}, + {Humanoid::State::Jump, "jump"}, + {Humanoid::State::Fall, "fall"}, + {Humanoid::State::Swim, "swim"}, + {Humanoid::State::SwimIdle, "swimIdle"}, + {Humanoid::State::Duck, "duck"}, + {Humanoid::State::Sit, "sit"}, + {Humanoid::State::Lay, "lay"}, +}; + +Humanoid::Humanoid(Json const& config) { + m_timing = HumanoidTiming(config.getObject("humanoidTiming")); + + m_globalOffset = jsonToVec2F(config.get("globalOffset")) / TilePixels; + m_headRunOffset = jsonToVec2F(config.get("headRunOffset")) / TilePixels; + m_headSwimOffset = jsonToVec2F(config.get("headSwimOffset")) / TilePixels; + m_runFallOffset = config.get("runFallOffset").toDouble() / TilePixels; + m_duckOffset = config.get("duckOffset").toDouble() / TilePixels; + m_headDuckOffset = jsonToVec2F(config.get("headDuckOffset")) / TilePixels; + m_sitOffset = config.get("sitOffset").toDouble() / TilePixels; + m_layOffset = config.get("layOffset").toDouble() / TilePixels; + m_headSitOffset = jsonToVec2F(config.get("headSitOffset")) / TilePixels; + m_headLayOffset = jsonToVec2F(config.get("headLayOffset")) / TilePixels; + m_recoilOffset = jsonToVec2F(config.get("recoilOffset")) / TilePixels; + m_mouthOffset = jsonToVec2F(config.get("mouthOffset")) / TilePixels; + m_feetOffset = jsonToVec2F(config.get("feetOffset")) / TilePixels; + + m_bodyFullbright = config.getBool("bodyFullbright", false); + + m_headArmorOffset = jsonToVec2F(config.get("headArmorOffset")) / TilePixels; + m_chestArmorOffset = jsonToVec2F(config.get("chestArmorOffset")) / TilePixels; + m_legsArmorOffset = jsonToVec2F(config.get("legsArmorOffset")) / TilePixels; + m_backArmorOffset = jsonToVec2F(config.get("backArmorOffset")) / TilePixels; + + m_bodyHidden = config.getBool("bodyHidden", false); + + m_armWalkSeq = jsonToIntList(config.get("armWalkSeq")); + m_armRunSeq = jsonToIntList(config.get("armRunSeq")); + + for (auto const& v : config.get("walkBob").toArray()) + m_walkBob.append(v.toDouble() / TilePixels); + for (auto const& v : config.get("runBob").toArray()) + m_runBob.append(v.toDouble() / TilePixels); + for (auto const& v : config.get("swimBob").toArray()) + m_swimBob.append(v.toDouble() / TilePixels); + + m_jumpBob = config.get("jumpBob").toDouble() / TilePixels; + m_frontArmRotationCenter = jsonToVec2F(config.get("frontArmRotationCenter")) / TilePixels; + m_backArmRotationCenter = jsonToVec2F(config.get("backArmRotationCenter")) / TilePixels; + m_frontHandPosition = jsonToVec2F(config.get("frontHandPosition")) / TilePixels; + m_backArmOffset = jsonToVec2F(config.get("backArmOffset")) / TilePixels; + m_vaporTrailFrames = config.get("vaporTrailFrames").toInt(); + m_vaporTrailCycle = config.get("vaporTrailCycle").toFloat(); + + m_defaultDeathParticles = config.getString("deathParticles"); + m_particleEmitters = config.get("particleEmitters"); + + m_defaultMovementParameters = config.get("movementParameters"); + + m_twoHanded = false; + m_primaryHand.holdingItem = false; + m_altHand.holdingItem = false; + + m_movingBackwards = false; + m_altHand.angle = 0; + m_facingDirection = Direction::Left; + m_rotation = 0; + m_drawVaporTrail = false; + m_state = State::Idle; + m_emoteState = HumanoidEmote::Idle; + m_dance = {}; + m_emoteAnimationTimer = 0; + + m_primaryHand.angle = 0; + m_animationTimer = 0.0f; +} + +Humanoid::Humanoid(HumanoidIdentity const& identity) + : Humanoid(Root::singleton().speciesDatabase()->species(identity.species)->humanoidConfig()) { + setIdentity(identity); +} + +void Humanoid::setIdentity(HumanoidIdentity const& identity) { + m_identity = identity; + m_headFrameset = getHeadFromIdentity(); + m_bodyFrameset = getBodyFromIdentity(); + m_emoteFrameset = getFacialEmotesFromIdentity(); + m_hairFrameset = getHairFromIdentity(); + m_facialHairFrameset = getFacialHairFromIdentity(); + m_facialMaskFrameset = getFacialMaskFromIdentity(); + m_backArmFrameset = getBackArmFromIdentity(); + m_frontArmFrameset = getFrontArmFromIdentity(); + m_vaporTrailFrameset = getVaporTrailFrameset(); +} + +HumanoidIdentity const& Humanoid::identity() const { + return m_identity; +} + +void Humanoid::setHeadArmorDirectives(String directives) { + m_headArmorDirectives = move(directives); +} + +void Humanoid::setHeadArmorFrameset(String headFrameset) { + m_headArmorFrameset = move(headFrameset); +} + +void Humanoid::setChestArmorDirectives(String directives) { + m_chestArmorDirectives = move(directives); +} + +void Humanoid::setChestArmorFrameset(String chest) { + m_chestArmorFrameset = move(chest); +} + +void Humanoid::setBackSleeveFrameset(String backSleeveFrameset) { + m_backSleeveFrameset = move(backSleeveFrameset); +} + +void Humanoid::setFrontSleeveFrameset(String frontSleeveFrameset) { + m_frontSleeveFrameset = move(frontSleeveFrameset); +} + +void Humanoid::setLegsArmorDirectives(String directives) { + m_legsArmorDirectives = move(directives); +} + +void Humanoid::setLegsArmorFrameset(String legsFrameset) { + m_legsArmorFrameset = move(legsFrameset); +} + +void Humanoid::setBackArmorDirectives(String directives) { + m_backArmorDirectives = move(directives); +} + +void Humanoid::setBackArmorFrameset(String backFrameset) { + m_backArmorFrameset = move(backFrameset); +} + +void Humanoid::setHelmetMaskDirectives(String helmetMaskDirectives) { + m_helmetMaskDirectives = move(helmetMaskDirectives); +} + +void Humanoid::setBodyHidden(bool hidden) { + m_bodyHidden = hidden; +} + +void Humanoid::setState(State state) { + if (m_state != state) + m_animationTimer = 0.0f; + m_state = state; +} + +void Humanoid::setEmoteState(HumanoidEmote state) { + if (m_emoteState != state) + m_emoteAnimationTimer = 0.0f; + m_emoteState = state; +} + +void Humanoid::setDance(Maybe<String> const& dance) { + if (m_dance != dance) + m_danceTimer = 0.0f; + m_dance = dance; +} + +void Humanoid::setFacingDirection(Direction facingDirection) { + m_facingDirection = facingDirection; +} + +void Humanoid::setMovingBackwards(bool movingBackwards) { + m_movingBackwards = movingBackwards; +} + +void Humanoid::setRotation(float rotation) { + m_rotation = rotation; +} + +void Humanoid::setVaporTrail(bool enabled) { + m_drawVaporTrail = enabled; +} + +Humanoid::State Humanoid::state() const { + return m_state; +} + +HumanoidEmote Humanoid::emoteState() const { + return m_emoteState; +} + +Maybe<String> Humanoid::dance() const { + return m_dance; +} + +Direction Humanoid::facingDirection() const { + return m_facingDirection; +} + +bool Humanoid::movingBackwards() const { + return m_movingBackwards; +} + +void Humanoid::setPrimaryHandParameters(bool holdingItem, float angle, float itemAngle, bool twoHanded, + bool recoil, bool outsideOfHand) { + m_primaryHand.holdingItem = holdingItem; + m_primaryHand.angle = angle; + m_primaryHand.itemAngle = itemAngle; + m_twoHanded = twoHanded; + m_primaryHand.recoil = recoil; + m_primaryHand.outsideOfHand = outsideOfHand; +} + +void Humanoid::setPrimaryHandFrameOverrides(String backFrameOverride, String frontFrameOverride) { + m_primaryHand.backFrame = !backFrameOverride.empty() ? move(backFrameOverride) : "rotation"; + m_primaryHand.frontFrame = !frontFrameOverride.empty() ? move(frontFrameOverride) : "rotation"; +} + +void Humanoid::setPrimaryHandDrawables(List<Drawable> drawables) { + m_primaryHand.itemDrawables = move(drawables); +} + +void Humanoid::setPrimaryHandNonRotatedDrawables(List<Drawable> drawables) { + m_primaryHand.nonRotatedDrawables = move(drawables); +} + +void Humanoid::setAltHandParameters(bool holdingItem, float angle, float itemAngle, bool recoil, + bool outsideOfHand) { + m_altHand.holdingItem = holdingItem; + m_altHand.angle = angle; + m_altHand.itemAngle = itemAngle; + m_altHand.recoil = recoil; + m_altHand.outsideOfHand = outsideOfHand; +} + +void Humanoid::setAltHandFrameOverrides(String backFrameOverride, String frontFrameOverride) { + m_altHand.backFrame = !backFrameOverride.empty() ? move(backFrameOverride) : "rotation"; + m_altHand.frontFrame = !frontFrameOverride.empty() ? move(frontFrameOverride) : "rotation"; +} + +void Humanoid::setAltHandDrawables(List<Drawable> drawables) { + m_altHand.itemDrawables = move(drawables); +} + +void Humanoid::setAltHandNonRotatedDrawables(List<Drawable> drawables) { + m_altHand.nonRotatedDrawables = move(drawables); +} + +void Humanoid::animate(float dt) { + m_animationTimer += dt; + m_emoteAnimationTimer += dt; + m_danceTimer += dt; +} + +void Humanoid::resetAnimation() { + m_animationTimer = 0.0f; + m_emoteAnimationTimer = 0.0f; + m_danceTimer = 0.0f; +} + +List<Drawable> Humanoid::render() { + List<Drawable> drawables; + + int armStateSeq = getArmStateSequence(); + int bodyStateSeq = getBodyStateSequence(); + int emoteStateSeq = m_timing.emoteStateSeq(m_emoteAnimationTimer, m_emoteState); + float bobYOffset = getBobYOffset(); + Maybe<DancePtr> dance = getDance(); + Maybe<DanceStep> danceStep = {}; + if (dance.isValid()) { + if (!(*dance)->states.contains(StateNames.getRight(m_state))) + dance = {}; + else + danceStep = (*dance)->steps[m_timing.danceSeq(m_danceTimer, *dance)]; + } + + auto frontHand = (m_facingDirection == Direction::Left || m_twoHanded) ? m_primaryHand : m_altHand; + auto backHand = (m_facingDirection == Direction::Right || m_twoHanded) ? m_primaryHand : m_altHand; + + Vec2F frontArmFrameOffset = Vec2F(0, bobYOffset); + if (frontHand.recoil) + frontArmFrameOffset += m_recoilOffset; + Vec2F backArmFrameOffset = Vec2F(0, bobYOffset); + if (backHand.recoil) + backArmFrameOffset += m_recoilOffset; + + auto addDrawable = [&](Drawable drawable, bool forceFullbright = false) { + if (m_facingDirection == Direction::Left) + drawable.scale(Vec2F(-1, 1)); + drawable.fullbright = drawable.fullbright || forceFullbright; + drawables.append(move(drawable)); + }; + + auto backArmDrawable = [&](String const& frameSet, String const& directives) -> Drawable { + String image = strf("%s:%s%s", frameSet, backHand.backFrame, directives); + Drawable backArm = Drawable::makeImage(move(image), 1.0f / TilePixels, true, backArmFrameOffset); + backArm.rotate(backHand.angle, backArmFrameOffset + m_backArmRotationCenter + m_backArmOffset); + return backArm; + }; + + if (!m_backArmorFrameset.empty()) { + auto frameGroup = frameBase(m_state); + if (m_movingBackwards && (m_state == State::Run)) + frameGroup = "runbackwards"; + String image; + if (dance.isValid() && danceStep->bodyFrame) + image = strf("%s:%s%s", m_backArmorFrameset, *danceStep->bodyFrame, getBackDirectives()); + else if (m_state == Idle) + image = strf("%s:%s%s", m_backArmorFrameset, m_identity.personality.idle, getBackDirectives()); + else + image = strf("%s:%s.%s%s", m_backArmorFrameset, frameGroup, bodyStateSeq, getBackDirectives()); + + addDrawable(Drawable::makeImage(move(image), 1.0f / TilePixels, true, Vec2F())); + } + + if (backHand.holdingItem && !dance.isValid()) { + auto drawItem = [&]() { + for (auto backHandItem : backHand.itemDrawables) { + backHandItem.translate(m_frontHandPosition + backArmFrameOffset + m_backArmOffset); + backHandItem.rotate(backHand.itemAngle, backArmFrameOffset + m_backArmRotationCenter + m_backArmOffset); + addDrawable(move(backHandItem)); + } + }; + if (!m_twoHanded && backHand.outsideOfHand) + drawItem(); + if (!m_backArmFrameset.empty() && !m_bodyHidden) + addDrawable(backArmDrawable(m_backArmFrameset, getBodyDirectives()), m_bodyFullbright); + if (!m_backSleeveFrameset.empty()) + addDrawable(backArmDrawable(m_backSleeveFrameset, getChestDirectives())); + if (!m_twoHanded && !backHand.outsideOfHand) + drawItem(); + } else { + if (!m_backArmFrameset.empty() && !m_bodyHidden) { + String image; + Vec2F position; + if (dance.isValid() && danceStep->backArmFrame) { + image = strf("%s:%s%s", m_backArmFrameset, *danceStep->backArmFrame, getBodyDirectives()); + position = danceStep->backArmOffset / TilePixels; + } else if (m_state == Idle) { + image = strf("%s:%s%s", m_backArmFrameset, m_identity.personality.armIdle, getBodyDirectives()); + position = m_identity.personality.armOffset / TilePixels; + } else + image = strf("%s:%s.%s%s", m_backArmFrameset, frameBase(m_state), armStateSeq, getBodyDirectives()); + auto drawable = Drawable::makeImage(move(image), 1.0f / TilePixels, true, position); + if (dance.isValid()) + drawable.rotate(danceStep->backArmRotation); + addDrawable(drawable, m_bodyFullbright); + } + if (!m_backSleeveFrameset.empty()) { + String image; + Vec2F position; + if (dance.isValid() && danceStep->backArmFrame) { + image = strf("%s:%s%s", m_backSleeveFrameset, *danceStep->backArmFrame, getChestDirectives()); + position = danceStep->backArmOffset / TilePixels; + } else if (m_state == Idle) { + image = strf("%s:%s%s", m_backSleeveFrameset, m_identity.personality.armIdle, getChestDirectives()); + position = m_identity.personality.armOffset / TilePixels; + } else + image = strf("%s:%s.%s%s", m_backSleeveFrameset, frameBase(m_state), armStateSeq, getChestDirectives()); + auto drawable = Drawable::makeImage(move(image), 1.0f / TilePixels, true, position); + if (dance.isValid()) + drawable.rotate(danceStep->backArmRotation); + addDrawable(drawable); + } + } + + Vec2F headPosition(0, bobYOffset); + if (dance.isValid()) + headPosition += danceStep->headOffset / TilePixels; + else if (m_state == Idle) + headPosition += m_identity.personality.headOffset / TilePixels; + else if (m_state == Run) + headPosition += m_headRunOffset; + else if (m_state == Swim || m_state == SwimIdle) + headPosition += m_headSwimOffset; + else if (m_state == Duck) + headPosition += m_headDuckOffset; + else if (m_state == Sit) + headPosition += m_headSitOffset; + else if (m_state == Lay) + headPosition += m_headLayOffset; + + if (!m_headFrameset.empty() && !m_bodyHidden) { + String image = strf("%s:normal%s", m_headFrameset, getBodyDirectives()); + addDrawable(Drawable::makeImage(move(image), 1.0f / TilePixels, true, headPosition), m_bodyFullbright); + } + + if (!m_emoteFrameset.empty() && !m_bodyHidden) { + String image = strf("%s:%s.%s%s", m_emoteFrameset, emoteFrameBase(m_emoteState), emoteStateSeq, getEmoteDirectives()); + addDrawable(Drawable::makeImage(move(image), 1.0f / TilePixels, true, headPosition), m_bodyFullbright); + } + + if (!m_hairFrameset.empty() && !m_bodyHidden) { + String image = strf("%s:normal%s", m_hairFrameset, getHairDirectives() + getHelmetMaskDirectives()); + addDrawable(Drawable::makeImage(move(image), 1.0f / TilePixels, true, headPosition), m_bodyFullbright); + } + + if (!m_bodyFrameset.empty() && !m_bodyHidden) { + String image; + if (dance.isValid() && danceStep->bodyFrame) + image = strf("%s:%s%s", m_bodyFrameset, *danceStep->bodyFrame, getBodyDirectives()); + else if (m_state == Idle) + image = strf("%s:%s%s", m_bodyFrameset, m_identity.personality.idle, getBodyDirectives()); + else + image = strf("%s:%s.%s%s", m_bodyFrameset, frameBase(m_state), bodyStateSeq, getBodyDirectives()); + addDrawable(Drawable::makeImage(move(image), 1.0f / TilePixels, true, {}), m_bodyFullbright); + } + + if (!m_legsArmorFrameset.empty()) { + String image; + if (dance.isValid() && danceStep->bodyFrame) + image = strf("%s:%s%s", m_legsArmorFrameset, *danceStep->bodyFrame, getLegsDirectives()); + else if (m_state == Idle) + image = strf("%s:%s%s", m_legsArmorFrameset, m_identity.personality.idle, getLegsDirectives()); + else + image = strf("%s:%s.%s%s", m_legsArmorFrameset, frameBase(m_state), bodyStateSeq, getLegsDirectives()); + addDrawable(Drawable::makeImage(move(image), 1.0f / TilePixels, true, {})); + } + + if (!m_chestArmorFrameset.empty()) { + String image; + Vec2F position; + if (dance.isValid() && danceStep->bodyFrame) + image = strf("%s:%s%s", m_chestArmorFrameset, *danceStep->bodyFrame, getChestDirectives()); + else if (m_state == Run) + image = strf("%s:run%s", m_chestArmorFrameset, getChestDirectives()); + else if (m_state == Idle) + image = strf("%s:%s%s", m_chestArmorFrameset, m_identity.personality.idle, getChestDirectives()); + else if (m_state == Duck) + image = strf("%s:duck%s", m_chestArmorFrameset, getChestDirectives()); + else if ((m_state == Swim) || (m_state == SwimIdle)) + image = strf("%s:swim%s", m_chestArmorFrameset, getChestDirectives()); + else + image = strf("%s:chest.1%s", m_chestArmorFrameset, getChestDirectives()); + if (m_state != Duck) + position[1] += bobYOffset; + addDrawable(Drawable::makeImage(move(image), 1.0f / TilePixels, true, position)); + } + + if (!m_facialHairFrameset.empty() && !m_bodyHidden) { + String image = strf("%s:normal%s", m_facialHairFrameset, getFacialHairDirectives() + getHelmetMaskDirectives()); + addDrawable(Drawable::makeImage(move(image), 1.0f / TilePixels, true, headPosition), m_bodyFullbright); + } + + if (!m_facialMaskFrameset.empty() && !m_bodyHidden) { + String image = strf("%s:normal%s", m_facialMaskFrameset, getFacialMaskDirectives() + getHelmetMaskDirectives()); + addDrawable(Drawable::makeImage(move(image), 1.0f / TilePixels, true, headPosition)); + } + + if (!m_headArmorFrameset.empty()) { + String image = strf("%s:normal%s", m_headArmorFrameset, getHeadDirectives()); + addDrawable(Drawable::makeImage(move(image), 1.0f / TilePixels, true, headPosition)); + } + + auto frontArmDrawable = [&](String const& frameSet, String const& directives) -> Drawable { + String image = strf("%s:%s%s", frameSet, frontHand.frontFrame, directives); + Drawable frontArm = Drawable::makeImage(image, 1.0f / TilePixels, true, frontArmFrameOffset); + frontArm.rotate(frontHand.angle, frontArmFrameOffset + m_frontArmRotationCenter); + return frontArm; + }; + + if (frontHand.holdingItem && !dance.isValid()) { + auto drawItem = [&]() { + for (size_t i = 0; i < frontHand.itemDrawables.size(); i++) { + Drawable frontHandItem = frontHand.itemDrawables[i]; + frontHandItem.translate(m_frontHandPosition + frontArmFrameOffset); + frontHandItem.rotate(frontHand.itemAngle, frontArmFrameOffset + m_frontArmRotationCenter); + addDrawable(frontHandItem); + } + }; + if (!frontHand.outsideOfHand) + drawItem(); + if (!m_frontArmFrameset.empty() && !m_bodyHidden) + addDrawable(frontArmDrawable(m_frontArmFrameset, getBodyDirectives()), m_bodyFullbright); + if (!m_frontSleeveFrameset.empty()) + addDrawable(frontArmDrawable(m_frontSleeveFrameset, getChestDirectives())); + if (frontHand.outsideOfHand) + drawItem(); + } else { + if (!m_frontArmFrameset.empty() && !m_bodyHidden) { + String image; + Vec2F position; + if (dance.isValid() && danceStep->frontArmFrame) { + image = strf("%s:%s%s", m_frontArmFrameset, *danceStep->frontArmFrame, getBodyDirectives()); + position = danceStep->frontArmOffset / TilePixels; + } else if (m_state == Idle) { + image = strf("%s:%s%s", m_frontArmFrameset, m_identity.personality.armIdle, getBodyDirectives()); + position = m_identity.personality.armOffset / TilePixels; + } else + image = strf("%s:%s.%s%s", m_frontArmFrameset, frameBase(m_state), armStateSeq, getBodyDirectives()); + auto drawable = Drawable::makeImage(move(image), 1.0f / TilePixels, true, position); + if (dance.isValid()) + drawable.rotate(danceStep->frontArmRotation); + addDrawable(drawable, m_bodyFullbright); + } + + if (!m_frontSleeveFrameset.empty()) { + String image; + Vec2F position; + if (dance.isValid() && danceStep->frontArmFrame) { + image = strf("%s:%s%s", m_frontSleeveFrameset, *danceStep->frontArmFrame, getChestDirectives()); + position = danceStep->frontArmOffset / TilePixels; + } else if (m_state == Idle) { + image = strf("%s:%s%s", m_frontSleeveFrameset, m_identity.personality.armIdle, getChestDirectives()); + position = m_identity.personality.armOffset / TilePixels; + } else + image = strf("%s:%s.%s%s", m_frontSleeveFrameset, frameBase(m_state), armStateSeq, getChestDirectives()); + auto drawable = Drawable::makeImage(move(image), 1.0f / TilePixels, true, position); + if (dance.isValid()) + drawable.rotate(danceStep->frontArmRotation); + addDrawable(drawable); + } + } + + if (m_drawVaporTrail) { + auto image = strf("%s:%d", + m_vaporTrailFrameset, + m_timing.genericSeq(m_animationTimer, m_vaporTrailCycle, m_vaporTrailFrames, true)); + addDrawable(Drawable::makeImage(move(image), 1.0f / TilePixels, true, {})); + } + + if (m_primaryHand.nonRotatedDrawables.size()) + drawables.insertAllAt(0, m_primaryHand.nonRotatedDrawables); + if (m_altHand.nonRotatedDrawables.size()) + drawables.insertAllAt(0, m_altHand.nonRotatedDrawables); + + Drawable::rotateAll(drawables, m_rotation); + Drawable::translateAll(drawables, m_globalOffset); + Drawable::rebaseAll(drawables); + + return drawables; +} + +List<Drawable> Humanoid::renderPortrait(PortraitMode mode) const { + List<Drawable> drawables; + int emoteStateSeq = m_timing.emoteStateSeq(m_emoteAnimationTimer, m_emoteState); + + auto addDrawable = [&](Drawable drawable) { + if (mode != PortraitMode::Full && mode != PortraitMode::FullNeutral + && mode != PortraitMode::FullNude && mode != PortraitMode::FullNeutralNude) { + // TODO: make this configurable + drawable.imagePart().addDirectives("addmask=/humanoid/portraitMask.png;0;0", false); + } + drawables.append(std::move(drawable)); + }; + + bool dressed = !(mode == PortraitMode::FullNude || mode == PortraitMode::FullNeutralNude); + + auto helmetMaskDirective = dressed ? getHelmetMaskDirectives() : ""; + + auto personality = m_identity.personality; + if (mode == PortraitMode::FullNeutral || mode == PortraitMode::FullNeutralNude) + personality = Root::singleton().speciesDatabase()->species(m_identity.species)->personalities()[0]; + + if (mode != PortraitMode::Head) { + if (!m_backArmFrameset.empty()) { + String image = strf("%s:%s%s", m_backArmFrameset, personality.armIdle, getBodyDirectives()); + addDrawable(Drawable::makeImage(move(image), 1.0f, true, personality.armOffset)); + } + if (dressed && !m_backSleeveFrameset.empty()) { + String image = strf("%s:%s%s", m_backSleeveFrameset, personality.armIdle, getChestDirectives()); + addDrawable(Drawable::makeImage(move(image), 1.0f, true, personality.armOffset)); + } + if (mode != PortraitMode::Bust) { + if (dressed && !m_backArmorFrameset.empty()) { + String image = strf("%s:%s%s", m_backArmorFrameset, personality.idle, getBackDirectives()); + addDrawable(Drawable::makeImage(move(image), 1.0f, true, {})); + } + } + } + + if (!m_headFrameset.empty()) { + String image = strf("%s:normal%s", m_headFrameset, getBodyDirectives()); + addDrawable(Drawable::makeImage(move(image), 1.0f, true, personality.headOffset)); + } + + if (!m_emoteFrameset.empty()) { + String image = + strf("%s:%s.%s%s", m_emoteFrameset, emoteFrameBase(m_emoteState), emoteStateSeq, getEmoteDirectives()); + addDrawable(Drawable::makeImage(move(image), 1.0f, true, personality.headOffset)); + } + + if (!m_hairFrameset.empty()) { + String image = strf("%s:normal%s", m_hairFrameset, getHairDirectives() + helmetMaskDirective); + addDrawable(Drawable::makeImage(move(image), 1.0f, true, personality.headOffset)); + } + + if (!m_bodyFrameset.empty()) { + String image = strf("%s:%s%s", m_bodyFrameset, personality.idle, getBodyDirectives()); + addDrawable(Drawable::makeImage(move(image), 1.0f, true, {})); + } + + if (mode != PortraitMode::Head) { + if (dressed && !m_legsArmorFrameset.empty()) { + String image = strf("%s:%s%s", m_legsArmorFrameset, personality.idle, getLegsDirectives()); + addDrawable(Drawable::makeImage(move(image), 1.0f, true, {})); + } + + if (dressed && !m_chestArmorFrameset.empty()) { + String image = strf("%s:%s%s", m_chestArmorFrameset, personality.idle, getChestDirectives()); + addDrawable(Drawable::makeImage(move(image), 1.0f, true, {})); + } + } + + if (!m_facialHairFrameset.empty()) { + String image = strf("%s:normal%s", m_facialHairFrameset, getFacialHairDirectives() + helmetMaskDirective); + addDrawable(Drawable::makeImage(move(image), 1.0f, true, personality.headOffset)); + } + + if (!m_facialMaskFrameset.empty()) { + String image = strf("%s:normal%s", m_facialMaskFrameset, getFacialMaskDirectives() + helmetMaskDirective); + addDrawable(Drawable::makeImage(move(image), 1.0f, true, personality.headOffset)); + } + + if (dressed && !m_headArmorFrameset.empty()) { + String image = strf("%s:normal%s", m_headArmorFrameset, getHeadDirectives()); + addDrawable(Drawable::makeImage(move(image), 1.0f, true, personality.headOffset)); + } + + if (mode != PortraitMode::Head) { + if (!m_frontArmFrameset.empty()) { + String image = strf("%s:%s%s", m_frontArmFrameset, personality.armIdle, getBodyDirectives()); + addDrawable(Drawable::makeImage(move(image), 1.0f, true, personality.armOffset)); + } + + if (dressed && !m_frontSleeveFrameset.empty()) { + String image = strf("%s:%s%s", m_frontSleeveFrameset, personality.armIdle, getChestDirectives()); + addDrawable(Drawable::makeImage(move(image), 1.0f, true, personality.armOffset)); + } + } + + return drawables; +} + +List<Drawable> Humanoid::renderSkull() const { + return {Drawable::makeImage( + Root::singleton().speciesDatabase()->species(m_identity.species)->skull(), 1.0f, true, Vec2F())}; +} + +List<Drawable> Humanoid::renderDummy(Gender gender, HeadArmor const* head, ChestArmor const* chest, LegsArmor const* legs, BackArmor const* back) { + auto assets = Root::singleton().assets(); + Humanoid humanoid(assets->json("/humanoid.config")); + + humanoid.m_headFrameset = assets->json("/humanoid/any/dummy.config:head").toString(); + humanoid.m_bodyFrameset = assets->json("/humanoid/any/dummy.config:body").toString(); + humanoid.m_frontArmFrameset = assets->json("/humanoid/any/dummy.config:frontArm").toString(); + humanoid.m_backArmFrameset = assets->json("/humanoid/any/dummy.config:backArm").toString(); + humanoid.setFacingDirection(DirectionNames.getLeft(assets->json("/humanoid/any/dummy.config:direction").toString())); + + if (head) { + humanoid.setHeadArmorFrameset(head->frameset(gender)); + humanoid.setHeadArmorDirectives(head->directives()); + humanoid.setHelmetMaskDirectives(head->maskDirectives()); + } + + if (chest) { + humanoid.setBackSleeveFrameset(chest->backSleeveFrameset(gender)); + humanoid.setFrontSleeveFrameset(chest->frontSleeveFrameset(gender)); + humanoid.setChestArmorFrameset(chest->bodyFrameset(gender)); + humanoid.setChestArmorDirectives(chest->directives()); + } + + if (legs) { + humanoid.setLegsArmorFrameset(legs->frameset(gender)); + humanoid.setLegsArmorDirectives(legs->directives()); + } + + if (back) { + humanoid.setBackArmorFrameset(back->frameset(gender)); + humanoid.setBackArmorDirectives(back->directives()); + } + + auto drawables = humanoid.render(); + Drawable::scaleAll(drawables, TilePixels); + + return drawables; +} + +Vec2F Humanoid::primaryHandPosition(Vec2F const& offset) const { + return primaryArmPosition(m_facingDirection, m_primaryHand.angle, primaryHandOffset(m_facingDirection) + offset); +} + +Vec2F Humanoid::altHandPosition(Vec2F const& offset) const { + return altArmPosition(m_facingDirection, m_altHand.angle, altHandOffset(m_facingDirection) + offset); +} + +Vec2F Humanoid::primaryArmPosition(Direction facingDirection, float armAngle, Vec2F const& offset) const { + float bobYOffset = getBobYOffset(); + + if (m_primaryHand.holdingItem) { + Vec2F rotationCenter; + if (facingDirection == Direction::Left || m_twoHanded) + rotationCenter = m_frontArmRotationCenter + Vec2F(0, bobYOffset); + else + rotationCenter = m_backArmRotationCenter + Vec2F(0, bobYOffset) + m_backArmOffset; + + Vec2F position = offset.rotate(armAngle) + rotationCenter; + + if (facingDirection == Direction::Left) + position[0] *= -1; + + return position; + } else { + // TODO: non-aiming item holding not supported yet + return Vec2F(); + } +} + +Vec2F Humanoid::altArmPosition(Direction facingDirection, float armAngle, Vec2F const& offset) const { + float bobYOffset = getBobYOffset(); + + if (m_altHand.holdingItem) { + Vec2F rotationCenter; + if (facingDirection == Direction::Right) + rotationCenter = m_frontArmRotationCenter + Vec2F(0, bobYOffset); + else + rotationCenter = m_backArmRotationCenter + Vec2F(0, bobYOffset) + m_backArmOffset; + + Vec2F position = Vec2F(offset).rotate(armAngle) + rotationCenter; + + if (facingDirection == Direction::Left) + position[0] *= -1; + + return position; + } else { + // TODO: non-aiming item holding not supported yet + return Vec2F(); + } +} + +Vec2F Humanoid::primaryHandOffset(Direction facingDirection) const { + if (facingDirection == Direction::Left || m_twoHanded) + return m_frontHandPosition - m_frontArmRotationCenter; + else + return m_frontHandPosition - m_backArmRotationCenter; +} + +Vec2F Humanoid::altHandOffset(Direction facingDirection) const { + if (facingDirection == Direction::Left || m_twoHanded) + return m_frontHandPosition - m_backArmRotationCenter; + else + return m_frontHandPosition - m_frontArmRotationCenter; +} + +String Humanoid::frameBase(State state) const { + switch (state) { + case State::Idle: + return "idle"; + case State::Walk: + return "walk"; + case State::Run: + return "run"; + case State::Jump: + return "jump"; + case State::Swim: + return "swim"; + case State::SwimIdle: + return "swimIdle"; + case State::Duck: + return "duck"; + case State::Fall: + return "fall"; + case State::Sit: + return "sit"; + case State::Lay: + return "lay"; + + default: + throw StarException(strf("No such state '%s'", state)); + } +} + +String Humanoid::emoteFrameBase(HumanoidEmote state) const { + switch (state) { + case HumanoidEmote::Idle: + return "idle"; + case HumanoidEmote::Blabbering: + return "blabber"; + case HumanoidEmote::Shouting: + return "shout"; + case HumanoidEmote::Happy: + return "happy"; + case HumanoidEmote::Sad: + return "sad"; + case HumanoidEmote::NEUTRAL: + return "neutral"; + case HumanoidEmote::Laugh: + return "laugh"; + case HumanoidEmote::Annoyed: + return "annoyed"; + case HumanoidEmote::Oh: + return "oh"; + case HumanoidEmote::OOOH: + return "oooh"; + case HumanoidEmote::Blink: + return "blink"; + case HumanoidEmote::Wink: + return "wink"; + case HumanoidEmote::Eat: + return "eat"; + case HumanoidEmote::Sleep: + return "sleep"; + + default: + throw StarException(strf("No such emote state '%s'", HumanoidEmoteNames.getRight(state))); + } +} + +String Humanoid::getHeadFromIdentity() const { + return strf("/humanoid/%s/%shead.png", + m_identity.imagePath ? *m_identity.imagePath : m_identity.species, + GenderNames.getRight(m_identity.gender)); +} + +String Humanoid::getBodyFromIdentity() const { + return strf("/humanoid/%s/%sbody.png", + m_identity.imagePath ? *m_identity.imagePath : m_identity.species, + GenderNames.getRight(m_identity.gender)); +} + +String Humanoid::getFacialEmotesFromIdentity() const { + return strf("/humanoid/%s/emote.png", m_identity.imagePath ? *m_identity.imagePath : m_identity.species); +} + +String Humanoid::getHairFromIdentity() const { + if (m_identity.hairType.empty()) + return ""; + return strf("/humanoid/%s/%s/%s.png", + m_identity.imagePath ? *m_identity.imagePath : m_identity.species, + m_identity.hairGroup, + m_identity.hairType); +} + +String Humanoid::getFacialHairFromIdentity() const { + if (m_identity.facialHairType.empty()) + return ""; + return strf("/humanoid/%s/%s/%s.png", + m_identity.imagePath ? *m_identity.imagePath : m_identity.species, + m_identity.facialHairGroup, + m_identity.facialHairType); +} + +String Humanoid::getFacialMaskFromIdentity() const { + if (m_identity.facialMaskType.empty()) + return ""; + return strf("/humanoid/%s/%s/%s.png", + m_identity.imagePath ? *m_identity.imagePath : m_identity.species, + m_identity.facialMaskGroup, + m_identity.facialMaskType); +} + +String Humanoid::getBackArmFromIdentity() const { + return strf("/humanoid/%s/backarm.png", m_identity.imagePath ? *m_identity.imagePath : m_identity.species); +} + +String Humanoid::getFrontArmFromIdentity() const { + return strf("/humanoid/%s/frontarm.png", m_identity.imagePath ? *m_identity.imagePath : m_identity.species); +} + +String Humanoid::getVaporTrailFrameset() const { + return "/humanoid/any/flames.png"; +} + +String Humanoid::getBodyDirectives() const { + return m_identity.bodyDirectives; +} + +String Humanoid::getHairDirectives() const { + return m_identity.hairDirectives; +} + +String Humanoid::getEmoteDirectives() const { + return m_identity.emoteDirectives; +} + +String Humanoid::getFacialHairDirectives() const { + return m_identity.facialHairDirectives; +} + +String Humanoid::getFacialMaskDirectives() const { + return m_identity.facialMaskDirectives; +} + +String Humanoid::getHelmetMaskDirectives() const { + return m_helmetMaskDirectives; +} + +String Humanoid::getHeadDirectives() const { + return m_headArmorDirectives; +} + +String Humanoid::getChestDirectives() const { + return m_chestArmorDirectives; +} + +String Humanoid::getLegsDirectives() const { + return m_legsArmorDirectives; +} + +String Humanoid::getBackDirectives() const { + return m_backArmorDirectives; +} + +int Humanoid::getArmStateSequence() const { + int stateSeq = m_timing.stateSeq(m_animationTimer, m_state); + + int armStateSeq; + if (m_state == Walk) { + armStateSeq = m_movingBackwards ? m_armWalkSeq.at(m_armWalkSeq.size() - stateSeq) : m_armWalkSeq.at(stateSeq - 1); + } else if (m_state == Run) { + armStateSeq = m_movingBackwards ? m_armRunSeq.at(m_armRunSeq.size() - stateSeq) : m_armRunSeq.at(stateSeq - 1); + } else { + armStateSeq = stateSeq; + } + + return armStateSeq; +} + +int Humanoid::getBodyStateSequence() const { + int stateSeq = m_timing.stateSeq(m_animationTimer, m_state); + + int bodyStateSeq; + if (m_movingBackwards && (m_state == Walk || m_state == Run)) + bodyStateSeq = m_timing.stateFrames[m_state] - stateSeq + 1; + else + bodyStateSeq = stateSeq; + + return bodyStateSeq; +} + +Maybe<DancePtr> Humanoid::getDance() const { + if (m_dance.isNothing()) + return {}; + + auto danceDatabase = Root::singleton().danceDatabase(); + return danceDatabase->getDance(*m_dance); +} + +float Humanoid::getBobYOffset() const { + int bodyStateSeq = getBodyStateSequence(); + float bobYOffset = 0.0f; + if (m_state == Run) { + bobYOffset = m_runFallOffset + m_runBob.at(bodyStateSeq - 1); + } else if (m_state == Fall) { + bobYOffset = m_runFallOffset; + } else if (m_state == Jump) { + bobYOffset = m_jumpBob; + } else if (m_state == Walk) { + bobYOffset = m_walkBob.at(bodyStateSeq - 1); + } else if (m_state == Swim) { + bobYOffset = m_swimBob.at(bodyStateSeq - 1); + } else if (m_state == Duck) { + bobYOffset = m_duckOffset; + } else if (m_state == Sit) { + bobYOffset = m_sitOffset; + } else if (m_state == Lay) { + bobYOffset = m_layOffset; + } + return bobYOffset; +} + +Vec2F Humanoid::armAdjustment() const { + return Vec2F(0, getBobYOffset()); +} + +Vec2F Humanoid::mouthOffset(bool ignoreAdjustments) const { + if (ignoreAdjustments) { + return (m_mouthOffset).rotate(m_rotation); + } else { + Vec2F headPosition(0, getBobYOffset()); + if (m_state == Idle) + headPosition += m_identity.personality.headOffset / TilePixels; + else if (m_state == Run) + headPosition += m_headRunOffset; + else if (m_state == Swim || m_state == SwimIdle) + headPosition += m_headSwimOffset; + else if (m_state == Duck) + headPosition += m_headDuckOffset; + else if (m_state == Sit) + headPosition += m_headSitOffset; + else if (m_state == Lay) + headPosition += m_headLayOffset; + + return (m_mouthOffset + headPosition).rotate(m_rotation); + } +} + +Vec2F Humanoid::feetOffset() const { + return m_feetOffset.rotate(m_rotation); +} + +Vec2F Humanoid::headArmorOffset() const { + Vec2F headPosition(0, getBobYOffset()); + if (m_state == Idle) + headPosition += m_identity.personality.headOffset / TilePixels; + else if (m_state == Run) + headPosition += m_headRunOffset; + else if (m_state == Swim || m_state == SwimIdle) + headPosition += m_headSwimOffset; + else if (m_state == Duck) + headPosition += m_headDuckOffset; + else if (m_state == Sit) + headPosition += m_headSitOffset; + else if (m_state == Lay) + headPosition += m_headLayOffset; + + return (m_headArmorOffset + headPosition).rotate(m_rotation); +} + +Vec2F Humanoid::chestArmorOffset() const { + Vec2F position(0, getBobYOffset()); + return (m_chestArmorOffset + position).rotate(m_rotation); +} + +Vec2F Humanoid::legsArmorOffset() const { + return m_legsArmorOffset.rotate(m_rotation); +} + +Vec2F Humanoid::backArmorOffset() const { + Vec2F position(0, getBobYOffset()); + return (m_backArmorOffset + position).rotate(m_rotation); +} + +String Humanoid::defaultDeathParticles() const { + return m_defaultDeathParticles; +} + +List<Particle> Humanoid::particles(String const& name) const { + auto particleDatabase = Root::singleton().particleDatabase(); + List<Particle> res; + Json particles = m_particleEmitters.get(name).get("particles", {}); + for (auto particle : particles.toArray()) { + auto particleSpec = particle.get("particle", {}); + res.push_back(particleDatabase->particle(particleSpec)); + } + + return res; +} + +Json const& Humanoid::defaultMovementParameters() const { + return m_defaultMovementParameters; +} + +} |