diff options
author | Kae <80987908+Novaenia@users.noreply.github.com> | 2023-06-20 14:33:09 +1000 |
---|---|---|
committer | Kae <80987908+Novaenia@users.noreply.github.com> | 2023-06-20 14:33:09 +1000 |
commit | 6352e8e3196f78388b6c771073f9e03eaa612673 (patch) | |
tree | e23772f79a7fbc41bc9108951e9e136857484bf4 /source/game/items | |
parent | 6741a057e5639280d85d0f88ba26f000baa58f61 (diff) |
everything everywhere
all at once
Diffstat (limited to 'source/game/items')
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 |