diff options
author | Kae <80987908+Novaenia@users.noreply.github.com> | 2023-06-20 14:33:09 +1000 |
---|---|---|
committer | Kae <80987908+Novaenia@users.noreply.github.com> | 2023-06-20 14:33:09 +1000 |
commit | 6352e8e3196f78388b6c771073f9e03eaa612673 (patch) | |
tree | e23772f79a7fbc41bc9108951e9e136857484bf4 /source/game/StarMonster.cpp | |
parent | 6741a057e5639280d85d0f88ba26f000baa58f61 (diff) |
everything everywhere
all at once
Diffstat (limited to 'source/game/StarMonster.cpp')
-rw-r--r-- | source/game/StarMonster.cpp | 859 |
1 files changed, 859 insertions, 0 deletions
diff --git a/source/game/StarMonster.cpp b/source/game/StarMonster.cpp new file mode 100644 index 0000000..a4ce597 --- /dev/null +++ b/source/game/StarMonster.cpp @@ -0,0 +1,859 @@ +#include "StarMonster.hpp" +#include "StarWorld.hpp" +#include "StarLogging.hpp" +#include "StarRoot.hpp" +#include "StarDamageManager.hpp" +#include "StarDamageDatabase.hpp" +#include "StarTreasure.hpp" +#include "StarJsonExtra.hpp" +#include "StarConfigLuaBindings.hpp" +#include "StarEntityLuaBindings.hpp" +#include "StarWorldLuaBindings.hpp" +#include "StarNetworkedAnimatorLuaBindings.hpp" +#include "StarStatusControllerLuaBindings.hpp" +#include "StarScriptedAnimatorLuaBindings.hpp" +#include "StarRootLuaBindings.hpp" +#include "StarBehaviorLuaBindings.hpp" +#include "StarStoredFunctions.hpp" +#include "StarItemDrop.hpp" +#include "StarAssets.hpp" +#include "StarTime.hpp" +#include "StarStatusController.hpp" + +namespace Star { + +Monster::Monster(MonsterVariant const& monsterVariant, Maybe<float> level) { + m_monsterLevel = level; + + m_damageOnTouch = false; + m_aggressive = false; + + m_knockedOut = false; + m_knockoutTimer = 0.0; + + m_dropPool = monsterVariant.dropPoolConfig; + + m_monsterVariant = monsterVariant; + + m_questIndicatorOffset = jsonToVec2F(Root::singleton().assets()->json("/quests/quests.config:defaultIndicatorOffset")); + + setTeam(EntityDamageTeam(m_monsterVariant.damageTeamType, m_monsterVariant.damageTeam)); + + m_networkedAnimator = NetworkedAnimator(m_monsterVariant.animatorConfig); + for (auto const& pair : m_monsterVariant.animatorPartTags) + m_networkedAnimator.setPartTag(pair.first, "partImage", pair.second); + m_networkedAnimator.setZoom(m_monsterVariant.animatorZoom); + auto colorSwap = m_monsterVariant.colorSwap.value(Root::singleton().monsterDatabase()->colorSwap(m_monsterVariant.parameters.getString("colors", "default"), m_monsterVariant.seed)); + if (!colorSwap.empty()) + m_networkedAnimator.setProcessingDirectives(imageOperationToString(ColorReplaceImageOperation{colorSwap})); + + m_statusController = make_shared<StatusController>(m_monsterVariant.statusSettings); + + m_scriptComponent.setScripts(m_monsterVariant.parameters.optArray("scripts").apply(jsonToStringList).value(m_monsterVariant.scripts)); + m_scriptComponent.setUpdateDelta(m_monsterVariant.initialScriptDelta); + + auto movementParameters = ActorMovementParameters::sensibleDefaults().merge(ActorMovementParameters(monsterVariant.movementSettings)); + if (movementParameters.standingPoly) + movementParameters.standingPoly->scale(m_monsterVariant.animatorZoom); + if (movementParameters.crouchingPoly) + movementParameters.crouchingPoly->scale(m_monsterVariant.animatorZoom); + *movementParameters.walkSpeed *= monsterVariant.walkMultiplier; + *movementParameters.runSpeed *= monsterVariant.runMultiplier; + *movementParameters.airJumpProfile.jumpSpeed *= monsterVariant.jumpMultiplier; + *movementParameters.liquidJumpProfile.jumpSpeed *= monsterVariant.jumpMultiplier; + *movementParameters.mass *= monsterVariant.weightMultiplier; + if (!movementParameters.physicsEffectCategories) + movementParameters.physicsEffectCategories = StringSet{"monster"}; + m_movementController = make_shared<ActorMovementController>(movementParameters); + + setPersistent(m_monsterVariant.persistent); + + setupNetStates(); + setNetStates(); +} + +Monster::Monster(Json const& diskStore) + : Monster(Root::singleton().monsterDatabase()->readMonsterVariantFromJson(diskStore.get("monsterVariant"))) { + m_monsterLevel = diskStore.optFloat("monsterLevel"); + m_movementController->loadState(diskStore.get("movementState")); + m_statusController->diskLoad(diskStore.get("statusController")); + m_damageOnTouch = diskStore.getBool("damageOnTouch"); + m_aggressive = diskStore.getBool("aggressive"); + m_deathParticleBurst = diskStore.getString("deathParticleBurst"); + m_deathSound = diskStore.getString("deathSound"); + m_activeSkillName = diskStore.getString("activeSkillName"); + m_dropPool = diskStore.get("dropPool"); + m_effectEmitter.fromJson(diskStore.get("effectEmitter")); + m_scriptComponent.setScriptStorage(diskStore.getObject("scriptStorage")); + + setUniqueId(diskStore.optString("uniqueId")); + if (diskStore.contains("team")) + setTeam(EntityDamageTeam(diskStore.get("team"))); +} + +Json Monster::diskStore() const { + return JsonObject{ + {"monsterLevel", jsonFromMaybe(m_monsterLevel)}, + {"movementState", m_movementController->storeState()}, + {"statusController", m_statusController->diskStore()}, + {"damageOnTouch", m_damageOnTouch}, + {"aggressive", aggressive()}, + {"deathParticleBurst", m_deathParticleBurst}, + {"deathSound", m_deathSound}, + {"activeSkillName", m_activeSkillName}, + {"dropPool", m_dropPool}, + {"effectEmitter", m_effectEmitter.toJson()}, + {"monsterVariant", Root::singleton().monsterDatabase()->writeMonsterVariantToJson(m_monsterVariant)}, + {"scriptStorage", m_scriptComponent.getScriptStorage()}, + {"uniqueId", jsonFromMaybe(uniqueId())}, + {"team", getTeam().toJson()} + }; +} + +ByteArray Monster::netStore() { + return Root::singleton().monsterDatabase()->writeMonsterVariant(m_monsterVariant); +} + +EntityType Monster::entityType() const { + return EntityType::Monster; +} + +ClientEntityMode Monster::clientEntityMode() const { + return m_monsterVariant.clientEntityMode; +} + +void Monster::init(World* world, EntityId entityId, EntityMode mode) { + Entity::init(world, entityId, mode); + + m_movementController->init(world); + m_movementController->setIgnorePhysicsEntities({entityId}); + m_statusController->init(this, m_movementController.get()); + + if (!m_monsterLevel) + m_monsterLevel = world->threatLevel(); + + if (isMaster()) { + auto functionDatabase = Root::singleton().functionDatabase(); + float healthMultiplier = m_monsterVariant.healthMultiplier * functionDatabase->function(m_monsterVariant.healthLevelFunction)->evaluate(*m_monsterLevel); + m_statusController->setPersistentEffects("innate", {StatModifier(StatBaseMultiplier{"maxHealth", healthMultiplier})}); + + m_scriptComponent.addCallbacks("monster", makeMonsterCallbacks()); + m_scriptComponent.addCallbacks("config", LuaBindings::makeConfigCallbacks([this](String const& name, Json const& def) { + return m_monsterVariant.parameters.query(name, def); + })); + m_scriptComponent.addCallbacks("entity", LuaBindings::makeEntityCallbacks(this)); + m_scriptComponent.addCallbacks("animator", LuaBindings::makeNetworkedAnimatorCallbacks(&m_networkedAnimator)); + m_scriptComponent.addCallbacks("status", LuaBindings::makeStatusControllerCallbacks(m_statusController.get())); + m_scriptComponent.addCallbacks("behavior", LuaBindings::makeBehaviorLuaCallbacks(&m_behaviors)); + m_scriptComponent.addActorMovementCallbacks(m_movementController.get()); + m_scriptComponent.init(world); + } + + if (world->isClient()) { + m_scriptedAnimator.setScripts(m_monsterVariant.animationScripts); + + m_scriptedAnimator.addCallbacks("animationConfig", LuaBindings::makeScriptedAnimatorCallbacks(&m_networkedAnimator, + [this](String const& name, Json const& defaultValue) -> Json { + return m_scriptedAnimationParameters.value(name, defaultValue); + })); + m_scriptedAnimator.addCallbacks("config", LuaBindings::makeConfigCallbacks([this](String const& name, Json const& def) { + return m_monsterVariant.parameters.query(name, def); + })); + m_scriptedAnimator.addCallbacks("entity", LuaBindings::makeEntityCallbacks(this)); + m_scriptedAnimator.init(world); + } + + setPosition(position()); +} + +void Monster::uninit() { + if (isMaster()) { + m_scriptComponent.uninit(); + m_scriptComponent.removeCallbacks("monster"); + m_scriptComponent.removeCallbacks("config"); + m_scriptComponent.removeCallbacks("entity"); + m_scriptComponent.removeCallbacks("animator"); + m_scriptComponent.removeCallbacks("status"); + m_scriptComponent.removeActorMovementCallbacks(); + } + if (world()->isClient()) { + m_scriptedAnimator.removeCallbacks("animationConfig"); + m_scriptedAnimator.removeCallbacks("config"); + m_scriptedAnimator.removeCallbacks("entity"); + } + m_statusController->uninit(); + m_movementController->uninit(); + Entity::uninit(); +} + +Vec2F Monster::mouthOffset() const { + return getAbsolutePosition(m_monsterVariant.mouthOffset) - position(); +} + +Vec2F Monster::feetOffset() const { + return getAbsolutePosition(m_monsterVariant.feetOffset) - position(); +} + +Vec2F Monster::position() const { + return m_movementController->position(); +} + +RectF Monster::metaBoundBox() const { + return m_monsterVariant.metaBoundBox; +} + +RectF Monster::collisionArea() const { + return m_movementController->collisionPoly().boundBox(); +} + +Vec2F Monster::velocity() const { + return m_movementController->velocity(); +} + +pair<ByteArray, uint64_t> Monster::writeNetState(uint64_t fromVersion) { + return m_netGroup.writeNetState(fromVersion); +} + +void Monster::readNetState(ByteArray data, float interpolationTime) { + m_netGroup.readNetState(move(data), interpolationTime); +} + +void Monster::enableInterpolation(float extrapolationHint) { + m_netGroup.enableNetInterpolation(extrapolationHint); +} + +void Monster::disableInterpolation() { + m_netGroup.disableNetInterpolation(); +} + +String Monster::description() const { + return m_monsterVariant.description.value("Some indescribable horror"); +} + +Maybe<HitType> Monster::queryHit(DamageSource const& source) const { + if (!inWorld() || m_knockedOut || m_statusController->statPositive("invulnerable")) + return {}; + + if (source.intersectsWithPoly(world()->geometry(), hitPoly().get())) + return HitType::Hit; + + return {}; +} + +Maybe<PolyF> Monster::hitPoly() const { + PolyF hitBody = m_monsterVariant.selfDamagePoly; + hitBody.rotate(m_movementController->rotation()); + hitBody.translate(position()); + return hitBody; +} + +List<DamageNotification> Monster::applyDamage(DamageRequest const& damage) { + if (!inWorld()) + return {}; + + auto notifications = m_statusController->applyDamageRequest(damage); + + float totalDamage = 0.0f; + for (auto const& notification : notifications) + totalDamage += notification.healthLost; + + if (totalDamage > 0.0f) { + m_scriptComponent.invoke("damage", JsonObject{ + {"sourceId", damage.sourceEntityId}, + {"damage", totalDamage}, + {"sourceDamage", damage.damage}, + {"sourceKind", damage.damageSourceKind} + }); + } + + if (!m_statusController->resourcePositive("health")) + m_deathDamageSourceKinds.add(damage.damageSourceKind); + + return notifications; +} + +List<DamageNotification> Monster::selfDamageNotifications() { + return m_statusController->pullSelfDamageNotifications(); +} + +List<DamageSource> Monster::damageSources() const { + List<DamageSource> damageSources = m_damageSources.get(); + + float levelPowerMultiplier = Root::singleton().functionDatabase()->function(m_monsterVariant.powerLevelFunction)->evaluate(*m_monsterLevel); + if (m_damageOnTouch && !m_monsterVariant.touchDamageConfig.isNull()) { + DamageSource damageSource(m_monsterVariant.touchDamageConfig); + if (auto damagePoly = damageSource.damageArea.ptr<PolyF>()) + damagePoly->rotate(m_movementController->rotation()); + damageSource.damage *= m_monsterVariant.touchDamageMultiplier * levelPowerMultiplier * m_statusController->stat("powerMultiplier"); + damageSource.sourceEntityId = entityId(); + damageSource.team = getTeam(); + damageSources.append(damageSource); + } + + auto animationDamageParts = m_monsterVariant.animationDamageParts; + for (auto pair : m_monsterVariant.animationDamageParts) { + if (!m_animationDamageParts.get().contains(pair.first)) + continue; + + String anchorPart = pair.second.getString("anchorPart"); + DamageSource ds = DamageSource(pair.second.get("damageSource")); + ds.damage *= levelPowerMultiplier * m_statusController->stat("powerMultiplier"); + ds.damageArea.call([this,&anchorPart](auto& poly) { + poly.transform(m_networkedAnimator.partTransformation(anchorPart)); + if (m_networkedAnimator.flipped()) + poly.flipHorizontal(m_networkedAnimator.flippedRelativeCenterLine()); + }); + if (ds.knockback.is<Vec2F>()) { + Vec2F knockback = ds.knockback.get<Vec2F>(); + knockback = m_networkedAnimator.partTransformation(anchorPart).transformVec2(knockback); + if (m_networkedAnimator.flipped()) + knockback = Vec2F(-knockback[0], knockback[1]); + ds.knockback = knockback; + } + + List<DamageSource> partSources; + if (auto line = ds.damageArea.maybe<Line2F>()) { + if (pair.second.getBool("checkLineCollision", false)) { + Line2F worldLine = line.value().translated(position()); + float length = worldLine.length(); + + auto bounces = pair.second.getInt("bounces", 0); + while (auto collision = world()->lineTileCollisionPoint(worldLine.min(), worldLine.max())) { + worldLine = Line2F(worldLine.min(), collision.value().first); + ds.damageArea = worldLine.translated(-position()); + length = length - worldLine.length(); + + if (--bounces >= 0 && length > 0.0f) { + partSources.append(ds); + ds = DamageSource(ds); + Vec2F dir = worldLine.direction(); + Vec2F normal = Vec2F((*collision).second); + Vec2F reflection = dir - (2 * dir.piecewiseMultiply(normal).sum() * normal); + if (ds.knockback.is<Vec2F>()) + ds.knockback = ds.knockback.get<Vec2F>().rotate(reflection.angleBetween(worldLine.direction())); + + worldLine.min() = (*collision).first; + worldLine.max() = worldLine.min() + (reflection * length); + ds.damageArea = worldLine.translated(-position()); + } else { + break; + } + } + partSources.append(ds); + } + } else { + partSources.append(ds); + } + damageSources.appendAll(partSources); + } + + return damageSources; +} + +bool Monster::shouldDie() { + if (auto res = m_scriptComponent.invoke<bool>("shouldDie")) + return *res; + else if (!m_statusController->resourcePositive("health") || m_scriptComponent.error()) + return true; + else + return false; +} + +void Monster::knockout() { + m_knockedOut = true; + m_knockoutTimer = m_monsterVariant.parameters.getFloat("knockoutTime", 1.0f); + + m_damageOnTouch = false; + + String knockoutEffect = m_monsterVariant.parameters.getString("knockoutEffect"); + if (!knockoutEffect.empty()) + m_networkedAnimator.setEffectEnabled(knockoutEffect, true); + + auto knockoutAnimationStates = m_monsterVariant.parameters.getObject("knockoutAnimationStates", JsonObject()); + for (auto pair : knockoutAnimationStates) + m_networkedAnimator.setState(pair.first, pair.second.toString()); +} + +bool Monster::shouldDestroy() const { + return m_knockedOut && m_knockoutTimer <= 0; +} + +void Monster::destroy(RenderCallback* renderCallback) { + m_scriptComponent.invoke("die"); + + if (isMaster() && !m_dropPool.isNull()) { + auto treasureDatabase = Root::singleton().treasureDatabase(); + + String treasurePool; + if (m_dropPool.isType(Json::Type::String)) { + treasurePool = m_dropPool.toString(); + } else { + // Check to see whether any of the damage types that were used to cause + // death are in the damage pool map, if so spawn treasure from that, + // otherwise set the treasure pool to the "default" entry. + + for (auto const& damageSourceKind : m_deathDamageSourceKinds) { + if (m_dropPool.contains(damageSourceKind)) + treasurePool = m_dropPool.getString(damageSourceKind); + } + + if (treasurePool.empty()) + treasurePool = m_dropPool.getString("default"); + } + + for (auto const& treasureItem : treasureDatabase->createTreasure(treasurePool, *m_monsterLevel)) + world()->addEntity(ItemDrop::createRandomizedDrop(treasureItem, position())); + } + + if (renderCallback) { + if (!m_deathParticleBurst.empty()) + m_networkedAnimator.burstParticleEmitter(m_deathParticleBurst); + + if (!m_deathSound.empty()) + m_networkedAnimator.playSound(m_deathSound); + + m_networkedAnimator.update(0.0, &m_networkedAnimatorDynamicTarget); + + renderCallback->addAudios(m_networkedAnimatorDynamicTarget.pullNewAudios()); + renderCallback->addParticles(m_networkedAnimatorDynamicTarget.pullNewParticles()); + renderCallback->addParticles(m_statusController->pullNewParticles()); + } + + m_deathDamageSourceKinds.clear(); + + if (isMaster()) + setNetStates(); +} + +List<LightSource> Monster::lightSources() const { + auto lightSources = m_networkedAnimator.lightSources(position()); + lightSources.appendAll(m_statusController->lightSources()); + return lightSources; +} + +void Monster::hitOther(EntityId targetEntityId, DamageRequest const& damageRequest) { + if (inWorld() && isMaster()) + m_statusController->hitOther(targetEntityId, damageRequest); +} + +void Monster::damagedOther(DamageNotification const& damage) { + if (inWorld() && isMaster()) + m_statusController->damagedOther(damage); +} + +void Monster::update(uint64_t) { + if (!inWorld()) + return; + + if (isMaster()) { + m_networkedAnimator.setFlipped((m_movementController->facingDirection() == Direction::Left) != m_monsterVariant.reversed); + + if (m_knockedOut) { + m_knockoutTimer -= WorldTimestep; + } else { + if (m_scriptComponent.updateReady()) + m_physicsForces.set({}); + m_scriptComponent.update(m_scriptComponent.updateDt()); + + if (shouldDie()) + knockout(); + } + + m_movementController->tickMaster(); + + m_statusController->tickMaster(); + updateStatus(); + } else { + m_netGroup.tickNetInterpolation(WorldTimestep); + + m_statusController->tickSlave(); + updateStatus(); + + m_movementController->tickSlave(); + } + + if (world()->isServer()) { + m_networkedAnimator.update(WorldTimestep, nullptr); + } else { + m_networkedAnimator.update(WorldTimestep, &m_networkedAnimatorDynamicTarget); + m_networkedAnimatorDynamicTarget.updatePosition(position()); + + m_scriptedAnimator.update(); + } + + SpatialLogger::logPoly("world", m_movementController->collisionBody(), {255, 0, 0, 255}); +} + +void Monster::render(RenderCallback* renderCallback) { + for (auto& drawable : m_networkedAnimator.drawables(position())) { + if (drawable.isImage()) + drawable.imagePart().addDirectives(m_statusController->parentDirectives(), true); + renderCallback->addDrawable(move(drawable), m_monsterVariant.renderLayer); + } + + renderCallback->addAudios(m_networkedAnimatorDynamicTarget.pullNewAudios()); + renderCallback->addParticles(m_networkedAnimatorDynamicTarget.pullNewParticles()); + + renderCallback->addLightSources(m_networkedAnimator.lightSources(position())); + + renderCallback->addDrawables(m_statusController->drawables(), m_monsterVariant.renderLayer); + renderCallback->addLightSources(m_statusController->lightSources()); + renderCallback->addParticles(m_statusController->pullNewParticles()); + renderCallback->addAudios(m_statusController->pullNewAudios()); + + m_effectEmitter.render(renderCallback); + + for (auto drawablePair : m_scriptedAnimator.drawables()) + renderCallback->addDrawable(drawablePair.first, drawablePair.second.value(m_monsterVariant.renderLayer)); + renderCallback->addLightSources(m_scriptedAnimator.lightSources()); + renderCallback->addAudios(m_scriptedAnimator.pullNewAudios()); + renderCallback->addParticles(m_scriptedAnimator.pullNewParticles()); +} + +void Monster::setPosition(Vec2F const& pos) { + m_movementController->setPosition(pos); +} + +Maybe<Json> Monster::receiveMessage(ConnectionId sendingConnection, String const& message, JsonArray const& args) { + Maybe<Json> result = m_scriptComponent.handleMessage(message, world()->connection() == sendingConnection, args); + if (!result) + result = m_statusController->receiveMessage(message, world()->connection() == sendingConnection, args); + return result; +} + +float Monster::maxHealth() const { + return *m_statusController->resourceMax("health"); +} + +float Monster::health() const { + return m_statusController->resource("health"); +} + +DamageBarType Monster::damageBar() const { + return m_damageBar.get(); +} + +Vec2F Monster::getAbsolutePosition(Vec2F relativePosition) const { + if (m_movementController->facingDirection() == Direction::Left) + relativePosition[0] *= -1; + if (m_movementController->rotation() != 0) + relativePosition = relativePosition.rotate(m_movementController->rotation()); + return m_movementController->position() + relativePosition; +} + +void Monster::updateStatus() { + m_effectEmitter.setSourcePosition("normal", position()); + m_effectEmitter.setSourcePosition("mouth", position() + mouthOffset()); + m_effectEmitter.setSourcePosition("feet", position() + feetOffset()); + m_effectEmitter.setDirection(m_movementController->facingDirection()); + m_effectEmitter.tick(*entityMode()); +} + +LuaCallbacks Monster::makeMonsterCallbacks() { + LuaCallbacks callbacks; + + callbacks.registerCallback("type", [this]() { + return m_monsterVariant.type; + }); + + callbacks.registerCallback("seed", [this]() { + return strf("%d", m_monsterVariant.seed); + }); + + callbacks.registerCallback("uniqueParameters", [this]() { + return m_monsterVariant.uniqueParameters; + }); + + callbacks.registerCallback("level", [this]() { + return *m_monsterLevel; + }); + + callbacks.registerCallback("setDamageOnTouch", [this](bool arg1) { + m_damageOnTouch = arg1; + }); + + callbacks.registerCallback("setDamageSources", [this](Maybe<JsonArray> const& damageSources) { + m_damageSources.set(damageSources.value().transformed(construct<DamageSource>())); + }); + + callbacks.registerCallback("setDamageParts", [this](StringSet const& parts) { + m_animationDamageParts.set(parts); + }); + + callbacks.registerCallback("setAggressive", [this](bool arg1) { + m_aggressive = arg1; + }); + + callbacks.registerCallback("setActiveSkillName", [this](Maybe<String> const& activeSkillName) { + m_activeSkillName = activeSkillName.value(); + }); + + callbacks.registerCallback("setDropPool", [this](Json dropPool) { + m_dropPool = move(dropPool); + }); + + callbacks.registerCallback("toAbsolutePosition", [this](Vec2F const& p) { + return getAbsolutePosition(p); + }); + + callbacks.registerCallback("mouthPosition", [this]() { + return mouthPosition(); + }); + + // This callback is registered here rather than in + // makeActorMovementControllerCallbacks + // because it requires access to world + callbacks.registerCallback("flyTo", [this](Vec2F const& arg1) { + m_movementController->controlFly(world()->geometry().diff(arg1, position())); + }); + + callbacks.registerCallback("setDeathParticleBurst", [this](Maybe<String> const& arg1) { + m_deathParticleBurst = arg1.value(); + }); + + callbacks.registerCallback("setDeathSound", [this](Maybe<String> const& arg1) { + m_deathSound = arg1.value(); + }); + + callbacks.registerCallback("setPhysicsForces", [this](JsonArray const& forces) { + m_physicsForces.set(forces.transformed(jsonToPhysicsForceRegion)); + }); + + callbacks.registerCallback("setName", [this](String const& name) { + m_name.set(name); + }); + callbacks.registerCallback("setDisplayNametag", [this](bool display) { + m_displayNametag.set(display); + }); + + callbacks.registerCallback("say", [this](String line, Maybe<StringMap<String>> const& tags) { + if (tags) + line = line.replaceTags(*tags, false); + + if (!line.empty()) { + addChatMessage(line); + return true; + } + + return false; + }); + + callbacks.registerCallback("sayPortrait", [this](String line, String portrait, Maybe<StringMap<String>> const& tags) { + if (tags) + line = line.replaceTags(*tags, false); + + if (!line.empty()) { + addChatMessage(line, portrait); + return true; + } + + return false; + }); + + callbacks.registerCallback("setDamageTeam", [this](Json const& team) { + setTeam(EntityDamageTeam(team)); + }); + + callbacks.registerCallback("setUniqueId", [this](Maybe<String> uniqueId) { + setUniqueId(uniqueId); + }); + + callbacks.registerCallback("setDamageBar", [this](String const& damageBarType) { + m_damageBar.set(DamageBarTypeNames.getLeft(damageBarType)); + }); + + callbacks.registerCallback("setInteractive", [this](bool interactive) { + m_interactive.set(interactive); + }); + + callbacks.registerCallback("setAnimationParameter", [this](String name, Json value) { + m_scriptedAnimationParameters.set(move(name), move(value)); + }); + + return callbacks; +} + +void Monster::addChatMessage(String const& message, String const& portrait) { + m_chatMessage.set(message); + m_chatPortrait.set(portrait); + m_newChatMessageEvent.trigger(); + if (portrait.empty()) + m_pendingChatActions.append(SayChatAction{entityId(), message, mouthPosition()}); + else + m_pendingChatActions.append(PortraitChatAction{entityId(), portrait, message, mouthPosition()}); +} + +void Monster::setupNetStates() { + m_netGroup.addNetElement(&m_uniqueIdNetState); + m_netGroup.addNetElement(&m_teamNetState); + m_netGroup.addNetElement(&m_monsterLevelNetState); + m_netGroup.addNetElement(&m_damageOnTouchNetState); + m_netGroup.addNetElement(&m_damageSources); + m_netGroup.addNetElement(&m_aggressiveNetState); + m_netGroup.addNetElement(&m_knockedOutNetState); + m_netGroup.addNetElement(&m_deathParticleBurstNetState); + m_netGroup.addNetElement(&m_deathSoundNetState); + m_netGroup.addNetElement(&m_activeSkillNameNetState); + m_netGroup.addNetElement(&m_name); + m_netGroup.addNetElement(&m_displayNametag); + m_netGroup.addNetElement(&m_dropPoolNetState); + m_netGroup.addNetElement(&m_physicsForces); + + m_netGroup.addNetElement(&m_networkedAnimator); + m_netGroup.addNetElement(m_movementController.get()); + m_netGroup.addNetElement(m_statusController.get()); + m_netGroup.addNetElement(&m_effectEmitter); + + m_netGroup.addNetElement(&m_newChatMessageEvent); + m_netGroup.addNetElement(&m_chatMessage); + m_netGroup.addNetElement(&m_chatPortrait); + + m_netGroup.addNetElement(&m_damageBar); + m_netGroup.addNetElement(&m_interactive); + + // don't interpolate scripted animation parameters or animationdamageparts + m_netGroup.addNetElement(&m_animationDamageParts, false); + m_netGroup.addNetElement(&m_scriptedAnimationParameters, false); + + m_netGroup.setNeedsLoadCallback(bind(&Monster::getNetStates, this, _1)); + m_netGroup.setNeedsStoreCallback(bind(&Monster::setNetStates, this)); +} + +void Monster::setNetStates() { + m_uniqueIdNetState.set(uniqueId()); + m_teamNetState.set(getTeam()); + m_monsterLevelNetState.set(m_monsterLevel); + m_damageOnTouchNetState.set(m_damageOnTouch); + m_aggressiveNetState.set(aggressive()); + m_knockedOutNetState.set(m_knockedOut); + m_deathParticleBurstNetState.set(m_deathParticleBurst); + m_deathSoundNetState.set(m_deathSound); + m_activeSkillNameNetState.set(m_activeSkillName); + m_dropPoolNetState.set(m_dropPool); +} + +void Monster::getNetStates(bool initial) { + setUniqueId(m_uniqueIdNetState.get()); + setTeam(m_teamNetState.get()); + m_monsterLevel = m_monsterLevelNetState.get(); + m_damageOnTouch = m_damageOnTouchNetState.get(); + m_aggressive = m_aggressiveNetState.get(); + m_knockedOut = m_knockedOutNetState.get(); + if (m_deathParticleBurstNetState.pullUpdated()) + m_deathParticleBurst = m_deathParticleBurstNetState.get(); + if (m_deathSoundNetState.pullUpdated()) + m_deathSound = m_deathSoundNetState.get(); + if (m_activeSkillNameNetState.pullUpdated()) + m_activeSkillName = m_activeSkillNameNetState.get(); + if (m_dropPoolNetState.pullUpdated()) + m_dropPool = m_dropPoolNetState.get(); + + if (m_newChatMessageEvent.pullOccurred() && !initial) { + if (m_chatPortrait.get().empty()) + m_pendingChatActions.append(SayChatAction{entityId(), m_chatMessage.get(), mouthPosition()}); + else + m_pendingChatActions.append( + PortraitChatAction{entityId(), m_chatPortrait.get(), m_chatMessage.get(), mouthPosition()}); + } +} + +float Monster::monsterLevel() const { + return *m_monsterLevel; +} + +Monster::SkillInfo Monster::activeSkillInfo() const { + SkillInfo skillInfo; + + if (!m_activeSkillName.empty()) { + auto monsterDatabase = Root::singleton().monsterDatabase(); + auto monsterSkillInfo = monsterDatabase->skillInfo(m_activeSkillName); + skillInfo.label = monsterSkillInfo.first; + skillInfo.image = monsterSkillInfo.second; + } + + return skillInfo; +} + +List<Drawable> Monster::portrait(PortraitMode) const { + if (m_monsterVariant.portraitIcon) { + return {Drawable::makeImage(*m_monsterVariant.portraitIcon, 1.0f, true, Vec2F())}; + } else { + auto animator = m_networkedAnimator; + animator.setFlipped(!m_monsterVariant.reversed); + auto drawables = animator.drawables(); + Drawable::scaleAll(drawables, TilePixels); + return drawables; + } +} + +String Monster::name() const { + return m_name.get().orMaybe(m_monsterVariant.shortDescription).value(""); +} + +String Monster::typeName() const { + return m_monsterVariant.type; +} + +MonsterVariant Monster::monsterVariant() const { + return m_monsterVariant; +} + +Maybe<String> Monster::statusText() const { + return {}; +} + +bool Monster::displayNametag() const { + return m_displayNametag.get(); +} + +Vec3B Monster::nametagColor() const { + return m_monsterVariant.nametagColor; +} + +bool Monster::aggressive() const { + return m_aggressive; +} + +Maybe<LuaValue> Monster::callScript(String const& func, LuaVariadic<LuaValue> const& args) { + return m_scriptComponent.invoke(func, args); +} + +Maybe<LuaValue> Monster::evalScript(String const& code) { + return m_scriptComponent.eval(code); +} + +Vec2F Monster::mouthPosition() const { + return mouthOffset() + position(); +} + +List<ChatAction> Monster::pullPendingChatActions() { + return std::move(m_pendingChatActions); +} + +List<PhysicsForceRegion> Monster::forceRegions() const { + return m_physicsForces.get(); +} + +InteractAction Monster::interact(InteractRequest const& request) { + auto result = m_scriptComponent.invoke<Json>("interact", JsonObject{{"sourceId", request.sourceId}, {"sourcePosition", jsonFromVec2F(request.sourcePosition)}}).value(); + + if (result.isNull()) + return {}; + + if (result.isType(Json::Type::String)) + return InteractAction(result.toString(), entityId(), {}); + + return InteractAction(result.getString(0), entityId(), result.get(1)); +} + +bool Monster::isInteractive() const { + return m_interactive.get(); +} + +Vec2F Monster::questIndicatorPosition() const { + Vec2F pos = position() + m_questIndicatorOffset; + pos[1] += collisionArea().yMax(); + return pos; +} + +} |