Веб-сайт самохостера Lotigara

summaryrefslogtreecommitdiff
path: root/source/game/objects/StarContainerObject.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'source/game/objects/StarContainerObject.cpp')
-rw-r--r--source/game/objects/StarContainerObject.cpp557
1 files changed, 557 insertions, 0 deletions
diff --git a/source/game/objects/StarContainerObject.cpp b/source/game/objects/StarContainerObject.cpp
new file mode 100644
index 0000000..ee0505d
--- /dev/null
+++ b/source/game/objects/StarContainerObject.cpp
@@ -0,0 +1,557 @@
+#include "StarContainerObject.hpp"
+#include "StarRoot.hpp"
+#include "StarAssets.hpp"
+#include "StarLexicalCast.hpp"
+#include "StarTreasure.hpp"
+#include "StarItemDatabase.hpp"
+#include "StarItemDrop.hpp"
+#include "StarLogging.hpp"
+#include "StarWorld.hpp"
+#include "StarEntityRendering.hpp"
+#include "StarMixer.hpp"
+#include "StarObjectDatabase.hpp"
+#include "StarAugmentItem.hpp"
+
+namespace Star {
+
+ContainerObject::ContainerObject(ObjectConfigConstPtr config, Json const& parameters) : Object(config, parameters) {
+ m_opened.set(0);
+ m_count = 0;
+ m_currentState = 0;
+ m_animationFrameCooldown = 0;
+ m_autoCloseCooldown = 0;
+
+ m_crafting.set(false);
+ m_craftingProgress.set(0);
+
+ m_initialized = false;
+ m_itemsUpdated = true;
+ m_runUpdatedCallback = true;
+
+ m_items = make_shared<ItemBag>(configValue("slotCount").toInt());
+
+ m_netGroup.addNetElement(&m_opened);
+ m_netGroup.addNetElement(&m_crafting);
+ m_netGroup.addNetElement(&m_craftingProgress);
+ m_netGroup.addNetElement(&m_itemsNetState);
+
+ m_craftingProgress.setInterpolator(lerp<float, float>);
+}
+
+void ContainerObject::init(World* world, EntityId entityId, EntityMode mode) {
+ if (mode == EntityMode::Master)
+ m_interactive.set(true);
+
+ Object::init(world, entityId, mode);
+ if (mode == EntityMode::Master) {
+ if (!m_initialized) {
+ m_initialized = true;
+ float level = world->threatLevel();
+ uint64_t seed = configValue("treasureSeed", Random::randu64()).toUInt();
+ level = configValue("level", level).toFloat();
+ level += configValue("levelAdjustment", 0).toFloat();
+ if (!configValue("initialItems").isNull()) {
+ List<ItemDescriptor> items;
+ for (auto const& spec : configValue("initialItems").iterateArray())
+ m_items->addItems({Root::singleton().itemDatabase()->item(ItemDescriptor(spec), level, ++seed)});
+ }
+ if (!configValue("treasurePools").isNull()) {
+ String treasurePool = Random::randValueFrom(configValue("treasurePools").toArray()).toString();
+ Root::singleton().treasureDatabase()->fillWithTreasure(m_items, treasurePool, level, ++seed);
+ }
+ itemsUpdated();
+ }
+ }
+}
+
+void ContainerObject::update(uint64_t currentStep) {
+ Object::update(currentStep);
+
+ if (isMaster()) {
+ for (auto const& drop : take(m_lostItems))
+ world()->addEntity(ItemDrop::createRandomizedDrop(drop, position()));
+
+ if (m_crafting.get())
+ tickCrafting();
+
+ if (m_autoCloseCooldown > 0) {
+ m_autoCloseCooldown -= 1;
+ if (m_autoCloseCooldown <= 0) {
+ --m_count;
+ if (m_count <= 0) {
+ m_count = 0;
+ m_opened.set(0);
+ } else {
+ m_autoCloseCooldown = configValue("autoCloseCooldown").toInt();
+ }
+ }
+ }
+
+ m_ageItemsTimer.update(world()->epochTime());
+ if (m_ageItemsTimer.elapsedTime() > configValue("ageItemsEvery", 10).toDouble()) {
+ double elapsedTime = m_ageItemsTimer.elapsedTime() * configValue("itemAgeMultiplier", 1.0f).toDouble();
+ for (auto& item : m_items->items()) {
+ if (Root::singleton().itemDatabase()->ageItem(item, elapsedTime))
+ itemsUpdated();
+ }
+ m_ageItemsTimer.setElapsedTime(0.0);
+ }
+
+ if (take(m_runUpdatedCallback))
+ m_scriptComponent.invoke("containerCallback");
+
+ } else {
+ setImageKey("key", toString(m_currentState));
+ setImageKey("state", m_crafting.get() ? "crafting" : "idle");
+
+ m_animationFrameCooldown -= 1;
+ }
+}
+
+void ContainerObject::render(RenderCallback* renderCallback) {
+ auto assets = Root::singleton().assets();
+
+ if (m_animationFrameCooldown <= 0) {
+ if (m_opened.get() != m_currentState) {
+ if (m_currentState == 0) {
+ // opening, or flipping to the other side
+ if (!configValue("openSounds").isNull()) {
+ auto audio = make_shared<AudioInstance>(*assets->audio(Random::randValueFrom(configValue("openSounds").toArray()).toString()));
+ audio->setPosition(position());
+ audio->setRangeMultiplier(config()->soundEffectRangeMultiplier);
+ renderCallback->addAudio(move(audio));
+ }
+ }
+ if (m_currentState == configValue("openFrameIndex", 2).toInt()) {
+ // closing
+ if (!configValue("closeSounds").isNull()) {
+ auto audio = make_shared<AudioInstance>(*assets->audio(Random::randValueFrom(configValue("closeSounds").toArray()).toString()));
+ audio->setPosition(position());
+ audio->setRangeMultiplier(config()->soundEffectRangeMultiplier);
+ renderCallback->addAudio(move(audio));
+ }
+ }
+ if (m_opened.get() < m_currentState) {
+ m_currentState -= 1;
+ } else {
+ m_currentState += 1;
+ }
+ m_animationFrameCooldown = configValue("frameCooldown").toInt();
+ } else {
+ m_animationFrameCooldown = 0;
+ }
+ }
+
+ Object::render(renderCallback);
+}
+
+void ContainerObject::destroy(RenderCallback* renderCallback) {
+ Object::destroy(renderCallback);
+ if (isMaster()) {
+ for (auto const& drop : m_items->items())
+ world()->addEntity(ItemDrop::createRandomizedDrop(drop, position()));
+ }
+}
+
+Maybe<Json> ContainerObject::receiveMessage(ConnectionId sendingConnection, String const& message, JsonArray const& args) {
+ auto itemDb = Root::singleton().itemDatabase();
+
+ if (message.equalsIgnoreCase("startCrafting")) {
+ startCrafting();
+ return Json();
+
+ } else if (message.equalsIgnoreCase("stopCrafting")) {
+ stopCrafting();
+ return Json();
+
+ } else if (message.equalsIgnoreCase("burnContainerContents")) {
+ burnContainerContents();
+ return Json();
+
+ } else if (message.equalsIgnoreCase("addItems")) {
+ return itemSafeDescriptor(doAddItems(itemDb->fromJson(args.at(0)))).toJson();
+
+ } else if (message.equalsIgnoreCase("putItems")) {
+ return itemSafeDescriptor(doPutItems(args.at(0).toUInt(), itemDb->fromJson(args.at(1)))).toJson();
+
+ } else if (message.equalsIgnoreCase("takeItems")) {
+ return itemSafeDescriptor(doTakeItems(args.at(0).toUInt(), args.at(1).toUInt())).toJson();
+
+ } else if (message.equalsIgnoreCase("swapItems")) {
+ return itemSafeDescriptor(doSwapItems(args.at(0).toUInt(), itemDb->fromJson(args.at(1)), args.get(2).optBool().value(true))).toJson();
+
+ } else if (message.equalsIgnoreCase("applyAugment")) {
+ return itemSafeDescriptor(doApplyAugment(args.at(0).toUInt(), itemDb->fromJson(args.at(1)))).toJson();
+
+ } else if (message.equalsIgnoreCase("consumeItems")) {
+ return Json(doConsumeItems(ItemDescriptor(args.at(0))));
+
+ } else if (message.equalsIgnoreCase("consumeItemsAt")) {
+ return Json(doConsumeItems(args.at(0).toUInt(), args.at(1).toUInt()));
+
+ } else if (message.equalsIgnoreCase("clearContainer")) {
+ return Json(transform<JsonArray>(doClearContainer(), [](auto const& item) {
+ return itemSafeDescriptor(item).toJson();
+ }));
+
+ } else {
+ return Object::receiveMessage(sendingConnection, message, args);
+ }
+}
+
+InteractAction ContainerObject::interact(InteractRequest const&) {
+ return InteractAction(InteractActionType::OpenContainer, entityId(), Json());
+}
+
+Json ContainerObject::containerGuiConfig() const {
+ return Root::singleton().assets()->json(configValue("uiConfig").toString().replace("<slots>", strf("%s", m_items->size())));
+}
+
+String ContainerObject::containerDescription() const {
+ return Object::shortDescription();
+}
+
+String ContainerObject::containerSubTitle() const {
+ Json categories = Root::singleton().assets()->json("/items/categories.config:labels");
+ return categories.getString(Object::category(), Object::category());
+}
+
+ItemDescriptor ContainerObject::iconItem() const {
+ if (configValue("hasWindowIcon", true).toBool())
+ return ItemDescriptor(name(), 1);
+ return {};
+}
+
+ItemBagConstPtr ContainerObject::itemBag() const {
+ return m_items;
+}
+
+void ContainerObject::containerOpen() {
+ m_opened.set(configValue("openFrameIndex", 2).toInt());
+ m_count++;
+ m_autoCloseCooldown = configValue("autoCloseCooldown").toInt();
+}
+
+void ContainerObject::containerClose() {
+ --m_count;
+ if (m_count <= 0) {
+ m_count = 0;
+ m_opened.set(0);
+ }
+}
+
+RpcPromise<ItemPtr> ContainerObject::addItems(ItemPtr const& items) {
+ if (isSlave()) {
+ return world()->sendEntityMessage(entityId(), "addItems", {itemSafeDescriptor(items).toJson()}).wrap([](Json res) {
+ return Root::singleton().itemDatabase()->item(ItemDescriptor(res));
+ });
+ } else {
+ return RpcPromise<ItemPtr>::createFulfilled(doAddItems(items));
+ }
+}
+
+RpcPromise<ItemPtr> ContainerObject::putItems(size_t pos, ItemPtr const& items) {
+ if (isSlave()) {
+ return world()->sendEntityMessage(entityId(), "putItems", {itemSafeDescriptor(items).toJson()}).wrap([](Json res) {
+ return Root::singleton().itemDatabase()->item(ItemDescriptor(res));
+ });
+ } else {
+ return RpcPromise<ItemPtr>::createFulfilled(doPutItems(pos, items));
+ }
+}
+
+RpcPromise<ItemPtr> ContainerObject::takeItems(size_t slot, size_t count) {
+ if (isSlave()) {
+ return world()->sendEntityMessage(entityId(), "takeItems", {slot, count}).wrap([](Json res) {
+ return Root::singleton().itemDatabase()->item(ItemDescriptor(res));
+ });
+ } else {
+ return RpcPromise<ItemPtr>::createFulfilled(doTakeItems(slot, count));
+ }
+}
+
+RpcPromise<ItemPtr> ContainerObject::swapItems(size_t slot, ItemPtr const& items, bool tryCombine) {
+ if (isSlave()) {
+ return world()->sendEntityMessage(entityId(), "swapItems", {slot, itemSafeDescriptor(items).toJson(), tryCombine}).wrap([](Json res) {
+ return Root::singleton().itemDatabase()->item(ItemDescriptor(res));
+ });
+ } else {
+ return RpcPromise<ItemPtr>::createFulfilled(doSwapItems(slot, items, tryCombine));
+ }
+}
+
+RpcPromise<ItemPtr> ContainerObject::applyAugment(size_t slot, ItemPtr const& augment) {
+ if (isSlave()) {
+ return world()->sendEntityMessage(entityId(), "applyAugment", {slot, itemSafeDescriptor(augment).toJson()}).wrap([](Json res) {
+ return Root::singleton().itemDatabase()->item(ItemDescriptor(res));
+ });
+ } else {
+ return RpcPromise<ItemPtr>::createFulfilled(doApplyAugment(slot, augment));
+ }
+}
+
+RpcPromise<bool> ContainerObject::consumeItems(ItemDescriptor const& descriptor) {
+ if (isSlave()) {
+ return world()->sendEntityMessage(entityId(), "consumeItems", {descriptor.toJson()}).wrap([](Json res) {
+ return res.toBool();
+ });
+ } else {
+ return RpcPromise<bool>::createFulfilled(doConsumeItems(descriptor));
+ }
+}
+
+RpcPromise<bool> ContainerObject::consumeItems(size_t pos, size_t count) {
+ if (isSlave()) {
+ return world()->sendEntityMessage(entityId(), "consumeItemsAt", {pos, count}).wrap([](Json res) {
+ return res.toBool();
+ });
+ } else {
+ return RpcPromise<bool>::createFulfilled(doConsumeItems(pos, count));
+ }
+}
+
+RpcPromise<List<ItemPtr>> ContainerObject::clearContainer() {
+ if (isSlave()) {
+ return world()->sendEntityMessage(entityId(), "clearContainer", {}).wrap([](Json res) {
+ auto itemDb = Root::singleton().itemDatabase();
+ return res.toArray().transformed([itemDb](Json const& item) {
+ return itemDb->item(ItemDescriptor(item));
+ });
+ });
+ } else {
+ return RpcPromise<List<ItemPtr>>::createFulfilled(doClearContainer());
+ }
+}
+
+bool ContainerObject::isCrafting() const {
+ return m_crafting.get();
+}
+
+void ContainerObject::startCrafting() {
+ if (isSlave()) {
+ world()->sendEntityMessage(entityId(), "startCrafting");
+ } else {
+ if (m_crafting.get())
+ return;
+ auto inputItems = m_items->items();
+ inputItems.removeLast();
+ m_goalRecipe = recipeForMaterials(inputItems);
+ m_crafting.set(true);
+ itemsUpdated();
+ // tickCrafting validates
+ }
+}
+
+void ContainerObject::stopCrafting() {
+ if (isSlave()) {
+ world()->sendEntityMessage(entityId(), "stopCrafting");
+ } else {
+ if (!m_crafting.get())
+ return;
+ m_crafting.set(false);
+ m_craftingProgress.set(0);
+ m_goalRecipe = ItemRecipe();
+ }
+}
+
+float ContainerObject::craftingProgress() const {
+ if (!isCrafting())
+ return 1;
+ return clamp(m_craftingProgress.get(), 0.0f, 1.0f);
+}
+
+void ContainerObject::burnContainerContents() {
+ if (isSlave()) {
+ world()->sendEntityMessage(entityId(), "burnContainerContents");
+ } else {
+ stopCrafting();
+ auto level = world()->getProperty("ship.fuel", 0).toUInt();
+ auto maxLevel = world()->getProperty("ship.maxFuel", 0).toUInt();
+ for (auto& item : m_items->items()) {
+ if (level > maxLevel)
+ level = maxLevel;
+ if (maxLevel == level)
+ break;
+
+ auto leftToFill = maxLevel - level;
+
+ if (item) {
+ auto fuelSingle = item->instanceValue("fuelAmount", 0).toUInt();
+ if (fuelSingle > 0) {
+ auto itemsToConsume = min<uint64_t>((leftToFill + fuelSingle - 1) / fuelSingle, item->count());
+ level = min(maxLevel, level + fuelSingle * itemsToConsume);
+ auto consumed = item->consume(itemsToConsume);
+ starAssert(consumed);
+ _unused(consumed);
+ }
+ }
+ }
+ itemsUpdated();
+ world()->setProperty("ship.fuel", level);
+ }
+}
+
+void ContainerObject::getNetStates(bool initial) {
+ Object::getNetStates(initial);
+ if (m_itemsNetState.pullUpdated()) {
+ DataStreamBuffer ds(m_itemsNetState.get());
+ m_items->read(ds);
+ itemsUpdated();
+ }
+}
+
+void ContainerObject::setNetStates() {
+ Object::setNetStates();
+ if (take(m_itemsUpdated)) {
+ DataStreamBuffer ds;
+ m_items->write(ds);
+ m_itemsNetState.set(ds.takeData());
+ }
+}
+
+void ContainerObject::readStoredData(Json const& diskStore) {
+ Object::readStoredData(diskStore);
+
+ m_opened.set(diskStore.getInt("opened"));
+ m_currentState = diskStore.getInt("currentState");
+ m_crafting.set(diskStore.getBool("crafting"));
+ m_craftingProgress.set(diskStore.getFloat("craftingProgress"));
+ m_initialized = diskStore.getBool("initialized");
+ m_items = make_shared<ItemBag>(ItemBag::loadStore(diskStore.get("items")));
+ m_ageItemsTimer = EpochTimer(diskStore.get("ageItemsTimer"));
+
+ m_lostItems.appendAll(m_items->resize(configValue("slotCount").toUInt()));
+}
+
+Json ContainerObject::writeStoredData() const {
+ return Object::writeStoredData().setAll({
+ {"opened", m_opened.get()},
+ {"currentState", m_currentState},
+ {"crafting", m_crafting.get()},
+ {"craftingProgress", m_craftingProgress.get()},
+ {"initialized", m_initialized},
+ {"items", m_items->diskStore()},
+ {"ageItemsTimer", m_ageItemsTimer.toJson()}
+ });
+}
+
+ItemRecipe ContainerObject::recipeForMaterials(List<ItemPtr> const& inputItems) {
+ auto& root = Root::singleton();
+ auto itemDatabase = root.itemDatabase();
+
+ Json recipeGroup = configValue("recipeGroup");
+ if (!recipeGroup.isNull())
+ return itemDatabase->getPreciseRecipeForMaterials(recipeGroup.toString(), inputItems, {});
+
+ Maybe<Json> result = m_scriptComponent.invoke<Json>("craftingRecipe", inputItems.filtered([](ItemPtr const& item) {
+ return (bool)item;
+ }).transformed([](ItemPtr const& item) {
+ return item->descriptor().toJson();
+ }));
+ if (!result || result->isNull())
+ return ItemRecipe();
+ return itemDatabase->parseRecipe(*result);
+}
+
+void ContainerObject::tickCrafting() {
+ if (!m_crafting.get())
+ return;
+
+ auto inputItems = m_items->items();
+ inputItems.removeLast();
+ auto recipe = recipeForMaterials(inputItems);
+ bool craftingFail = false;
+ if (recipe.isNull() || m_goalRecipe != recipe)
+ craftingFail = true;
+ ItemPtr targetItem = m_items->at(m_items->size() - 1);
+ if (targetItem) {
+ if (!targetItem->matches(m_goalRecipe.output, true))
+ craftingFail = true;
+ else if (targetItem->count() + m_goalRecipe.output.count() > targetItem->maxStack())
+ craftingFail = true;
+ }
+ if (craftingFail) {
+ m_crafting.set(false);
+ m_craftingProgress.set(0);
+ m_goalRecipe = ItemRecipe();
+ return;
+ }
+ if (m_goalRecipe.duration > 0)
+ m_craftingProgress.set(m_craftingProgress.get() + WorldTimestep / m_goalRecipe.duration);
+ else
+ m_craftingProgress.set(1.0f);
+ if (m_craftingProgress.get() >= 1.0f) {
+ m_craftingProgress.set(0);
+ for (auto const& input : m_goalRecipe.inputs) {
+ bool consumed = m_items->consumeItems(input);
+ _unused(consumed);
+ starAssert(consumed);
+ }
+ ItemPtr overflow =
+ m_items->putItems(m_items->size() - 1, Root::singleton().itemDatabase()->item(m_goalRecipe.output));
+ if (overflow)
+ world()->addEntity(ItemDrop::createRandomizedDrop(overflow, position()));
+ itemsUpdated();
+ }
+}
+
+ItemPtr ContainerObject::doAddItems(ItemPtr const& items) {
+ itemsUpdated();
+ return m_items->addItems(items);
+}
+
+ItemPtr ContainerObject::doPutItems(size_t slot, ItemPtr const& items) {
+ itemsUpdated();
+ return m_items->putItems(slot, items);
+}
+
+ItemPtr ContainerObject::doTakeItems(size_t slot, size_t count) {
+ itemsUpdated();
+ return m_items->takeItems(slot, count);
+}
+
+ItemPtr ContainerObject::doSwapItems(size_t slot, ItemPtr const& items, bool tryCombine) {
+ itemsUpdated();
+ return m_items->swapItems(slot, items, tryCombine);
+}
+
+ItemPtr ContainerObject::doApplyAugment(size_t slot, ItemPtr const& item) {
+ itemsUpdated();
+ if (auto augment = as<AugmentItem>(item))
+ if (auto slotItem = m_items->at(slot))
+ m_items->setItem(slot, augment->applyTo(slotItem));
+ return item;
+}
+
+bool ContainerObject::doConsumeItems(ItemDescriptor const& descriptor) {
+ if (m_items->consumeItems(descriptor)) {
+ itemsUpdated();
+ return true;
+ }
+
+ return false;
+}
+
+bool ContainerObject::doConsumeItems(size_t slot, size_t count) {
+ if (m_items->consumeItems(slot, count)) {
+ itemsUpdated();
+ return true;
+ }
+
+ return false;
+}
+
+List<ItemPtr> ContainerObject::doClearContainer() {
+ stopCrafting();
+ List<ItemPtr> result = m_items->takeAll();
+ m_items->clearItems();
+ itemsUpdated();
+ return result;
+}
+
+void ContainerObject::itemsUpdated() {
+ m_itemsUpdated = true;
+ m_runUpdatedCallback = true;
+}
+
+}