The script log showed the only remaining error after the module-binding fix: ActionContinuousBase has no GetDuration(ActionData) to override. Continuous action timing is set via a CAContinuousTime component (in seconds) inside CreateActionComponent(). Replaced the GetDuration override accordingly; falls back to 5s on the client where MainConfig is null (config is server-only, server stays authoritative on channel completion). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
250 lines
No EOL
8.6 KiB
C
250 lines
No EOL
8.6 KiB
C
// ============================================================
|
|
// YALootBoxes_ActionOpen.c
|
|
// Custom UserAction: hold-to-open a locked lootbox.
|
|
// Lives in: scripts/4_World/YALootBoxes_ActionOpen.c
|
|
//
|
|
// Registration of this action onto YALootBoxes_Base happens by
|
|
// overriding SetActions()/AddAction() on the entity — see note
|
|
// at bottom of file.
|
|
// ============================================================
|
|
|
|
class YALB_OpenLootboxActionData : ActionData
|
|
{
|
|
// currently empty - base ActionData carries everything we need
|
|
};
|
|
|
|
class YALB_ActionOpenLootbox : ActionContinuousBase
|
|
{
|
|
void YALB_ActionOpenLootbox()
|
|
{
|
|
m_CommandUID = DayZPlayerConstants.CMD_ACTIONFB_ONCE; // generic "use" anim hook
|
|
m_FullBody = false;
|
|
m_ReloadFinishAnim = false;
|
|
}
|
|
|
|
override void CreateConditionComponents()
|
|
{
|
|
m_ConditionItem = new CCINone();
|
|
m_ConditionTarget = new CCTObject(UAMaxDistances.DEFAULT);
|
|
}
|
|
|
|
override string GetText()
|
|
{
|
|
return "Open lootbox";
|
|
}
|
|
|
|
override typename GetInputType()
|
|
{
|
|
return ContinuousInteractActionInput;
|
|
}
|
|
|
|
// Offer the action when the target is our lootbox, it's locked, and the
|
|
// player is holding *some* item. We deliberately do NOT check the specific
|
|
// key item here: ActionCondition runs client-side to display the prompt,
|
|
// but the box-type config (required items) lives in $profile and is loaded
|
|
// server-side only, so GetTypeDef() is null on clients. The exact key-item
|
|
// requirement is enforced authoritatively in OnFinishProgressServer; the
|
|
// proximity "Requires: X" message tells the player what to hold.
|
|
override bool ActionCondition(PlayerBase player, ActionTarget target, ItemBase item)
|
|
{
|
|
YALootBoxes_Base box = YALootBoxes_Base.Cast(target.GetObject());
|
|
if (!box) return false;
|
|
if (!box.IsLocked()) return false;
|
|
|
|
ItemBase handItem = player.GetItemInHands();
|
|
if (!handItem) return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
// Continuous-action duration is set via a CAContinuousTime component (in
|
|
// SECONDS), NOT a GetDuration() override - ActionContinuousBase has no such
|
|
// method. Falls back to 5s on the client, where MainConfig is null (config
|
|
// is server-only); the server is authoritative on when the channel ends.
|
|
override void CreateActionComponent()
|
|
{
|
|
float secs = 5.0;
|
|
if (YALB_ConfigLoader.MainConfig && YALB_ConfigLoader.MainConfig.OpenChannelDurationSeconds > 0)
|
|
secs = YALB_ConfigLoader.MainConfig.OpenChannelDurationSeconds;
|
|
m_ActionData.m_ActionComponent = new CAContinuousTime(secs);
|
|
}
|
|
|
|
// Fired server-side when the channel completes WITHOUT being cancelled.
|
|
override void OnFinishProgressServer(ActionData action_data)
|
|
{
|
|
super.OnFinishProgressServer(action_data);
|
|
|
|
YALootBoxes_Base box = YALootBoxes_Base.Cast(action_data.m_Target.GetObject());
|
|
PlayerBase player = action_data.m_Player;
|
|
if (!box || !player) return;
|
|
|
|
YALB_BoxTypeDef def = box.GetTypeDef();
|
|
if (!def) return;
|
|
|
|
ItemBase handItem = player.GetItemInHands();
|
|
|
|
// Authoritative key-item gate (ActionCondition can't do this on the
|
|
// client - see note there). Wrong/empty item: tell the player what's
|
|
// needed and bail without rolling.
|
|
if (!handItem || !def.IsValidKeyItem(handItem.GetType()))
|
|
{
|
|
YALB_NotifyPlayer(player, "Requires: " + def.GetRequiredItemsDisplayString());
|
|
return;
|
|
}
|
|
|
|
float roll = Math.RandomFloat01();
|
|
if (roll <= def.openChance)
|
|
{
|
|
// SUCCESS
|
|
box.UnlockAndFill();
|
|
if (handItem)
|
|
handItem.DecreaseHealth(def.itemDamageOnOpen, false);
|
|
|
|
YALB_NotifyPlayer(player, "The lock gives way.");
|
|
}
|
|
else
|
|
{
|
|
// FAILURE - apply exactly one weighted-random effect
|
|
YALB_ApplyRandomFailEffect(player, def, handItem);
|
|
YALB_NotifyPlayer(player, "The attempt failed.");
|
|
}
|
|
}
|
|
|
|
// NOTE: early bail-out (per requirements - fixed channel, instant roll
|
|
// at the end, interruption allowed) is handled by ActionContinuousBase
|
|
// itself: releasing the button cancels the action WITHOUT calling
|
|
// OnFinishProgressServer, so no roll/penalty/progress is saved. No
|
|
// override is needed here.
|
|
};
|
|
|
|
// ------------------------------------------------------------
|
|
// Failure-effect resolution
|
|
// ------------------------------------------------------------
|
|
void YALB_ApplyRandomFailEffect(PlayerBase player, YALB_BoxTypeDef def, ItemBase handItem)
|
|
{
|
|
if (!def.failEffects || def.failEffects.Count() == 0) return;
|
|
|
|
// Weighted pick: sum chances, roll within sum, walk the list.
|
|
float totalWeight = 0;
|
|
foreach (YALB_FailEffect e : def.failEffects)
|
|
totalWeight += e.chance;
|
|
|
|
if (totalWeight <= 0) return;
|
|
|
|
float roll = Math.RandomFloat(0, totalWeight);
|
|
float cumulative = 0;
|
|
YALB_FailEffect chosen = null;
|
|
|
|
foreach (YALB_FailEffect effect : def.failEffects)
|
|
{
|
|
cumulative += effect.chance;
|
|
if (roll <= cumulative)
|
|
{
|
|
chosen = effect;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!chosen) return;
|
|
|
|
string playerName = "<unknown>";
|
|
if (player.GetIdentity()) playerName = player.GetIdentity().GetName();
|
|
Log_Verbose(string.Format("Player %1 triggered fail effect '%2' on box type '%3'.",
|
|
playerName, chosen.type, def.type));
|
|
|
|
switch (chosen.type)
|
|
{
|
|
case "EXPLOSIVE":
|
|
YALB_ApplyExplosiveDamage(player, chosen.damage, chosen.radius);
|
|
break;
|
|
|
|
case "HEALTH":
|
|
player.DecreaseHealth("", "Health", chosen.damage);
|
|
break;
|
|
|
|
case "BLEEDING":
|
|
for (int i = 0; i < chosen.bleedSources; i++)
|
|
{
|
|
player.GetBleedingManagerServer().AttemptAddBleedingSourceByMaxCount();
|
|
}
|
|
break;
|
|
|
|
case "SICKNESS":
|
|
YALB_ApplySickness(player, chosen.agent);
|
|
break;
|
|
|
|
case "BROKEN_BONE":
|
|
// Vanilla broken-leg API. The player modifier system drives
|
|
// the HUD icon / forced-prone reaction off this state.
|
|
player.SetBrokenLegs(eBrokenLegs.BROKEN_LEGS);
|
|
break;
|
|
|
|
default:
|
|
Log_Warn("Unknown fail effect type: " + chosen.type);
|
|
break;
|
|
}
|
|
}
|
|
|
|
void YALB_ApplyExplosiveDamage(PlayerBase player, float damage, float radius)
|
|
{
|
|
// Direct damage to the player rather than a real grenade explosion entity,
|
|
// since we don't want a separate live explosive object to manage/clean up.
|
|
vector pos = player.GetPosition();
|
|
player.DecreaseHealth("", "Health", damage);
|
|
|
|
// Optional: damage nearby players too, to sell the "explosion" effect
|
|
array<Man> nearbyPlayers = new array<Man>();
|
|
GetGame().GetWorld().GetPlayerList(nearbyPlayers);
|
|
foreach (Man m : nearbyPlayers)
|
|
{
|
|
PlayerBase other = PlayerBase.Cast(m);
|
|
if (!other || other == player) continue;
|
|
float dist = vector.Distance(other.GetPosition(), pos);
|
|
if (dist <= radius)
|
|
{
|
|
float falloff = 1.0 - (dist / radius);
|
|
other.DecreaseHealth("", "Health", damage * falloff);
|
|
}
|
|
}
|
|
}
|
|
|
|
void YALB_ApplySickness(PlayerBase player, string agentName)
|
|
{
|
|
// Maps to vanilla DayZ agent system (e.g. eAgents.SALMONELLA, eAgents.INFLUENZA)
|
|
int agentId = YALB_AgentNameToId(agentName);
|
|
if (agentId >= 0)
|
|
{
|
|
player.InsertAgent(agentId, 1000); // 1000 = full dose, matches vanilla scale
|
|
}
|
|
}
|
|
|
|
int YALB_AgentNameToId(string agentName)
|
|
{
|
|
if (agentName == "Salmonella") return eAgents.SALMONELLA;
|
|
if (agentName == "Influenza") return eAgents.INFLUENZA;
|
|
if (agentName == "Cholera") return eAgents.CHOLERA;
|
|
if (agentName == "Brain") return eAgents.BRAIN;
|
|
if (agentName == "Wound_Bacterial") return eAgents.WOUND_AGENT;
|
|
return -1;
|
|
}
|
|
|
|
void YALB_NotifyPlayer(PlayerBase player, string message)
|
|
{
|
|
if (!player || !player.GetIdentity()) return;
|
|
GetGame().RPCSingleParam(player, ERPCs.RPC_USER_ACTION_MESSAGE, new Param1<string>(message), true, player.GetIdentity());
|
|
}
|
|
|
|
// ------------------------------------------------------------
|
|
// NOTE: Hooking this action onto the lootbox class
|
|
// ------------------------------------------------------------
|
|
// The cleanest approach for a fully custom action without
|
|
// touching vanilla configs is to override SetActions() directly
|
|
// on YALootBoxes_Base:
|
|
//
|
|
// override void SetActions()
|
|
// {
|
|
// super.SetActions();
|
|
// AddAction(YALB_ActionOpenLootbox);
|
|
// }
|
|
//
|
|
// This override lives in YALootBoxes_Lootbox.c.
|