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

summaryrefslogtreecommitdiff
path: root/source/game/items
diff options
context:
space:
mode:
authorKae <80987908+Novaenia@users.noreply.github.com>2023-06-20 14:33:09 +1000
committerKae <80987908+Novaenia@users.noreply.github.com>2023-06-20 14:33:09 +1000
commit6352e8e3196f78388b6c771073f9e03eaa612673 (patch)
treee23772f79a7fbc41bc9108951e9e136857484bf4 /source/game/items
parent6741a057e5639280d85d0f88ba26f000baa58f61 (diff)
everything everywhere
all at once
Diffstat (limited to 'source/game/items')
-rw-r--r--source/game/items/StarActiveItem.cpp522
-rw-r--r--source/game/items/StarActiveItem.hpp101
-rw-r--r--source/game/items/StarArmors.cpp203
-rw-r--r--source/game/items/StarArmors.hpp127
-rw-r--r--source/game/items/StarAugmentItem.cpp29
-rw-r--r--source/game/items/StarAugmentItem.hpp27
-rw-r--r--source/game/items/StarBlueprintItem.cpp54
-rw-r--r--source/game/items/StarBlueprintItem.hpp32
-rw-r--r--source/game/items/StarCodexItem.cpp49
-rw-r--r--source/game/items/StarCodexItem.hpp30
-rw-r--r--source/game/items/StarConsumableItem.cpp110
-rw-r--r--source/game/items/StarConsumableItem.hpp38
-rw-r--r--source/game/items/StarCurrency.cpp42
-rw-r--r--source/game/items/StarCurrency.hpp33
-rw-r--r--source/game/items/StarInspectionTool.cpp173
-rw-r--r--source/game/items/StarInspectionTool.hpp74
-rw-r--r--source/game/items/StarInstrumentItem.cpp88
-rw-r--r--source/game/items/StarInstrumentItem.hpp57
-rw-r--r--source/game/items/StarLiquidItem.cpp151
-rw-r--r--source/game/items/StarLiquidItem.hpp47
-rw-r--r--source/game/items/StarMaterialItem.cpp175
-rw-r--r--source/game/items/StarMaterialItem.hpp50
-rw-r--r--source/game/items/StarObjectItem.cpp107
-rw-r--r--source/game/items/StarObjectItem.hpp39
-rw-r--r--source/game/items/StarThrownItem.cpp56
-rw-r--r--source/game/items/StarThrownItem.hpp32
-rw-r--r--source/game/items/StarTools.cpp709
-rw-r--r--source/game/items/StarTools.hpp244
-rw-r--r--source/game/items/StarUnlockItem.cpp69
-rw-r--r--source/game/items/StarUnlockItem.hpp35
30 files changed, 3503 insertions, 0 deletions
diff --git a/source/game/items/StarActiveItem.cpp b/source/game/items/StarActiveItem.cpp
new file mode 100644
index 0000000..80e0b3e
--- /dev/null
+++ b/source/game/items/StarActiveItem.cpp
@@ -0,0 +1,522 @@
+#include "StarActiveItem.hpp"
+#include "StarRoot.hpp"
+#include "StarAssets.hpp"
+#include "StarConfigLuaBindings.hpp"
+#include "StarItemLuaBindings.hpp"
+#include "StarStatusControllerLuaBindings.hpp"
+#include "StarNetworkedAnimatorLuaBindings.hpp"
+#include "StarScriptedAnimatorLuaBindings.hpp"
+#include "StarPlayerLuaBindings.hpp"
+#include "StarEntityLuaBindings.hpp"
+#include "StarJsonExtra.hpp"
+#include "StarDataStreamExtra.hpp"
+#include "StarPlayer.hpp"
+#include "StarEmoteEntity.hpp"
+
+namespace Star {
+
+ActiveItem::ActiveItem(Json const& config, String const& directory, Json const& parameters)
+ : Item(config, directory, parameters) {
+ auto assets = Root::singleton().assets();
+ auto animationConfig = assets->fetchJson(instanceValue("animation"), directory);
+ if (auto customConfig = instanceValue("animationCustom"))
+ animationConfig = jsonMerge(animationConfig, customConfig);
+ m_itemAnimator = NetworkedAnimator(animationConfig, directory);
+ for (auto const& pair : instanceValue("animationParts", JsonObject()).iterateObject())
+ m_itemAnimator.setPartTag(pair.first, "partImage", pair.second.toString());
+ m_scriptedAnimationParameters.reset(config.getObject("scriptedAnimationParameters", {}));
+
+ addNetElement(&m_itemAnimator);
+ addNetElement(&m_holdingItem);
+ addNetElement(&m_backArmFrame);
+ addNetElement(&m_frontArmFrame);
+ addNetElement(&m_twoHandedGrip);
+ addNetElement(&m_recoil);
+ addNetElement(&m_outsideOfHand);
+ addNetElement(&m_armAngle);
+ addNetElement(&m_facingDirection);
+ addNetElement(&m_damageSources);
+ addNetElement(&m_itemDamageSources);
+ addNetElement(&m_shieldPolys);
+ addNetElement(&m_itemShieldPolys);
+ addNetElement(&m_forceRegions);
+ addNetElement(&m_itemForceRegions);
+
+ // don't interpolate scripted animation parameters
+ addNetElement(&m_scriptedAnimationParameters, false);
+
+ m_holdingItem.set(true);
+ m_armAngle.setFixedPointBase(0.01f);
+}
+
+ActiveItem::ActiveItem(ActiveItem const& rhs) : ActiveItem(rhs.config(), rhs.directory(), rhs.parameters()) {}
+
+ItemPtr ActiveItem::clone() const {
+ return make_shared<ActiveItem>(*this);
+}
+
+void ActiveItem::init(ToolUserEntity* owner, ToolHand hand) {
+ ToolUserItem::init(owner, hand);
+ if (entityMode() == EntityMode::Master) {
+ m_script.setScripts(jsonToStringList(instanceValue("scripts")).transformed(bind(&AssetPath::relativeTo, directory(), _1)));
+ m_script.setUpdateDelta(instanceValue("scriptDelta", 1).toUInt());
+ m_twoHandedGrip.set(twoHanded());
+
+ if (auto previousStorage = instanceValue("scriptStorage"))
+ m_script.setScriptStorage(previousStorage.toObject());
+
+ m_script.addCallbacks("activeItem", makeActiveItemCallbacks());
+ m_script.addCallbacks("item", LuaBindings::makeItemCallbacks(this));
+ m_script.addCallbacks("config", LuaBindings::makeConfigCallbacks(bind(&Item::instanceValue, as<Item>(this), _1, _2)));
+ m_script.addCallbacks("animator", LuaBindings::makeNetworkedAnimatorCallbacks(&m_itemAnimator));
+ m_script.addCallbacks("status", LuaBindings::makeStatusControllerCallbacks(owner->statusController()));
+ m_script.addActorMovementCallbacks(owner->movementController());
+ if (auto player = as<Player>(owner))
+ m_script.addCallbacks("player", LuaBindings::makePlayerCallbacks(player));
+ m_script.addCallbacks("entity", LuaBindings::makeEntityCallbacks(as<Entity>(owner)));
+ m_script.init(world());
+ m_currentFireMode = FireMode::None;
+ }
+ if (world()->isClient()) {
+ if (auto animationScripts = instanceValue("animationScripts")) {
+ m_scriptedAnimator.setScripts(jsonToStringList(animationScripts).transformed(bind(&AssetPath::relativeTo, directory(), _1)));
+ m_scriptedAnimator.setUpdateDelta(instanceValue("animationDelta", 1).toUInt());
+
+ m_scriptedAnimator.addCallbacks("animationConfig", LuaBindings::makeScriptedAnimatorCallbacks(&m_itemAnimator,
+ [this](String const& name, Json const& defaultValue) -> Json {
+ return m_scriptedAnimationParameters.value(name, defaultValue);
+ }));
+ m_scriptedAnimator.addCallbacks("activeItemAnimation", makeScriptedAnimationCallbacks());
+ m_scriptedAnimator.addCallbacks("config", LuaBindings::makeConfigCallbacks(bind(&Item::instanceValue, as<Item>(this), _1, _2)));
+ m_scriptedAnimator.init(world());
+ }
+ }
+}
+
+void ActiveItem::uninit() {
+ if (entityMode() == EntityMode::Master) {
+ m_script.uninit();
+ m_script.removeCallbacks("activeItem");
+ m_script.removeCallbacks("item");
+ m_script.removeCallbacks("config");
+ m_script.removeCallbacks("animator");
+ m_script.removeCallbacks("status");
+ m_script.removeActorMovementCallbacks();
+ m_script.removeCallbacks("player");
+ m_script.removeCallbacks("entity");
+ }
+ if (world()->isClient()) {
+ if (auto animationScripts = instanceValue("animationScripts")) {
+ m_scriptedAnimator.uninit();
+ m_scriptedAnimator.removeCallbacks("animationConfig");
+ m_scriptedAnimator.removeCallbacks("activeItemAnimation");
+ m_scriptedAnimator.removeCallbacks("config");
+ }
+ }
+
+ m_itemAnimatorDynamicTarget.stopAudio();
+ ToolUserItem::uninit();
+ m_activeAudio.clear();
+}
+
+void ActiveItem::update(FireMode fireMode, bool shifting, HashSet<MoveControlType> const& moves) {
+ StringMap<bool> moveMap;
+ for (auto m : moves)
+ moveMap[MoveControlTypeNames.getRight(m)] = true;
+
+ if (entityMode() == EntityMode::Master) {
+ if (fireMode != m_currentFireMode) {
+ m_currentFireMode = fireMode;
+ if (fireMode != FireMode::None)
+ m_script.invoke("activate", FireModeNames.getRight(fireMode), shifting, moveMap);
+ }
+ m_script.update(m_script.updateDt(), FireModeNames.getRight(fireMode), shifting, moveMap);
+
+ if (instanceValue("retainScriptStorageInItem", false).toBool()) {
+ setInstanceValue("scriptStorage", m_script.getScriptStorage());
+ }
+ }
+
+ if (world()->isClient()) {
+ m_itemAnimator.update(WorldTimestep, &m_itemAnimatorDynamicTarget);
+ m_scriptedAnimator.update(m_scriptedAnimator.updateDt());
+ } else {
+ m_itemAnimator.update(WorldTimestep, nullptr);
+ }
+
+ eraseWhere(m_activeAudio, [this](pair<AudioInstancePtr const, Vec2F> const& a) {
+ a.first->setPosition(owner()->position() + handPosition(a.second));
+ return a.first->finished();
+ });
+
+ for (auto shieldPoly : shieldPolys()) {
+ shieldPoly.translate(owner()->position());
+ SpatialLogger::logPoly("world", shieldPoly, {255, 255, 0, 255});
+ }
+
+ for (auto forceRegion : forceRegions()) {
+ if (auto dfr = forceRegion.ptr<DirectionalForceRegion>())
+ SpatialLogger::logPoly("world", dfr->region, {155, 0, 255, 255});
+ else if (auto rfr = forceRegion.ptr<RadialForceRegion>())
+ SpatialLogger::logPoint("world", rfr->center, {155, 0, 255, 255});
+ }
+}
+
+List<DamageSource> ActiveItem::damageSources() const {
+ List<DamageSource> damageSources = m_damageSources.get();
+ for (auto ds : m_itemDamageSources.get()) {
+ if (ds.damageArea.is<PolyF>()) {
+ auto& poly = ds.damageArea.get<PolyF>();
+ poly.rotate(m_armAngle.get());
+ if (owner()->facingDirection() == Direction::Left)
+ poly.flipHorizontal(0.0f);
+ poly.translate(handPosition(Vec2F()));
+ } else if (ds.damageArea.is<Line2F>()) {
+ auto& line = ds.damageArea.get<Line2F>();
+ line.rotate(m_armAngle.get());
+ if (owner()->facingDirection() == Direction::Left)
+ line.flipHorizontal(0.0f);
+ line.translate(handPosition(Vec2F()));
+ }
+ damageSources.append(move(ds));
+ }
+ return damageSources;
+}
+
+List<PolyF> ActiveItem::shieldPolys() const {
+ List<PolyF> shieldPolys = m_shieldPolys.get();
+ for (auto sp : m_itemShieldPolys.get()) {
+ sp.rotate(m_armAngle.get());
+ if (owner()->facingDirection() == Direction::Left)
+ sp.flipHorizontal(0.0f);
+ sp.translate(handPosition(Vec2F()));
+ shieldPolys.append(move(sp));
+ }
+ return shieldPolys;
+}
+
+List<PhysicsForceRegion> ActiveItem::forceRegions() const {
+ List<PhysicsForceRegion> forceRegions = m_forceRegions.get();
+ for (auto fr : m_itemForceRegions.get()) {
+ if (auto dfr = fr.ptr<DirectionalForceRegion>()) {
+ dfr->region.rotate(m_armAngle.get());
+ if (owner()->facingDirection() == Direction::Left)
+ dfr->region.flipHorizontal(0.0f);
+ dfr->region.translate(owner()->position() + handPosition(Vec2F()));
+ } else if (auto rfr = fr.ptr<RadialForceRegion>()) {
+ rfr->center = rfr->center.rotate(m_armAngle.get());
+ if (owner()->facingDirection() == Direction::Left)
+ rfr->center[0] *= -1;
+ rfr->center += owner()->position() + handPosition(Vec2F());
+ }
+ forceRegions.append(move(fr));
+ }
+ return forceRegions;
+}
+
+bool ActiveItem::holdingItem() const {
+ return m_holdingItem.get();
+}
+
+Maybe<String> ActiveItem::backArmFrame() const {
+ return m_backArmFrame.get();
+}
+
+Maybe<String> ActiveItem::frontArmFrame() const {
+ return m_frontArmFrame.get();
+}
+
+bool ActiveItem::twoHandedGrip() const {
+ return m_twoHandedGrip.get();
+}
+
+bool ActiveItem::recoil() const {
+ return m_recoil.get();
+}
+
+bool ActiveItem::outsideOfHand() const {
+ return m_outsideOfHand.get();
+}
+
+float ActiveItem::armAngle() const {
+ return m_armAngle.get();
+}
+
+Maybe<Direction> ActiveItem::facingDirection() const {
+ return m_facingDirection.get();
+}
+
+List<Drawable> ActiveItem::handDrawables() const {
+ if (m_itemAnimator.parts().empty()) {
+ auto drawables = Item::iconDrawables();
+ Drawable::scaleAll(drawables, 1.0f / TilePixels);
+ return drawables;
+ } else {
+ return m_itemAnimator.drawables();
+ }
+}
+
+List<pair<Drawable, Maybe<EntityRenderLayer>>> ActiveItem::entityDrawables() const {
+ return m_scriptedAnimator.drawables();
+}
+
+List<LightSource> ActiveItem::lights() const {
+ // Same as pullNewAudios, we translate and flip ourselves.
+ List<LightSource> result;
+ for (auto& light : m_itemAnimator.lightSources()) {
+ light.position = owner()->position() + handPosition(light.position);
+ light.beamAngle += m_armAngle.get();
+ if (owner()->facingDirection() == Direction::Left) {
+ if (light.beamAngle > 0)
+ light.beamAngle = Constants::pi / 2 + constrainAngle(Constants::pi / 2 - light.beamAngle);
+ else
+ light.beamAngle = -Constants::pi / 2 - constrainAngle(light.beamAngle + Constants::pi / 2);
+ }
+ result.append(move(light));
+ }
+ result.appendAll(m_scriptedAnimator.lightSources());
+ return result;
+}
+
+List<AudioInstancePtr> ActiveItem::pullNewAudios() {
+ // Because the item animator is in hand-space, and Humanoid does all the
+ // translation *and flipping*, we cannot use NetworkedAnimator's built-in
+ // functionality to rotate and flip, and instead must do it manually. We do
+ // not call animatorTarget.setPosition, and keep track of running audio
+ // ourselves. It would be easier if (0, 0) for the NetworkedAnimator was,
+ // say, the shoulder and un-rotated, but it gets a bit weird with Humanoid
+ // modifications.
+ List<AudioInstancePtr> result;
+ for (auto& audio : m_itemAnimatorDynamicTarget.pullNewAudios()) {
+ m_activeAudio[audio] = *audio->position();
+ audio->setPosition(owner()->position() + handPosition(*audio->position()));
+ result.append(move(audio));
+ }
+ result.appendAll(m_scriptedAnimator.pullNewAudios());
+ return result;
+}
+
+List<Particle> ActiveItem::pullNewParticles() {
+ // Same as pullNewAudios, we translate, rotate, and flip ourselves
+ List<Particle> result;
+ for (auto& particle : m_itemAnimatorDynamicTarget.pullNewParticles()) {
+ particle.position = owner()->position() + handPosition(particle.position);
+ particle.velocity = particle.velocity.rotate(m_armAngle.get());
+ if (owner()->facingDirection() == Direction::Left) {
+ particle.velocity[0] *= -1;
+ particle.flip = !particle.flip;
+ }
+ result.append(move(particle));
+ }
+ result.appendAll(m_scriptedAnimator.pullNewParticles());
+ return result;
+}
+
+Maybe<String> ActiveItem::cursor() const {
+ return m_cursor;
+}
+
+Maybe<Json> ActiveItem::receiveMessage(String const& message, bool localMessage, JsonArray const& args) {
+ return m_script.handleMessage(message, localMessage, args);
+}
+
+float ActiveItem::durabilityStatus() {
+ auto durability = instanceValue("durability").optFloat();
+ if (durability) {
+ auto durabilityHit = instanceValue("durabilityHit").optFloat().value(*durability);
+ return durabilityHit / *durability;
+ }
+ return 1.0;
+}
+
+Vec2F ActiveItem::armPosition(Vec2F const& offset) const {
+ return owner()->armPosition(hand(), owner()->facingDirection(), m_armAngle.get(), offset);
+}
+
+Vec2F ActiveItem::handPosition(Vec2F const& offset) const {
+ return armPosition(offset + owner()->handOffset(hand(), owner()->facingDirection()));
+}
+
+LuaCallbacks ActiveItem::makeActiveItemCallbacks() {
+ LuaCallbacks callbacks;
+ callbacks.registerCallback("ownerEntityId", [this]() {
+ return owner()->entityId();
+ });
+ callbacks.registerCallback("ownerTeam", [this]() {
+ return owner()->getTeam().toJson();
+ });
+ callbacks.registerCallback("ownerAimPosition", [this]() {
+ return owner()->aimPosition();
+ });
+ callbacks.registerCallback("ownerPowerMultiplier", [this]() {
+ return owner()->powerMultiplier();
+ });
+ callbacks.registerCallback("fireMode", [this]() {
+ return FireModeNames.getRight(m_currentFireMode);
+ });
+ callbacks.registerCallback("hand", [this]() {
+ return ToolHandNames.getRight(hand());
+ });
+ callbacks.registerCallback("handPosition", [this](Maybe<Vec2F> offset) {
+ return handPosition(offset.value());
+ });
+
+ // Gets the required aim angle to aim a "barrel" of the item that has the given
+ // vertical offset from the hand at the given target. The line that is aimed
+ // at the target is the horizontal line going through the aimVerticalOffset.
+ callbacks.registerCallback("aimAngleAndDirection", [this](float aimVerticalOffset, Vec2F targetPosition) {
+ // This was figured out using pencil and paper geometry from the hand
+ // rotation center, the target position, and the 90 deg vertical offset of
+ // the "barrel".
+
+ Vec2F handRotationCenter = owner()->armPosition(hand(), owner()->facingDirection(), 0.0f, Vec2F());
+ Vec2F ownerPosition = owner()->position();
+
+ // Vector in owner entity space from hand rotation center to target.
+ Vec2F toTarget = owner()->world()->geometry().diff(targetPosition, (ownerPosition + handRotationCenter));
+ float toTargetDist = toTarget.magnitude();
+
+ // If the aim position is inside the circle formed by the barrel line as it
+ // goes around (aimVerticalOffset <= toTargetDist) absolutely no angle will
+ // give you an intersect, so we just bail out and assume the target is at the
+ // edge of the circle to retain continuity.
+ float angleAdjust = -std::asin(clamp(aimVerticalOffset / toTargetDist, -1.0f, 1.0f));
+ auto angleSide = getAngleSide(toTarget.angle());
+ return luaTupleReturn(angleSide.first + angleAdjust, numericalDirection(angleSide.second));
+ });
+
+ // Similar to aimAngleAndDirection, but only provides the offset-adjusted aimAngle for the current facing direction
+ callbacks.registerCallback("aimAngle", [this](float aimVerticalOffset, Vec2F targetPosition) {
+ Vec2F handRotationCenter = owner()->armPosition(hand(), owner()->facingDirection(), 0.0f, Vec2F());
+ Vec2F ownerPosition = owner()->position();
+ Vec2F toTarget = owner()->world()->geometry().diff(targetPosition, (ownerPosition + handRotationCenter));
+ float toTargetDist = toTarget.magnitude();
+ float angleAdjust = -std::asin(clamp(aimVerticalOffset / toTargetDist, -1.0f, 1.0f));
+ return toTarget.angle() + angleAdjust;
+ });
+
+ callbacks.registerCallback("setHoldingItem", [this](bool holdingItem) {
+ m_holdingItem.set(holdingItem);
+ });
+
+ callbacks.registerCallback("setBackArmFrame", [this](Maybe<String> armFrame) {
+ m_backArmFrame.set(armFrame);
+ });
+
+ callbacks.registerCallback("setFrontArmFrame", [this](Maybe<String> armFrame) {
+ m_frontArmFrame.set(armFrame);
+ });
+
+ callbacks.registerCallback("setTwoHandedGrip", [this](bool twoHandedGrip) {
+ m_twoHandedGrip.set(twoHandedGrip);
+ });
+
+ callbacks.registerCallback("setRecoil", [this](bool recoil) {
+ m_recoil.set(recoil);
+ });
+
+ callbacks.registerCallback("setOutsideOfHand", [this](bool outsideOfHand) {
+ m_outsideOfHand.set(outsideOfHand);
+ });
+
+ callbacks.registerCallback("setArmAngle", [this](float armAngle) {
+ m_armAngle.set(armAngle);
+ });
+
+ callbacks.registerCallback("setFacingDirection", [this](float direction) {
+ m_facingDirection.set(directionOf(direction));
+ });
+
+ callbacks.registerCallback("setDamageSources", [this](Maybe<JsonArray> const& damageSources) {
+ m_damageSources.set(damageSources.value().transformed(construct<DamageSource>()));
+ });
+
+ callbacks.registerCallback("setItemDamageSources", [this](Maybe<JsonArray> const& damageSources) {
+ m_itemDamageSources.set(damageSources.value().transformed(construct<DamageSource>()));
+ });
+
+ callbacks.registerCallback("setShieldPolys", [this](Maybe<List<PolyF>> const& shieldPolys) {
+ m_shieldPolys.set(shieldPolys.value());
+ });
+
+ callbacks.registerCallback("setItemShieldPolys", [this](Maybe<List<PolyF>> const& shieldPolys) {
+ m_itemShieldPolys.set(shieldPolys.value());
+ });
+
+ callbacks.registerCallback("setForceRegions", [this](Maybe<JsonArray> const& forceRegions) {
+ if (forceRegions)
+ m_forceRegions.set(forceRegions->transformed(jsonToPhysicsForceRegion));
+ else
+ m_forceRegions.set({});
+ });
+
+ callbacks.registerCallback("setItemForceRegions", [this](Maybe<JsonArray> const& forceRegions) {
+ if (forceRegions)
+ m_itemForceRegions.set(forceRegions->transformed(jsonToPhysicsForceRegion));
+ else
+ m_itemForceRegions.set({});
+ });
+
+ callbacks.registerCallback("setCursor", [this](Maybe<String> cursor) {
+ m_cursor = move(cursor);
+ });
+
+ callbacks.registerCallback("setScriptedAnimationParameter", [this](String name, Json value) {
+ m_scriptedAnimationParameters.set(move(name), move(value));
+ });
+
+ callbacks.registerCallback("setInventoryIcon", [this](String image) {
+ setInstanceValue("inventoryIcon", image);
+ setIconDrawables({Drawable::makeImage(move(image), 1.0f, true, Vec2F())});
+ });
+
+ callbacks.registerCallback("setInstanceValue", [this](String name, Json val) {
+ setInstanceValue(move(name), move(val));
+ });
+
+ callbacks.registerCallback("callOtherHandScript", [this](String const& func, LuaVariadic<LuaValue> const& args) {
+ if (auto otherHandItem = owner()->handItem(hand() == ToolHand::Primary ? ToolHand::Alt : ToolHand::Primary)) {
+ if (auto otherActiveItem = as<ActiveItem>(otherHandItem))
+ return otherActiveItem->m_script.invoke(func, args).value();
+ }
+ return LuaValue();
+ });
+
+ callbacks.registerCallback("interact", [this](String const& type, Json const& configData, Maybe<EntityId> const& sourceEntityId) {
+ owner()->interact(InteractAction(type, sourceEntityId.value(NullEntityId), configData));
+ });
+
+ callbacks.registerCallback("emote", [this](String const& emoteName) {
+ auto emote = HumanoidEmoteNames.getLeft(emoteName);
+ if (auto entity = as<EmoteEntity>(owner()))
+ entity->playEmote(emote);
+ });
+
+ callbacks.registerCallback("setCameraFocusEntity", [this](Maybe<EntityId> const& cameraFocusEntity) {
+ owner()->setCameraFocusEntity(cameraFocusEntity);
+ });
+
+ return callbacks;
+}
+
+LuaCallbacks ActiveItem::makeScriptedAnimationCallbacks() {
+ LuaCallbacks callbacks;
+ callbacks.registerCallback("ownerPosition", [this]() {
+ return owner()->position();
+ });
+ callbacks.registerCallback("ownerAimPosition", [this]() {
+ return owner()->aimPosition();
+ });
+ callbacks.registerCallback("ownerArmAngle", [this]() {
+ return m_armAngle.get();
+ });
+ callbacks.registerCallback("ownerFacingDirection", [this]() {
+ return numericalDirection(owner()->facingDirection());
+ });
+ callbacks.registerCallback("handPosition", [this](Maybe<Vec2F> offset) {
+ return handPosition(offset.value());
+ });
+ return callbacks;
+}
+
+}
diff --git a/source/game/items/StarActiveItem.hpp b/source/game/items/StarActiveItem.hpp
new file mode 100644
index 0000000..61c0e70
--- /dev/null
+++ b/source/game/items/StarActiveItem.hpp
@@ -0,0 +1,101 @@
+#ifndef STAR_ACTIVE_ITEM_HPP
+#define STAR_ACTIVE_ITEM_HPP
+
+#include "StarNetElementBasicFields.hpp"
+#include "StarNetElementFloatFields.hpp"
+#include "StarItem.hpp"
+#include "StarToolUserItem.hpp"
+#include "StarLuaComponents.hpp"
+#include "StarLuaActorMovementComponent.hpp"
+#include "StarNetworkedAnimator.hpp"
+#include "StarLuaAnimationComponent.hpp"
+#include "StarDurabilityItem.hpp"
+
+namespace Star {
+
+STAR_CLASS(AudioInstance);
+STAR_CLASS(ActiveItem);
+
+class ActiveItem :
+ public Item,
+ public DurabilityItem,
+ public virtual ToolUserItem,
+ public virtual NetElementGroup {
+public:
+ ActiveItem(Json const& config, String const& directory, Json const& parameters = JsonObject());
+ ActiveItem(ActiveItem const& rhs);
+
+ ItemPtr clone() const override;
+
+ void init(ToolUserEntity* owner, ToolHand hand) override;
+ void uninit() override;
+
+ void update(FireMode fireMode, bool shifting, HashSet<MoveControlType> const& moves) override;
+
+ List<DamageSource> damageSources() const override;
+ List<PolyF> shieldPolys() const override;
+
+ List<PhysicsForceRegion> forceRegions() const override;
+
+ bool holdingItem() const;
+ Maybe<String> backArmFrame() const;
+ Maybe<String> frontArmFrame() const;
+ bool twoHandedGrip() const;
+ bool recoil() const;
+ bool outsideOfHand() const;
+
+ float armAngle() const;
+ Maybe<Direction> facingDirection() const;
+
+ // Hand drawables are in hand-space, everything else is in world space.
+ List<Drawable> handDrawables() const;
+ List<pair<Drawable, Maybe<EntityRenderLayer>>> entityDrawables() const;
+ List<LightSource> lights() const;
+ List<AudioInstancePtr> pullNewAudios();
+ List<Particle> pullNewParticles();
+
+ Maybe<String> cursor() const;
+
+ Maybe<Json> receiveMessage(String const& message, bool localMessage, JsonArray const& args = {});
+
+ float durabilityStatus() override;
+
+private:
+ Vec2F armPosition(Vec2F const& offset) const;
+ Vec2F handPosition(Vec2F const& offset) const;
+
+ LuaCallbacks makeActiveItemCallbacks();
+ LuaCallbacks makeScriptedAnimationCallbacks();
+
+ mutable LuaMessageHandlingComponent<LuaActorMovementComponent<LuaUpdatableComponent<LuaStorableComponent<LuaWorldComponent<LuaBaseComponent>>>>> m_script;
+
+ NetworkedAnimator m_itemAnimator;
+ NetworkedAnimator::DynamicTarget m_itemAnimatorDynamicTarget;
+
+ mutable LuaAnimationComponent<LuaUpdatableComponent<LuaWorldComponent<LuaBaseComponent>>> m_scriptedAnimator;
+
+ HashMap<AudioInstancePtr, Vec2F> m_activeAudio;
+
+ FireMode m_currentFireMode;
+ Maybe<String> m_cursor;
+
+ NetElementBool m_holdingItem;
+ NetElementData<Maybe<String>> m_backArmFrame;
+ NetElementData<Maybe<String>> m_frontArmFrame;
+ NetElementBool m_twoHandedGrip;
+ NetElementBool m_recoil;
+ NetElementBool m_outsideOfHand;
+ NetElementFloat m_armAngle;
+ NetElementData<Maybe<Direction>> m_facingDirection;
+ NetElementData<List<DamageSource>> m_damageSources;
+ NetElementData<List<DamageSource>> m_itemDamageSources;
+ NetElementData<List<PolyF>> m_shieldPolys;
+ NetElementData<List<PolyF>> m_itemShieldPolys;
+ NetElementData<List<PhysicsForceRegion>> m_forceRegions;
+ NetElementData<List<PhysicsForceRegion>> m_itemForceRegions;
+ NetElementHashMap<String, Json> m_scriptedAnimationParameters;
+};
+
+}
+
+#endif
diff --git a/source/game/items/StarArmors.cpp b/source/game/items/StarArmors.cpp
new file mode 100644
index 0000000..17714fb
--- /dev/null
+++ b/source/game/items/StarArmors.cpp
@@ -0,0 +1,203 @@
+#include "StarArmors.hpp"
+#include "StarAssets.hpp"
+#include "StarJsonExtra.hpp"
+#include "StarImageProcessing.hpp"
+#include "StarHumanoid.hpp"
+#include "StarRoot.hpp"
+#include "StarStoredFunctions.hpp"
+#include "StarPlayer.hpp"
+
+namespace Star {
+
+ArmorItem::ArmorItem(Json const& config, String const& directory, Json const& data) : Item(config, directory, data) {
+ refreshStatusEffects();
+ m_effectSources = jsonToStringSet(instanceValue("effectSources", JsonArray()));
+ m_techModule = instanceValue("techModule", "").toString();
+ if (m_techModule->empty())
+ m_techModule = {};
+ else
+ m_techModule = AssetPath::relativeTo(directory, *m_techModule);
+
+ m_directives = instanceValue("directives", "").toString();
+ m_colorOptions = colorDirectivesFromConfig(config.getArray("colorOptions", JsonArray{""}));
+ if (m_directives.empty())
+ m_directives = "?" + m_colorOptions.wrap(instanceValue("colorIndex", 0).toUInt());
+ refreshIconDrawables();
+
+ m_hideBody = config.getBool("hideBody", false);
+}
+
+List<PersistentStatusEffect> ArmorItem::statusEffects() const {
+ return m_statusEffects;
+}
+
+StringSet ArmorItem::effectSources() const {
+ return m_effectSources;
+}
+
+List<String> const& ArmorItem::colorOptions() {
+ return m_colorOptions;
+}
+
+String const& ArmorItem::directives() const {
+ return m_directives;
+}
+
+bool ArmorItem::hideBody() const {
+ return m_hideBody;
+}
+
+Maybe<String> const& ArmorItem::techModule() const {
+ return m_techModule;
+}
+
+void ArmorItem::refreshIconDrawables() {
+ auto drawables = iconDrawables();
+ for (auto& drawable : drawables) {
+ if (drawable.isImage()) {
+ drawable.imagePart().removeDirectives(true);
+ drawable.imagePart().addDirectives(m_directives, true);
+ }
+ }
+ setIconDrawables(move(drawables));
+}
+
+void ArmorItem::refreshStatusEffects() {
+ m_statusEffects = instanceValue("statusEffects", JsonArray()).toArray().transformed(jsonToPersistentStatusEffect);
+ if (auto leveledStatusEffects = instanceValue("leveledStatusEffects", Json())) {
+ auto functionDatabase = Root::singleton().functionDatabase();
+ float level = instanceValue("level", 1).toFloat();
+ for (auto effectConfig : leveledStatusEffects.iterateArray()) {
+ float levelFunctionFactor = functionDatabase->function(effectConfig.getString("levelFunction"))->evaluate(level);
+ auto statModifier = jsonToStatModifier(effectConfig);
+ if (auto p = statModifier.ptr<StatBaseMultiplier>())
+ p->baseMultiplier = 1 + (p->baseMultiplier - 1) * levelFunctionFactor;
+ else if (auto p = statModifier.ptr<StatValueModifier>())
+ p->value *= levelFunctionFactor;
+ else if (auto p = statModifier.ptr<StatEffectiveMultiplier>())
+ p->effectiveMultiplier = 1 + (p->effectiveMultiplier - 1) * levelFunctionFactor;
+ m_statusEffects.append(statModifier);
+ }
+ }
+ if (auto augmentConfig = instanceValue("currentAugment", Json()))
+ m_statusEffects.appendAll(augmentConfig.getArray("effects", JsonArray()).transformed(jsonToPersistentStatusEffect));
+}
+
+HeadArmor::HeadArmor(Json const& config, String const& directory, Json const& data)
+ : ArmorItem(config, directory, data) {
+ m_maleImage = AssetPath::relativeTo(directory, config.getString("maleFrames"));
+ m_femaleImage = AssetPath::relativeTo(directory, config.getString("femaleFrames"));
+
+ m_maskDirectives = instanceValue("mask").toString();
+ if (!m_maskDirectives.empty() && !m_maskDirectives.contains("?"))
+ m_maskDirectives = "?addmask=" + AssetPath::relativeTo(directory, m_maskDirectives) + ";0;0";
+}
+
+ItemPtr HeadArmor::clone() const {
+ return make_shared<HeadArmor>(*this);
+}
+
+String const& HeadArmor::frameset(Gender gender) const {
+ if (gender == Gender::Male)
+ return m_maleImage;
+ else
+ return m_femaleImage;
+}
+
+String const& HeadArmor::maskDirectives() const {
+ return m_maskDirectives;
+}
+
+List<Drawable> HeadArmor::preview(PlayerPtr const& viewer) const {
+ Gender gender = viewer ? viewer->gender() : Gender::Male;
+ return Humanoid::renderDummy(gender, this);
+}
+
+ChestArmor::ChestArmor(Json const& config, String const& directory, Json const& data)
+ : ArmorItem(config, directory, data) {
+ Json maleImages = config.get("maleFrames");
+ m_maleBodyImage = AssetPath::relativeTo(directory, maleImages.getString("body"));
+ m_maleFrontSleeveImage = AssetPath::relativeTo(directory, maleImages.getString("frontSleeve"));
+ m_maleBackSleeveImage = AssetPath::relativeTo(directory, maleImages.getString("backSleeve"));
+
+ Json femaleImages = config.get("femaleFrames");
+ m_femaleBodyImage = AssetPath::relativeTo(directory, femaleImages.getString("body"));
+ m_femaleFrontSleeveImage = AssetPath::relativeTo(directory, femaleImages.getString("frontSleeve"));
+ m_femaleBackSleeveImage = AssetPath::relativeTo(directory, femaleImages.getString("backSleeve"));
+}
+
+ItemPtr ChestArmor::clone() const {
+ return make_shared<ChestArmor>(*this);
+}
+
+String const& ChestArmor::bodyFrameset(Gender gender) const {
+ if (gender == Gender::Male)
+ return m_maleBodyImage;
+ else
+ return m_femaleBodyImage;
+}
+
+String const& ChestArmor::frontSleeveFrameset(Gender gender) const {
+ if (gender == Gender::Male)
+ return m_maleFrontSleeveImage;
+ else
+ return m_femaleFrontSleeveImage;
+}
+
+String const& ChestArmor::backSleeveFrameset(Gender gender) const {
+ if (gender == Gender::Male)
+ return m_maleBackSleeveImage;
+ else
+ return m_femaleBackSleeveImage;
+}
+
+List<Drawable> ChestArmor::preview(PlayerPtr const& viewer) const {
+ Gender gender = viewer ? viewer->gender() : Gender::Male;
+ return Humanoid::renderDummy(gender, nullptr, this);
+}
+
+LegsArmor::LegsArmor(Json const& config, String const& directory, Json const& data)
+ : ArmorItem(config, directory, data) {
+ m_maleImage = AssetPath::relativeTo(directory, config.getString("maleFrames"));
+ m_femaleImage = AssetPath::relativeTo(directory, config.getString("femaleFrames"));
+}
+
+ItemPtr LegsArmor::clone() const {
+ return make_shared<LegsArmor>(*this);
+}
+
+String const& LegsArmor::frameset(Gender gender) const {
+ if (gender == Gender::Male)
+ return m_maleImage;
+ else
+ return m_femaleImage;
+}
+
+List<Drawable> LegsArmor::preview(PlayerPtr const& viewer) const {
+ Gender gender = viewer ? viewer->gender() : Gender::Male;
+ return Humanoid::renderDummy(gender, nullptr, nullptr, this);
+}
+
+BackArmor::BackArmor(Json const& config, String const& directory, Json const& data)
+ : ArmorItem(config, directory, data) {
+ m_maleImage = AssetPath::relativeTo(directory, config.getString("maleFrames"));
+ m_femaleImage = AssetPath::relativeTo(directory, config.getString("femaleFrames"));
+}
+
+ItemPtr BackArmor::clone() const {
+ return make_shared<BackArmor>(*this);
+}
+
+String const& BackArmor::frameset(Gender gender) const {
+ if (gender == Gender::Male)
+ return m_maleImage;
+ else
+ return m_femaleImage;
+}
+
+List<Drawable> BackArmor::preview(PlayerPtr const& viewer) const {
+ Gender gender = viewer ? viewer->gender() : Gender::Male;
+ return Humanoid::renderDummy(gender, nullptr, nullptr, nullptr, this);
+}
+
+}
diff --git a/source/game/items/StarArmors.hpp b/source/game/items/StarArmors.hpp
new file mode 100644
index 0000000..9438259
--- /dev/null
+++ b/source/game/items/StarArmors.hpp
@@ -0,0 +1,127 @@
+#ifndef STAR_ARMORS_HPP
+#define STAR_ARMORS_HPP
+
+#include "StarGameTypes.hpp"
+#include "StarItem.hpp"
+#include "StarStatusEffectItem.hpp"
+#include "StarEffectSourceItem.hpp"
+#include "StarPreviewableItem.hpp"
+
+namespace Star {
+
+STAR_CLASS(ArmorItem);
+STAR_CLASS(HeadArmor);
+STAR_CLASS(ChestArmor);
+STAR_CLASS(LegsArmor);
+STAR_CLASS(BackArmor);
+
+class ArmorItem : public Item, public StatusEffectItem, public EffectSourceItem {
+public:
+ ArmorItem(Json const& config, String const& directory, Json const& data);
+ virtual ~ArmorItem() {}
+
+ virtual List<PersistentStatusEffect> statusEffects() const override;
+ virtual StringSet effectSources() const override;
+
+ List<String> const& colorOptions();
+
+ String const& directives() const;
+
+ bool hideBody() const;
+
+ Maybe<String> const& techModule() const;
+
+private:
+ void refreshIconDrawables();
+ void refreshStatusEffects();
+
+ List<String> m_colorOptions;
+ List<PersistentStatusEffect> m_statusEffects;
+ StringSet m_effectSources;
+ String m_directives;
+ bool m_hideBody;
+ Maybe<String> m_techModule;
+};
+
+class HeadArmor : public ArmorItem, public PreviewableItem {
+public:
+ HeadArmor(Json const& config, String const& directory, Json const& data);
+ virtual ~HeadArmor() {}
+
+ virtual ItemPtr clone() const;
+
+ String const& frameset(Gender gender) const;
+ String const& maskDirectives() const;
+
+ virtual List<Drawable> preview(PlayerPtr const& viewer = {}) const;
+
+private:
+ String m_maleImage;
+ String m_femaleImage;
+ String m_maskDirectives;
+};
+
+class ChestArmor : public ArmorItem, public PreviewableItem {
+public:
+ ChestArmor(Json const& config, String const& directory, Json const& data);
+ virtual ~ChestArmor() {}
+
+ virtual ItemPtr clone() const;
+
+ // Will have :run, :normal, :duck, and :portrait
+ String const& bodyFrameset(Gender gender) const;
+ // Will have :idle[1-5], :duck, :rotation, :walk[1-5], :run[1-5], :jump[1-4],
+ // :fall[1-4]
+ String const& frontSleeveFrameset(Gender gender) const;
+ // Same as FSleeve
+ String const& backSleeveFrameset(Gender gender) const;
+
+ virtual List<Drawable> preview(PlayerPtr const& viewer = {}) const;
+
+private:
+ String m_maleBodyImage;
+ String m_maleFrontSleeveImage;
+ String m_maleBackSleeveImage;
+
+ String m_femaleBodyImage;
+ String m_femaleFrontSleeveImage;
+ String m_femaleBackSleeveImage;
+};
+
+class LegsArmor : public ArmorItem, public PreviewableItem {
+public:
+ LegsArmor(Json const& config, String const& directory, Json const& data);
+ virtual ~LegsArmor() {}
+
+ virtual ItemPtr clone() const;
+
+ // Will have :idle, :duck, :walk[1-8], :run[1-8], :jump[1-4], :fall[1-4]
+ String const& frameset(Gender gender) const;
+
+ virtual List<Drawable> preview(PlayerPtr const& viewer = {}) const;
+
+private:
+ String m_maleImage;
+ String m_femaleImage;
+};
+
+class BackArmor : public ArmorItem, public PreviewableItem {
+public:
+ BackArmor(Json const& config, String const& directory, Json const& data);
+ virtual ~BackArmor() {}
+
+ virtual ItemPtr clone() const;
+
+ // Will have :idle, :duck, :walk[1-8], :run[1-8], :jump[1-4], :fall[1-4]
+ String const& frameset(Gender gender) const;
+
+ virtual List<Drawable> preview(PlayerPtr const& viewer = {}) const;
+
+private:
+ String m_maleImage;
+ String m_femaleImage;
+};
+
+}
+
+#endif
diff --git a/source/game/items/StarAugmentItem.cpp b/source/game/items/StarAugmentItem.cpp
new file mode 100644
index 0000000..d34121a
--- /dev/null
+++ b/source/game/items/StarAugmentItem.cpp
@@ -0,0 +1,29 @@
+#include "StarAugmentItem.hpp"
+#include "StarRoot.hpp"
+#include "StarAssets.hpp"
+#include "StarItemDatabase.hpp"
+#include "StarLuaComponents.hpp"
+#include "StarItemLuaBindings.hpp"
+#include "StarConfigLuaBindings.hpp"
+#include "StarJsonExtra.hpp"
+
+namespace Star {
+
+AugmentItem::AugmentItem(Json const& config, String const& directory, Json const& parameters)
+ : Item(config, directory, parameters) {}
+
+AugmentItem::AugmentItem(AugmentItem const& rhs) : AugmentItem(rhs.config(), rhs.directory(), rhs.parameters()) {}
+
+ItemPtr AugmentItem::clone() const {
+ return make_shared<AugmentItem>(*this);
+}
+
+StringList AugmentItem::augmentScripts() const {
+ return jsonToStringList(instanceValue("scripts")).transformed(bind(&AssetPath::relativeTo, directory(), _1));
+}
+
+ItemPtr AugmentItem::applyTo(ItemPtr const item) {
+ return Root::singleton().itemDatabase()->applyAugment(item, this);
+}
+
+}
diff --git a/source/game/items/StarAugmentItem.hpp b/source/game/items/StarAugmentItem.hpp
new file mode 100644
index 0000000..f50f092
--- /dev/null
+++ b/source/game/items/StarAugmentItem.hpp
@@ -0,0 +1,27 @@
+#ifndef STAR_AUGMENT_ITEM_HPP
+#define STAR_AUGMENT_ITEM_HPP
+
+#include "StarItem.hpp"
+
+namespace Star {
+
+STAR_CLASS(AugmentItem);
+
+class AugmentItem : public Item {
+public:
+ AugmentItem(Json const& config, String const& directory, Json const& parameters = JsonObject());
+ AugmentItem(AugmentItem const& rhs);
+
+ ItemPtr clone() const override;
+
+ StringList augmentScripts() const;
+
+ // Makes no change to the given item if the augment can't be applied.
+ // Consumes itself and returns true if the augment is applied.
+ // Has no effect if augmentation fails.
+ ItemPtr applyTo(ItemPtr const item);
+};
+
+}
+
+#endif
diff --git a/source/game/items/StarBlueprintItem.cpp b/source/game/items/StarBlueprintItem.cpp
new file mode 100644
index 0000000..7a17d24
--- /dev/null
+++ b/source/game/items/StarBlueprintItem.cpp
@@ -0,0 +1,54 @@
+#include "StarBlueprintItem.hpp"
+#include "StarJsonExtra.hpp"
+#include "StarRoot.hpp"
+#include "StarPlayer.hpp"
+#include "StarAssets.hpp"
+#include "StarPlayerBlueprints.hpp"
+
+namespace Star {
+
+BlueprintItem::BlueprintItem(Json const& config, String const& directory, Json const& data)
+ : Item(config, directory, data), SwingableItem(config) {
+ setWindupTime(0.2f);
+ setCooldownTime(0.1f);
+ setMaxStack(1);
+ m_requireEdgeTrigger = true;
+ m_recipe = ItemDescriptor(instanceValue("recipe"));
+
+ m_recipeIconUnderlay = Drawable(Root::singleton().assets()->json("/blueprint.config:iconUnderlay"));
+ m_inHandDrawable = {Drawable::makeImage(
+ Root::singleton().assets()->json("/blueprint.config:inHandImage").toString(),
+ 1.0f / TilePixels,
+ true,
+ Vec2F())};
+
+ setPrice(int(price() * Root::singleton().assets()->json("/items/defaultParameters.config:blueprintPriceFactor").toFloat()));
+}
+
+ItemPtr BlueprintItem::clone() const {
+ return make_shared<BlueprintItem>(*this);
+}
+
+List<Drawable> BlueprintItem::drawables() const {
+ return m_inHandDrawable;
+}
+
+void BlueprintItem::fireTriggered() {
+ if (count())
+ if (auto player = as<Player>(owner()))
+ if (player->addBlueprint(m_recipe, true))
+ setCount(count() - 1);
+}
+
+List<Drawable> BlueprintItem::iconDrawables() const {
+ List<Drawable> result;
+ result.append(m_recipeIconUnderlay);
+ result.appendAll(Item::iconDrawables());
+ return result;
+}
+
+List<Drawable> BlueprintItem::dropDrawables() const {
+ return m_inHandDrawable;
+}
+
+}
diff --git a/source/game/items/StarBlueprintItem.hpp b/source/game/items/StarBlueprintItem.hpp
new file mode 100644
index 0000000..995e6da
--- /dev/null
+++ b/source/game/items/StarBlueprintItem.hpp
@@ -0,0 +1,32 @@
+#ifndef STAR_BLUEPRINT_ITEM_HPP
+#define STAR_BLUEPRINT_ITEM_HPP
+
+#include "StarItem.hpp"
+#include "StarWorld.hpp"
+#include "StarSwingableItem.hpp"
+
+namespace Star {
+
+STAR_CLASS(BlueprintItem);
+
+class BlueprintItem : public Item, public SwingableItem {
+public:
+ BlueprintItem(Json const& config, String const& directory, Json const& data);
+ virtual ItemPtr clone() const override;
+
+ virtual List<Drawable> drawables() const override;
+
+ virtual void fireTriggered() override;
+
+ virtual List<Drawable> iconDrawables() const override;
+ virtual List<Drawable> dropDrawables() const override;
+
+private:
+ ItemDescriptor m_recipe;
+ Drawable m_recipeIconUnderlay;
+ List<Drawable> m_inHandDrawable;
+};
+
+}
+
+#endif
diff --git a/source/game/items/StarCodexItem.cpp b/source/game/items/StarCodexItem.cpp
new file mode 100644
index 0000000..aaaca3b
--- /dev/null
+++ b/source/game/items/StarCodexItem.cpp
@@ -0,0 +1,49 @@
+#include "StarCodexItem.hpp"
+#include "StarRoot.hpp"
+#include "StarJsonExtra.hpp"
+#include "StarPlayer.hpp"
+#include "StarAssets.hpp"
+#include "StarClientContext.hpp"
+#include "StarCodex.hpp"
+
+namespace Star {
+
+CodexItem::CodexItem(Json const& config, String const& directory, Json const& data)
+ : Item(config, directory, data), SwingableItem(config) {
+ setWindupTime(0.2f);
+ setCooldownTime(0.5f);
+ m_requireEdgeTrigger = true;
+ m_codexId = instanceValue("codexId").toString();
+ String iconPath = instanceValue("codexIcon").toString();
+ m_iconDrawables = {Drawable::makeImage(iconPath, 1.0f, true, Vec2F())};
+ m_worldDrawables = {Drawable::makeImage(iconPath, 1.0f / TilePixels, true, Vec2F())};
+}
+
+ItemPtr CodexItem::clone() const {
+ return make_shared<CodexItem>(*this);
+}
+
+List<Drawable> CodexItem::drawables() const {
+ return m_worldDrawables;
+}
+
+void CodexItem::fireTriggered() {
+ if (auto player = as<Player>(owner())) {
+ auto codexLearned = player->codexes()->learnCodex(m_codexId);
+ if (codexLearned) {
+ player->queueUIMessage(Root::singleton().assets()->json("/codex.config:messages.learned").toString());
+ } else {
+ player->queueUIMessage(Root::singleton().assets()->json("/codex.config:messages.alreadyKnown").toString());
+ }
+ }
+}
+
+List<Drawable> CodexItem::iconDrawables() const {
+ return m_iconDrawables;
+}
+
+List<Drawable> CodexItem::dropDrawables() const {
+ return m_worldDrawables;
+}
+
+}
diff --git a/source/game/items/StarCodexItem.hpp b/source/game/items/StarCodexItem.hpp
new file mode 100644
index 0000000..b1c825d
--- /dev/null
+++ b/source/game/items/StarCodexItem.hpp
@@ -0,0 +1,30 @@
+#ifndef STAR_CODEX_ITEM_HPP
+#define STAR_CODEX_ITEM_HPP
+
+#include "StarItem.hpp"
+#include "StarPlayerCodexes.hpp"
+#include "StarSwingableItem.hpp"
+
+namespace Star {
+
+class CodexItem : public Item, public SwingableItem {
+public:
+ CodexItem(Json const& config, String const& directory, Json const& data);
+ virtual ItemPtr clone() const override;
+
+ virtual List<Drawable> drawables() const override;
+
+ virtual void fireTriggered() override;
+
+ virtual List<Drawable> iconDrawables() const override;
+ virtual List<Drawable> dropDrawables() const override;
+
+private:
+ String m_codexId;
+ List<Drawable> m_iconDrawables;
+ List<Drawable> m_worldDrawables;
+};
+
+}
+
+#endif
diff --git a/source/game/items/StarConsumableItem.cpp b/source/game/items/StarConsumableItem.cpp
new file mode 100644
index 0000000..53729b1
--- /dev/null
+++ b/source/game/items/StarConsumableItem.cpp
@@ -0,0 +1,110 @@
+#include "StarConsumableItem.hpp"
+#include "StarRoot.hpp"
+#include "StarJsonExtra.hpp"
+#include "StarRandom.hpp"
+#include "StarStatusController.hpp"
+
+namespace Star {
+
+ConsumableItem::ConsumableItem(Json const& config, String const& directory, Json const& data)
+ : Item(config, directory, data), SwingableItem(config) {
+ setWindupTime(0);
+ setCooldownTime(0.25f);
+ m_requireEdgeTrigger = true;
+ m_swingStart = config.getFloat("swingStart", -60) * Constants::pi / 180;
+ m_swingFinish = config.getFloat("swingFinish", 40) * Constants::pi / 180;
+ m_swingAimFactor = config.getFloat("swingAimFactor", 0.2f);
+ m_blockingEffects = jsonToStringSet(instanceValue("blockingEffects", JsonArray()));
+ if (auto foodValue = instanceValue("foodValue")) {
+ m_foodValue = foodValue.toFloat();
+ m_blockingEffects.add("wellfed");
+ }
+ m_emitters = jsonToStringSet(instanceValue("emitters", JsonArray{"eating"}));
+ m_emote = instanceValue("emote", "eat").toString();
+ m_consuming = false;
+}
+
+ItemPtr ConsumableItem::clone() const {
+ return make_shared<ConsumableItem>(*this);
+}
+
+List<Drawable> ConsumableItem::drawables() const {
+ auto drawables = iconDrawables();
+ Drawable::scaleAll(drawables, 1.0f / TilePixels);
+ Drawable::translateAll(drawables, -handPosition() / TilePixels);
+ return drawables;
+}
+
+void ConsumableItem::update(FireMode fireMode, bool shifting, HashSet<MoveControlType> const& moves) {
+ SwingableItem::update(fireMode, shifting, moves);
+
+ if (entityMode() == EntityMode::Master) {
+ if (m_consuming)
+ owner()->addEffectEmitters(m_emitters);
+ if (ready())
+ maybeConsume();
+ }
+}
+
+void ConsumableItem::fire(FireMode mode, bool shifting, bool edgeTriggered) {
+ if (canUse())
+ FireableItem::fire(mode, shifting, edgeTriggered);
+}
+
+void ConsumableItem::fireTriggered() {
+ if (canUse()) {
+ triggerEffects();
+ FireableItem::fireTriggered();
+ }
+}
+
+void ConsumableItem::uninit() {
+ maybeConsume();
+ FireableItem::uninit();
+}
+
+bool ConsumableItem::canUse() const {
+ if (!count() || m_consuming)
+ return false;
+
+ for (auto pair : owner()->statusController()->activeUniqueStatusEffectSummary()) {
+ if (m_blockingEffects.contains(pair.first))
+ return false;
+ }
+ return true;
+}
+
+void ConsumableItem::triggerEffects() {
+ auto options = instanceValue("effects", JsonArray()).toArray();
+ if (options.size()) {
+ auto option = Random::randFrom(options).toArray().transformed(jsonToEphemeralStatusEffect);
+ owner()->statusController()->addEphemeralEffects(option);
+ }
+
+ if (m_foodValue) {
+ owner()->statusController()->giveResource("food", *m_foodValue);
+ if (owner()->statusController()->resourcePercentage("food") == 1.0f)
+ owner()->statusController()->addEphemeralEffect(EphemeralStatusEffect{UniqueStatusEffect("wellfed"), {}});
+ }
+
+ if (!m_emote.empty())
+ owner()->requestEmote(m_emote);
+
+ m_consuming = true;
+}
+
+void ConsumableItem::maybeConsume() {
+ if (m_consuming) {
+ m_consuming = false;
+
+ world()->sendEntityMessage(owner()->entityId(), "recordEvent", {"useItem", JsonObject {
+ {"itemType", name()}
+ }});
+ if (count())
+ setCount(count() - 1);
+ else
+ setCount(0);
+ }
+}
+
+}
diff --git a/source/game/items/StarConsumableItem.hpp b/source/game/items/StarConsumableItem.hpp
new file mode 100644
index 0000000..0596b61
--- /dev/null
+++ b/source/game/items/StarConsumableItem.hpp
@@ -0,0 +1,38 @@
+#ifndef STAR_CONSUMABLE_ITEM_HPP
+#define STAR_CONSUMABLE_ITEM_HPP
+
+#include "StarItem.hpp"
+#include "StarGameTypes.hpp"
+#include "StarSwingableItem.hpp"
+
+namespace Star {
+
+class ConsumableItem : public Item, public SwingableItem {
+public:
+ ConsumableItem(Json const& config, String const& directory, Json const& data);
+
+ ItemPtr clone() const override;
+
+ List<Drawable> drawables() const override;
+
+ void update(FireMode fireMode, bool shifting, HashSet<MoveControlType> const& moves) override;
+ void fire(FireMode mode, bool shifting, bool edgeTriggered) override;
+ void fireTriggered() override;
+ void uninit() override;
+
+private:
+ bool canUse() const;
+
+ void triggerEffects();
+ void maybeConsume();
+
+ StringSet m_blockingEffects;
+ Maybe<float> m_foodValue;
+ StringSet m_emitters;
+ String m_emote;
+ bool m_consuming;
+};
+
+}
+
+#endif
diff --git a/source/game/items/StarCurrency.cpp b/source/game/items/StarCurrency.cpp
new file mode 100644
index 0000000..c627cdc
--- /dev/null
+++ b/source/game/items/StarCurrency.cpp
@@ -0,0 +1,42 @@
+#include "StarCurrency.hpp"
+#include "StarRandom.hpp"
+#include "StarJsonExtra.hpp"
+
+namespace Star {
+
+CurrencyItem::CurrencyItem(Json const& config, String const& directory) : Item(config, directory) {
+ m_currency = config.getString("currency");
+ m_value = config.getUInt("value");
+}
+
+ItemPtr CurrencyItem::clone() const {
+ return make_shared<CurrencyItem>(*this);
+}
+
+String CurrencyItem::pickupSound() const {
+ if (count() <= instanceValue("smallStackLimit", 100).toUInt()) {
+ if (!instanceValue("pickupSoundsSmall", {}).isNull())
+ return Random::randFrom(jsonToStringSet(instanceValue("pickupSoundsSmall")));
+ } else if (count() <= instanceValue("mediumStackLimit", 10000).toUInt()) {
+ if (!instanceValue("pickupSoundsMedium", {}).isNull())
+ return Random::randFrom(jsonToStringSet(instanceValue("pickupSoundsMedium")));
+ } else {
+ if (!instanceValue("pickupSoundsLarge", {}).isNull())
+ return Random::randFrom(jsonToStringSet(instanceValue("pickupSoundsLarge")));
+ }
+ return Item::pickupSound();
+}
+
+String CurrencyItem::currencyType() {
+ return m_currency;
+}
+
+uint64_t CurrencyItem::currencyValue() {
+ return m_value;
+}
+
+uint64_t CurrencyItem::totalValue() {
+ return m_value * count();
+}
+
+}
diff --git a/source/game/items/StarCurrency.hpp b/source/game/items/StarCurrency.hpp
new file mode 100644
index 0000000..860cdaf
--- /dev/null
+++ b/source/game/items/StarCurrency.hpp
@@ -0,0 +1,33 @@
+#ifndef STAR_CURRENCY_HPP
+#define STAR_CURRENCY_HPP
+
+#include "StarItem.hpp"
+
+namespace Star {
+
+STAR_CLASS(CurrencyItem);
+
+class CurrencyItem : public Item {
+public:
+ CurrencyItem(Json const& config, String const& directory);
+
+ virtual ItemPtr clone() const override;
+
+ virtual String pickupSound() const override;
+
+ String currencyType();
+
+ // Value of a single instance of this currency
+ uint64_t currencyValue();
+
+ // Total value of all currencies (so currencyValue * count)
+ uint64_t totalValue();
+
+private:
+ String m_currency;
+ uint64_t m_value;
+};
+
+}
+
+#endif
diff --git a/source/game/items/StarInspectionTool.cpp b/source/game/items/StarInspectionTool.cpp
new file mode 100644
index 0000000..19e37d8
--- /dev/null
+++ b/source/game/items/StarInspectionTool.cpp
@@ -0,0 +1,173 @@
+#include "StarInspectionTool.hpp"
+#include "StarJsonExtra.hpp"
+#include "StarAssets.hpp"
+#include "StarMaterialDatabase.hpp"
+#include "StarLiquidsDatabase.hpp"
+
+namespace Star {
+
+InspectionTool::InspectionTool(Json const& config, String const& directory, Json const& parameters)
+ : Item(config, directory, parameters) {
+ m_image = AssetPath::relativeTo(directory, instanceValue("image").toString());
+ m_handPosition = jsonToVec2F(instanceValue("handPosition"));
+ m_lightPosition = jsonToVec2F(instanceValue("lightPosition"));
+ m_lightColor = jsonToColor(instanceValue("lightColor"));
+ m_beamWidth = instanceValue("beamLevel").toFloat();
+ m_ambientFactor = instanceValue("beamAmbience").toFloat();
+
+ m_showHighlights = instanceValue("showHighlights").toBool();
+ m_allowScanning = instanceValue("allowScanning").toBool();
+
+ m_inspectionAngles = jsonToVec2F(instanceValue("inspectionAngles"));
+ m_inspectionRanges = jsonToVec2F(instanceValue("inspectionRanges"));
+ m_ambientInspectionRadius = instanceValue("ambientInspectionRadius").toFloat();
+ m_fullInspectionSpaces = instanceValue("fullInspectionSpaces").toUInt();
+ m_minimumInspectionLevel = instanceValue("minimumInspectionLevel").toFloat();
+
+ m_lastFireMode = FireMode::None;
+}
+
+ItemPtr InspectionTool::clone() const {
+ return make_shared<InspectionTool>(*this);
+}
+
+void InspectionTool::update(FireMode fireMode, bool, HashSet<MoveControlType> const&) {
+ m_currentAngle = world()->geometry().diff(owner()->aimPosition(), owner()->position()).angle();
+ m_currentPosition = owner()->position() + owner()->handPosition(hand(), m_lightPosition - m_handPosition);
+ SpatialLogger::logPoint("world", m_currentPosition, {0, 0, 255, 255});
+
+ if (fireMode != m_lastFireMode) {
+ if (fireMode != FireMode::None)
+ m_inspectionResults.append(inspect(owner()->aimPosition()));
+ }
+
+ m_lastFireMode = fireMode;
+}
+
+List<Drawable> InspectionTool::drawables() const {
+ return {Drawable::makeImage(m_image, 1.0f / TilePixels, true, -m_handPosition)};
+}
+
+List<LightSource> InspectionTool::lightSources() const {
+ if (!initialized())
+ return {};
+
+ float angle = world()->geometry().diff(owner()->aimPosition(), owner()->position()).angle();
+ LightSource lightSource;
+ lightSource.pointLight = true;
+ lightSource.position = owner()->position() + owner()->handPosition(hand(), m_lightPosition - m_handPosition);
+ lightSource.color = m_lightColor.toRgb();
+ lightSource.pointBeam = m_beamWidth;
+ lightSource.beamAngle = angle;
+ lightSource.beamAmbience = m_ambientFactor;
+ return {move(lightSource)};
+}
+
+float InspectionTool::inspectionHighlightLevel(InspectableEntityPtr const& inspectable) const {
+ if (m_showHighlights)
+ return inspectionLevel(inspectable);
+ return 0;
+}
+
+List<InspectionTool::InspectionResult> InspectionTool::pullInspectionResults() {
+ return Star::take(m_inspectionResults);
+}
+
+float InspectionTool::inspectionLevel(InspectableEntityPtr const& inspectable) const {
+ if (!initialized() || !inspectable->inspectable())
+ return 0;
+
+ float totalLevel = 0;
+
+ // convert spaces to a set of world positions
+ Set<Vec2I> spaceSet;
+ for (auto space : inspectable->spaces())
+ spaceSet.add(inspectable->tilePosition() + space);
+
+ for (auto space : spaceSet) {
+ float pointLevel = pointInspectionLevel(centerOfTile(space));
+ if (pointLevel > 0 && hasLineOfSight(space, spaceSet))
+ totalLevel += pointLevel;
+ }
+ return clamp(totalLevel / min(spaceSet.size(), m_fullInspectionSpaces), 0.0f, 1.0f);
+}
+
+float InspectionTool::pointInspectionLevel(Vec2F const& position) const {
+ Vec2F gdiff = world()->geometry().diff(position, m_currentPosition);
+ float gdist = gdiff.magnitude();
+ float angleFactor = (abs(angleDiff(gdiff.angle(), m_currentAngle)) - m_inspectionAngles[0]) / (m_inspectionAngles[1] - m_inspectionAngles[0]);
+ float distFactor = (gdist - m_inspectionRanges[0]) / (m_inspectionRanges[1] - m_inspectionRanges[0]);
+ float ambientFactor = gdist / m_ambientInspectionRadius;
+ return 1 - clamp(max(distFactor, min(ambientFactor, angleFactor)), 0.0f, 1.0f);
+}
+
+bool InspectionTool::hasLineOfSight(Vec2I const& position, Set<Vec2I> const& targetSpaces) const {
+ auto collisions = world()->collidingTilesAlongLine(centerOfTile(m_currentPosition), centerOfTile(position));
+ for (auto collision : collisions) {
+ if (collision != position && !targetSpaces.contains(collision))
+ return false;
+ }
+ return true;
+}
+
+InspectionTool::InspectionResult InspectionTool::inspect(Vec2F const& position) {
+ auto assets = Root::singleton().assets();
+ auto species = owner()->species();
+
+ // if there's a candidate InspectableEntity at the position, make sure that entity's total inspection level
+ // is above the minimum threshold
+ for (auto entity : world()->atTile<InspectableEntity>(Vec2I::floor(position))) {
+ if (entity->inspectable() && inspectionLevel(entity) >= m_minimumInspectionLevel) {
+ if (m_allowScanning)
+ return {entity->inspectionDescription(species).value(), entity->inspectionLogName(), entity->entityId()};
+ else
+ return {entity->inspectionDescription(species).value(), {}, {}};
+ }
+ }
+
+ // check the inspection level at the selected tile
+ if (!hasLineOfSight(Vec2I::floor(position)) || pointInspectionLevel(centerOfTile(position)) < m_minimumInspectionLevel)
+ return {inspectionFailureText("outOfRangeText", species), {}};
+
+ // check the tile for foreground mod or material
+ MaterialId fgMaterial = world()->material(Vec2I::floor(position), TileLayer::Foreground);
+ MaterialId fgMod = world()->mod(Vec2I(position.floor()), TileLayer::Foreground);
+ auto materialDatabase = Root::singleton().materialDatabase();
+ if (isRealMaterial(fgMaterial)) {
+ if (isRealMod(fgMod))
+ return {materialDatabase->modDescription(fgMod, species), {}};
+ else
+ return {materialDatabase->materialDescription(fgMaterial, species), {}};
+ }
+
+ // check for liquid at the tile
+ auto liquidLevel = world()->liquidLevel(Vec2I::floor(position));
+ auto liquidsDatabase = Root::singleton().liquidsDatabase();
+ if (liquidLevel.liquid != EmptyLiquidId)
+ return {liquidsDatabase->liquidDescription(liquidLevel.liquid), {}};
+
+ // check the tile for background mod or material
+ MaterialId bgMaterial = world()->material(Vec2I::floor(position), TileLayer::Background);
+ MaterialId bgMod = world()->mod(Vec2I(position.floor()), TileLayer::Background);
+ if (isRealMaterial(bgMaterial)) {
+ if (isRealMod(bgMod))
+ return {materialDatabase->modDescription(bgMod, species), {}};
+ else
+ return {materialDatabase->materialDescription(bgMaterial, species), {}};
+ }
+
+ // at this point you're just staring into the void
+ return {inspectionFailureText("nothingThereText", species), {}};
+}
+
+String InspectionTool::inspectionFailureText(String const& failureType, String const& species) const {
+ JsonArray textOptions;
+ Json nothingThere = instanceValue(failureType);
+ if (nothingThere.contains(species))
+ textOptions = nothingThere.getArray(species);
+ else
+ textOptions = nothingThere.getArray("default");
+ return textOptions.wrap(Random::randu64()).toString();
+}
+
+}
diff --git a/source/game/items/StarInspectionTool.hpp b/source/game/items/StarInspectionTool.hpp
new file mode 100644
index 0000000..d80e0a3
--- /dev/null
+++ b/source/game/items/StarInspectionTool.hpp
@@ -0,0 +1,74 @@
+#ifndef STAR_INSPECTION_TOOL_HPP
+#define STAR_INSPECTION_TOOL_HPP
+
+#include "StarItem.hpp"
+#include "StarPointableItem.hpp"
+#include "StarToolUserItem.hpp"
+#include "StarEntityRendering.hpp"
+#include "StarInspectableEntity.hpp"
+
+namespace Star {
+
+STAR_CLASS(InspectionTool);
+
+class InspectionTool
+ : public Item,
+ public PointableItem,
+ public ToolUserItem {
+public:
+
+ struct InspectionResult {
+ String message;
+ Maybe<String> objectName;
+ Maybe<EntityId> entityId;
+ };
+
+ InspectionTool(Json const& config, String const& directory, Json const& parameters = JsonObject());
+
+ ItemPtr clone() const override;
+
+ void update(FireMode fireMode, bool shifting, HashSet<MoveControlType> const& moves) override;
+
+ List<Drawable> drawables() const override;
+
+ List<LightSource> lightSources() const;
+
+ float inspectionHighlightLevel(InspectableEntityPtr const& inspectableEntity) const;
+
+ List<InspectionResult> pullInspectionResults();
+
+private:
+ InspectionResult inspect(Vec2F const& position);
+
+ float inspectionLevel(InspectableEntityPtr const& inspectableEntity) const;
+ float pointInspectionLevel(Vec2F const& position) const;
+ bool hasLineOfSight(Vec2I const& targetPosition, Set<Vec2I> const& targetSpaces = {}) const;
+
+ String inspectionFailureText(String const& failureType, String const& species) const;
+
+ float m_currentAngle;
+ Vec2F m_currentPosition;
+
+ String m_image;
+ Vec2F m_handPosition;
+ Vec2F m_lightPosition;
+ Color m_lightColor;
+ float m_beamWidth;
+ float m_ambientFactor;
+
+ bool m_showHighlights;
+ bool m_allowScanning;
+
+ Vec2F m_inspectionAngles;
+ Vec2F m_inspectionRanges;
+ float m_ambientInspectionRadius;
+ size_t m_fullInspectionSpaces;
+ float m_minimumInspectionLevel;
+
+ FireMode m_lastFireMode;
+ List<InspectionResult> m_inspectionResults;
+};
+
+}
+
+#endif
diff --git a/source/game/items/StarInstrumentItem.cpp b/source/game/items/StarInstrumentItem.cpp
new file mode 100644
index 0000000..159212a
--- /dev/null
+++ b/source/game/items/StarInstrumentItem.cpp
@@ -0,0 +1,88 @@
+#include "StarInstrumentItem.hpp"
+#include "StarJsonExtra.hpp"
+#include "StarRoot.hpp"
+#include "StarAssets.hpp"
+
+namespace Star {
+
+InstrumentItem::InstrumentItem(Json const& config, String const& directory, Json const& data) : Item(config, directory, data) {
+ m_activeCooldown = 0;
+
+ auto image = AssetPath::relativeTo(directory, instanceValue("image").toString());
+ Vec2F position = jsonToVec2F(instanceValue("handPosition", JsonArray{0, 0}));
+ m_drawables.append(Drawable::makeImage(image, 1.0f / TilePixels, true, position));
+
+ image = AssetPath::relativeTo(directory, instanceValue("activeImage").toString());
+ position = jsonToVec2F(instanceValue("activeHandPosition", JsonArray{0, 0}));
+ m_activeDrawables.append(Drawable::makeImage(image, 1.0f / TilePixels, true, position));
+
+ m_activeAngle = (instanceValue("activeAngle").toFloat() / 180.0f) * Constants::pi;
+
+ m_activeStatusEffects = instanceValue("activeStatusEffects", JsonArray()).toArray().transformed(jsonToPersistentStatusEffect);
+ m_inactiveStatusEffects = instanceValue("inactiveStatusEffects", JsonArray()).toArray().transformed(jsonToPersistentStatusEffect);
+ m_activeEffectSources = jsonToStringSet(instanceValue("activeEffectSources", JsonArray()));
+ m_inactiveEffectSources = jsonToStringSet(instanceValue("inactiveEffectSources", JsonArray()));
+
+ m_kind = instanceValue("kind").toString();
+}
+
+ItemPtr InstrumentItem::clone() const {
+ return make_shared<InstrumentItem>(*this);
+}
+
+List<PersistentStatusEffect> InstrumentItem::statusEffects() const {
+ if (active())
+ return m_activeStatusEffects;
+ return m_inactiveStatusEffects;
+}
+
+StringSet InstrumentItem::effectSources() const {
+ if (active())
+ return m_activeEffectSources;
+ return m_inactiveEffectSources;
+}
+
+void InstrumentItem::update(FireMode, bool, HashSet<MoveControlType> const&) {
+ if (entityMode() == EntityMode::Master) {
+ if (active()) {
+ m_activeCooldown--;
+ owner()->addEffectEmitters({"music"});
+ }
+ }
+ owner()->instrumentEquipped(m_kind);
+}
+
+bool InstrumentItem::active() const {
+ if (!initialized())
+ return false;
+ return (m_activeCooldown > 0) || owner()->instrumentPlaying();
+}
+
+void InstrumentItem::setActive(bool active) {
+ if (active)
+ m_activeCooldown = 3;
+ else
+ m_activeCooldown = 0;
+}
+
+bool InstrumentItem::usable() const {
+ return true;
+}
+
+void InstrumentItem::activate() {
+ owner()->interact(InteractAction{InteractActionType::OpenSongbookInterface, owner()->entityId(), {}});
+}
+
+List<Drawable> InstrumentItem::drawables() const {
+ if (active())
+ return m_activeDrawables;
+ return m_drawables;
+}
+
+float InstrumentItem::getAngle(float angle) {
+ if (active())
+ return m_activeAngle;
+ return angle;
+}
+
+}
diff --git a/source/game/items/StarInstrumentItem.hpp b/source/game/items/StarInstrumentItem.hpp
new file mode 100644
index 0000000..861f681
--- /dev/null
+++ b/source/game/items/StarInstrumentItem.hpp
@@ -0,0 +1,57 @@
+#ifndef STAR_INSTRUMENT_ITEM_HPP
+#define STAR_INSTRUMENT_ITEM_HPP
+
+#include "StarItem.hpp"
+#include "StarInstrumentItem.hpp"
+#include "StarStatusEffectItem.hpp"
+#include "StarEffectSourceItem.hpp"
+#include "StarToolUserItem.hpp"
+#include "StarActivatableItem.hpp"
+#include "StarPointableItem.hpp"
+
+namespace Star {
+
+STAR_CLASS(World);
+STAR_CLASS(ToolUserEntity);
+STAR_CLASS(InstrumentItem);
+
+class InstrumentItem : public Item,
+ public StatusEffectItem,
+ public EffectSourceItem,
+ public ToolUserItem,
+ public ActivatableItem,
+ public PointableItem {
+public:
+ InstrumentItem(Json const& config, String const& directory, Json const& data);
+
+ ItemPtr clone() const override;
+
+ List<PersistentStatusEffect> statusEffects() const override;
+ StringSet effectSources() const override;
+
+ void update(FireMode fireMode, bool shifting, HashSet<MoveControlType> const& moves) override;
+
+ bool active() const override;
+ void setActive(bool active) override;
+ bool usable() const override;
+ void activate() override;
+
+ List<Drawable> drawables() const override;
+ float getAngle(float angle) override;
+
+private:
+ List<PersistentStatusEffect> m_activeStatusEffects;
+ List<PersistentStatusEffect> m_inactiveStatusEffects;
+ StringSet m_activeEffectSources;
+ StringSet m_inactiveEffectSources;
+ List<Drawable> m_drawables;
+ List<Drawable> m_activeDrawables;
+ int m_activeCooldown;
+
+ float m_activeAngle;
+ String m_kind;
+};
+
+}
+
+#endif
diff --git a/source/game/items/StarLiquidItem.cpp b/source/game/items/StarLiquidItem.cpp
new file mode 100644
index 0000000..c29f58e
--- /dev/null
+++ b/source/game/items/StarLiquidItem.cpp
@@ -0,0 +1,151 @@
+#include "StarLiquidItem.hpp"
+#include "StarJson.hpp"
+#include "StarLiquidsDatabase.hpp"
+#include "StarRoot.hpp"
+#include "StarAssets.hpp"
+#include "StarWorld.hpp"
+
+namespace Star {
+
+LiquidItem::LiquidItem(Json const& config, String const& directory, Json const& settings)
+ : Item(config, directory, settings), FireableItem(config), BeamItem(config) {
+ m_liquidId = Root::singleton().liquidsDatabase()->liquidId(config.getString("liquid"));
+
+ setTwoHanded(config.getBool("twoHanded", true));
+
+ auto assets = Root::singleton().assets();
+ m_quantity = assets->json("/items/defaultParameters.config:liquidItems.bucketSize").toUInt();
+ setCooldownTime(assets->json("/items/defaultParameters.config:liquidItems.cooldown").toFloat());
+ m_blockRadius = assets->json("/items/defaultParameters.config:blockRadius").toFloat();
+ m_altBlockRadius = assets->json("/items/defaultParameters.config:altBlockRadius").toFloat();
+ m_shifting = false;
+}
+
+ItemPtr LiquidItem::clone() const {
+ return make_shared<LiquidItem>(*this);
+}
+
+void LiquidItem::init(ToolUserEntity* owner, ToolHand hand) {
+ FireableItem::init(owner, hand);
+ BeamItem::init(owner, hand);
+}
+
+void LiquidItem::update(FireMode fireMode, bool shifting, HashSet<MoveControlType> const& moves) {
+ FireableItem::update(fireMode, shifting, moves);
+ BeamItem::update(fireMode, shifting, moves);
+ if (shifting || !multiplaceEnabled())
+ setEnd(BeamItem::EndType::Tile);
+ else
+ setEnd(BeamItem::EndType::TileGroup);
+
+ m_shifting = shifting;
+}
+
+List<Drawable> LiquidItem::nonRotatedDrawables() const {
+ return beamDrawables(canPlace(m_shifting));
+}
+
+void LiquidItem::fire(FireMode mode, bool shifting, bool edgeTriggered) {
+ if (!initialized() || !ready() || !owner()->inToolRange())
+ return;
+
+ PlaceLiquid placeLiquid{liquidId(), liquidQuantity()};
+ TileModificationList modifications;
+
+ float radius;
+
+ if (!shifting)
+ radius = m_blockRadius;
+ else
+ radius = m_altBlockRadius;
+
+ if (!multiplaceEnabled())
+ radius = 1;
+
+ for (auto pos : tileAreaBrush(radius, owner()->aimPosition(), true)) {
+ if (canPlaceAtTile(pos))
+ modifications.append({pos, placeLiquid});
+ }
+
+ // Make sure not to make any more modifications than we have consumables.
+ if (modifications.size() > count())
+ modifications.resize(count());
+ size_t failed = world()->applyTileModifications(modifications, false).size();
+ if (failed < modifications.size()) {
+ FireableItem::fire(mode, shifting, edgeTriggered);
+ consume(modifications.size() - failed);
+ }
+}
+
+LiquidId LiquidItem::liquidId() const {
+ return m_liquidId;
+}
+
+float LiquidItem::liquidQuantity() const {
+ return m_quantity;
+}
+
+List<PreviewTile> LiquidItem::preview(bool shifting) const {
+ List<PreviewTile> result;
+ if (initialized()) {
+ auto liquid = liquidId();
+
+ float radius;
+ if (!shifting)
+ radius = m_blockRadius;
+ else
+ radius = m_altBlockRadius;
+
+ if (!multiplaceEnabled())
+ radius = 1;
+
+ size_t c = 0;
+
+ for (auto pos : tileAreaBrush(radius, owner()->aimPosition(), true)) {
+ if (c >= count())
+ break;
+ if (canPlaceAtTile(pos))
+ c++;
+ result.append({pos, liquid});
+ }
+ }
+ return result;
+}
+
+bool LiquidItem::canPlace(bool shifting) const {
+ if (initialized()) {
+ float radius;
+ if (!shifting)
+ radius = m_blockRadius;
+ else
+ radius = m_altBlockRadius;
+
+ if (!multiplaceEnabled())
+ radius = 1;
+
+ for (auto pos : tileAreaBrush(radius, owner()->aimPosition(), true)) {
+ if (canPlaceAtTile(pos))
+ return true;
+ }
+ }
+ return false;
+}
+
+bool LiquidItem::canPlaceAtTile(Vec2I pos) const {
+ auto bgTileMaterial = world()->material(pos, TileLayer::Background);
+ if (bgTileMaterial != EmptyMaterialId) {
+ auto fgTileMaterial = world()->material(pos, TileLayer::Foreground);
+ if (fgTileMaterial == EmptyMaterialId) {
+ auto tileLiquid = world()->liquidLevel(pos).liquid;
+ if (tileLiquid == EmptyLiquidId || tileLiquid == liquidId())
+ return true;
+ }
+ }
+ return false;
+}
+
+bool LiquidItem::multiplaceEnabled() const {
+ return (count() > 1);
+}
+
+}
diff --git a/source/game/items/StarLiquidItem.hpp b/source/game/items/StarLiquidItem.hpp
new file mode 100644
index 0000000..35178e8
--- /dev/null
+++ b/source/game/items/StarLiquidItem.hpp
@@ -0,0 +1,47 @@
+#ifndef STAR_LIQUID_ITEM_HPP
+#define STAR_LIQUID_ITEM_HPP
+
+#include "StarItem.hpp"
+#include "StarFireableItem.hpp"
+#include "StarBeamItem.hpp"
+#include "StarEntityRendering.hpp"
+
+namespace Star {
+
+STAR_CLASS(LiquidItem);
+
+class LiquidItem : public Item, public FireableItem, public BeamItem {
+public:
+ LiquidItem(Json const& config, String const& directory, Json const& settings);
+ virtual ~LiquidItem() {}
+
+ ItemPtr clone() const override;
+
+ void init(ToolUserEntity* owner, ToolHand hand) override;
+ void update(FireMode fireMode, bool shifting, HashSet<MoveControlType> const& moves) override;
+
+ List<Drawable> nonRotatedDrawables() const override;
+
+ void fire(FireMode mode, bool shifting, bool edgeTriggered) override;
+
+ LiquidId liquidId() const;
+ float liquidQuantity() const;
+
+ List<PreviewTile> preview(bool shifting) const;
+
+ bool canPlace(bool shifting) const;
+ bool canPlaceAtTile(Vec2I pos) const;
+ bool multiplaceEnabled() const;
+
+private:
+ LiquidId m_liquidId;
+ float m_quantity;
+
+ float m_blockRadius;
+ float m_altBlockRadius;
+ bool m_shifting;
+};
+
+}
+
+#endif
diff --git a/source/game/items/StarMaterialItem.cpp b/source/game/items/StarMaterialItem.cpp
new file mode 100644
index 0000000..9edd1fe
--- /dev/null
+++ b/source/game/items/StarMaterialItem.cpp
@@ -0,0 +1,175 @@
+#include "StarMaterialItem.hpp"
+#include "StarJson.hpp"
+#include "StarMaterialDatabase.hpp"
+#include "StarRoot.hpp"
+#include "StarAssets.hpp"
+#include "StarWorld.hpp"
+#include "StarWorldClient.hpp"
+#include "StarWorldTemplate.hpp"
+
+namespace Star {
+
+MaterialItem::MaterialItem(Json const& config, String const& directory, Json const& settings)
+ : Item(config, directory, settings), FireableItem(config), BeamItem(config) {
+ m_material = config.getInt("materialId");
+ m_materialHueShift = materialHueFromDegrees(instanceValue("materialHueShift", 0).toFloat());
+
+ if (materialHueShift() != MaterialHue()) {
+ auto drawables = iconDrawables();
+ for (auto& d : drawables) {
+ if (d.isImage())
+ d.imagePart().addDirectives(strf("?hueshift=%s", materialHueToDegrees(m_materialHueShift)), false);
+ }
+ setIconDrawables(move(drawables));
+ }
+
+ setTwoHanded(config.getBool("twoHanded", true));
+
+ setCooldownTime(Root::singleton().assets()->json("/items/defaultParameters.config:materialItems.cooldown").toFloat());
+ m_blockRadius = Root::singleton().assets()->json("/items/defaultParameters.config:blockRadius").toFloat();
+ m_altBlockRadius = Root::singleton().assets()->json("/items/defaultParameters.config:altBlockRadius").toFloat();
+ m_multiplace = config.getBool("allowMultiplace", BlockCollisionSet.contains(Root::singleton().materialDatabase()->materialCollisionKind(m_material)));
+
+ m_shifting = false;
+}
+
+ItemPtr MaterialItem::clone() const {
+ return make_shared<MaterialItem>(*this);
+}
+
+void MaterialItem::init(ToolUserEntity* owner, ToolHand hand) {
+ FireableItem::init(owner, hand);
+ BeamItem::init(owner, hand);
+}
+
+void MaterialItem::update(FireMode fireMode, bool shifting, HashSet<MoveControlType> const& moves) {
+ FireableItem::update(fireMode, shifting, moves);
+ BeamItem::update(fireMode, shifting, moves);
+ if (shifting || !multiplaceEnabled())
+ setEnd(BeamItem::EndType::Tile);
+ else
+ setEnd(BeamItem::EndType::TileGroup);
+ m_shifting = shifting;
+}
+
+List<Drawable> MaterialItem::nonRotatedDrawables() const {
+ return beamDrawables(canPlace(m_shifting));
+}
+
+void MaterialItem::fire(FireMode mode, bool shifting, bool edgeTriggered) {
+ if (!initialized() || !ready() || !owner()->inToolRange())
+ return;
+
+ auto layer = (mode == FireMode::Primary || !twoHanded() ? TileLayer::Foreground : TileLayer::Background);
+ TileModificationList modifications;
+
+ float radius;
+
+ if (!shifting)
+ radius = m_blockRadius;
+ else
+ radius = m_altBlockRadius;
+
+ if (!multiplaceEnabled())
+ radius = 1;
+
+ for (auto pos : tileAreaBrush(radius, owner()->aimPosition(), true))
+ modifications.append({pos, PlaceMaterial{layer, materialId(), placementHueShift(pos)}});
+
+ // Make sure not to make any more modifications than we have consumables.
+ if (modifications.size() > count())
+ modifications.resize(count());
+ size_t failed = world()->applyTileModifications(modifications, false).size();
+ if (failed < modifications.size()) {
+ FireableItem::fire(mode, shifting, edgeTriggered);
+ consume(modifications.size() - failed);
+ }
+}
+
+MaterialId MaterialItem::materialId() const {
+ return m_material;
+}
+
+MaterialHue MaterialItem::materialHueShift() const {
+ return m_materialHueShift;
+}
+
+bool MaterialItem::canPlace(bool shifting) const {
+ if (initialized()) {
+ MaterialId material = materialId();
+
+ float radius;
+ if (!shifting)
+ radius = m_blockRadius;
+ else
+ radius = m_altBlockRadius;
+
+ if (!multiplaceEnabled())
+ radius = 1;
+
+ for (auto pos : tileAreaBrush(radius, owner()->aimPosition(), true)) {
+ MaterialHue hueShift = placementHueShift(pos);
+ if (world()->canModifyTile(pos, PlaceMaterial{TileLayer::Foreground, material, hueShift}, false)
+ || world()->canModifyTile(pos, PlaceMaterial{TileLayer::Background, material, hueShift}, false))
+ return true;
+ }
+ }
+ return false;
+}
+
+bool MaterialItem::multiplaceEnabled() const {
+ return m_multiplace && count() > 1;
+}
+
+List<PreviewTile> MaterialItem::preview(bool shifting) const {
+ List<PreviewTile> result;
+ if (initialized()) {
+ Color lightColor = Color::rgba(owner()->favoriteColor());
+ Vec3B light = lightColor.toRgb();
+
+ auto material = materialId();
+ auto color = DefaultMaterialColorVariant;
+
+ float radius;
+
+ if (!shifting)
+ radius = m_blockRadius;
+ else
+ radius = m_altBlockRadius;
+
+ if (!multiplaceEnabled())
+ radius = 1;
+
+ size_t c = 0;
+
+ for (auto pos : tileAreaBrush(radius, owner()->aimPosition(), true)) {
+ MaterialHue hueShift = placementHueShift(pos);
+ if (c >= count())
+ break;
+ if (world()->canModifyTile(pos, PlaceMaterial{TileLayer::Foreground, material, hueShift}, false)) {
+ result.append({pos, true, material, hueShift, true});
+ c++;
+ } else if (twoHanded()
+ && world()->canModifyTile(pos, PlaceMaterial{TileLayer::Background, material, hueShift}, false)) {
+ result.append({pos, true, material, hueShift, true, light, true, color});
+ c++;
+ } else {
+ result.append({pos, true, material, hueShift, true});
+ }
+ }
+ }
+ return result;
+}
+
+MaterialHue MaterialItem::placementHueShift(Vec2I const& pos) const {
+ if (auto hue = instanceValue("materialHueShift")) {
+ return materialHueFromDegrees(hue.toFloat());
+ } else if (auto worldClient = as<WorldClient>(world())) {
+ auto worldTemplate = worldClient->currentTemplate();
+ return worldTemplate->biomeMaterialHueShift(worldTemplate->blockBiomeIndex(pos[0], pos[1]), m_material);
+ } else {
+ return materialHueShift();
+ }
+}
+
+}
diff --git a/source/game/items/StarMaterialItem.hpp b/source/game/items/StarMaterialItem.hpp
new file mode 100644
index 0000000..0fe7b03
--- /dev/null
+++ b/source/game/items/StarMaterialItem.hpp
@@ -0,0 +1,50 @@
+#ifndef STAR_MATERIAL_ITEM_HPP
+#define STAR_MATERIAL_ITEM_HPP
+
+#include "StarItem.hpp"
+#include "StarFireableItem.hpp"
+#include "StarBeamItem.hpp"
+#include "StarEntityRendering.hpp"
+
+namespace Star {
+
+STAR_CLASS(MaterialItem);
+
+class MaterialItem : public Item, public FireableItem, public BeamItem {
+public:
+ MaterialItem(Json const& config, String const& directory, Json const& settings);
+ virtual ~MaterialItem() {}
+
+ ItemPtr clone() const override;
+
+ void init(ToolUserEntity* owner, ToolHand hand) override;
+ void update(FireMode fireMode, bool shifting, HashSet<MoveControlType> const& moves) override;
+
+ List<Drawable> nonRotatedDrawables() const override;
+
+ void fire(FireMode mode, bool shifting, bool edgeTriggered) override;
+
+ MaterialId materialId() const;
+ MaterialHue materialHueShift() const;
+
+ bool canPlace(bool shifting) const;
+ bool multiplaceEnabled() const;
+
+ // FIXME: Why isn't this a PreviewTileTool then??
+ List<PreviewTile> preview(bool shifting) const;
+
+private:
+ MaterialHue placementHueShift(Vec2I const& position) const;
+
+ MaterialId m_material;
+ MaterialHue m_materialHueShift;
+
+ float m_blockRadius;
+ float m_altBlockRadius;
+ bool m_shifting;
+ bool m_multiplace;
+};
+
+}
+
+#endif
diff --git a/source/game/items/StarObjectItem.cpp b/source/game/items/StarObjectItem.cpp
new file mode 100644
index 0000000..bd4749b
--- /dev/null
+++ b/source/game/items/StarObjectItem.cpp
@@ -0,0 +1,107 @@
+#include "StarObjectItem.hpp"
+#include "StarRoot.hpp"
+#include "StarObject.hpp"
+#include "StarLogging.hpp"
+#include "StarObjectDatabase.hpp"
+#include "StarWorld.hpp"
+#include "StarJsonExtra.hpp"
+
+namespace Star {
+
+ObjectItem::ObjectItem(Json const& config, String const& directory, Json const& objectParameters)
+ : Item(config, directory, objectParameters), FireableItem(config), BeamItem(config) {
+ setTwoHanded(config.getBool("twoHanded", true));
+
+ // Make sure that all script objects that have retainObjectParametersInItem
+ // start with a blank scriptStorage entry to help them stack properly.
+ if (instanceValue("retainObjectParametersInItem", false).toBool() && instanceValue("scriptStorage").isNull())
+ setInstanceValue("scriptStorage", JsonObject());
+ m_shifting = false;
+}
+
+ItemPtr ObjectItem::clone() const {
+ return make_shared<ObjectItem>(*this);
+}
+
+void ObjectItem::init(ToolUserEntity* owner, ToolHand hand) {
+ FireableItem::init(owner, hand);
+ BeamItem::init(owner, hand);
+}
+
+void ObjectItem::update(FireMode fireMode, bool shifting, HashSet<MoveControlType> const& moves) {
+ FireableItem::update(fireMode, shifting, moves);
+ BeamItem::update(fireMode, shifting, moves);
+ setEnd(BeamItem::EndType::Object);
+ m_shifting = shifting;
+}
+
+List<Drawable> ObjectItem::nonRotatedDrawables() const {
+ return beamDrawables(canPlace(m_shifting));
+}
+
+float ObjectItem::cooldownTime() const {
+ // TODO: Hardcoded
+ return 0.25f;
+}
+
+void ObjectItem::fire(FireMode mode, bool shifting, bool edgeTriggered) {
+ if (!ready())
+ return;
+
+ if (placeInWorld(mode, shifting))
+ FireableItem::fire(mode, shifting, edgeTriggered);
+}
+
+String ObjectItem::objectName() const {
+ return instanceValue("objectName", "<objectName missing>").toString();
+}
+
+Json ObjectItem::objectParameters() const {
+ Json objectParameters = parameters().opt().value(JsonObject{});
+ if (!initialized())
+ return objectParameters;
+ return objectParameters.set("owner", jsonFromMaybe(owner()->uniqueId()));
+}
+
+bool ObjectItem::placeInWorld(FireMode, bool shifting) {
+ if (!initialized())
+ throw ItemException("ObjectItem not init'd properly, or user not recognized as Tool User.");
+
+ if (!ready())
+ return false;
+
+ if (!canPlace(shifting))
+ return false;
+
+ auto pos = Vec2I(owner()->aimPosition().floor());
+ auto objectDatabase = Root::singleton().objectDatabase();
+ try {
+ if (auto object = objectDatabase->createForPlacement(world(), objectName(), pos, owner()->walkingDirection(), objectParameters())) {
+ if (consume(1)) {
+ world()->addEntity(object);
+ return true;
+ }
+ }
+ } catch (StarException const& e) {
+ Logger::error("Failed to instantiate object for placement. %s %s : %s",
+ objectName(),
+ objectParameters().repr(0, true),
+ outputException(e, true));
+ return true;
+ }
+
+ return false;
+}
+
+bool ObjectItem::canPlace(bool) const {
+ if (initialized()) {
+ if (owner()->isAdmin() || owner()->inToolRange()) {
+ auto pos = Vec2I(owner()->aimPosition().floor());
+ auto objectDatabase = Root::singleton().objectDatabase();
+ return objectDatabase->canPlaceObject(world(), pos, objectName());
+ }
+ }
+ return false;
+}
+
+}
diff --git a/source/game/items/StarObjectItem.hpp b/source/game/items/StarObjectItem.hpp
new file mode 100644
index 0000000..4e3043c
--- /dev/null
+++ b/source/game/items/StarObjectItem.hpp
@@ -0,0 +1,39 @@
+#ifndef STAR_OBJECT_ITEM_HPP
+#define STAR_OBJECT_ITEM_HPP
+
+#include "StarItem.hpp"
+#include "StarFireableItem.hpp"
+#include "StarBeamItem.hpp"
+
+namespace Star {
+
+STAR_CLASS(ObjectItem);
+
+class ObjectItem : public Item, public FireableItem, public BeamItem {
+public:
+ ObjectItem(Json const& config, String const& directory, Json const& objectParameters);
+ virtual ~ObjectItem() {}
+
+ ItemPtr clone() const override;
+
+ void init(ToolUserEntity* owner, ToolHand hand) override;
+ void update(FireMode fireMode, bool shifting, HashSet<MoveControlType> const& moves) override;
+
+ List<Drawable> nonRotatedDrawables() const override;
+
+ float cooldownTime() const override;
+ void fire(FireMode mode, bool shifting, bool edgeTriggered) override;
+
+ String objectName() const;
+ Json objectParameters() const;
+
+ bool placeInWorld(FireMode mode, bool shifting);
+ bool canPlace(bool shifting) const;
+
+private:
+ bool m_shifting;
+};
+
+}
+
+#endif
diff --git a/source/game/items/StarThrownItem.cpp b/source/game/items/StarThrownItem.cpp
new file mode 100644
index 0000000..c688464
--- /dev/null
+++ b/source/game/items/StarThrownItem.cpp
@@ -0,0 +1,56 @@
+#include "StarThrownItem.hpp"
+#include "StarProjectile.hpp"
+#include "StarRoot.hpp"
+#include "StarAssets.hpp"
+#include "StarProjectileDatabase.hpp"
+#include "StarWorld.hpp"
+
+namespace Star {
+
+ThrownItem::ThrownItem(Json const& config, String const& directory, Json const& itemParameters)
+ : Item(config, directory, itemParameters), SwingableItem(config) {
+ m_projectileType = instanceValue("projectileType").toString();
+ m_projectileConfig = instanceValue("projectileConfig", {});
+ m_ammoUsage = instanceValue("ammoUsage", 1).toUInt();
+
+ auto image = AssetPath::relativeTo(directory, instanceValue("image").toString());
+ m_drawables = {Drawable::makeImage(image, 1.0f / TilePixels, true, Vec2F())};
+}
+
+ItemPtr ThrownItem::clone() const {
+ return make_shared<ThrownItem>(*this);
+}
+
+List<Drawable> ThrownItem::drawables() const {
+ return m_drawables;
+}
+
+List<Drawable> ThrownItem::preview(PlayerPtr const&) const {
+ return iconDrawables();
+}
+
+void ThrownItem::fireTriggered() {
+ auto& root = Root::singleton();
+
+ if (initialized()) {
+ Vec2F direction = world()->geometry().diff(owner()->aimPosition(), owner()->position()).normalized();
+ Vec2F firePosition = owner()->position() + ownerFirePosition();
+ if (world()->lineTileCollision(owner()->position(), firePosition))
+ return;
+
+ if (consume(m_ammoUsage)) {
+ auto projectile = root.projectileDatabase()->createProjectile(m_projectileType, m_projectileConfig);
+ projectile->setInitialPosition(firePosition);
+ projectile->setInitialDirection(direction);
+ projectile->setSourceEntity(owner()->entityId(), false);
+ projectile->setPowerMultiplier(owner()->powerMultiplier());
+ world()->addEntity(projectile);
+ }
+
+ FireableItem::fireTriggered();
+ } else {
+ throw ItemException("Thrown item not init'd properly, or user not recognized as Tool User.");
+ }
+}
+
+}
diff --git a/source/game/items/StarThrownItem.hpp b/source/game/items/StarThrownItem.hpp
new file mode 100644
index 0000000..81f3932
--- /dev/null
+++ b/source/game/items/StarThrownItem.hpp
@@ -0,0 +1,32 @@
+#ifndef STAR_THROWN_ITEM_HPP
+#define STAR_THROWN_ITEM_HPP
+
+#include "StarItem.hpp"
+#include "StarDrawable.hpp"
+#include "StarSwingableItem.hpp"
+#include "StarPreviewableItem.hpp"
+
+namespace Star {
+
+class ThrownItem : public Item, public SwingableItem, public PreviewableItem {
+public:
+ ThrownItem(Json const& config, String const& directory, Json const& itemParameters = JsonObject());
+
+ ItemPtr clone() const override;
+
+ List<Drawable> drawables() const override;
+ List<Drawable> preview(PlayerPtr const& viewer = {}) const override;
+
+protected:
+ void fireTriggered() override;
+
+private:
+ String m_projectileType;
+ Json m_projectileConfig;
+ size_t m_ammoUsage;
+ List<Drawable> m_drawables;
+};
+
+}
+
+#endif
diff --git a/source/game/items/StarTools.cpp b/source/game/items/StarTools.cpp
new file mode 100644
index 0000000..aef26a5
--- /dev/null
+++ b/source/game/items/StarTools.cpp
@@ -0,0 +1,709 @@
+#include "StarTools.hpp"
+#include "StarRoot.hpp"
+#include "StarMaterialDatabase.hpp"
+#include "StarJsonExtra.hpp"
+#include "StarAssets.hpp"
+#include "StarWiring.hpp"
+#include "StarWorld.hpp"
+#include "StarWorldClient.hpp"
+#include "StarParticleDatabase.hpp"
+
+namespace Star {
+
+MiningTool::MiningTool(Json const& config, String const& directory, Json const& parameters)
+ : Item(config, directory, parameters), SwingableItem(config) {
+ auto assets = Root::singleton().assets();
+
+ m_image = AssetPath::relativeTo(directory, instanceValue("image").toString());
+ m_frames = instanceValue("frames", 1).toInt();
+ m_frameCycle = instanceValue("animationCycle", 1.0f).toFloat();
+ m_frameTiming = 0;
+ for (size_t i = 0; i < (size_t)m_frames; i++)
+ m_animationFrame.append(m_image.replace("{frame}", strf("%s", i)));
+ m_idleFrame = m_image.replace("{frame}", "idle");
+ m_handPosition = jsonToVec2F(instanceValue("handPosition"));
+ m_blockRadius = instanceValue("blockRadius").toFloat();
+ m_altBlockRadius = instanceValue("altBlockRadius").toFloat();
+ m_strikeSounds = jsonToStringList(instanceValue("strikeSounds"));
+ m_breakSound = instanceValue("breakSound", "").toString();
+ m_pointable = instanceValue("pointable", false).toBool();
+
+ m_toolVolume = assets->json("/sfx.config:miningToolVolume").toFloat();
+ m_blockVolume = assets->json("/sfx.config:miningBlockVolume").toFloat();
+}
+
+ItemPtr MiningTool::clone() const {
+ return make_shared<MiningTool>(*this);
+}
+
+List<Drawable> MiningTool::drawables() const {
+ if (m_frameTiming == 0) {
+ return {Drawable::makeImage(m_idleFrame, 1.0f / TilePixels, true, -handPosition() / TilePixels)};
+ } else {
+ int frame = std::max(0, std::min(m_frames - 1, (int)std::floor((m_frameTiming / m_frameCycle) * m_frames)));
+ return {Drawable::makeImage(m_animationFrame[frame], 1.0f / TilePixels, true, -handPosition() / TilePixels)};
+ }
+}
+
+Vec2F MiningTool::handPosition() const {
+ return m_handPosition;
+}
+
+void MiningTool::fire(FireMode mode, bool shifting, bool edgeTriggered) {
+ if (!ready())
+ return;
+
+ auto materialDatabase = Root::singleton().materialDatabase();
+
+ if (initialized()) {
+ bool used = false;
+ int radius = !shifting ? m_blockRadius : m_altBlockRadius;
+ String blockSound;
+ List<Vec2I> brushArea;
+
+ auto layer = (mode == FireMode::Primary ? TileLayer::Foreground : TileLayer::Background);
+ if (owner()->isAdmin() || owner()->inToolRange()) {
+ brushArea = tileAreaBrush(radius, owner()->aimPosition(), true);
+ for (auto pos : brushArea) {
+ blockSound = materialDatabase->miningSound(world()->material(pos, layer), world()->mod(pos, layer));
+ if (!blockSound.empty())
+ break;
+ }
+ if (blockSound.empty()) {
+ for (auto pos : brushArea) {
+ blockSound = materialDatabase->footstepSound(world()->material(pos, layer), world()->mod(pos, layer));
+ if (!blockSound.empty()
+ && blockSound != Root::singleton().assets()->json("/client.config:defaultFootstepSound").toString())
+ break;
+ }
+ }
+
+ TileDamage damage;
+ damage.type = TileDamageTypeNames.getLeft(instanceValue("tileDamageType", "blockish").toString());
+
+ if (durabilityStatus() == 0)
+ damage.amount = instanceValue("tileDamageBlunted", 0.1f).toFloat();
+ else
+ damage.amount = instanceValue("tileDamage", 1.0f).toFloat();
+
+ damage.harvestLevel = instanceValue("harvestLevel", 1).toUInt();
+
+ auto damageResult = world()->damageTiles(brushArea, layer, owner()->position(), damage, owner()->entityId());
+
+ if (damageResult != TileDamageResult::None) {
+ used = true;
+ if (!owner()->isAdmin())
+ changeDurability(instanceValue("durabilityPerUse", 1.0f).toFloat());
+ }
+
+ if (damageResult == TileDamageResult::Protected) {
+ blockSound = Root::singleton().assets()->json("/client.config:defaultDingSound").toString();
+ }
+ }
+
+ if (used) {
+ owner()->addSound(Random::randValueFrom(m_strikeSounds), m_toolVolume);
+ owner()->addSound(blockSound, m_blockVolume);
+ List<Particle> miningParticles;
+ for (auto pos : brushArea) {
+ if (auto miningParticleConfig = materialDatabase->miningParticle(world()->material(pos, layer), world()->mod(pos, layer))) {
+ auto miningParticle = miningParticleConfig->instance();
+ miningParticle.position += (Vec2F)pos;
+ miningParticles.append(miningParticle);
+ }
+ }
+ owner()->addParticles(miningParticles);
+ SwingableItem::fire(mode, shifting, edgeTriggered);
+ }
+ }
+}
+
+void MiningTool::update(FireMode mode, bool shifting, HashSet<MoveControlType> const& moves) {
+ SwingableItem::update(mode, shifting, moves);
+
+ if (!ready() && !coolingDown())
+ m_frameTiming = std::fmod((m_frameTiming + WorldTimestep), m_frameCycle);
+ else
+ m_frameTiming = 0;
+}
+
+float MiningTool::durabilityStatus() {
+ return clamp(
+ 1.0f - instanceValue("durabilityHit", 0.0f).toFloat() / instanceValue("durability").toFloat(), 0.0f, 1.0f);
+}
+
+float MiningTool::getAngle(float aimAngle) {
+ if ((!ready() && !coolingDown()) || !m_pointable)
+ return SwingableItem::getAngle(aimAngle);
+ return aimAngle;
+}
+
+void MiningTool::changeDurability(float amount) {
+ setInstanceValue("durabilityHit", clamp(instanceValue("durabilityHit", 0.0f).toFloat() + amount, 0.0f, instanceValue("durability").toFloat()));
+ if (durabilityStatus() == 0.0f && !instanceValue("canBeRepaired", false).toBool()) {
+ owner()->addSound(m_breakSound);
+ consume(1);
+ }
+}
+
+HarvestingTool::HarvestingTool(Json const& config, String const& directory, Json const& parameters)
+ : Item(config, directory, parameters), SwingableItem(config) {
+ auto assets = Root::singleton().assets();
+
+ m_image = AssetPath::relativeTo(directory, instanceValue("image").toString());
+ m_frames = instanceValue("frames", 1).toInt();
+ m_frameCycle = instanceValue("animationCycle", 1.0f).toFloat();
+ for (size_t i = 0; i < (size_t)m_frames; i++)
+ m_animationFrame.append(m_image.replace("{frame}", strf("%s", i)));
+ m_idleFrame = m_image.replace("{frame}", "idle");
+
+ m_handPosition = jsonToVec2F(instanceValue("handPosition"));
+ m_strikeSounds = jsonToStringList(instanceValue("strikeSounds"));
+ m_toolVolume = assets->json("/sfx.config:harvestToolVolume").toFloat();
+ m_harvestPower = instanceValue("harvestPower", 1.0f).toFloat();
+ m_frameTiming = 0;
+}
+
+ItemPtr HarvestingTool::clone() const {
+ return make_shared<HarvestingTool>(*this);
+}
+
+List<Drawable> HarvestingTool::drawables() const {
+ if (m_frameTiming == 0)
+ return {Drawable::makeImage(m_idleFrame, 1.0f / TilePixels, true, -handPosition() / TilePixels)};
+ else {
+ int frame = std::max(0, std::min(m_frames - 1, (int)std::floor((m_frameTiming / m_frameCycle) * m_frames)));
+ return {Drawable::makeImage(m_animationFrame[frame], 1.0f / TilePixels, true, -handPosition() / TilePixels)};
+ }
+}
+
+Vec2F HarvestingTool::handPosition() const {
+ return m_handPosition;
+}
+
+void HarvestingTool::fire(FireMode mode, bool shifting, bool edgeTriggered) {
+ if (!ready())
+ return;
+
+ if (owner()) {
+ bool used = false;
+
+ if (owner()->isAdmin() || owner()->inToolRange()) {
+ auto layer = (mode == FireMode::Primary ? TileLayer::Foreground : TileLayer::Background);
+ used = world()->damageTile(Vec2I::floor(owner()->aimPosition()), layer, owner()->position(), {TileDamageType::Plantish, m_harvestPower}) != TileDamageResult::None;
+ }
+
+ if (used) {
+ owner()->addSound(Random::randValueFrom(m_strikeSounds), m_toolVolume);
+ SwingableItem::fire(mode, shifting, edgeTriggered);
+ }
+ }
+}
+
+void HarvestingTool::update(FireMode fireMode, bool shifting, HashSet<MoveControlType> const& moves) {
+ SwingableItem::update(fireMode, shifting, moves);
+
+ if (!ready() && !coolingDown())
+ m_frameTiming = std::fmod((m_frameTiming + WorldTimestep), m_frameCycle);
+ else
+ m_frameTiming = 0;
+}
+
+float HarvestingTool::getAngle(float aimAngle) {
+ if (!ready() && !coolingDown())
+ return SwingableItem::getAngle(aimAngle);
+ return aimAngle;
+}
+
+Flashlight::Flashlight(Json const& config, String const& directory, Json const& parameters)
+ : Item(config, directory, parameters) {
+ m_image = AssetPath::relativeTo(directory, instanceValue("image").toString());
+ m_handPosition = jsonToVec2F(instanceValue("handPosition"));
+ m_lightPosition = jsonToVec2F(instanceValue("lightPosition"));
+ m_lightColor = jsonToColor(instanceValue("lightColor"));
+ m_beamWidth = instanceValue("beamLevel").toFloat();
+ m_ambientFactor = instanceValue("beamAmbience").toFloat();
+}
+
+ItemPtr Flashlight::clone() const {
+ return make_shared<Flashlight>(*this);
+}
+
+List<Drawable> Flashlight::drawables() const {
+ return {Drawable::makeImage(m_image, 1.0f / TilePixels, true, -m_handPosition / TilePixels)};
+}
+
+List<LightSource> Flashlight::lightSources() const {
+ if (!initialized())
+ return {};
+
+ float angle = world()->geometry().diff(owner()->aimPosition(), owner()->position()).angle();
+ LightSource lightSource;
+ lightSource.pointLight = true;
+ lightSource.position = owner()->position() + owner()->handPosition(hand(), (m_lightPosition - m_handPosition) / TilePixels);
+ lightSource.color = m_lightColor.toRgb();
+ lightSource.pointBeam = m_beamWidth;
+ lightSource.beamAngle = angle;
+ lightSource.beamAmbience = m_ambientFactor;
+ return {move(lightSource)};
+}
+
+WireTool::WireTool(Json const& config, String const& directory, Json const& parameters)
+ : Item(config, directory, parameters), FireableItem(config), BeamItem(config.setAll(parameters.toObject())) {
+ auto assets = Root::singleton().assets();
+
+ m_handPosition = jsonToVec2F(instanceValue("handPosition"));
+ m_strikeSounds = jsonToStringList(instanceValue("strikeSounds"));
+ m_toolVolume = assets->json("/sfx.config:miningToolVolume").toFloat();
+ m_wireConnector = 0;
+ m_endType = EndType::Wire;
+}
+
+ItemPtr WireTool::clone() const {
+ return make_shared<WireTool>(*this);
+}
+
+void WireTool::init(ToolUserEntity* owner, ToolHand hand) {
+ FireableItem::init(owner, hand);
+ BeamItem::init(owner, hand);
+ m_wireConnector = 0;
+}
+
+void WireTool::update(FireMode fireMode, bool shifting, HashSet<MoveControlType> const& moves) {
+ FireableItem::update(fireMode, shifting, moves);
+ BeamItem::update(fireMode, shifting, moves);
+}
+
+List<Drawable> WireTool::drawables() const {
+ return BeamItem::drawables();
+}
+
+List<Drawable> WireTool::nonRotatedDrawables() const {
+ if (m_wireConnector && m_wireConnector->connecting())
+ return BeamItem::nonRotatedDrawables();
+ return {};
+}
+
+void WireTool::setEnd(EndType) {
+ m_endType = EndType::Wire;
+}
+
+Vec2F WireTool::handPosition() const {
+ return m_handPosition;
+}
+
+void WireTool::fire(FireMode mode, bool shifting, bool edgeTriggered) {
+ if (!ready())
+ return;
+
+ auto ownerp = owner();
+ auto worldp = world();
+
+ if (ownerp && worldp && m_wireConnector) {
+ Vec2F pos(ownerp->aimPosition());
+ if (ownerp->isAdmin() || ownerp->inToolRange()) {
+ auto swingResult = m_wireConnector->swing(worldp->geometry(), pos, mode);
+ if (swingResult == WireConnector::Connect) {
+ ownerp->addSound(Random::randValueFrom(m_strikeSounds), m_toolVolume);
+ FireableItem::fire(mode, shifting, edgeTriggered);
+ } else if (swingResult == WireConnector::Mismatch || swingResult == WireConnector::Protected) {
+ auto wireErrorSound = Root::singleton().assets()->json("/client.config:wireFailSound").toString();
+ ownerp->addSound(wireErrorSound, m_toolVolume);
+ FireableItem::fire(mode, shifting, edgeTriggered);
+ }
+ }
+ }
+}
+
+float WireTool::getAngle(float aimAngle) {
+ return BeamItem::getAngle(aimAngle);
+}
+
+void WireTool::setConnector(WireConnector* connector) {
+ m_wireConnector = connector;
+}
+
+BeamMiningTool::BeamMiningTool(Json const& config, String const& directory, Json const& parameters)
+ : Item(config, directory, parameters), FireableItem(config), BeamItem(config.setAll(parameters.toObject())) {
+ auto assets = Root::singleton().assets();
+
+ m_blockRadius = instanceValue("blockRadius").toFloat();
+ m_altBlockRadius = instanceValue("altBlockRadius").toFloat();
+ m_tileDamage = instanceValue("tileDamage", 1.0f).toFloat();
+ m_harvestLevel = instanceValue("harvestLevel", 1).toUInt();
+ m_canCollectLiquid = instanceValue("canCollectLiquid", false).toBool();
+ m_strikeSounds = jsonToStringList(instanceValue("strikeSounds"));
+ m_toolVolume = assets->json("/sfx.config:miningToolVolume").toFloat();
+ m_blockVolume = assets->json("/sfx.config:miningBlockVolume").toFloat();
+ m_endType = EndType::Object;
+
+ m_inhandStatusEffects = instanceValue("inhandStatusEffects", JsonArray()).toArray().transformed(jsonToPersistentStatusEffect);
+}
+
+ItemPtr BeamMiningTool::clone() const {
+ return make_shared<BeamMiningTool>(*this);
+}
+
+List<Drawable> BeamMiningTool::drawables() const {
+ return BeamItem::drawables();
+}
+
+void BeamMiningTool::setEnd(EndType) {
+ m_endType = EndType::Object;
+}
+
+List<PreviewTile> BeamMiningTool::preview(bool shifting) const {
+ List<PreviewTile> result;
+ auto ownerp = owner();
+ auto worldp = world();
+
+ if (ownerp && worldp) {
+ if (ownerp->isAdmin() || ownerp->inToolRange()) {
+ Vec3B light = Color::rgba(ownerp->favoriteColor()).toRgb();
+ int radius = !shifting ? m_blockRadius : m_altBlockRadius;
+ for (auto pos : tileAreaBrush(radius, ownerp->aimPosition(), true)) {
+ if (worldp->tileIsOccupied(pos, TileLayer::Foreground, true)) {
+ result.append({pos, true, light, true});
+ } else if (worldp->tileIsOccupied(pos, TileLayer::Background, true)) {
+ result.append({pos, false, light, true});
+ }
+ }
+ }
+ }
+ return result;
+}
+
+void BeamMiningTool::init(ToolUserEntity* owner, ToolHand hand) {
+ FireableItem::init(owner, hand);
+ BeamItem::init(owner, hand);
+}
+
+void BeamMiningTool::update(FireMode fireMode, bool shifting, HashSet<MoveControlType> const& moves) {
+ FireableItem::update(fireMode, shifting, moves);
+ BeamItem::update(fireMode, shifting, moves);
+}
+
+List<PersistentStatusEffect> BeamMiningTool::statusEffects() const {
+ return m_inhandStatusEffects;
+}
+
+List<Drawable> BeamMiningTool::nonRotatedDrawables() const {
+ if (!ready() && !coolingDown())
+ return BeamItem::nonRotatedDrawables();
+ return {};
+}
+
+void BeamMiningTool::fire(FireMode mode, bool shifting, bool edgeTriggered) {
+ if (!ready())
+ return;
+
+ auto materialDatabase = Root::singleton().materialDatabase();
+
+ auto worldp = world();
+ auto ownerp = owner();
+ if (ownerp && worldp) {
+ bool used = false;
+ int radius = !shifting ? m_blockRadius : m_altBlockRadius;
+ String blockSound;
+ List<Vec2I> brushArea;
+
+ auto layer = (mode == FireMode::Primary ? TileLayer::Foreground : TileLayer::Background);
+ if (ownerp->isAdmin() || ownerp->inToolRange()) {
+ brushArea = tileAreaBrush(radius, ownerp->aimPosition(), true);
+ auto aimPosition = Vec2I(ownerp->aimPosition());
+
+ for (auto pos : brushArea) {
+ blockSound = materialDatabase->miningSound(worldp->material(pos, layer), worldp->mod(pos, layer));
+ if (!blockSound.empty())
+ break;
+ }
+ if (blockSound.empty()) {
+ for (auto pos : brushArea) {
+ blockSound = materialDatabase->footstepSound(worldp->material(pos, layer), worldp->mod(pos, layer));
+ if (!blockSound.empty()
+ && blockSound != Root::singleton().assets()->json("/client.config:defaultFootstepSound").toString())
+ break;
+ }
+ }
+
+ auto damageResult = worldp->damageTiles(List<Vec2I>{brushArea}, layer, ownerp->position(), {TileDamageType::Beamish, m_tileDamage, m_harvestLevel}, ownerp->entityId());
+ used = damageResult != TileDamageResult::None;
+
+ if (damageResult == TileDamageResult::Protected) {
+ blockSound = Root::singleton().assets()->json("/client.config:defaultDingSound").toString();
+ }
+
+ if (!used && m_canCollectLiquid && layer == TileLayer::Foreground && worldp->material(aimPosition, TileLayer::Foreground) == EmptyMaterialId) {
+ auto targetLiquid = worldp->liquidLevel(aimPosition).liquid;
+ List<Vec2I> drainTiles;
+ float totalLiquid = 0;
+ for (auto pos : brushArea) {
+ if (worldp->isTileProtected(pos))
+ continue;
+
+ auto liquid = worldp->liquidLevel(pos);
+ if (liquid.liquid != EmptyLiquidId) {
+ if (targetLiquid == EmptyLiquidId)
+ targetLiquid = liquid.liquid;
+
+ if (liquid.liquid == targetLiquid) {
+ totalLiquid += liquid.level;
+ drainTiles.append(pos);
+ }
+ }
+ }
+
+ float bucketSize = Root::singleton().assets()->json("/items/defaultParameters.config:liquidItems.bucketSize").toUInt();
+ if (totalLiquid >= bucketSize) {
+ if (auto clientWorld = as<WorldClient>(worldp))
+ clientWorld->collectLiquid(drainTiles, targetLiquid);
+
+ blockSound = Root::singleton().assets()->json("/items/defaultParameters.config:liquidBlockSound").toString();
+
+ used = true;
+ }
+ }
+ }
+
+ if (used) {
+ ownerp->addSound(Random::randValueFrom(m_strikeSounds), m_toolVolume);
+ ownerp->addSound(blockSound, m_blockVolume);
+ List<Particle> miningParticles;
+ for (auto pos : brushArea) {
+ if (auto miningParticleConfig = materialDatabase->miningParticle(worldp->material(pos, layer), worldp->mod(pos, layer))) {
+ auto miningParticle = miningParticleConfig->instance();
+ miningParticle.position += (Vec2F)pos;
+ miningParticles.append(miningParticle);
+ }
+ }
+ ownerp->addParticles(miningParticles);
+ FireableItem::fire(mode, shifting, edgeTriggered);
+ }
+ }
+}
+
+float BeamMiningTool::getAngle(float angle) {
+ return BeamItem::getAngle(angle);
+}
+
+TillingTool::TillingTool(Json const& config, String const& directory, Json const& parameters)
+ : Item(config, directory, parameters), SwingableItem(config) {
+ auto assets = Root::singleton().assets();
+
+ m_image = AssetPath::relativeTo(directory, instanceValue("image").toString());
+ m_frames = instanceValue("frames", 1).toInt();
+ m_frameCycle = instanceValue("animationCycle", 1.0f).toFloat();
+ for (size_t i = 0; i < (size_t)m_frames; i++)
+ m_animationFrame.append(m_image.replace("{frame}", strf("%s", i)));
+ m_idleFrame = m_image.replace("{frame}", "idle");
+
+ m_handPosition = jsonToVec2F(instanceValue("handPosition"));
+ m_strikeSounds = jsonToStringList(instanceValue("strikeSounds"));
+ m_toolVolume = assets->json("/sfx.config:harvestToolVolume").toFloat();
+ m_frameTiming = 0;
+}
+
+ItemPtr TillingTool::clone() const {
+ return make_shared<TillingTool>(*this);
+}
+
+List<Drawable> TillingTool::drawables() const {
+ if (m_frameTiming == 0)
+ return {Drawable::makeImage(m_idleFrame, 1.0f / TilePixels, true, -handPosition() / TilePixels)};
+ else {
+ int frame = std::max(0, std::min(m_frames - 1, (int)std::floor((m_frameTiming / m_frameCycle) * m_frames)));
+ return {Drawable::makeImage(m_animationFrame[frame], 1.0f / TilePixels, true, -handPosition() / TilePixels)};
+ }
+}
+
+Vec2F TillingTool::handPosition() const {
+ return m_handPosition;
+}
+
+void TillingTool::fire(FireMode mode, bool shifting, bool edgeTriggered) {
+ if (!ready())
+ return;
+
+ auto strikeSound = Random::randValueFrom(m_strikeSounds);
+
+ if (owner() && world()) {
+ auto materialDatabase = Root::singleton().materialDatabase();
+ Vec2I pos(owner()->aimPosition().floor());
+
+ if (world()->material(pos + Vec2I(0, 1), TileLayer::Foreground) != EmptyMaterialId)
+ return;
+
+ bool used = false;
+ for (auto layer : {TileLayer::Foreground, TileLayer::Background}) {
+ if (world()->material(pos, layer) == EmptyMaterialId)
+ pos = pos - Vec2I(0, 1);
+
+ if ((layer == TileLayer::Background)
+ && world()->material(pos + Vec2I(0, 1), TileLayer::Background) != EmptyMaterialId)
+ continue;
+
+ if (owner()->isAdmin() || owner()->inToolRange()) {
+ auto currentMod = world()->mod(pos, layer);
+ auto material = world()->material(pos, layer);
+ auto tilledMod = materialDatabase->tilledModFor(material);
+
+ if (tilledMod != NoModId && currentMod == NoModId) {
+ if (world()->modifyTile(pos, PlaceMod{layer, tilledMod, MaterialHue()}, true))
+ used = true;
+ } else if (currentMod != tilledMod) {
+ auto damageResult = world()->damageTile(pos, layer, owner()->position(), {TileDamageType::Tilling, 1.0f});
+ used = damageResult != TileDamageResult::None;
+ if (damageResult == TileDamageResult::Protected) {
+ strikeSound = Root::singleton().assets()->json("/client.config:defaultDingSound").toString();
+ }
+ }
+ }
+ }
+
+ if (used) {
+ owner()->addSound(strikeSound, m_toolVolume);
+ SwingableItem::fire(mode, shifting, edgeTriggered);
+ }
+ }
+}
+
+void TillingTool::update(FireMode fireMode, bool shifting, HashSet<MoveControlType> const& moves) {
+ SwingableItem::update(fireMode, shifting, moves);
+
+ if (!ready() && !coolingDown())
+ m_frameTiming = std::fmod((m_frameTiming + WorldTimestep), m_frameCycle);
+ else
+ m_frameTiming = 0;
+}
+
+float TillingTool::getAngle(float aimAngle) {
+ if (!ready() && !coolingDown())
+ return SwingableItem::getAngle(aimAngle);
+ return aimAngle;
+}
+
+PaintingBeamTool::PaintingBeamTool(Json const& config, String const& directory, Json const& parameters)
+ : Item(config, directory, parameters), FireableItem(config), BeamItem(config) {
+ auto assets = Root::singleton().assets();
+
+ m_blockRadius = instanceValue("blockRadius").toFloat();
+ m_altBlockRadius = instanceValue("altBlockRadius").toFloat();
+ m_strikeSounds = jsonToStringList(instanceValue("strikeSounds"));
+ m_toolVolume = assets->json("/sfx.config:miningToolVolume").toFloat();
+ m_blockVolume = assets->json("/sfx.config:miningBlockVolume").toFloat();
+ m_endType = EndType::Object;
+
+ for (auto color : instanceValue("colorNumbers").toArray())
+ m_colors.append(jsonToColor(color));
+
+ m_colorKeys = jsonToStringList(instanceValue("colorKeys"));
+
+ m_colorIndex = instanceValue("colorIndex", 0).toInt();
+ m_color = m_colors[m_colorIndex].toRgba();
+}
+
+ItemPtr PaintingBeamTool::clone() const {
+ return make_shared<PaintingBeamTool>(*this);
+}
+
+List<Drawable> PaintingBeamTool::drawables() const {
+ auto result = BeamItem::drawables();
+ for (auto& entry : result) {
+ if (entry.isImage())
+ entry.imagePart().image = entry.imagePart().image + m_colorKeys[m_colorIndex];
+ }
+ return result;
+}
+
+void PaintingBeamTool::setEnd(EndType type) {
+ _unused(type);
+ m_endType = EndType::Object;
+}
+
+void PaintingBeamTool::update(FireMode fireMode, bool shifting, HashSet<MoveControlType> const& moves) {
+ BeamItem::update(fireMode, shifting, moves);
+ FireableItem::update(fireMode, shifting, moves);
+}
+
+List<PreviewTile> PaintingBeamTool::preview(bool shifting) const {
+ List<PreviewTile> result;
+ auto ownerp = owner();
+ auto worldp = world();
+ if (ownerp && worldp) {
+ Vec3B light = Color::White.toRgb();
+
+ if (ownerp->isAdmin() || ownerp->inToolRange()) {
+ int radius = !shifting ? m_blockRadius : m_altBlockRadius;
+
+ for (auto pos : tileAreaBrush(radius, ownerp->aimPosition(), true)) {
+ if (worldp->canModifyTile(pos, PlaceMaterialColor{TileLayer::Foreground, (MaterialColorVariant)m_colorIndex}, true)) {
+ result.append({pos, true, NullMaterialId, MaterialHue(), false, light, true, (MaterialColorVariant)m_colorIndex});
+ } else if (worldp->canModifyTile(pos, PlaceMaterialColor{TileLayer::Background, (MaterialColorVariant)m_colorIndex}, true)) {
+ result.append({pos, false, NullMaterialId, MaterialHue(), false, light, true, (MaterialColorVariant)m_colorIndex});
+ } else if (worldp->canModifyTile(pos, PlaceMaterialColor{TileLayer::Foreground, DefaultMaterialColorVariant}, true)) {
+ result.append({pos, true, NullMaterialId, MaterialHue(), false, light, true, DefaultMaterialColorVariant});
+ } else if (worldp->canModifyTile(pos, PlaceMaterialColor{TileLayer::Background, DefaultMaterialColorVariant}, true)) {
+ result.append({pos, false, NullMaterialId, MaterialHue(), false, light, true, DefaultMaterialColorVariant});
+ }
+ }
+ }
+ }
+
+ return result;
+}
+
+void PaintingBeamTool::init(ToolUserEntity* owner, ToolHand hand) {
+ FireableItem::init(owner, hand);
+ BeamItem::init(owner, hand);
+ m_color = m_colors[m_colorIndex].toRgba();
+}
+
+List<Drawable> PaintingBeamTool::nonRotatedDrawables() const {
+ if (!coolingDown())
+ return BeamItem::nonRotatedDrawables();
+ return {};
+}
+
+void PaintingBeamTool::fire(FireMode mode, bool shifting, bool edgeTriggered) {
+ if (!ready())
+ return;
+
+ if (mode == FireMode::Alt && edgeTriggered) {
+ m_colorIndex = (m_colorIndex + 1) % m_colors.size();
+ m_color = m_colors[m_colorIndex].toRgba();
+ setInstanceValue("colorIndex", m_colorIndex);
+ return;
+ }
+
+ if (mode == FireMode::Primary) {
+ auto worldp = world();
+ auto ownerp = owner();
+ if (ownerp && worldp) {
+ bool used = false;
+ int radius = !shifting ? m_blockRadius : m_altBlockRadius;
+
+ if (ownerp->isAdmin() || ownerp->inToolRange()) {
+ for (auto pos : tileAreaBrush(radius, ownerp->aimPosition(), true)) {
+ TileModificationList modifications = {
+ {pos, PlaceMaterialColor{TileLayer::Foreground, (MaterialColorVariant)m_colorIndex}},
+ {pos, PlaceMaterialColor{TileLayer::Background, (MaterialColorVariant)m_colorIndex}}
+ };
+ auto failed = worldp->applyTileModifications(modifications, true);
+ if (failed.count() < 2)
+ used = true;
+ }
+ }
+
+ if (used) {
+ ownerp->addSound(Random::randValueFrom(m_strikeSounds), m_toolVolume);
+ FireableItem::fire(mode, shifting, edgeTriggered);
+ }
+ }
+ }
+}
+
+float PaintingBeamTool::getAngle(float angle) {
+ return BeamItem::getAngle(angle);
+}
+
+}
diff --git a/source/game/items/StarTools.hpp b/source/game/items/StarTools.hpp
new file mode 100644
index 0000000..cd2441b
--- /dev/null
+++ b/source/game/items/StarTools.hpp
@@ -0,0 +1,244 @@
+#ifndef STAR_TOOLS_HPP
+#define STAR_TOOLS_HPP
+
+#include "StarItem.hpp"
+#include "StarBeamItem.hpp"
+#include "StarSwingableItem.hpp"
+#include "StarDurabilityItem.hpp"
+#include "StarPointableItem.hpp"
+#include "StarFireableItem.hpp"
+#include "StarEntityRendering.hpp"
+#include "StarPreviewTileTool.hpp"
+
+namespace Star {
+
+STAR_CLASS(World);
+STAR_CLASS(WireConnector);
+STAR_CLASS(ToolUserEntity);
+
+STAR_CLASS(MiningTool);
+STAR_CLASS(HarvestingTool);
+STAR_CLASS(WireTool);
+STAR_CLASS(Flashlight);
+STAR_CLASS(BeamMiningTool);
+STAR_CLASS(TillingTool);
+STAR_CLASS(PaintingBeamTool);
+
+class MiningTool : public Item, public SwingableItem, public DurabilityItem {
+public:
+ MiningTool(Json const& config, String const& directory, Json const& parameters = JsonObject());
+
+ ItemPtr clone() const override;
+
+ List<Drawable> drawables() const override;
+ // In pixels, offset from image center
+ Vec2F handPosition() const override;
+ void fire(FireMode mode, bool shifting, bool edgeTriggered) override;
+ void update(FireMode fireMode, bool shifting, HashSet<MoveControlType> const& moves) override;
+
+ float durabilityStatus() override;
+
+ float getAngle(float aimAngle) override;
+
+private:
+ void changeDurability(float amount);
+
+ String m_image;
+ int m_frames;
+ float m_frameCycle;
+ float m_frameTiming;
+ List<String> m_animationFrame;
+ String m_idleFrame;
+
+ Vec2F m_handPosition;
+ float m_blockRadius;
+ float m_altBlockRadius;
+
+ StringList m_strikeSounds;
+ String m_breakSound;
+ float m_toolVolume;
+ float m_blockVolume;
+
+ bool m_pointable;
+};
+
+class HarvestingTool : public Item, public SwingableItem {
+public:
+ HarvestingTool(Json const& config, String const& directory, Json const& parameters = JsonObject());
+
+ ItemPtr clone() const override;
+
+ List<Drawable> drawables() const override;
+ // In pixels, offset from image center
+ Vec2F handPosition() const override;
+ void fire(FireMode mode, bool shifting, bool edgeTriggered) override;
+ void update(FireMode fireMode, bool shifting, HashSet<MoveControlType> const& moves) override;
+ float getAngle(float aimAngle) override;
+
+private:
+ String m_image;
+ int m_frames;
+ float m_frameCycle;
+ float m_frameTiming;
+ List<String> m_animationFrame;
+ String m_idleFrame;
+
+ Vec2F m_handPosition;
+
+ String m_idleSound;
+ StringList m_strikeSounds;
+ float m_toolVolume;
+ float m_harvestPower;
+};
+
+class Flashlight : public Item, public PointableItem, public ToolUserItem {
+public:
+ Flashlight(Json const& config, String const& directory, Json const& parameters = JsonObject());
+
+ ItemPtr clone() const override;
+
+ List<Drawable> drawables() const override;
+
+ List<LightSource> lightSources() const;
+
+private:
+ String m_image;
+ Vec2F m_handPosition;
+ Vec2F m_lightPosition;
+ Color m_lightColor;
+ float m_beamWidth;
+ float m_ambientFactor;
+};
+
+class WireTool : public Item, public FireableItem, public PointableItem, public BeamItem {
+public:
+ WireTool(Json const& config, String const& directory, Json const& parameters = JsonObject());
+
+ ItemPtr clone() const override;
+
+ void init(ToolUserEntity* owner, ToolHand hand) override;
+ void update(FireMode fireMode, bool shifting, HashSet<MoveControlType> const& moves) override;
+
+ List<Drawable> drawables() const override;
+ List<Drawable> nonRotatedDrawables() const override;
+
+ void setEnd(EndType type) override;
+
+ // In pixels, offset from image center
+ Vec2F handPosition() const override;
+ void fire(FireMode mode, bool shifting, bool edgeTriggered) override;
+ float getAngle(float aimAngle) override;
+
+ void setConnector(WireConnector* connector);
+
+private:
+ String m_image;
+ Vec2F m_handPosition;
+
+ StringList m_strikeSounds;
+ float m_toolVolume;
+
+ WireConnector* m_wireConnector;
+};
+
+class BeamMiningTool : public Item, public FireableItem, public PreviewTileTool, public PointableItem, public BeamItem {
+public:
+ BeamMiningTool(Json const& config, String const& directory, Json const& parameters = JsonObject());
+
+ ItemPtr clone() const override;
+
+ List<Drawable> drawables() const override;
+
+ virtual void setEnd(EndType type) override;
+ virtual List<PreviewTile> preview(bool shifting) const override;
+ virtual List<Drawable> nonRotatedDrawables() const override;
+ virtual void fire(FireMode mode, bool shifting, bool edgeTriggered) override;
+
+ float getAngle(float angle) override;
+
+ void init(ToolUserEntity* owner, ToolHand hand) override;
+ void update(FireMode fireMode, bool shifting, HashSet<MoveControlType> const& moves) override;
+
+ List<PersistentStatusEffect> statusEffects() const override;
+
+private:
+ float m_blockRadius;
+ float m_altBlockRadius;
+
+ float m_tileDamage;
+ unsigned m_harvestLevel;
+ bool m_canCollectLiquid;
+
+ StringList m_strikeSounds;
+ float m_toolVolume;
+ float m_blockVolume;
+
+ List<PersistentStatusEffect> m_inhandStatusEffects;
+};
+
+class TillingTool : public Item, public SwingableItem {
+public:
+ TillingTool(Json const& config, String const& directory, Json const& parameters = JsonObject());
+
+ ItemPtr clone() const override;
+
+ List<Drawable> drawables() const override;
+ // In pixels, offset from image center
+ Vec2F handPosition() const override;
+ void fire(FireMode mode, bool shifting, bool edgeTriggered) override;
+ void update(FireMode fireMode, bool shifting, HashSet<MoveControlType> const& moves) override;
+ float getAngle(float aimAngle) override;
+
+private:
+ String m_image;
+ int m_frames;
+ float m_frameCycle;
+ float m_frameTiming;
+ List<String> m_animationFrame;
+ String m_idleFrame;
+
+ Vec2F m_handPosition;
+
+ String m_idleSound;
+ StringList m_strikeSounds;
+ float m_toolVolume;
+};
+
+class PaintingBeamTool
+ : public Item,
+ public FireableItem,
+ public PreviewTileTool,
+ public PointableItem,
+ public BeamItem {
+public:
+ PaintingBeamTool(Json const& config, String const& directory, Json const& parameters = JsonObject());
+
+ ItemPtr clone() const override;
+
+ List<Drawable> drawables() const override;
+
+ void setEnd(EndType type) override;
+ void update(FireMode fireMode, bool shifting, HashSet<MoveControlType> const& moves) override;
+ List<PreviewTile> preview(bool shifting) const override;
+ void init(ToolUserEntity* owner, ToolHand hand) override;
+ List<Drawable> nonRotatedDrawables() const override;
+ void fire(FireMode mode, bool shifting, bool edgeTriggered) override;
+
+ float getAngle(float angle) override;
+
+private:
+ List<Color> m_colors;
+ List<String> m_colorKeys;
+ int m_colorIndex;
+
+ float m_blockRadius;
+ float m_altBlockRadius;
+
+ StringList m_strikeSounds;
+ float m_toolVolume;
+ float m_blockVolume;
+};
+
+}
+
+#endif
diff --git a/source/game/items/StarUnlockItem.cpp b/source/game/items/StarUnlockItem.cpp
new file mode 100644
index 0000000..face195
--- /dev/null
+++ b/source/game/items/StarUnlockItem.cpp
@@ -0,0 +1,69 @@
+#include "StarUnlockItem.hpp"
+#include "StarPlayer.hpp"
+#include "StarRoot.hpp"
+#include "StarAssets.hpp"
+#include "StarClientContext.hpp"
+#include "StarPlayerBlueprints.hpp"
+
+namespace Star {
+
+UnlockItem::UnlockItem(Json const& config, String const& directory, Json const& itemParameters)
+ : Item(config, directory, itemParameters), SwingableItem(config) {
+ m_tierRecipesUnlock = instanceValue("tierRecipesUnlock").optString();
+ m_shipUpgrade = instanceValue("shipUpgrade").optUInt();
+ m_unlockMessage = instanceValue("unlockMessage").optString().value();
+ auto image = AssetPath::relativeTo(directory, instanceValue("image").toString());
+ m_drawables = {Drawable::makeImage(image, 1.0f / TilePixels, true, Vec2F())};
+}
+
+ItemPtr UnlockItem::clone() const {
+ return make_shared<UnlockItem>(*this);
+}
+
+List<Drawable> UnlockItem::drawables() const {
+ return m_drawables;
+}
+
+List<Drawable> UnlockItem::preview(PlayerPtr const& viewer) const {
+ return iconDrawables();
+}
+
+void UnlockItem::fireTriggered() {
+ if (!initialized())
+ throw ItemException("Item not init'd properly, or user not recognized as Tool User.");
+
+ // Only the player can use an unlock item, for any other entity it should do
+ // nothing.
+ if (auto player = as<Player>(owner())) {
+ if (instanceValue("consume", true).toBool() && !consume(1))
+ return;
+
+ if (auto clientContext = player->clientContext()) {
+ if (m_shipUpgrade)
+ clientContext->rpcInterface()->invokeRemote("ship.applyShipUpgrades", JsonObject{{"shipLevel", *m_shipUpgrade}});
+ }
+
+ if (!m_unlockMessage.empty()) {
+ JsonObject message;
+ message["message"] = m_unlockMessage;
+ owner()->interact(InteractAction(InteractActionType::ShowPopup, owner()->entityId(), message));
+ }
+
+ if (m_tierRecipesUnlock) {
+ auto playerConfig = Root::singleton().assets()->json("/player.config");
+
+ List<ItemDescriptor> blueprints;
+ for (Json v : playerConfig.get("defaultBlueprints", JsonObject()).getArray(*m_tierRecipesUnlock, JsonArray()))
+ blueprints.append(ItemDescriptor(v));
+
+ auto speciesConfig = Root::singleton().assets()->json(strf("/species/%s.species", player->species()));
+ for (Json v : speciesConfig.get("defaultBlueprints", JsonObject()).getArray(*m_tierRecipesUnlock, JsonArray()))
+ blueprints.append(ItemDescriptor(v));
+
+ for (auto b : blueprints)
+ player->addBlueprint(b);
+ }
+ }
+}
+
+}
diff --git a/source/game/items/StarUnlockItem.hpp b/source/game/items/StarUnlockItem.hpp
new file mode 100644
index 0000000..281f819
--- /dev/null
+++ b/source/game/items/StarUnlockItem.hpp
@@ -0,0 +1,35 @@
+#ifndef STAR_CELESTIAL_ITEM_HPP
+#define STAR_CELESTIAL_ITEM_HPP
+
+#include "StarItem.hpp"
+#include "StarWorld.hpp"
+#include "StarSwingableItem.hpp"
+#include "StarPreviewableItem.hpp"
+
+namespace Star {
+
+STAR_CLASS(UnlockItem);
+
+class UnlockItem : public Item, public SwingableItem, public PreviewableItem {
+public:
+ UnlockItem(Json const& config, String const& directory, Json const& itemParameters = JsonObject());
+
+ ItemPtr clone() const override;
+
+ List<Drawable> drawables() const override;
+ List<Drawable> preview(PlayerPtr const& viewer = {}) const override;
+
+protected:
+ void fireTriggered() override;
+
+private:
+ Maybe<String> m_sectorUnlock;
+ Maybe<String> m_tierRecipesUnlock;
+ Maybe<unsigned> m_shipUpgrade;
+ String m_unlockMessage;
+ List<Drawable> m_drawables;
+};
+
+}
+
+#endif