// ============================================================ // 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 }; // Continuous-action timing lives on the CALLBACK class (ActionContinuousBaseCB), // not the action - CreateActionComponent() is defined there. The action points // at this CB via m_CallbackClass. CAContinuousTime takes the channel time in // SECONDS. Falls back to 5s on the client, where MainConfig is null (config is // server-only); the server is authoritative on when the channel actually ends. class YALB_OpenLootboxCB : ActionContinuousBaseCB { 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); } }; class YALB_ActionOpenLootbox : ActionContinuousBase { void YALB_ActionOpenLootbox() { m_CallbackClass = YALB_OpenLootboxCB; m_CommandUID = DayZPlayerConstants.CMD_ACTIONMOD_OPENITEM; // "open item" channel anim m_FullBody = false; } override void CreateConditionComponents() { m_ConditionItem = new CCINone(); // no item-in-hands restriction here m_ConditionTarget = new CCTCursor(UAMaxDistances.DEFAULT); // look-at-object targeting } 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; } // 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 = ""; 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": { // AttemptAddBleedingSource(component) needs a valid body-component // bit; GetBleedingSourcesLocations() is the engine's set of those. // Pick random ones (a duplicate just no-ops if already bleeding). BleedingSourcesManagerServer bleedMgr = player.GetBleedingManagerServer(); if (bleedMgr) { set bleedLocs = bleedMgr.GetBleedingSourcesLocations(); if (bleedLocs && bleedLocs.Count() > 0) { for (int bi = 0; bi < chosen.bleedSources; bi++) { int comp = bleedLocs.Get(Math.RandomInt(0, bleedLocs.Count())); bleedMgr.AttemptAddBleedingSource(comp); } } } 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 nearbyPlayers = new array(); 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(message), true, player.GetIdentity()); } // ------------------------------------------------------------ // ACTION REGISTRATION (REQUIRED) // ------------------------------------------------------------ // DayZ does NOT auto-discover custom actions. ActionConstructor // holds a hardcoded list of every action; AddAction() looks an // action up there and silently drops it if missing. So a custom // action MUST be added via a modded ActionConstructor, or it will // never attach to anything (this is why the open prompt was // missing even though the box and SetActions() were correct). // ------------------------------------------------------------ modded class ActionConstructor { override void RegisterActions(TTypenameArray actions) { super.RegisterActions(actions); actions.Insert(YALB_ActionOpenLootbox); } } // ------------------------------------------------------------ // NOTE: the action is attached to the box via SetActions()/ // AddAction() in YALootBoxes_Lootbox.c. ContinuousInteractActionInput // detects actions from the TARGET object you look at, so registering // it on the box (not the held tool) is correct. // ------------------------------------------------------------