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

View file

@ -0,0 +1,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;
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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.");
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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