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/StarObject.cpp | |
parent | 6741a057e5639280d85d0f88ba26f000baa58f61 (diff) |
everything everywhere
all at once
Diffstat (limited to 'source/game/StarObject.cpp')
-rw-r--r-- | source/game/StarObject.cpp | 1298 |
1 files changed, 1298 insertions, 0 deletions
diff --git a/source/game/StarObject.cpp b/source/game/StarObject.cpp new file mode 100644 index 0000000..0a35ee7 --- /dev/null +++ b/source/game/StarObject.cpp @@ -0,0 +1,1298 @@ +#include "StarObject.hpp" +#include "StarDataStreamExtra.hpp" +#include "StarJsonExtra.hpp" +#include "StarWorld.hpp" +#include "StarLexicalCast.hpp" +#include "StarRoot.hpp" +#include "StarLogging.hpp" +#include "StarDamageManager.hpp" +#include "StarTreasure.hpp" +#include "StarItemDrop.hpp" +#include "StarItemDescriptor.hpp" +#include "StarObjectDatabase.hpp" +#include "StarMixer.hpp" +#include "StarEntityRendering.hpp" +#include "StarAssets.hpp" +#include "StarConfigLuaBindings.hpp" +#include "StarEntityLuaBindings.hpp" +#include "StarRootLuaBindings.hpp" +#include "StarNetworkedAnimatorLuaBindings.hpp" +#include "StarLuaGameConverters.hpp" +#include "StarParticleDatabase.hpp" +#include "StarMaterialDatabase.hpp" +#include "StarScriptedAnimatorLuaBindings.hpp" + +namespace Star { + +Object::Object(ObjectConfigConstPtr config, Json const& parameters) { + m_config = config; + if (!parameters.isNull()) + m_parameters.reset(parameters.toObject()); + + m_animationTimer = 0.0f; + m_currentFrame = 0; + + m_lightFlickering = m_config->lightFlickering; + + m_tileDamageStatus = make_shared<EntityTileDamageStatus>(); + + m_orientationIndex = NPos; + + m_interactive.set(!configValue("interactAction", Json()).isNull()); + + m_broken = false; + m_unbreakable = m_config->unbreakable || configValue("unbreakable", false).toBool(); + m_direction.set(Direction::Left); + + m_health.set(m_config->health); + + m_currentFrame = -1; + + if (m_config->animationConfig) + m_networkedAnimator = make_shared<NetworkedAnimator>(m_config->animationConfig, m_config->path); + else + m_networkedAnimator = make_shared<NetworkedAnimator>(); + + if (m_config->damageTeam.type != TeamType::Null) { + setTeam(m_config->damageTeam); + } else { + setTeam(EntityDamageTeam(TeamType::Environment)); + } + + for (auto const& node : configValue("inputNodes", JsonArray()).iterateArray()) + m_inputNodes.append({jsonToVec2I(node), {}, {}}); + + for (auto const& node : configValue("outputNodes", JsonArray()).iterateArray()) + m_outputNodes.append({jsonToVec2I(node), {}, {}}); + + m_offeredQuests.set(configValue("offeredQuests", JsonArray()).toArray().transformed(&QuestArcDescriptor::fromJson)); + m_turnInQuests.set(jsonToStringSet(configValue("turnInQuests", JsonArray()))); + if (!m_offeredQuests.get().empty() || !m_turnInQuests.get().empty()) + m_interactive.set(true); + + setUniqueId(configValue("uniqueId").optString()); + + m_netGroup.addNetElement(&m_parameters); + m_netGroup.addNetElement(&m_uniqueIdNetState); + m_netGroup.addNetElement(&m_interactive); + m_netGroup.addNetElement(&m_materialSpaces); + m_netGroup.addNetElement(&m_xTilePosition); + m_netGroup.addNetElement(&m_yTilePosition); + m_netGroup.addNetElement(&m_direction); + m_netGroup.addNetElement(&m_health); + m_netGroup.addNetElement(&m_orientationIndexNetState); + m_netGroup.addNetElement(&m_netImageKeys); + m_netGroup.addNetElement(&m_soundEffectEnabled); + m_netGroup.addNetElement(&m_lightSourceColor); + m_netGroup.addNetElement(&m_newChatMessageEvent); + m_netGroup.addNetElement(&m_chatMessage); + m_netGroup.addNetElement(&m_chatPortrait); + m_netGroup.addNetElement(&m_chatConfig); + + for (auto& i : m_inputNodes) { + m_netGroup.addNetElement(&i.connections); + m_netGroup.addNetElement(&i.state); + } + for (auto& o : m_outputNodes) { + m_netGroup.addNetElement(&o.connections); + m_netGroup.addNetElement(&o.state); + } + + m_netGroup.addNetElement(&m_offeredQuests); + m_netGroup.addNetElement(&m_turnInQuests); + m_netGroup.addNetElement(&m_damageSources); + + // don't interpolate scripted animation parameters + m_netGroup.addNetElement(&m_scriptedAnimationParameters, false); + + m_netGroup.addNetElement(m_tileDamageStatus.get()); + m_netGroup.addNetElement(m_networkedAnimator.get()); + + m_netGroup.setNeedsLoadCallback(bind(&Object::getNetStates, this, _1)); + m_netGroup.setNeedsStoreCallback(bind(&Object::setNetStates, this)); +} + +Json Object::diskStore() const { + return writeStoredData().setAll({{"name", m_config->name}, {"parameters", m_parameters.baseMap()}}); +} + +ByteArray Object::netStore() { + DataStreamBuffer ds; + ds.write(m_config->name); + ds.write<Json>(m_parameters.baseMap()); + return ds.takeData(); +} + +EntityType Object::entityType() const { + return EntityType::Object; +} + +void Object::init(World* world, EntityId entityId, EntityMode mode) { + Entity::init(world, entityId, mode); + // Only try and find a new orientation if we do not already have one, + // otherwise we may have a valid orientation that depends on non-tile data + // that is not loaded yet. + if (m_orientationIndex == NPos) { + updateOrientation(); + } else if (auto orientation = currentOrientation()) { + // update direction in case orientation config direction has changed + if (orientation->directionAffinity) + m_direction.set(*orientation->directionAffinity); + m_materialSpaces.set(orientation->materialSpaces); + } + + m_orientationDrawablesCache.reset(); + + if (isMaster()) { + auto colorName = configValue("color", "default").toString(); + setImageKey("color", colorName); + + if (m_config->lightColors.contains(colorName)) + m_lightSourceColor.set(m_config->lightColors.get(colorName)); + else + m_lightSourceColor.set(Color::Clear); + + m_soundEffectEnabled.set(true); + + m_liquidCheckTimer = GameTimer(m_config->liquidCheckInterval); + m_liquidCheckTimer.setDone(); + + setKeepAlive(configValue("keepAlive", false).toBool()); + + m_scriptComponent.setScripts(m_config->scripts); + m_scriptComponent.setUpdateDelta(configValue("scriptDelta", 5).toInt()); + + m_scriptComponent.addCallbacks("object", makeObjectCallbacks()); + m_scriptComponent.addCallbacks("config", LuaBindings::makeConfigCallbacks(bind(&Object::configValue, this, _1, _2))); + m_scriptComponent.addCallbacks("entity", LuaBindings::makeEntityCallbacks(this)); + m_scriptComponent.addCallbacks("animator", LuaBindings::makeNetworkedAnimatorCallbacks(m_networkedAnimator.get())); + m_scriptComponent.init(world); + } + + if (world->isClient()) { + m_scriptedAnimator.setScripts(m_config->animationScripts); + + m_scriptedAnimator.addCallbacks("animationConfig", LuaBindings::makeScriptedAnimatorCallbacks(m_networkedAnimator.get(), + [this](String const& name, Json const& defaultValue) -> Json { + return m_scriptedAnimationParameters.value(name, defaultValue); + })); + m_scriptedAnimator.addCallbacks("objectAnimator", makeAnimatorObjectCallbacks()); + m_scriptedAnimator.addCallbacks("config", LuaBindings::makeConfigCallbacks(bind(&Object::configValue, this, _1, _2))); + m_scriptedAnimator.addCallbacks("entity", LuaBindings::makeEntityCallbacks(this)); + m_scriptedAnimator.init(world); + } + + m_xTilePosition.set(world->geometry().xwrap((int)m_xTilePosition.get())); + + // Compute all the relevant animation information after the final orientation + // has been selected and after the script is initialized + + for (auto const& pair : configValue("animationParts", JsonObject()).iterateObject()) + m_networkedAnimator->setPartTag(pair.first, "partImage", pair.second.toString()); + + m_animationPosition = jsonToVec2F(configValue("animationPosition", JsonArray{0, 0})) / TilePixels; + + m_networkedAnimator->setFlipped(false); + m_animationCenterLine = configValue("animationCenterLine", Drawable::boundBoxAll(m_networkedAnimator->drawables(), false).center()[0]).toFloat(); + m_networkedAnimator->setFlipped(direction() == Direction::Left, m_animationCenterLine); + + // Don't animate the initial state when first spawned IF you're dumb, which by default + // you would be, and don't know how to use transition and static states properly. Someday + // I'll be brave and delete shit garbage entirely and we'll see what breaks. + if (configValue("forceFinishAnimationsInInit", true) != false) + m_networkedAnimator->finishAnimations(); +} + +void Object::uninit() { + if (isMaster()) { + m_scriptComponent.uninit(); + m_scriptComponent.removeCallbacks("object"); + m_scriptComponent.removeCallbacks("config"); + m_scriptComponent.removeCallbacks("entity"); + m_scriptComponent.removeCallbacks("animator"); + } + + if (world()->isClient()) { + m_scriptedAnimator.uninit(); + m_scriptedAnimator.removeCallbacks("animationConfig"); + m_scriptedAnimator.removeCallbacks("objectAnimator"); + m_scriptedAnimator.removeCallbacks("config"); + m_scriptedAnimator.removeCallbacks("entity"); + } + + if (m_soundEffect) + m_soundEffect->stop(); + + Entity::uninit(); +} + +List<LightSource> Object::lightSources() const { + List<LightSource> lights; + lights.appendAll(m_networkedAnimator->lightSources(position() + m_animationPosition)); + + auto orientation = currentOrientation(); + if (!m_lightSourceColor.get().isClear() && orientation) { + Color color = m_lightSourceColor.get(); + if (m_lightFlickering) + color.setValue(clamp(color.value() * m_lightFlickering->value(SinWeightOperator<float>()), 0.0f, 1.0f)); + + LightSource lightSource; + lightSource.position = position() + centerOfTile(orientation->lightPosition); + lightSource.color = color.toRgb(); + lightSource.pointLight = m_config->pointLight; + lightSource.pointBeam = m_config->pointBeam; + lightSource.beamAngle = orientation->beamAngle; + lightSource.beamAmbience = m_config->beamAmbience; + + lights.append(lightSource); + } + + return lights; +} + +Vec2F Object::position() const { + return Vec2F(m_xTilePosition.get(), m_yTilePosition.get()); +} + +RectF Object::metaBoundBox() const { + if (auto orientation = currentOrientation()) { + // default metaboundbox extends the bounding box of the orientation's + // spaces by one block + return orientation->metaBoundBox.value(RectF(Vec2F(orientation->boundBox.min()) - Vec2F(1, 1), Vec2F(orientation->boundBox.max()) + Vec2F(2, 2))); + } else { + return RectF(-1, -1, 1, 1); + } +} + +pair<ByteArray, uint64_t> Object::writeNetState(uint64_t fromVersion) { + DataStreamBuffer ds; + return m_netGroup.writeNetState(fromVersion); +} + +void Object::readNetState(ByteArray delta, float interpolationTime) { + m_netGroup.readNetState(move(delta), interpolationTime); +} + +Vec2I Object::tilePosition() const { + return Vec2I(m_xTilePosition.get(), m_yTilePosition.get()); +} + +void Object::setTilePosition(Vec2I const& pos) { + if (m_xTilePosition.get() != pos[0] || m_yTilePosition.get() != pos[1]) { + m_xTilePosition.set(pos[0]); + m_yTilePosition.set(pos[1]); + if (inWorld()) + updateOrientation(); + } +} + +Direction Object::direction() const { + return m_direction.get(); +} + +void Object::setDirection(Direction direction) { + m_direction.set(direction); +} + +void Object::updateOrientation() { + setOrientationIndex(m_config->findValidOrientation(world(), tilePosition(), m_direction.get())); + if (auto orientation = currentOrientation()) { + if (orientation->directionAffinity) + m_direction.set(*orientation->directionAffinity); + m_materialSpaces.set(orientation->materialSpaces); + } + resetEmissionTimers(); +} + +List<Vec2I> Object::anchorPositions() const { + if (auto orientation = currentOrientation()) { + List<Vec2I> positions; + for (auto anchor : orientation->anchors) + positions.append(anchor.position + tilePosition()); + return positions; + } else { + return {}; + } +} + +List<Vec2I> Object::spaces() const { + if (auto orientation = currentOrientation()) + return orientation->spaces; + else + return {}; +} + +List<MaterialSpace> Object::materialSpaces() const { + return m_materialSpaces.get(); +} + +List<Vec2I> Object::roots() const { + if (m_config->rooting) { + if (auto orientation = currentOrientation()) { + List<Vec2I> res; + for (auto anchor : orientation->anchors) + res.append(anchor.position); + return res; + } + } + return {}; +} + +void Object::update(uint64_t) { + if (!inWorld()) + return; + + if (isMaster()) { + m_tileDamageStatus->recover(m_config->tileDamageParameters, WorldTimestep); + + if (m_liquidCheckTimer.wrapTick()) + checkLiquidBroken(); + + if (auto orientation = currentOrientation()) { + auto frame = clamp<int>(std::floor(m_animationTimer / orientation->animationCycle * orientation->frames), 0, orientation->frames - 1); + if (m_currentFrame != frame) { + m_currentFrame = frame; + setImageKey("frame", toString(frame)); + } + + m_animationTimer = std::fmod(m_animationTimer + WorldTimestep, orientation->animationCycle); + } + + m_networkedAnimator->update(WorldTimestep, nullptr); + m_networkedAnimator->setFlipped(direction() == Direction::Left, m_animationCenterLine); + + m_scriptComponent.update(m_scriptComponent.updateDt()); + + } else { + m_networkedAnimator->update(WorldTimestep, &m_networkedAnimatorDynamicTarget); + m_networkedAnimatorDynamicTarget.updatePosition(position() + m_animationPosition); + } + + if (m_lightFlickering) + m_lightFlickering->update(WorldTimestep); + + for (auto& timer : m_emissionTimers) + timer.tick(); + + if (world()->isClient()) + m_scriptedAnimator.update(); +} + +void Object::render(RenderCallback* renderCallback) { + renderParticles(renderCallback); + renderLights(renderCallback); + renderSounds(renderCallback); + + for (auto const& imageKeyPair : m_imageKeys) + m_networkedAnimator->setGlobalTag(imageKeyPair.first, imageKeyPair.second); + + renderCallback->addAudios(m_networkedAnimatorDynamicTarget.pullNewAudios()); + renderCallback->addParticles(m_networkedAnimatorDynamicTarget.pullNewParticles()); + + if (m_networkedAnimator->parts().count() > 0) { + renderCallback->addDrawables(m_networkedAnimator->drawables(position() + m_animationPosition + damageShake()), renderLayer()); + } else { + if (m_orientationIndex != NPos) + renderCallback->addDrawables(orientationDrawables(m_orientationIndex), renderLayer(), position()); + } + + for (auto drawablePair : m_scriptedAnimator.drawables()) + renderCallback->addDrawable(drawablePair.first, drawablePair.second.value(renderLayer())); + renderCallback->addLightSources(m_scriptedAnimator.lightSources()); + renderCallback->addParticles(m_scriptedAnimator.pullNewParticles()); + renderCallback->addAudios(m_scriptedAnimator.pullNewAudios()); +} + +bool Object::damageTiles(List<Vec2I> const&, Vec2F const&, TileDamage const& tileDamage) { + if (m_unbreakable) + return false; + m_tileDamageStatus->damage(m_config->tileDamageParameters, tileDamage); + if (m_tileDamageStatus->dead()) + m_broken = true; + return m_broken; +} + +bool Object::checkBroken() { + if (!m_broken && !m_unbreakable) { + auto orientation = currentOrientation(); + if (orientation) { + if (!orientation->anchorsValid(world(), tilePosition())) + m_broken = true; + } else { + m_broken = true; + } + } + return m_broken; +} + +bool Object::shouldDestroy() const { + return m_broken || (m_health.get() <= 0); +} + +void Object::destroy(RenderCallback* renderCallback) { + bool doSmash = m_health.get() <= 0 || configValue("smashOnBreak", m_config->smashOnBreak).toBool(); + + if (isMaster()) { + m_scriptComponent.invoke("die", m_health.get() <= 0); + + try { + if (doSmash) { + auto smashDropPool = configValue("smashDropPool", "").toString(); + if (!smashDropPool.empty()) { + for (auto const& treasureItem : Root::singleton().treasureDatabase()->createTreasure(smashDropPool, world()->threatLevel())) + world()->addEntity(ItemDrop::createRandomizedDrop(treasureItem, position())); + } else if (!m_config->smashDropOptions.empty()) { + List<ItemDescriptor> drops; + auto dropOption = Random::randFrom(m_config->smashDropOptions); + for (auto o : dropOption) + world()->addEntity(ItemDrop::createRandomizedDrop(o, position())); + } + } else { + auto breakDropPool = configValue("breakDropPool", "").toString(); + if (!breakDropPool.empty()) { + for (auto const& treasureItem : Root::singleton().treasureDatabase()->createTreasure(breakDropPool, world()->threatLevel())) + world()->addEntity(ItemDrop::createRandomizedDrop(treasureItem, position())); + } else if (!m_config->breakDropOptions.empty()) { + List<ItemDescriptor> drops; + auto dropOption = Random::randFrom(m_config->breakDropOptions); + for (auto o : dropOption) + world()->addEntity(ItemDrop::createRandomizedDrop(o, position())); + } else if (m_config->hasObjectItem) { + ItemDescriptor objectItem(m_config->name, 1); + if (m_config->retainObjectParametersInItem) { + auto parameters = m_parameters.baseMap(); + parameters.remove("owner"); + parameters["scriptStorage"] = m_scriptComponent.getScriptStorage(); + objectItem = objectItem.applyParameters(parameters); + } + world()->addEntity(ItemDrop::createRandomizedDrop(objectItem, position())); + } + } + } catch (StarException const& e) { + Logger::warn("Invalid dropID in entity death. %s", outputException(e, false)); + } + } + + if (renderCallback && doSmash && !m_config->smashSoundOptions.empty()) { + auto audio = make_shared<AudioInstance>(*Root::singleton().assets()->audio(Random::randFrom(m_config->smashSoundOptions))); + renderCallback->addAudios({move(audio)}, position()); + } + + if (renderCallback && doSmash && !m_config->smashParticles.empty()) { + auto& root = Root::singleton(); + List<Particle> particles; + for (auto const& config : m_config->smashParticles) { + auto creator = root.particleDatabase()->particleCreator(config.get("particle"), m_config->path); + unsigned count = config.getUInt("count", 1); + Vec2F offset = jsonToVec2F(config.get("offset", JsonArray{0, 0})); + bool flip = config.getBool("flip", false); + for (unsigned i = 0; i < count; ++i) { + Particle particle = creator(); + particle.position += offset; + if (flip) + particle.flip = !particle.flip; + if (m_direction.get() == Direction::Left) { + particle.position[0] *= -1; + particle.velocity[0] *= -1; + particle.flip = !particle.flip; + } + particle.translate(position() + volume().center()); + particles.append(move(particle)); + } + } + renderCallback->addParticles(particles); + } + + if (m_soundEffect) + m_soundEffect->stop(1.0f); +} + +String Object::name() const { + return m_config->name; +} + +String Object::shortDescription() const { + return configValue("shortdescription", name()).toString(); +} + +String Object::description() const { + return configValue("description", shortDescription()).toString(); +} + +bool Object::inspectable() const { + return m_config->scannable; +} + +Maybe<String> Object::inspectionLogName() const { + return configValue("inspectionLogName").optString().value(m_config->name); +} + +Maybe<String> Object::inspectionDescription(String const& species) const { + return configValue("inspectionDescription").optString() + .orMaybe(configValue(strf("%sDescription", species)).optString()) + .value(description()); +} + +String Object::category() const { + return m_config->category; +} + +ObjectOrientationPtr Object::currentOrientation() const { + if (m_orientationIndex != NPos) + return m_config->orientations.at(m_orientationIndex); + else + return {}; +} + +List<Drawable> Object::cursorHintDrawables() const { + if (configValue("placementImage")) { + String placementImage = configValue("placementImage").toString(); + if (m_direction.get() == Direction::Left) + placementImage += "?flipx"; + Drawable imageDrawable = Drawable::makeImage(AssetPath::relativeTo(m_config->path, placementImage), + 1.0 / TilePixels, false, jsonToVec2F(configValue("placementImagePosition")) / TilePixels); + return {imageDrawable}; + } else { + if (m_orientationIndex != NPos) { + return orientationDrawables(m_orientationIndex); + } else { + // If we aren't in a valid orientation, still need to draw something at + // the cursor. Draw the first orientation whose direction affinity + // matches our current direction, or if that fails just the first + // orientation. + List<Drawable> result; + for (size_t i = 0; i < m_config->orientations.size(); ++i) { + if (m_config->orientations[i]->directionAffinity && *m_config->orientations[i]->directionAffinity == m_direction.get()) { + result = orientationDrawables(i); + break; + } + } + if (result.empty()) + result = orientationDrawables(0); + return result; + } + } +} + +void Object::getNetStates(bool initial) { + setUniqueId(m_uniqueIdNetState.get()); + if (m_orientationIndex != m_orientationIndexNetState.get()) + setOrientationIndex(m_orientationIndexNetState.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(), m_chatConfig.get()}); + } + + if (m_netImageKeys.pullUpdated()) { + m_imageKeys.merge(m_netImageKeys.baseMap(), true); + m_orientationDrawablesCache.reset(); + } +} + +void Object::setNetStates() { + m_uniqueIdNetState.set(uniqueId()); + m_orientationIndexNetState.set(m_orientationIndex); +} + +List<QuestArcDescriptor> Object::offeredQuests() const { + return m_offeredQuests.get(); +} + +StringSet Object::turnInQuests() const { + return m_turnInQuests.get(); +} + +Vec2F Object::questIndicatorPosition() const { + if (auto orientation = currentOrientation()) { + auto pos = position() + Vec2F(orientation->boundBox.center()[0], orientation->boundBox.max()[1] + 2.5); + if (!(orientation->boundBox.size()[0] % 2)) + pos[0] += 0.5; + if (auto configPosition = configValue("questIndicatorPosition", Json())) { + auto indicatorOffset = jsonToVec2F(configPosition); + if (m_direction.get() == Direction::Left) + indicatorOffset[0] = -indicatorOffset[0]; + pos += indicatorOffset; + } + return pos; + } else { + return position(); + } +} + +Maybe<Json> Object::receiveMessage(ConnectionId sendingConnection, String const& message, JsonArray const& args) { + return m_scriptComponent.handleMessage(message, sendingConnection == world()->connection(), args); +} + +Json Object::configValue(String const& name, Json const& def) const { + if (auto orientation = currentOrientation()) + return jsonMergeQueryDef(name, def, m_config->config, orientation->config, m_parameters.baseMap()); + else + return jsonMergeQueryDef(name, def, m_config->config, m_parameters.baseMap()); +} + +ObjectConfigConstPtr Object::config() const { + return m_config; +} + +void Object::readStoredData(Json const& diskStore) { + setUniqueId(diskStore.optString("uniqueId")); + setTilePosition(jsonToVec2I(diskStore.get("tilePosition"))); + setOrientationIndex(jsonToSize(diskStore.get("orientationIndex"))); + m_direction.set(DirectionNames.getLeft(diskStore.getString("direction"))); + + m_interactive.set(diskStore.getBool("interactive", !configValue("interactAction", Json()).isNull())); + + m_scriptComponent.setScriptStorage(diskStore.getObject("scriptStorage", JsonObject())); + + JsonArray inputNodes = diskStore.getArray("inputWireNodes"); + for (size_t i = 0; i < m_inputNodes.size(); ++i) { + if (i < inputNodes.size()) { + auto& in = m_inputNodes[i]; + List<WireConnection> connections; + for (auto const& conn : inputNodes[i].getArray("connections")) + connections.append(WireConnection{jsonToVec2I(conn.get(0)), (size_t)conn.get(1).toUInt()}); + in.connections.set(move(connections)); + in.state.set(inputNodes[i].getBool("state")); + } + } + + JsonArray outputNodes = diskStore.getArray("outputWireNodes"); + for (size_t i = 0; i < m_outputNodes.size(); ++i) { + if (i < outputNodes.size()) { + auto& in = m_outputNodes[i]; + List<WireConnection> connections; + for (auto const& conn : outputNodes[i].getArray("connections")) + connections.append(WireConnection{jsonToVec2I(conn.get(0)), (size_t)conn.get(1).toUInt()}); + in.connections.set(move(connections)); + in.state.set(outputNodes[i].getBool("state")); + } + } +} + +Json Object::writeStoredData() const { + JsonArray inputNodes; + for (size_t i = 0; i < m_inputNodes.size(); ++i) { + auto const& in = m_inputNodes[i]; + JsonArray connections; + for (auto const& node : in.connections.get()) + connections.append(JsonArray{jsonFromVec2I(node.entityLocation), node.nodeIndex}); + + inputNodes.append(JsonObject{ + {"connections", move(connections)}, + {"state", in.state.get()} + }); + } + + JsonArray outputNodes; + for (size_t i = 0; i < m_outputNodes.size(); ++i) { + auto const& in = m_outputNodes[i]; + JsonArray connections; + for (auto const& node : in.connections.get()) + connections.append(JsonArray{jsonFromVec2I(node.entityLocation), node.nodeIndex}); + + outputNodes.append(JsonObject{ + {"connections", move(connections)}, + {"state", in.state.get()} + }); + } + + return JsonObject{ + {"uniqueId", jsonFromMaybe(uniqueId())}, + {"tilePosition", jsonFromVec2I(tilePosition())}, + {"orientationIndex", jsonFromSize(m_orientationIndex)}, + {"direction", DirectionNames.getRight(m_direction.get())}, + {"scriptStorage", m_scriptComponent.getScriptStorage()}, + {"interactive", m_interactive.get()}, + {"inputWireNodes", move(inputNodes)}, + {"outputWireNodes", move(outputNodes)} + }; +} + +void Object::breakObject(bool smash) { + m_broken = true; + if (smash) + m_health.set(0.0f); +} + +size_t Object::nodeCount(WireDirection direction) const { + if (direction == WireDirection::Input) + return m_inputNodes.size(); + else + return m_outputNodes.size(); +} + +Vec2I Object::nodePosition(WireNode wireNode) const { + if (wireNode.direction == WireDirection::Input) + return m_inputNodes.at(wireNode.nodeIndex).position; + else + return m_outputNodes.at(wireNode.nodeIndex).position; +} + +List<WireConnection> Object::connectionsForNode(WireNode wireNode) const { + if (wireNode.direction == WireDirection::Input) + return m_inputNodes.at(wireNode.nodeIndex).connections.get(); + else + return m_outputNodes.at(wireNode.nodeIndex).connections.get(); +} + +bool Object::nodeState(WireNode wireNode) const { + if (wireNode.direction == WireDirection::Input) + return m_inputNodes.at(wireNode.nodeIndex).state.get(); + else + return m_outputNodes.at(wireNode.nodeIndex).state.get(); +} + +void Object::addNodeConnection(WireNode wireNode, WireConnection nodeConnection) { + if (wireNode.direction == WireDirection::Input) { + m_inputNodes.at(wireNode.nodeIndex).connections.update([&](auto& list) { + if (list.contains(nodeConnection)) + return false; + list.append(nodeConnection); + return true; + }); + } else { + m_outputNodes.at(wireNode.nodeIndex).connections.update([&](auto& list) { + if (list.contains(nodeConnection)) + return false; + list.append(nodeConnection); + return true; + }); + } + m_scriptComponent.invoke("onNodeConnectionChange"); +} + +void Object::removeNodeConnection(WireNode wireNode, WireConnection nodeConnection) { + if (wireNode.direction == WireDirection::Input) { + m_inputNodes.at(wireNode.nodeIndex).connections.update([&](auto& list) { + return list.remove(nodeConnection); + }); + } else { + m_outputNodes.at(wireNode.nodeIndex).connections.update([&](auto& list) { + return list.remove(nodeConnection); + }); + } + m_scriptComponent.invoke("onNodeConnectionChange"); +} + +void Object::evaluate(WireCoordinator* coordinator) { + for (size_t i = 0; i < m_inputNodes.size(); ++i) { + auto& in = m_inputNodes[i]; + bool nextState = false; + for (auto const& connection : in.connections.get()) + nextState |= coordinator->readInputConnection(connection); + + if (in.state.get() != nextState) { + in.state.set(nextState); + m_scriptComponent.invoke("onInputNodeChange", JsonObject{ + {"node", i}, + {"level", nextState} + }); + } + } +} + +void Object::setImageKey(String const& name, String const& value) { + if (!isSlave()) + m_netImageKeys.set(name, value); + + if (auto p = m_imageKeys.ptr(name)) { + if (*p != value) { + *p = value; + m_orientationDrawablesCache.reset(); + } + } else { + m_imageKeys.set(name, value); + m_orientationDrawablesCache.reset(); + } +} + +void Object::resetEmissionTimers() { + m_emissionTimers.clear(); + if (auto orientation = currentOrientation()) + for (size_t i = 0; i < orientation->particleEmitters.size(); i++) + m_emissionTimers.append(GameTimer()); +} + +size_t Object::orientationIndex() const { + return m_orientationIndex; +} + +void Object::setOrientationIndex(size_t orientationIndex) { + m_orientationIndex = orientationIndex; +} + +PolyF Object::volume() const { + if (auto orientation = currentOrientation()) { + RectF box = RectF(orientation->boundBox); + box.max()[0]++; + box.max()[1]++; + return PolyF(box); + } else { + return PolyF(RectF(0, 0, 1, 1)); + } +} + +float Object::liquidFillLevel() const { + if (auto orientation = currentOrientation()) + return spacesLiquidFillLevel(orientation->spaces); + + return 0; +} + +bool Object::biomePlaced() const { + return m_config->biomePlaced; +} + +LuaCallbacks Object::makeObjectCallbacks() { + LuaCallbacks callbacks; + + callbacks.registerCallback("name", [this]() { + return name(); + }); + + callbacks.registerCallback("direction", [this]() { + return numericalDirection(direction()); + }); + + callbacks.registerCallback("position", [this]() { + return position(); + }); + + callbacks.registerCallback("setInteractive", [this](bool interactive) { + m_interactive.set(interactive); + }); + + callbacks.registerCallbackWithSignature<Maybe<String>>("uniqueId", bind(&Object::uniqueId, this)); + callbacks.registerCallbackWithSignature<void, Maybe<String>>("setUniqueId", bind(&Object::setUniqueId, this, _1)); + + callbacks.registerCallback("boundBox", [this]() { + return metaBoundBox().translated(position()); + }); + + callbacks.registerCallback("spaces", [this]() { + return spaces(); + }); + + callbacks.registerCallback("setProcessingDirectives", [this](String const& directives) { + m_networkedAnimator->setProcessingDirectives(directives); + }); + + callbacks.registerCallback("setSoundEffectEnabled", [this](bool soundEffectEnabled) { + m_soundEffectEnabled.set(soundEffectEnabled); + }); + + callbacks.registerCallback("smash", [this](Maybe<bool> smash) { + breakObject(smash.value(false)); + }); + + callbacks.registerCallback("level", [this]() { + return configValue("level", this->world()->threatLevel()); + }); + + callbacks.registerCallback("toAbsolutePosition", [this](Vec2F const& p) { + return p + position(); + }); + + callbacks.registerCallback("say", [this](String line, Maybe<StringMap<String>> const& tags, Json const& config) { + if (tags) + line = line.replaceTags(*tags, false); + + if (!line.empty()) { + addChatMessage(line, config); + return true; + } + + return false; + }); + + callbacks.registerCallback("sayPortrait", [this](String line, String portrait, Maybe<StringMap<String>> const& tags, Json const& config) { + if (tags) + line = line.replaceTags(*tags, false); + + if (!line.empty()) { + addChatMessage(line, config, portrait); + return true; + } + + return false; + }); + + callbacks.registerCallback("isTouching", [this](EntityId entityId) { + if (auto entity = this->world()->entity(entityId)) + return !entity->collisionArea().overlap(volume().boundBox()).isEmpty(); + return false; + }); + + callbacks.registerCallback("setLightColor", [this](Color const& color) { + m_lightSourceColor.set(color); + }); + + callbacks.registerCallback("getLightColor", [this]() { + return m_lightSourceColor.get(); + }); + + callbacks.registerCallback("inputNodeCount", [this]() { + return m_inputNodes.size(); + }); + + callbacks.registerCallback("outputNodeCount", [this]() { + return m_outputNodes.size(); + }); + + callbacks.registerCallback("getInputNodePosition", [this](size_t i) { + return m_inputNodes.at(i).position; + }); + + callbacks.registerCallback("getOutputNodePosition", [this](size_t i) { + return m_outputNodes.at(i).position; + }); + + callbacks.registerCallback("getInputNodeLevel", [this](size_t i) { + return m_inputNodes.at(i).state.get(); + }); + + callbacks.registerCallback("getOutputNodeLevel", [this](size_t i) { + return m_outputNodes.at(i).state.get(); + }); + + callbacks.registerCallback("isInputNodeConnected", [this](size_t i) { + return !m_inputNodes.at(i).connections.get().empty(); + }); + + callbacks.registerCallback("isOutputNodeConnected", [this](size_t i) { + return !m_outputNodes.at(i).connections.get().empty(); + }); + + callbacks.registerCallback("getInputNodeIds", [this](LuaEngine& engine, size_t i) { + auto result = engine.createTable(); + for (auto const& conn : m_inputNodes.at(i).connections.get()) { + for (auto const& entity : worldPtr()->atTile<WireEntity>(conn.entityLocation)) + result.set(entity->entityId(), conn.nodeIndex); + } + return result; + }); + + callbacks.registerCallback("getOutputNodeIds", [this](LuaEngine& engine, size_t i) { + auto result = engine.createTable(); + for (auto const& conn : m_outputNodes.at(i).connections.get()) { + for (auto const& entity : worldPtr()->atTile<WireEntity>(conn.entityLocation)) + result.set(entity->entityId(), conn.nodeIndex); + } + return result; + }); + + callbacks.registerCallback("setOutputNodeLevel", [this](size_t i, bool l) { + m_outputNodes.at(i).state.set(l); + }); + + callbacks.registerCallback("setAllOutputNodes", [this](bool l) { + for (auto& out : m_outputNodes) + out.state.set(l); + }); + + callbacks.registerCallback("setOfferedQuests", [this](Maybe<JsonArray> const& offeredQuests) { + m_offeredQuests.set(offeredQuests.value().transformed(&QuestArcDescriptor::fromJson)); + }); + + callbacks.registerCallback("setTurnInQuests", [this](Maybe<StringList> const& turnInQuests) { + m_turnInQuests.set(StringSet::from(turnInQuests.value())); + }); + + callbacks.registerCallback("setConfigParameter", [this](String key, Json value) { + m_parameters.set(move(key), move(value)); + }); + + callbacks.registerCallback("setAnimationParameter", [this](String key, Json value) { + m_scriptedAnimationParameters.set(move(key), move(value)); + }); + + callbacks.registerCallback("setMaterialSpaces", [this](Maybe<JsonArray> const& newSpaces) { + List<MaterialSpace> materialSpaces; + auto materialDatabase = Root::singleton().materialDatabase(); + for (auto space : newSpaces.value()) + materialSpaces.append({jsonToVec2I(space.get(0)), materialDatabase->materialId(space.get(1).toString())}); + m_materialSpaces.set(materialSpaces); + }); + + callbacks.registerCallback("setDamageSources", [this](Maybe<JsonArray> damageSources) { + m_damageSources.set(damageSources.value().transformed(construct<DamageSource>())); + }); + + callbacks.registerCallback("health", [this]() { + return m_health.get(); + }); + + callbacks.registerCallback("setHealth", [this](float health) { + m_health.set(health); + }); + + return callbacks; +} + +LuaCallbacks Object::makeAnimatorObjectCallbacks() { + LuaCallbacks callbacks; + + callbacks.registerCallback("getParameter", [this](String const& name, Json const& def) { + return configValue(name, def); + }); + + callbacks.registerCallback("direction", [this]() { + return numericalDirection(direction()); + }); + + callbacks.registerCallback("position", [this]() { + return position(); + }); + + return callbacks; +} + +List<DamageSource> Object::damageSources() const { + auto damageSources = m_damageSources.get(); + + if (auto orientation = currentOrientation()) { + Json touchDamageConfig = jsonMerge(m_config->touchDamageConfig, orientation->touchDamageConfig); + if (!touchDamageConfig.isNull()) { + DamageSource ds(touchDamageConfig); + ds.sourceEntityId = entityId(); + ds.team = getTeam(); + damageSources.append(ds); + } + } + + return damageSources; +} + +List<PersistentStatusEffect> Object::statusEffects() const { + return m_config->statusEffects; +} + +PolyF Object::statusEffectArea() const { + if (auto orientation = currentOrientation()) { + if (orientation->statusEffectArea) + return orientation->statusEffectArea.get(); + } + return volume(); +} + +Maybe<HitType> Object::queryHit(DamageSource const& source) const { + if (!m_config->smashable || !inWorld() || m_health.get() <= 0.0f || m_unbreakable) + return {}; + + if (source.intersectsWithPoly(world()->geometry(), hitPoly().get())) + return HitType::Hit; + + return {}; +} + +Maybe<PolyF> Object::hitPoly() const { + auto poly = volume(); + poly.translate(position()); + return poly; +} + +List<DamageNotification> Object::applyDamage(DamageRequest const& damage) { + if (!m_config->smashable || !inWorld() || m_health.get() <= 0.0f) + return {}; + + float dmg = std::min(m_health.get(), damage.damage); + m_health.set(m_health.get() - dmg); + + return {{ + damage.sourceEntityId, + entityId(), + position(), + damage.damage, + dmg, + m_health.get() <= 0 ? HitType::Kill : HitType::Hit, + damage.damageSourceKind, + m_config->damageMaterialKind + }}; +} + +RectF Object::interactiveBoundBox() const { + if (auto orientation = currentOrientation()) { + auto rect = RectF(orientation->boundBox); + rect.setMax(Vec2F(orientation->boundBox.xMax() + 1, orientation->boundBox.yMax() + 1)); + return rect; + } else { + return RectF::null(); + } +} + +bool Object::isInteractive() const { + return m_interactive.get(); +} + +InteractAction Object::interact(InteractRequest const& request) { + Vec2F diff = world()->geometry().diff(request.sourcePosition, position()); + auto result = m_scriptComponent.invoke<Json>( + "onInteraction", JsonObject{{"source", JsonArray{diff[0], diff[1]}}, {"sourceId", request.sourceId}}); + + if (result) { + if (result->isNull()) + return {}; + else if (result->isType(Json::Type::String)) + return InteractAction(result->toString(), entityId(), Json()); + else + return InteractAction(result->getString(0), entityId(), result->get(1)); + } else if (!configValue("interactAction", Json()).isNull()) { + return InteractAction(configValue("interactAction").toString(), entityId(), configValue("interactData", Json())); + } + + return {}; +} + +List<Vec2I> Object::interactiveSpaces() const { + if (auto orientation = currentOrientation()) { + if (auto iSpaces = orientation->interactiveSpaces) + return *iSpaces; + } + return spaces(); +} + +Maybe<LuaValue> Object::callScript(String const& func, LuaVariadic<LuaValue> const& args) { + return m_scriptComponent.invoke(func, args); +} + +Maybe<LuaValue> Object::evalScript(String const& code) { + return m_scriptComponent.eval(code); +} + +Vec2F Object::mouthPosition() const { + if (auto orientation = currentOrientation()) { + auto pos = position() + Vec2F(orientation->boundBox.center()[0], orientation->boundBox.max()[1]); + if (!(orientation->boundBox.size()[0] % 2)) + pos[0] += 0.5; + if (auto configPosition = configValue("mouthPosition", Json())) { + auto mouthOffset = jsonToVec2F(configPosition); + if (m_direction.get() == Direction::Left) + mouthOffset[0] = -mouthOffset[0]; + pos += mouthOffset; + } + return pos; + } else { + return position(); + } +} + +List<ChatAction> Object::pullPendingChatActions() { + return std::move(m_pendingChatActions); +} + +void Object::addChatMessage(String const& message, Json const& config, String const& portrait) { + assert(!isSlave()); + m_chatMessage.set(message); + m_chatPortrait.set(portrait); + m_chatConfig.set(config); + m_newChatMessageEvent.trigger(); + if (portrait.empty()) + m_pendingChatActions.append(SayChatAction{entityId(), message, mouthPosition()}); + else + m_pendingChatActions.append(PortraitChatAction{entityId(), portrait, message, mouthPosition()}); +} + +List<Drawable> Object::orientationDrawables(size_t orientationIndex) const { + if (orientationIndex == NPos) + return {}; + + auto orientation = m_config->orientations.at(orientationIndex); + + if (!m_orientationDrawablesCache || orientationIndex != m_orientationDrawablesCache->first) { + m_orientationDrawablesCache = make_pair(orientationIndex, List<Drawable>()); + for (auto const& layer : orientation->imageLayers) { + auto drawable = layer; + drawable.imagePart().image = drawable.imagePart().image.replaceTags(m_imageKeys, true, "default"); + if (orientation->flipImages) + drawable.scale(Vec2F(-1, 1), drawable.boundBox(false).center() - drawable.position); + m_orientationDrawablesCache->second.append(move(drawable)); + } + } + + auto drawables = m_orientationDrawablesCache->second; + Drawable::translateAll(drawables, orientation->imagePosition + damageShake()); + return drawables; +} + +EntityRenderLayer Object::renderLayer() const { + if (auto orientation = currentOrientation()) + return orientation->renderLayer; + else + return RenderLayerObject; +} + +void Object::renderLights(RenderCallback* renderCallback) const { + renderCallback->addLightSources(lightSources()); +} + +void Object::renderParticles(RenderCallback* renderCallback) { + if (!inWorld()) + return; + + if (auto orientation = currentOrientation()) { + if (m_emissionTimers.size() != orientation->particleEmitters.size()) + resetEmissionTimers(); + + for (size_t i = 0; i < orientation->particleEmitters.size(); i++) { + auto& particleEmitter = orientation->particleEmitters.at(i); + + if (particleEmitter.particleEmissionRate <= 0.0f) + continue; + + auto& timer = m_emissionTimers.at(i); + if (timer.ready()) { + auto particle = particleEmitter.particle; + particle.applyVariance(particleEmitter.particleVariance); + if (particleEmitter.placeInSpaces) + particle.translate(Vec2F(Random::randFrom(orientation->spaces)) + Vec2F(0.5, 0.5)); + particle.translate(position()); + renderCallback->addParticle(move(particle)); + timer = GameTimer(1.0f / (particleEmitter.particleEmissionRate + Random::randf(-particleEmitter.particleEmissionRateVariance, particleEmitter.particleEmissionRateVariance))); + } + } + } +} + +void Object::renderSounds(RenderCallback* renderCallback) { + if (m_soundEffectEnabled.get()) { + if (!m_config->soundEffect.empty() && (!m_soundEffect || m_soundEffect->finished())) { + auto& root = Root::singleton(); + + m_soundEffect = make_shared<AudioInstance>(*root.assets()->audio(m_config->soundEffect)); + m_soundEffect->setLoops(-1); + // Randomize the start position of the looping persistent audio + m_soundEffect->seekTime(Random::randf() * m_soundEffect->totalTime()); + m_soundEffect->setRangeMultiplier(m_config->soundEffectRangeMultiplier); + m_soundEffect->setPosition(metaBoundBox().center() + position()); + // Fade the audio in slowly + m_soundEffect->setVolume(0); + m_soundEffect->setVolume(1, 1.0); + + renderCallback->addAudio(m_soundEffect); + } + } else { + if (m_soundEffect) + m_soundEffect->stop(); + } +} + +Vec2F Object::damageShake() const { + if (m_tileDamageStatus->damaged() && !m_tileDamageStatus->damageProtected()) + return Vec2F(Random::randf(-1, 1), Random::randf(-1, 1)) * m_tileDamageStatus->damageEffectPercentage() * m_config->damageShakeMagnitude; + return Vec2F(); +} + +void Object::checkLiquidBroken() { + if (m_config->minimumLiquidLevel || m_config->maximumLiquidLevel) { + float currentLiquidLevel = liquidFillLevel(); + if (m_config->minimumLiquidLevel && currentLiquidLevel < *m_config->minimumLiquidLevel) + m_broken = true; + if (m_config->maximumLiquidLevel && currentLiquidLevel > *m_config->maximumLiquidLevel) + m_broken = true; + } +} + +} |