This commit is contained in:
Aria 2025-03-21 22:23:30 +11:00
commit 9c94d113d3
Signed by untrusted user who does not match committer: aria
GPG key ID: 19AB7AA462B8AB3B
10260 changed files with 1237388 additions and 0 deletions

View file

@ -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;
}
}

View file

@ -0,0 +1,112 @@
#ifndef STAR_CONTAINER_OBJECT_HPP
#define STAR_CONTAINER_OBJECT_HPP
#include "StarItemBag.hpp"
#include "StarObject.hpp"
#include "StarWeightedPool.hpp"
#include "StarContainerEntity.hpp"
#include "StarItemRecipe.hpp"
namespace Star {
STAR_CLASS(ContainerObject);
class ContainerObject : public Object, public virtual ContainerEntity {
public:
ContainerObject(ObjectConfigConstPtr config, Json const& parameters);
void init(World* world, EntityId entityId, EntityMode mode) override;
void update(uint64_t currentStep) override;
void render(RenderCallback* renderCallback) override;
void destroy(RenderCallback* renderCallback) override;
InteractAction interact(InteractRequest const& request) override;
Maybe<Json> receiveMessage(ConnectionId sendingConnection, String const& message, JsonArray const& args) override;
Json containerGuiConfig() const override;
String containerDescription() const override;
String containerSubTitle() const override;
ItemDescriptor iconItem() const override;
ItemBagConstPtr itemBag() const override;
void containerOpen() override;
void containerClose() override;
void startCrafting() override;
void stopCrafting() override;
bool isCrafting() const override;
float craftingProgress() const override;
void burnContainerContents() override;
RpcPromise<ItemPtr> addItems(ItemPtr const& items) override;
RpcPromise<ItemPtr> putItems(size_t slot, ItemPtr const& items) override;
RpcPromise<ItemPtr> takeItems(size_t slot, size_t count = NPos) override;
RpcPromise<ItemPtr> swapItems(size_t slot, ItemPtr const& items, bool tryCombine = true) override;
RpcPromise<ItemPtr> applyAugment(size_t slot, ItemPtr const& augment) override;
RpcPromise<bool> consumeItems(ItemDescriptor const& descriptor) override;
RpcPromise<bool> consumeItems(size_t slot, size_t count) override;
RpcPromise<List<ItemPtr>> clearContainer() override;
protected:
void getNetStates(bool initial) override;
void setNetStates() override;
void readStoredData(Json const& diskStore) override;
Json writeStoredData() const override;
private:
typedef std::function<void(ContainerObject*)> ContainerCallback;
ItemRecipe recipeForMaterials(List<ItemPtr> const& inputItems);
void tickCrafting();
ItemPtr doAddItems(ItemPtr const& items);
ItemPtr doStackItems(ItemPtr const& items);
ItemPtr doPutItems(size_t slot, ItemPtr const& items);
ItemPtr doTakeItems(size_t slot, size_t count = NPos);
ItemPtr doSwapItems(size_t slot, ItemPtr const& items, bool tryCombine = true);
ItemPtr doApplyAugment(size_t slot, ItemPtr const& augment);
bool doConsumeItems(ItemDescriptor const& descriptor);
bool doConsumeItems(size_t slot, size_t count);
List<ItemPtr> doClearContainer();
template<typename T>
RpcPromise<T> addSlavePromise(String const& message, JsonArray const& args, function<T(Json)> converter);
void itemsUpdated();
NetElementInt m_opened;
NetElementBool m_crafting;
NetElementFloat m_craftingProgress;
ItemBagPtr m_items;
NetElementBytes m_itemsNetState;
// master only
bool m_initialized;
int m_count;
int m_currentState;
int64_t m_animationFrameCooldown;
int64_t m_autoCloseCooldown;
ItemRecipe m_goalRecipe;
bool m_itemsUpdated;
bool m_runUpdatedCallback;
ContainerCallback m_containerCallback;
EpochTimer m_ageItemsTimer;
List<ItemPtr> m_lostItems;
};
}
#endif

View file

@ -0,0 +1,187 @@
#include "StarFarmableObject.hpp"
#include "StarLexicalCast.hpp"
#include "StarJsonExtra.hpp"
#include "StarRoot.hpp"
#include "StarAssets.hpp"
#include "StarRandom.hpp"
#include "StarPlantDatabase.hpp"
#include "StarPlant.hpp"
#include "StarWorldServer.hpp"
#include "StarTreasure.hpp"
#include "StarItemDrop.hpp"
#include "StarLogging.hpp"
#include "StarObjectDatabase.hpp"
#include "StarMaterialDatabase.hpp"
namespace Star {
FarmableObject::FarmableObject(ObjectConfigConstPtr config, Json const& parameters) : Object(config, parameters) {
m_stages = configValue("stages", JsonArray({JsonObject()})).toArray();
m_stage = configValue("startingStage", 0).toInt();
m_stageAlt = -1;
m_stageEnterTime = 0.0;
m_nextStageTime = 0.0;
m_finalStage = false;
auto assets = Root::singleton().assets();
m_minImmersion = configValue("minImmersion", 0).toFloat();
m_maxImmersion = configValue("maxImmersion", 2).toFloat();
m_immersion = SlidingWindow(assets->json("/farming.config:immersionWindow").toFloat(),
assets->json("/farming.config:immersionResolution").toUInt(), (m_minImmersion + m_maxImmersion) / 2);
m_consumeSoilMoisture = configValue("consumeSoilMoisture", true).toBool();
}
void FarmableObject::update(uint64_t currentStep) {
Object::update(currentStep);
if (isMaster()) {
if (m_nextStageTime == 0) {
m_nextStageTime = world()->epochTime();
enterStage(m_stage);
}
while (!m_finalStage && world()->epochTime() >= m_nextStageTime)
enterStage(m_stage + 1);
// update immersion and check whether farmable should break
m_immersion.update(bind(&Object::liquidFillLevel, this));
if (m_immersion.average() > m_maxImmersion || m_immersion.average() < m_minImmersion)
breakObject(false);
}
}
bool FarmableObject::damageTiles(List<Vec2I> const& position, Vec2F const& sourcePosition, TileDamage const& tileDamage) {
if ((tileDamage.type != TileDamageType::Beamish && tileDamage.type != TileDamageType::Blockish && tileDamage.type != TileDamageType::Plantish) || !harvest())
return Object::damageTiles(position, sourcePosition, tileDamage);
return false;
}
InteractAction FarmableObject::interact(InteractRequest const&) {
harvest();
return {};
}
bool FarmableObject::harvest() {
if (isMaster() && m_stages.get(m_stage).contains("harvestPool")) {
for (auto const& treasureItem : Root::singleton().treasureDatabase()->createTreasure(m_stages.get(m_stage).getString("harvestPool"), world()->threatLevel()))
world()->addEntity(ItemDrop::createRandomizedDrop(treasureItem, position()));
if (m_stages.get(m_stage).contains("resetToStage")) {
m_nextStageTime = world()->epochTime();
enterStage(m_stages.get(m_stage).getInt("resetToStage"));
} else
breakObject(true);
return true;
}
return false;
}
int FarmableObject::stage() const {
return m_stage;
}
void FarmableObject::enterStage(int newStage) {
newStage = clamp<int>(newStage, 0, m_stages.size() - 1);
// attempt to consume water from the soil if needed
if (m_consumeSoilMoisture && newStage > m_stage) {
if (auto orientation = currentOrientation()) {
auto assets = Root::singleton().assets();
auto materialDatabase = Root::singleton().materialDatabase();
auto wetToDryMods = assets->json("/farming.config:wetToDryMods");
// try to transform all anchor spaces, back out and reset stage time if
// they're not wet
for (auto anchor : orientation->anchors) {
auto pos = tilePosition() + anchor.position;
if (auto newMod = wetToDryMods.optString(materialDatabase->modName(world()->mod(pos, anchor.layer)))) {
world()->modifyTile(pos, PlaceMod{anchor.layer, materialDatabase->modId(*newMod), MaterialHue()}, true);
} else {
Vec2F durationRange = jsonToVec2F(m_stages.get(m_stage).get("duration", JsonArray({0, 0})));
m_nextStageTime = world()->epochTime() + Random::randf(durationRange[0], durationRange[1]);
return;
}
}
}
}
// TODO: remove this hacky tree stuff and make plants handle it
if (m_stages.get(newStage).getBool("tree", false)) {
String stemName = configValue("stemName").toString();
float stemHueShift = configValue("stemHueShift", 0).toFloat();
String foliageName = configValue("foliageName", "").toString();
float foliageHueShift = configValue("foliageHueShift", 0).toFloat();
Vec2I position = tilePosition();
TreeVariant tv;
auto plantDatabase = Root::singleton().plantDatabase();
if (!foliageName.empty())
tv = plantDatabase->buildTreeVariant(stemName, stemHueShift, foliageName, foliageHueShift);
else
tv = plantDatabase->buildTreeVariant(stemName, stemHueShift);
auto plant = plantDatabase->createPlant(tv, Random::randi64());
plant->setTilePosition(position);
if (anySpacesOccupied(plant->spaces()) || !allSpacesOccupied(plant->roots())) {
newStage = 0;
} else {
world()->timer(2, [plant](World* world) {
world->addEntity(plant);
});
m_finalStage = true;
breakObject(true);
return;
}
}
if (newStage == (int)m_stages.size() - 1) {
m_finalStage = true;
} else {
m_finalStage = false;
m_stageEnterTime = m_nextStageTime;
Vec2F durationRange = jsonToVec2F(m_stages.get(newStage).get("duration", JsonArray({0, 0})));
m_nextStageTime += Random::randf(durationRange[0], durationRange[1]);
}
m_interactive.set(m_stages.get(newStage).contains("harvestPool"));
// keep the same variant if stages have same number of alts
if (m_stageAlt == -1 || m_stages.get(newStage).getInt("alts", 1) != m_stages.get(m_stage).getInt("alts", 1))
m_stageAlt = Random::randInt(m_stages.get(newStage).getInt("alts", 1) - 1);
m_stage = newStage;
setImageKey("stage", toString(m_stage));
setImageKey("alt", toString(m_stageAlt));
}
void FarmableObject::readStoredData(Json const& diskStore) {
Object::readStoredData(diskStore);
m_stage = diskStore.getInt("stage");
m_stageAlt = diskStore.getInt("stageAlt");
m_stageEnterTime = diskStore.getDouble("stageEnterTime");
m_nextStageTime = diskStore.getDouble("nextStageTime");
m_finalStage = (m_stage == (int)m_stages.size() - 1);
setImageKey("stage", toString(m_stage));
setImageKey("alt", toString(m_stageAlt));
}
Json FarmableObject::writeStoredData() const {
return Object::writeStoredData().setAll({
{"stage", m_stage},
{"stageAlt", m_stageAlt},
{"stageEnterTime", m_stageEnterTime},
{"nextStageTime", m_nextStageTime}
});
}
}

View file

@ -0,0 +1,44 @@
#ifndef STAR_FARMABLE_OBJECT_HPP
#define STAR_FARMABLE_OBJECT_HPP
#include "StarObject.hpp"
namespace Star {
class FarmableObject : public Object {
public:
FarmableObject(ObjectConfigConstPtr config, Json const& parameters);
void update(uint64_t currentStep) override;
bool damageTiles(List<Vec2I> const& position, Vec2F const& sourcePosition, TileDamage const& tileDamage) override;
InteractAction interact(InteractRequest const& request) override;
bool harvest();
int stage() const;
protected:
void readStoredData(Json const& diskStore) override;
Json writeStoredData() const override;
private:
void enterStage(int newStage);
int m_stage;
int m_stageAlt;
double m_stageEnterTime;
double m_nextStageTime;
SlidingWindow m_immersion;
float m_minImmersion;
float m_maxImmersion;
bool m_consumeSoilMoisture;
JsonArray m_stages;
bool m_finalStage;
};
}
#endif

View file

@ -0,0 +1,107 @@
#include "StarLoungeableObject.hpp"
#include "StarJsonExtra.hpp"
#include "StarRoot.hpp"
#include "StarObjectDatabase.hpp"
namespace Star {
LoungeableObject::LoungeableObject(ObjectConfigConstPtr config, Json const& parameters) : Object(config, parameters) {
m_interactive.set(true);
}
void LoungeableObject::render(RenderCallback* renderCallback) {
Object::render(renderCallback);
if (!m_sitCoverImage.empty()) {
if (!entitiesLounging().empty()) {
if (auto orientation = currentOrientation()) {
Drawable drawable =
Drawable::makeImage(m_sitCoverImage, 1.0f / TilePixels, false, position() + orientation->imagePosition);
if (m_flipImages)
drawable.scale(Vec2F(-1, 1), drawable.boundBox(false).center());
renderCallback->addDrawable(move(drawable), RenderLayerObject + 2);
}
}
}
}
InteractAction LoungeableObject::interact(InteractRequest const& request) {
auto res = Object::interact(request);
if (res.type == InteractActionType::None && !m_sitPositions.empty()) {
Maybe<size_t> index;
Vec2F interactOffset =
direction() == Direction::Right ? position() - request.interactPosition : request.interactPosition - position();
for (size_t i = 0; i < m_sitPositions.size(); ++i) {
if (!index || vmag(m_sitPositions[i] + interactOffset) < vmag(m_sitPositions[*index] + interactOffset))
index = i;
}
return InteractAction(InteractActionType::SitDown, entityId(), *index);
} else {
return res;
}
}
size_t LoungeableObject::anchorCount() const {
return m_sitPositions.size();
}
LoungeAnchorConstPtr LoungeableObject::loungeAnchor(size_t positionIndex) const {
if (positionIndex >= m_sitPositions.size())
return {};
auto loungeAnchor = make_shared<LoungeAnchor>();
loungeAnchor->controllable = false;
loungeAnchor->direction = m_sitFlipDirection ? -direction() : direction();
loungeAnchor->position = m_sitPositions.at(positionIndex);
if (loungeAnchor->direction == Direction::Left)
loungeAnchor->position[0] *= -1;
loungeAnchor->position += position();
loungeAnchor->exitBottomPosition = Vec2F(loungeAnchor->position[0], position()[1] + volume().boundBox().min()[1]);
loungeAnchor->angle = m_sitAngle;
if (loungeAnchor->direction == Direction::Left)
loungeAnchor->angle *= -1;
loungeAnchor->orientation = m_sitOrientation;
// Layer all anchored entities one above the object layer, in top to bottom
// order based on the anchor index.
loungeAnchor->loungeRenderLayer = RenderLayerObject + m_sitPositions.size() - positionIndex;
loungeAnchor->statusEffects = m_sitStatusEffects;
loungeAnchor->effectEmitters = m_sitEffectEmitters;
loungeAnchor->emote = m_sitEmote;
loungeAnchor->dance = m_sitDance;
loungeAnchor->armorCosmeticOverrides = m_sitArmorCosmeticOverrides;
loungeAnchor->cursorOverride = m_sitCursorOverride;
return loungeAnchor;
}
void LoungeableObject::setOrientationIndex(size_t orientationIndex) {
Object::setOrientationIndex(orientationIndex);
if (orientationIndex != NPos) {
if (auto sp = configValue("sitPosition")) {
m_sitPositions = {jsonToVec2F(configValue("sitPosition")) / TilePixels};
} else if (auto sps = configValue("sitPositions")) {
m_sitPositions.clear();
for (auto const& sp : sps.toArray())
m_sitPositions.append(jsonToVec2F(sp) / TilePixels);
}
m_sitFlipDirection = configValue("sitFlipDirection", false).toBool();
m_sitOrientation = LoungeOrientationNames.getLeft(configValue("sitOrientation", "sit").toString());
m_sitAngle = configValue("sitAngle", 0).toFloat() * Constants::pi / 180.0f;
m_sitCoverImage = configValue("sitCoverImage", "").toString();
m_flipImages = configValue("flipImages", false).toBool();
m_sitStatusEffects = configValue("sitStatusEffects", JsonArray()).toArray().transformed(jsonToPersistentStatusEffect);
m_sitEffectEmitters = jsonToStringSet(configValue("sitEffectEmitters", JsonArray()));
m_sitEmote = configValue("sitEmote").optString();
m_sitDance = configValue("sitDance").optString();
m_sitArmorCosmeticOverrides = configValue("sitArmorCosmeticOverrides", JsonObject()).toObject();
m_sitCursorOverride = configValue("sitCursorOverride").optString();
}
}
}

View file

@ -0,0 +1,40 @@
#ifndef STAR_INTERACTABLE_OBJECT_HPP
#define STAR_INTERACTABLE_OBJECT_HPP
#include "StarObject.hpp"
#include "StarLoungingEntities.hpp"
namespace Star {
class LoungeableObject : public Object, public virtual LoungeableEntity {
public:
LoungeableObject(ObjectConfigConstPtr config, Json const& parameters = Json());
void render(RenderCallback* renderCallback) override;
InteractAction interact(InteractRequest const& request) override;
size_t anchorCount() const override;
LoungeAnchorConstPtr loungeAnchor(size_t positionIndex) const override;
protected:
void setOrientationIndex(size_t orientationIndex) override;
private:
List<Vec2F> m_sitPositions;
bool m_sitFlipDirection;
LoungeOrientation m_sitOrientation;
float m_sitAngle;
String m_sitCoverImage;
bool m_flipImages;
List<PersistentStatusEffect> m_sitStatusEffects;
StringSet m_sitEffectEmitters;
Maybe<String> m_sitEmote;
Maybe<String> m_sitDance;
JsonObject m_sitArmorCosmeticOverrides;
Maybe<String> m_sitCursorOverride;
};
}
#endif

View file

@ -0,0 +1,114 @@
#include "StarPhysicsObject.hpp"
#include "StarJsonExtra.hpp"
#include "StarInterpolation.hpp"
#include "StarRoot.hpp"
#include "StarObjectDatabase.hpp"
#include "StarLuaConverters.hpp"
namespace Star {
PhysicsObject::PhysicsObject(ObjectConfigConstPtr config, Json const& parameters) : Object(move(config), parameters) {
for (auto const& p : configValue("physicsForces", JsonObject()).iterateObject()) {
auto& forceConfig = m_physicsForces[p.first];
forceConfig.forceRegion = jsonToPhysicsForceRegion(p.second);
forceConfig.enabled.set(p.second.getBool("enabled", true));
}
for (auto const& p : configValue("physicsCollisions", JsonObject()).iterateObject()) {
auto& collisionConfig = m_physicsCollisions[p.first];
collisionConfig.movingCollision = PhysicsMovingCollision::fromJson(p.second);
collisionConfig.xPosition.set(take(collisionConfig.movingCollision.position[0]));
collisionConfig.yPosition.set(take(collisionConfig.movingCollision.position[1]));
collisionConfig.enabled.set(p.second.getBool("enabled", true));
}
m_physicsForces.sortByKey();
for (auto& p : m_physicsForces)
m_netGroup.addNetElement(&p.second.enabled);
m_physicsCollisions.sortByKey();
for (auto& p : m_physicsCollisions) {
m_netGroup.addNetElement(&p.second.xPosition);
m_netGroup.addNetElement(&p.second.yPosition);
p.second.xPosition.setInterpolator(lerp<float, float>);
p.second.yPosition.setInterpolator(lerp<float, float>);
m_netGroup.addNetElement(&p.second.enabled);
}
}
void PhysicsObject::enableInterpolation(float extrapolationHint) {
m_netGroup.enableNetInterpolation(extrapolationHint);
}
void PhysicsObject::disableInterpolation() {
m_netGroup.disableNetInterpolation();
}
void PhysicsObject::init(World* world, EntityId entityId, EntityMode mode) {
if (mode == EntityMode::Master) {
LuaCallbacks physicsCallbacks;
physicsCallbacks.registerCallback("setForceEnabled", [this](String const& force, bool enabled) {
m_physicsForces.get(force).enabled.set(enabled);
});
physicsCallbacks.registerCallback("setCollisionPosition", [this](String const& collision, Vec2F const& pos) {
auto& collisionConfig = m_physicsCollisions.get(collision);
collisionConfig.xPosition.set(pos[0]);
collisionConfig.yPosition.set(pos[1]);
});
physicsCallbacks.registerCallback("setCollisionEnabled", [this](String const& collision, bool const& enabled) {
auto& collisionConfig = m_physicsCollisions.get(collision);
collisionConfig.enabled.set(enabled);
});
m_scriptComponent.addCallbacks("physics", move(physicsCallbacks));
}
Object::init(world, entityId, mode);
m_metaBoundBox = Object::metaBoundBox();
for (auto const& p : m_physicsForces) {
PhysicsForceRegion forceRegion = p.second.forceRegion;
forceRegion.call([pos = position()](auto& fr) { fr.translate(pos); });
m_metaBoundBox.combine(forceRegion.call([](auto& fr) { return fr.boundBox(); }));
}
}
void PhysicsObject::uninit() {
m_scriptComponent.removeCallbacks("physics");
Object::uninit();
}
void PhysicsObject::update(uint64_t currentStep) {
Object::update(currentStep);
if (isSlave())
m_netGroup.tickNetInterpolation(WorldTimestep);
}
RectF PhysicsObject::metaBoundBox() const {
return m_metaBoundBox;
}
List<PhysicsForceRegion> PhysicsObject::forceRegions() const {
List<PhysicsForceRegion> forces;
for (auto const& p : m_physicsForces) {
if (p.second.enabled.get()) {
PhysicsForceRegion forceRegion = p.second.forceRegion;
forceRegion.call([pos = position()](auto& fr) { fr.translate(pos); });
forces.append(move(forceRegion));
}
}
return forces;
}
size_t PhysicsObject::movingCollisionCount() const {
return m_physicsCollisions.size();
}
Maybe<PhysicsMovingCollision> PhysicsObject::movingCollision(size_t positionIndex) const {
auto const& collisionConfig = m_physicsCollisions.valueAt(positionIndex);
if (!collisionConfig.enabled.get())
return {};
PhysicsMovingCollision collision = collisionConfig.movingCollision;
collision.translate(position() + Vec2F(collisionConfig.xPosition.get(), collisionConfig.yPosition.get()));
return collision;
}
}

View file

@ -0,0 +1,49 @@
#ifndef STAR_PHYSICS_OBJECT_HPP
#define STAR_PHYSICS_OBJECT_HPP
#include "StarObject.hpp"
#include "StarPhysicsEntity.hpp"
namespace Star {
class PhysicsObject : public Object, public virtual PhysicsEntity {
public:
PhysicsObject(ObjectConfigConstPtr config, Json const& parameters = Json());
void enableInterpolation(float extrapolationHint = 0.0f) override;
void disableInterpolation() override;
void init(World* world, EntityId entityId, EntityMode mode) override;
void uninit() override;
void update(uint64_t currentStep) override;
RectF metaBoundBox() const override;
List<PhysicsForceRegion> forceRegions() const override;
size_t movingCollisionCount() const override;
Maybe<PhysicsMovingCollision> movingCollision(size_t positionIndex) const override;
private:
struct PhysicsForceConfig {
PhysicsForceRegion forceRegion;
NetElementBool enabled;
};
struct PhysicsCollisionConfig {
PhysicsMovingCollision movingCollision;
NetElementFloat xPosition;
NetElementFloat yPosition;
NetElementBool enabled;
};
OrderedHashMap<String, PhysicsForceConfig> m_physicsForces;
OrderedHashMap<String, PhysicsCollisionConfig> m_physicsCollisions;
RectF m_metaBoundBox;
};
}
#endif

View file

@ -0,0 +1,16 @@
#include "StarTeleporterObject.hpp"
#include "StarJsonExtra.hpp"
namespace Star {
TeleporterObject::TeleporterObject(ObjectConfigConstPtr config, Json const& parameters) : Object(config, parameters) {
setUniqueId(configValue("uniqueId", Uuid().hex()).optString());
}
Vec2F TeleporterObject::footPosition() const {
if (auto footPos = configValue("teleporterFootPosition"))
return jsonToVec2F(footPos);
return Vec2F();
}
}

View file

@ -0,0 +1,13 @@
#include "StarWarpTargetEntity.hpp"
#include "StarObject.hpp"
namespace Star {
class TeleporterObject : public Object, public WarpTargetEntity {
public:
TeleporterObject(ObjectConfigConstPtr config, Json const& parameters = JsonObject());
Vec2F footPosition() const override;
};
}