#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(*this); } List 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 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 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 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(*this); } List 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 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(*this); } List Flashlight::drawables() const { return {Drawable::makeImage(m_image, 1.0f / TilePixels, true, -m_handPosition / TilePixels)}; } List 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(*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 const& moves) { FireableItem::update(fireMode, shifting, moves); BeamItem::update(fireMode, shifting, moves); } List WireTool::drawables() const { return BeamItem::drawables(); } List 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(*this); } List BeamMiningTool::drawables() const { return BeamItem::drawables(); } void BeamMiningTool::setEnd(EndType) { m_endType = EndType::Object; } List BeamMiningTool::preview(bool shifting) const { List 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 const& moves) { FireableItem::update(fireMode, shifting, moves); BeamItem::update(fireMode, shifting, moves); } List BeamMiningTool::statusEffects() const { return m_inhandStatusEffects; } List 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 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{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 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(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 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(*this); } List 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 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(*this); } List PaintingBeamTool::drawables() const { auto result = BeamItem::drawables(); for (auto& entry : result) { if (entry.isImage()) entry.imagePart().image.directives += 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 const& moves) { BeamItem::update(fireMode, shifting, moves); FireableItem::update(fireMode, shifting, moves); } List PaintingBeamTool::preview(bool shifting) const { List 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 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); } }