Phase 2: structural/completeness fixes (correctness pass)

- config.cpp: fix script/config binding. Crates now inherit a shared
  YALootBoxes_Base config class (name-matches the script class) instead of
  relying on the non-binding simulationModule line. Base class changed from
  non-existent ContainerObject to vanilla Container_Base.
- Lootbox.c: same base class change; replace never-called GetActions
  override with the engine-correct SetActions()/AddAction().
- ActionOpen.c: BROKEN_BONE now uses player.SetBrokenLegs(eBrokenLegs.
  BROKEN_LEGS) instead of bogus GetHealthDamageManager().SetFracture();
  DecreaseHealth health-type arg empty-string changed to "Health" (3 sites).
- Config.c: restructure loot lists from a string-keyed map to a flat array
  of YALB_LootList{type,entries} for reliable JSON deserialization;
  GetLootListForType updated to linear scan; position made a ref array;
  document failEffects.chance as a weight, not a probability.
- loot_lists.json (configfiles.readme): match new array schema.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
smthspec 2026-06-19 21:49:37 +03:00
parent 354dfe34d5
commit a84b6b1a2b
5 changed files with 111 additions and 68 deletions

View file

@ -49,42 +49,55 @@ class CfgMods
};
// ------------------------------------------------------------
// Lootbox entity classes - one per box type defined in
// box_types.json. The "className" field in that JSON file
// MUST exactly match one of these class names.
// Lootbox entity classes.
//
// Swap "ContainerObject" inheritance/model/physics as needed -
// these reuse a vanilla static container model purely as a
// placeholder. Replace 'model=' with your own p3d if you have one.
// BINDING: DayZ binds a CfgVehicles class to a script class by
// NAME. The three crate classes below do NOT each need their own
// script class - instead they all inherit from a shared config
// class "YALootBoxes_Base" whose name matches the script class in
// YALootBoxes_Lootbox.c. The engine walks the config inheritance
// chain upward until it finds a matching script class, so every
// crate resolves to the YALootBoxes_Base script type. The old
// "simulationModule" line did NOT do this and has been removed.
//
// BASE CLASS: these inherit from vanilla "Container_Base" (a real
// lootable container base). "ContainerObject" used previously is
// not a vanilla class. >>> VERIFY against your SDK <<< and, if the
// crate spawns with no cargo space, add a Cargo grid definition
// (e.g. itemsCargoSize[] = {x,y};) to YALootBoxes_Base below - the
// wooden_crate model + Container_Base may need it to hold loot.
//
// MODEL: wooden_crate.p3d path is a placeholder; replace with a
// real p3d you have. >>> VERIFY model path against your SDK <<<
// ------------------------------------------------------------
class CfgVehicles
{
class ContainerObject;
class Container_Base;
class YALootBoxes_Crate_Military : ContainerObject
// Shared, non-spawnable base. Name MUST match the script class
// "YALootBoxes_Base" in YALootBoxes_Lootbox.c for binding.
class YALootBoxes_Base : Container_Base
{
scope = 0;
vehicleClass = "Containers";
model = "\dz\structures\wood\wooden_crate.p3d";
};
class YALootBoxes_Crate_Military : YALootBoxes_Base
{
scope = 2;
displayName = "Military Crate (Locked)";
model = "\dz\structures\wood\wooden_crate.p3d";
vehicleClass = "Containers";
simulationModule = "YALootBoxes_Base";
};
class YALootBoxes_Crate_Medical : ContainerObject
class YALootBoxes_Crate_Medical : YALootBoxes_Base
{
scope = 2;
displayName = "Medical Crate (Locked)";
model = "\dz\structures\wood\wooden_crate.p3d";
vehicleClass = "Containers";
simulationModule = "YALootBoxes_Base";
};
class YALootBoxes_Crate_Industrial : ContainerObject
class YALootBoxes_Crate_Industrial : YALootBoxes_Base
{
scope = 2;
displayName = "Industrial Crate (Locked)";
model = "\dz\structures\wood\wooden_crate.p3d";
vehicleClass = "Containers";
simulationModule = "YALootBoxes_Base";
};
};

View file

@ -81,29 +81,38 @@
// FILE: loot_lists.json
// ------------------------------------------------------------
{
"LootLists": {
"military": [
{ "classname": "AKM", "chance": 0.10, "minCount": 1, "maxCount": 1 },
{ "classname": "Mag_AKM_30Rnd", "chance": 0.40, "minCount": 1, "maxCount": 3 },
{ "classname": "PlateCarrierVest","chance": 0.20, "minCount": 1, "maxCount": 1 },
{ "classname": "TacticalHelmet", "chance": 0.15, "minCount": 1, "maxCount": 1 },
{ "classname": "Ammo_762x39", "chance": 0.50, "minCount": 1, "maxCount": 2 }
],
"medical": [
{ "classname": "Morphine", "chance": 0.30, "minCount": 1, "maxCount": 1 },
{ "classname": "SalineBagIV", "chance": 0.25, "minCount": 1, "maxCount": 2 },
{ "classname": "VitaminBottle", "chance": 0.50, "minCount": 1, "maxCount": 3 },
{ "classname": "Bandage", "chance": 0.60, "minCount": 2, "maxCount": 5 },
{ "classname": "TetracyclineAntibiotics", "chance": 0.20, "minCount": 1, "maxCount": 2 }
],
"industrial": [
{ "classname": "WeldingMask", "chance": 0.15, "minCount": 1, "maxCount": 1 },
{ "classname": "Battery_Car", "chance": 0.30, "minCount": 1, "maxCount": 1 },
{ "classname": "SparkPlug", "chance": 0.40, "minCount": 1, "maxCount": 2 },
{ "classname": "MetalPipe", "chance": 0.50, "minCount": 1, "maxCount": 4 },
{ "classname": "DuctTape", "chance": 0.45, "minCount": 1, "maxCount": 2 }
]
}
"LootLists": [
{
"type": "military",
"entries": [
{ "classname": "AKM", "chance": 0.10, "minCount": 1, "maxCount": 1 },
{ "classname": "Mag_AKM_30Rnd", "chance": 0.40, "minCount": 1, "maxCount": 3 },
{ "classname": "PlateCarrierVest","chance": 0.20, "minCount": 1, "maxCount": 1 },
{ "classname": "TacticalHelmet", "chance": 0.15, "minCount": 1, "maxCount": 1 },
{ "classname": "Ammo_762x39", "chance": 0.50, "minCount": 1, "maxCount": 2 }
]
},
{
"type": "medical",
"entries": [
{ "classname": "Morphine", "chance": 0.30, "minCount": 1, "maxCount": 1 },
{ "classname": "SalineBagIV", "chance": 0.25, "minCount": 1, "maxCount": 2 },
{ "classname": "VitaminBottle", "chance": 0.50, "minCount": 1, "maxCount": 3 },
{ "classname": "Bandage", "chance": 0.60, "minCount": 2, "maxCount": 5 },
{ "classname": "TetracyclineAntibiotics", "chance": 0.20, "minCount": 1, "maxCount": 2 }
]
},
{
"type": "industrial",
"entries": [
{ "classname": "WeldingMask", "chance": 0.15, "minCount": 1, "maxCount": 1 },
{ "classname": "Battery_Car", "chance": 0.30, "minCount": 1, "maxCount": 1 },
{ "classname": "SparkPlug", "chance": 0.40, "minCount": 1, "maxCount": 2 },
{ "classname": "MetalPipe", "chance": 0.50, "minCount": 1, "maxCount": 4 },
{ "classname": "DuctTape", "chance": 0.45, "minCount": 1, "maxCount": 2 }
]
}
]
}

View file

@ -3,9 +3,8 @@
// Custom UserAction: hold-to-open a locked lootbox.
// Lives in: scripts/4_World/YALootBoxes_ActionOpen.c
//
// Registration of this action onto YALootBoxes_Base happens
// via GetActionComponentName/cfgVehicles "actions[]" in config.cpp,
// OR (simpler, used here) by overriding GetActions() — see note
// Registration of this action onto YALootBoxes_Base happens by
// overriding SetActions()/AddAction() on the entity — see note
// at bottom of file.
// ============================================================
@ -144,7 +143,7 @@ void YALB_ApplyRandomFailEffect(PlayerBase player, YALB_BoxTypeDef def, ItemBase
break;
case "HEALTH":
player.DecreaseHealth("", "", chosen.damage);
player.DecreaseHealth("", "Health", chosen.damage);
break;
case "BLEEDING":
@ -159,9 +158,9 @@ void YALB_ApplyRandomFailEffect(PlayerBase player, YALB_BoxTypeDef def, ItemBase
break;
case "BROKEN_BONE":
player.GetHealthDamageManager().SetHealthLimitDamage(true);
player.SetHealth("", "Health", player.GetHealth("", "Health") - 1); // nudge to trigger fracture check
player.GetHealthDamageManager().SetFracture(true);
// 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:
@ -175,7 +174,7 @@ 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("", "", damage);
player.DecreaseHealth("", "Health", damage);
// Optional: damage nearby players too, to sell the "explosion" effect
array<Man> nearbyPlayers = new array<Man>();
@ -188,7 +187,7 @@ void YALB_ApplyExplosiveDamage(PlayerBase player, float damage, float radius)
if (dist <= radius)
{
float falloff = 1.0 - (dist / radius);
other.DecreaseHealth("", "", damage * falloff);
other.DecreaseHealth("", "Health", damage * falloff);
}
}
}
@ -223,13 +222,13 @@ void YALB_NotifyPlayer(PlayerBase player, string message)
// NOTE: Hooking this action onto the lootbox class
// ------------------------------------------------------------
// The cleanest approach for a fully custom action without
// touching vanilla configs is to override GetActions() directly
// touching vanilla configs is to override SetActions() directly
// on YALootBoxes_Base:
//
// override void GetActions(out array<typename> actions)
// override void SetActions()
// {
// actions.Insert(YALB_ActionOpenLootbox);
// super.SetActions();
// AddAction(YALB_ActionOpenLootbox);
// }
//
// This override is included in YALootBoxes_Lootbox.c via a
// partial addition — see the updated lootbox entity file.
// This override lives in YALootBoxes_Lootbox.c.

View file

@ -46,7 +46,9 @@ class YALB_RequiredItem
class YALB_FailEffect
{
string type; // EXPLOSIVE, HEALTH, BLEEDING, SICKNESS, BROKEN_BONE
float chance; // 0.0 - 1.0
float chance; // RELATIVE WEIGHT for the single weighted pick - NOT an
// independent probability. Exactly one effect is chosen
// per failure; an effect's odds = its chance / sum(chances).
float damage; // used by EXPLOSIVE / HEALTH
float radius; // used by EXPLOSIVE
int bleedSources; // used by BLEEDING
@ -116,14 +118,29 @@ class YALB_LootEntry
int maxCount;
};
// One loot list per box "type". Stored as a flat array of {type, entries}
// objects rather than a map<string, array<...>> - DayZ's JsonFileLoader is
// unreliable at deserializing nested generic containers (a map whose values
// are themselves ref arrays), whereas an array of typed objects deserializes
// dependably. Lookup is a small linear scan (only a handful of types).
class YALB_LootList
{
string type;
ref array<ref YALB_LootEntry> entries;
void YALB_LootList()
{
entries = new array<ref YALB_LootEntry>();
}
};
class YALB_LootListsFile
{
// Map: box "type" string -> array of loot entries
ref map<string, ref array<ref YALB_LootEntry>> LootLists;
ref array<ref YALB_LootList> LootLists;
void YALB_LootListsFile()
{
LootLists = new map<string, ref array<ref YALB_LootEntry>>();
LootLists = new array<ref YALB_LootList>();
}
};
@ -133,7 +150,7 @@ class YALB_LootListsFile
class YALB_SpawnLocationDef
{
string name;
array<float> position; // [x, y, z]
ref array<float> position; // [x, y, z] - 'ref' so the array member is owned/allocated
string boxType;
vector GetPosition()
@ -246,9 +263,11 @@ class YALB_ConfigLoader
static array<ref YALB_LootEntry> GetLootListForType(string type)
{
if (!LootLists) return null;
array<ref YALB_LootEntry> list;
if (LootLists.LootLists.Find(type, list))
return list;
foreach (YALB_LootList list : LootLists.LootLists)
{
if (list.type == type)
return list.entries;
}
return null;
}
};

View file

@ -9,7 +9,7 @@
// shared behavior for ALL of them.
// ============================================================
class YALootBoxes_Base extends ContainerObject
class YALootBoxes_Base extends Container_Base
{
protected bool m_IsLocked;
protected string m_BoxTypeId; // matches "type" in box_types.json
@ -147,9 +147,12 @@ class YALootBoxes_Base extends ContainerObject
// Hooks our custom hold-to-open action onto this object without
// needing to touch any vanilla cfgVehicles action arrays.
override void GetActions(out array<typename> actions)
// NOTE: the engine-called override is SetActions()/AddAction(),
// NOT GetActions(out array<typename>) - the latter is never
// invoked by the action manager and silently registers nothing.
override void SetActions()
{
super.GetActions(actions);
actions.Insert(YALB_ActionOpenLootbox);
super.SetActions();
AddAction(YALB_ActionOpenLootbox);
}
};