Searchable furniture (med cabinet, fridge, vending machine) with weighted, config-driven loot tables, and a two-stage repairable sink (repair then gather water). Data-driven registry, in-memory cooldowns/repair state with v2 CF hooks. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
141 lines
4.7 KiB
C
141 lines
4.7 KiB
C
// ============================================================================
|
|
// ActionYASearch
|
|
// ----------------------------------------------------------------------------
|
|
// Generic "Search" action. The target's registry entry (resolved by classname
|
|
// via YAInteractionManager) decides the loot table and the per-object cooldown.
|
|
// No classnames live here - everything comes from config.
|
|
//
|
|
// Cooldown model (v1): per-object, keyed by network id, in a static map.
|
|
// Resets on server restart.
|
|
// TODO (v2): swap the static map for CF persistence in IsOnCooldown()/
|
|
// StampCooldown() - those two methods are the only persistence surface.
|
|
// ============================================================================
|
|
|
|
class ActionYASearchCB : ActionContinuousBaseCB
|
|
{
|
|
override void CreateActionComponent()
|
|
{
|
|
m_ActionData.m_ActionComponent = new CAContinuousTime(YAInteractionConfig.SEARCH_TIME);
|
|
}
|
|
}
|
|
|
|
class ActionYASearch : ActionContinuousBase
|
|
{
|
|
// network id string -> server time (ms) when the cooldown expires.
|
|
protected static ref map<string, int> m_ObjectCooldowns = new map<string, int>();
|
|
|
|
void ActionYASearch()
|
|
{
|
|
m_CallbackClass = ActionYASearchCB;
|
|
m_CommandUID = DayZPlayerConstants.CMD_ACTIONFB_INTERACT; // generic full-body interact anim
|
|
m_FullBody = true;
|
|
m_StanceMask = DayZPlayerConstants.STANCEMASK_ERECT | DayZPlayerConstants.STANCEMASK_CROUCH;
|
|
m_Text = "Search";
|
|
}
|
|
|
|
override typename GetInputType()
|
|
{
|
|
return ContinuousInteractActionInput;
|
|
}
|
|
|
|
override void CreateConditionComponents()
|
|
{
|
|
m_ConditionItem = new CCINone();
|
|
m_ConditionTarget = new CCTNonRuined(UAMaxDistances.DEFAULT);
|
|
}
|
|
|
|
override bool HasTarget()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Empty-handed; target must be a registered "search" interactable.
|
|
override bool ActionCondition(PlayerBase player, ActionTarget target, ItemBase item)
|
|
{
|
|
Object targetObject = target.GetObject();
|
|
if (!targetObject)
|
|
return false;
|
|
|
|
if (player.GetItemInHands())
|
|
return false;
|
|
|
|
YAInteractableEntry entry = YAInteractionManager.GetEntry(targetObject.GetType());
|
|
return entry != null && entry.m_ActionType == YAInteractionConfig.ACTION_SEARCH;
|
|
}
|
|
|
|
override bool IsLockTargetOnUse()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
override void OnFinishProgressServer(ActionData action_data)
|
|
{
|
|
PlayerBase player = action_data.m_Player;
|
|
Object targetObject = action_data.m_Target.GetObject();
|
|
if (!player || !targetObject)
|
|
return;
|
|
|
|
YAInteractableEntry entry = YAInteractionManager.GetEntry(targetObject.GetType());
|
|
if (!entry || entry.m_ActionType != YAInteractionConfig.ACTION_SEARCH)
|
|
return;
|
|
|
|
string key = targetObject.GetNetworkIDString();
|
|
|
|
// --- Per-object cooldown gate ---
|
|
if (IsOnCooldown(key))
|
|
{
|
|
int remaining = GetCooldownRemaining(key);
|
|
Print(string.Format("[YAInteraction] Search on cooldown (%1s left) for %2", remaining, key));
|
|
player.MessageStatus(string.Format("You already searched this. (%1s)", remaining));
|
|
return;
|
|
}
|
|
|
|
// --- Roll & spawn ---
|
|
array<ref YALootEntry> table = YAInteractionManager.GetLootTableForEntry(entry);
|
|
int rolls = Math.RandomIntInclusive(YAInteractionConfig.SEARCH_ROLLS_MIN, YAInteractionConfig.SEARCH_ROLLS_MAX);
|
|
array<EntityAI> spawned = YALootTableManager.GetInstance().SpawnFromTable(table, rolls, player, player.GetPosition());
|
|
|
|
// Stamp cooldown even on an empty result so the object is gated either way.
|
|
StampCooldown(key, entry.m_CooldownSec);
|
|
|
|
Print(string.Format("[YAInteraction] %1 searched '%2' -> %3 item(s)", player.GetIdentity().GetId(), targetObject.GetType(), spawned.Count()));
|
|
|
|
if (spawned.Count() > 0)
|
|
player.MessageStatus(string.Format("You found %1 item(s).", spawned.Count()));
|
|
else
|
|
player.MessageStatus("You found nothing useful.");
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// Cooldown helpers (the only persistence-aware surface area).
|
|
// ------------------------------------------------------------------
|
|
|
|
// TODO (v2): read last-search time from CF storage instead of the static map.
|
|
protected static bool IsOnCooldown(string key)
|
|
{
|
|
int expiry;
|
|
if (!m_ObjectCooldowns.Find(key, expiry))
|
|
return false;
|
|
return GetGame().GetTime() < expiry;
|
|
}
|
|
|
|
protected static int GetCooldownRemaining(string key)
|
|
{
|
|
int expiry;
|
|
if (!m_ObjectCooldowns.Find(key, expiry))
|
|
return 0;
|
|
|
|
int remainingMs = expiry - GetGame().GetTime();
|
|
if (remainingMs < 0)
|
|
remainingMs = 0;
|
|
return remainingMs / 1000;
|
|
}
|
|
|
|
// TODO (v2): persist this stamp via CF so it survives server restarts.
|
|
protected static void StampCooldown(string key, float cooldownSec)
|
|
{
|
|
int expiry = GetGame().GetTime() + (int)(cooldownSec * 1000);
|
|
m_ObjectCooldowns.Set(key, expiry);
|
|
Print(string.Format("[YAInteraction] Search cooldown stamped for %1 (%2s)", key, cooldownSec));
|
|
}
|
|
}
|