// DropSpenserEntity.cpp // Declares the cDropSpenserEntity class representing a common ancestor to the cDispenserEntity and cDropperEntity // The dropper and dispenser only needs to override the DropSpenseFromSlot() function to provide the specific item behavior #include "Globals.h" #include "DropSpenserEntity.h" #include "../Bindings/PluginManager.h" #include "../EffectID.h" #include "../Entities/Player.h" #include "../Chunk.h" #include "../UI/DropSpenserWindow.h" cDropSpenserEntity::cDropSpenserEntity(BLOCKTYPE a_BlockType, NIBBLETYPE a_BlockMeta, Vector3i a_Pos, cWorld * a_World): Super(a_BlockType, a_BlockMeta, a_Pos, ContentsWidth, ContentsHeight, a_World), m_ShouldDropSpense(false) { } void cDropSpenserEntity::AddDropSpenserDir(Vector3i & a_RelCoord, NIBBLETYPE a_Direction) { switch (a_Direction & E_META_DROPSPENSER_FACING_MASK) { case E_META_DROPSPENSER_FACING_YM: a_RelCoord.y--; return; case E_META_DROPSPENSER_FACING_YP: a_RelCoord.y++; return; case E_META_DROPSPENSER_FACING_ZM: a_RelCoord.z--; return; case E_META_DROPSPENSER_FACING_ZP: a_RelCoord.z++; return; case E_META_DROPSPENSER_FACING_XM: a_RelCoord.x--; return; case E_META_DROPSPENSER_FACING_XP: a_RelCoord.x++; return; } LOGWARNING("%s: Unhandled direction: %d", __FUNCTION__, a_Direction); } void cDropSpenserEntity::DropSpense(cChunk & a_Chunk) { // Pick one of the occupied slots: int OccupiedSlots[9]; int SlotsCnt = 0; for (int i = m_Contents.GetNumSlots() - 1; i >= 0; i--) { if (!m_Contents.GetSlot(i).IsEmpty()) { OccupiedSlots[SlotsCnt] = i; SlotsCnt++; } } // for i - m_Contents[] if (SlotsCnt == 0) { // Nothing in the dropspenser, play the click sound m_World->BroadcastSoundEffect("block.dispenser.fail", m_Pos, 1.0f, 1.2f); return; } const int RandomSlot = m_World->GetTickRandomNumber(SlotsCnt - 1); const int SpenseSlot = OccupiedSlots[RandomSlot]; if (cPluginManager::Get()->CallHookDropSpense(*m_World, *this, SpenseSlot)) { // Plugin disagrees with the move return; } // DropSpense the item, using the specialized behavior in the subclasses: DropSpenseFromSlot(a_Chunk, SpenseSlot); // Broadcast a smoke and click effects: NIBBLETYPE Meta = a_Chunk.GetMeta(GetRelPos()); int SmokeDir = 0; switch (Meta & E_META_DROPSPENSER_FACING_MASK) { case E_META_DROPSPENSER_FACING_YP: SmokeDir = static_cast(SmokeDirection::CENTRE); break; // YP & YM don't have associated smoke dirs, just do 4 (centre of block) case E_META_DROPSPENSER_FACING_YM: SmokeDir = static_cast(SmokeDirection::CENTRE); break; case E_META_DROPSPENSER_FACING_XM: SmokeDir = static_cast(SmokeDirection::EAST); break; case E_META_DROPSPENSER_FACING_XP: SmokeDir = static_cast(SmokeDirection::WEST); break; case E_META_DROPSPENSER_FACING_ZM: SmokeDir = static_cast(SmokeDirection::SOUTH); break; case E_META_DROPSPENSER_FACING_ZP: SmokeDir = static_cast(SmokeDirection::NORTH); break; } m_World->BroadcastSoundParticleEffect(EffectID::PARTICLE_SMOKE, GetPos(), SmokeDir); m_World->BroadcastSoundEffect("block.dispenser.dispense", m_Pos, 1.0f, 1.0f); } void cDropSpenserEntity::Activate(void) { m_ShouldDropSpense = true; } void cDropSpenserEntity::CopyFrom(const cBlockEntity & a_Src) { Super::CopyFrom(a_Src); auto & src = static_cast(a_Src); m_Contents.CopyFrom(src.m_Contents); m_ShouldDropSpense = src.m_ShouldDropSpense; } void cDropSpenserEntity::OnRemoveFromWorld() { const auto Window = GetWindow(); if (Window != nullptr) { // Tell window its owner is destroyed: Window->OwnerDestroyed(); } } bool cDropSpenserEntity::Tick(std::chrono::milliseconds a_Dt, cChunk & a_Chunk) { UNUSED(a_Dt); if (!m_ShouldDropSpense) { return false; } m_ShouldDropSpense = false; DropSpense(a_Chunk); return true; } void cDropSpenserEntity::SendTo(cClientHandle & a_Client) { // Nothing needs to be sent UNUSED(a_Client); } bool cDropSpenserEntity::UsedBy(cPlayer * a_Player) { if (m_BlockType == E_BLOCK_DISPENSER) { a_Player->GetStatistics().Custom[CustomStatistic::InspectDispenser]++; } else // E_BLOCK_DROPPER { a_Player->GetStatistics().Custom[CustomStatistic::InspectDropper]++; } cWindow * Window = GetWindow(); if (Window == nullptr) { OpenWindow(new cDropSpenserWindow(this)); Window = GetWindow(); } if (Window != nullptr) { if (a_Player->GetWindow() != Window) { a_Player->OpenWindow(*Window); } } return true; } void cDropSpenserEntity::DropFromSlot(cChunk & a_Chunk, int a_SlotNum) { Vector3i dispCoord(m_Pos); auto Meta = a_Chunk.GetMeta(GetRelPos()); AddDropSpenserDir(dispCoord, Meta); const auto PickupSpeed = [Meta]() -> Vector3i { const int PickupSpeed = GetRandomProvider().RandInt(2, 6); // At least 2, at most 6. switch (Meta & E_META_DROPSPENSER_FACING_MASK) { case E_META_DROPSPENSER_FACING_YP: return { 0, PickupSpeed, 0 }; case E_META_DROPSPENSER_FACING_YM: return { 0, -PickupSpeed, 0 }; case E_META_DROPSPENSER_FACING_XM: return { -PickupSpeed, 0, 0 }; case E_META_DROPSPENSER_FACING_XP: return { PickupSpeed, 0, 0 }; case E_META_DROPSPENSER_FACING_ZM: return { 0, 0, -PickupSpeed }; case E_META_DROPSPENSER_FACING_ZP: return { 0, 0, PickupSpeed }; } UNREACHABLE("Unsupported DropSpenser direction"); }(); // Where to spawn the pickup. // Y offset is slightly less than half, to accomodate actual texture hole on DropSpenser. const auto HolePosition = Vector3d(0.5, 0.4, 0.5) + dispCoord; auto Pickup = m_Contents.RemoveOneItem(a_SlotNum); m_World->SpawnItemPickup(HolePosition, std::move(Pickup), PickupSpeed, 0_tick); // Spawn pickup with no collection delay. }