From 6a076dc7dc838a650efadc1cfccce7e670398f26 Mon Sep 17 00:00:00 2001 From: pokeunite Date: Mon, 30 Mar 2026 15:13:28 +0000 Subject: [PATCH] Automated Extension submission for issue #1980 --- extensions/community/FireBullet3D.json | 3440 ++++++++++++++++++++++++ 1 file changed, 3440 insertions(+) create mode 100644 extensions/community/FireBullet3D.json diff --git a/extensions/community/FireBullet3D.json b/extensions/community/FireBullet3D.json new file mode 100644 index 000000000..265ea7955 --- /dev/null +++ b/extensions/community/FireBullet3D.json @@ -0,0 +1,3440 @@ +{ + "author": "ByteBard", + "category": "Game mechanic", + "dimension": "3d", + "extensionNamespace": "FireBullet3D", + "fullName": "FireBullet3D Weapon System", + "gdevelopVersion": "", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLWJ1bGxldCIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0xNCwyMkgxMFYyMUgxNFYyMk0xMywxMFY3SDExVjEwTDEwLDExLjVWMjBIMTRWMTEuNUwxMywxME0xMiwyQzEyLDIgMTEsMyAxMSw1VjZIMTNWNUMxMyw1IDEzLDMgMTIsMloiIC8+PC9zdmc+", + "name": "FireBullet3D", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/0b1c1558bd66b570fb751fd2749a92dd080594208c0c30837e47a74bc4b43134_bullet.svg", + "shortDescription": "3D weapon behavior with projectile/hitscan fire, fire modes, ammo/reload systems, spread bloom, advanced ray filters, and pooling.", + "version": "1.1.0", + "description": [ + "Turn any 3D object into a complete weapon system for FPS/TPS projects.", + "", + "Key upgrades in v1.1:", + "- Fire modes (Semi/Auto/Burst) with per-mode RPM.", + "- Ammo cost modes (per trigger or per projectile).", + "- Spread bloom and recovery.", + "- Tactical/chambered reload options + cancel reload action.", + "- Hitscan filtering with optional penetration/ricochet.", + "- Safer save keys with WeaponId + ProfileId.", + "- Optional bullet pooling + recycle action.", + "- Extra expressions: max reserve, cooldown, reload progress, clip full." + ], + "origin": { + "identifier": "FireBullet3D", + "name": "gdevelop-extension-store" + }, + "tags": [ + "3d", + "shooter", + "gun", + "weapon", + "bullet", + "ammo", + "raycast", + "fps", + "mechanics", + "physics" + ], + "authorIds": [ + "4OuGzdcTnhefGk7Yv9A816YpKPo1" + ], + "dependencies": [ + + ], + "globalVariables": [ + + ], + "sceneVariables": [ + { + "name": "Temp", + "type": "number", + "value": 0 + } + ], + "eventsFunctions": [ + { + "description": "Deletes all data in the selected save slot only when confirmation phrase is provided.", + "fullName": "Clear all save data", + "functionType": "Action", + "name": "ClearAllSaveData", + "sentence": "Clear all save data from slot _PARAM1_ if confirm is _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "const saveSlot = eventsFunctionContext.getArgument(\"SaveSlotName\") || \"GameData\";", + "const confirmPhrase = String(eventsFunctionContext.getArgument(\"ConfirmPhrase\") || \"\");", + "if (confirmPhrase !== \"DELETE_ALL_WEAPON_DATA\") {", + " console.warn(\"FireBullet3D: ClearAllSaveData aborted (confirmation phrase mismatch).\");", + " return;", + "}", + "gdjs.evtTools.storage.clearJSONFile(saveSlot);", + "console.log(\"FireBullet3D: Wiped save slot -\u003e \" + saveSlot);" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Save Slot to wipe", + "name": "SaveSlotName", + "type": "string" + }, + { + "description": "Type DELETE_ALL_WEAPON_DATA to confirm", + "name": "ConfirmPhrase", + "type": "string" + } + ], + "objectGroups": [ + + ] + } + ], + "eventsBasedBehaviors": [ + { + "description": "Handles physical bullets, hitscan raycasting, ammo management, reloading logic, and smart-aiming.", + "fullName": "FireBullet3D Weapon System", + "name": "Firebullet3D", + "objectType": "Scene3D::Model3DObject", + "eventsFunctions": [ + { + "fullName": "Do Step Pre Events", + "functionType": "Action", + "name": "doStepPreEvents", + "sentence": "Do step pre events", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "(function(runtimeScene, objects, behaviorName) {", + " if (!objects || objects.length === 0) return;", + " const now = runtimeScene.getTimeManager().getTimeFromStart() / 1000;", + " const dt = runtimeScene.getTimeManager().getElapsedTime() / 1000;", + " for (let i = 0; i \u003c objects.length; i++) {", + " const gunObj = objects[i];", + " if (!gunObj) continue;", + " const behavior = gunObj.getBehavior(behaviorName);", + " if (!behavior) continue;", + " const d = behavior._behaviorData || {};", + " const vars = gunObj.getVariables();", + " const layer = runtimeScene.getLayer(gunObj.getLayer());", + " const layerRenderer = (layer \u0026\u0026 layer.getRenderer) ? layer.getRenderer() : null;", + " const threeScene = (layerRenderer \u0026\u0026 layerRenderer.getThreeScene) ? layerRenderer.getThreeScene() : null;", + " if (threeScene) {", + " const laserName = \"RayDebug_\" + gunObj.id;", + " const laserLine = threeScene.getObjectByName(laserName);", + " if (laserLine) {", + " const deleteAt = (laserLine.userData \u0026\u0026 laserLine.userData.fireBullet3DDeleteAt !== undefined) ? Number(laserLine.userData.fireBullet3DDeleteAt) : -1;", + " const keepDebug = !!d.ShowDebugRay;", + " if (!keepDebug || (deleteAt \u003e= 0 \u0026\u0026 now \u003e= deleteAt)) {", + " if (laserLine.geometry \u0026\u0026 typeof laserLine.geometry.dispose === \"function\") laserLine.geometry.dispose();", + " if (laserLine.material) {", + " if (Array.isArray(laserLine.material)) {", + " for (let m = 0; m \u003c laserLine.material.length; m++) { if (laserLine.material[m] \u0026\u0026 typeof laserLine.material[m].dispose === \"function\") laserLine.material[m].dispose(); }", + " } else if (typeof laserLine.material.dispose === \"function\") {", + " laserLine.material.dispose();", + " }", + " }", + " if (laserLine.parent) laserLine.parent.remove(laserLine);", + " }", + " }", + " }", + " if (!vars.has(\"IsReloading\")) vars.get(\"IsReloading\").setBoolean(false);", + " if (!vars.has(\"ReloadTimer\")) vars.get(\"ReloadTimer\").setNumber(0);", + " if (!vars.has(\"ShootTimer\")) vars.get(\"ShootTimer\").setNumber(0);", + " if (!vars.has(\"IsADS\")) vars.get(\"IsADS\").setBoolean(false);", + " if (!vars.has(\"CurrentHeat\")) vars.get(\"CurrentHeat\").setNumber(0);", + " if (!vars.has(\"IsOverheated\")) vars.get(\"IsOverheated\").setBoolean(false);", + " if (!vars.has(\"IsJammed\")) vars.get(\"IsJammed\").setBoolean(false);", + " if (!vars.has(\"LastRecoilPitch\")) vars.get(\"LastRecoilPitch\").setNumber(0);", + " if (!vars.has(\"LastRecoilYaw\")) vars.get(\"LastRecoilYaw\").setNumber(0);", + " if (!vars.has(\"ManagedBulletObjectName\")) vars.get(\"ManagedBulletObjectName\").setString(\"\");", + " const enableOverheat = !!d.EnableOverheat;", + " const maxHeat = Math.max(0.01, Number((d.MaxHeat === undefined) ? 100 : d.MaxHeat));", + " const heatCooldown = Math.max(0, Number((d.HeatCooldownPerSecond === undefined) ? 20 : d.HeatCooldownPerSecond));", + " const overheatRecover = Math.max(0, Math.min(maxHeat, Number((d.OverheatRecoverThreshold === undefined) ? (maxHeat * 0.35) : d.OverheatRecoverThreshold)));", + " let currentHeat = Number(vars.get(\"CurrentHeat\").getAsNumber() || 0);", + " currentHeat = Math.max(0, currentHeat - (heatCooldown * dt));", + " let isOverheated = vars.get(\"IsOverheated\").getAsBoolean();", + " if (enableOverheat) {", + " if (currentHeat \u003e= maxHeat) isOverheated = true;", + " else if (isOverheated \u0026\u0026 currentHeat \u003c= overheatRecover) isOverheated = false;", + " } else {", + " isOverheated = false;", + " }", + " vars.get(\"CurrentHeat\").setNumber(currentHeat);", + " vars.get(\"IsOverheated\").setBoolean(isOverheated);", + " const bulletMaxLifetime = Math.max(0, Number((d.BulletMaxLifetime === undefined) ? 0 : d.BulletMaxLifetime));", + " const bulletMaxDistance = Math.max(0, Number((d.BulletMaxDistance === undefined) ? 0 : d.BulletMaxDistance));", + " const pooled = (d.EnablePooling === undefined) ? true : !!d.EnablePooling;", + " const managedBulletName = String(vars.get(\"ManagedBulletObjectName\").getAsString() || \"\");", + " if ((bulletMaxLifetime \u003e 0 || bulletMaxDistance \u003e 0) \u0026\u0026 managedBulletName !== \"\" \u0026\u0026 typeof runtimeScene.getObjects === \"function\") {", + " const bullets = runtimeScene.getObjects(managedBulletName) || [];", + " for (let b = 0; b \u003c bullets.length; b++) {", + " const bulletObj = bullets[b]; if (!bulletObj) continue;", + " const bVars = bulletObj.getVariables();", + " if (bVars.has(\"IsActive\") \u0026\u0026 !bVars.get(\"IsActive\").getAsBoolean()) continue;", + " const bornAt = bVars.has(\"FireBullet3DSpawnTime\") ? Number(bVars.get(\"FireBullet3DSpawnTime\").getAsNumber() || now) : now;", + " const sx = bVars.has(\"FireBullet3DStartX\") ? Number(bVars.get(\"FireBullet3DStartX\").getAsNumber() || bulletObj.getX()) : bulletObj.getX();", + " const sy = bVars.has(\"FireBullet3DStartY\") ? Number(bVars.get(\"FireBullet3DStartY\").getAsNumber() || bulletObj.getY()) : bulletObj.getY();", + " const sz = bVars.has(\"FireBullet3DStartZ\") ? Number(bVars.get(\"FireBullet3DStartZ\").getAsNumber() || 0) : 0;", + " const bx = Number((typeof bulletObj.getX === \"function\") ? bulletObj.getX() : sx);", + " const by = Number((typeof bulletObj.getY === \"function\") ? bulletObj.getY() : sy);", + " const bz = Number((typeof bulletObj.getZ === \"function\") ? bulletObj.getZ() : ((typeof bulletObj.getElevation === \"function\") ? bulletObj.getElevation() : sz));", + " const dist = Math.sqrt(((bx - sx) * (bx - sx)) + ((by - sy) * (by - sy)) + ((bz - sz) * (bz - sz)));", + " const expiredByTime = (bulletMaxLifetime \u003e 0) \u0026\u0026 ((now - bornAt) \u003e= bulletMaxLifetime);", + " const expiredByDist = (bulletMaxDistance \u003e 0) \u0026\u0026 (dist \u003e= bulletMaxDistance);", + " if (!(expiredByTime || expiredByDist)) continue;", + " if (pooled) {", + " if (bVars.has(\"IsActive\")) bVars.get(\"IsActive\").setBoolean(false);", + " if (typeof bulletObj.hide === \"function\") bulletObj.hide(true);", + " if (typeof bulletObj.setX === \"function\") bulletObj.setX(-999999);", + " if (typeof bulletObj.setY === \"function\") bulletObj.setY(-999999);", + " if (typeof bulletObj.setZ === \"function\") bulletObj.setZ(-999999); else if (typeof bulletObj.setElevation === \"function\") bulletObj.setElevation(-999999);", + " } else if (typeof bulletObj.deleteFromScene === \"function\") {", + " bulletObj.deleteFromScene(runtimeScene);", + " }", + " }", + " }", + " const useSpreadBloom = (d.UseSpreadBloom === undefined) ? false : !!d.UseSpreadBloom;", + " const baseSpread = useSpreadBloom ? Number(d.BaseSpread || 0) : 0;", + " const maxSpread = useSpreadBloom ? Math.max(baseSpread, Number(d.MaxSpread || 8)) : 0;", + " const spreadRecover = useSpreadBloom ? Math.max(0, Number(d.SpreadRecoveryPerSecond || 0)) : 0;", + " if (!vars.has(\"CurrentSpread\")) vars.get(\"CurrentSpread\").setNumber(0);", + " let currentSpread = Number(vars.get(\"CurrentSpread\").getAsNumber() || 0);", + " if (useSpreadBloom) {", + " currentSpread = Math.max(baseSpread, currentSpread - (spreadRecover * dt));", + " if (currentSpread \u003e maxSpread) currentSpread = maxSpread;", + " } else {", + " currentSpread = 0;", + " }", + " vars.get(\"CurrentSpread\").setNumber(currentSpread);", + " const fireMode = String(d.FireMode || \"Semi\").toLowerCase();", + " const semiRPM = Math.max(1, Number(d.SemiRPM || 420));", + " const autoRPM = Math.max(1, Number(d.AutoRPM || 600));", + " const burstRPM = Math.max(1, Number(d.BurstRPM || 900));", + " const rpm = (fireMode === \"auto\") ? autoRPM : (fireMode === \"burst\" ? burstRPM : semiRPM);", + " const isADS = vars.get(\"IsADS\").getAsBoolean();", + " const adsRateMul = Math.max(0.05, Number((d.ADSFireRateMultiplier === undefined) ? 1 : d.ADSFireRateMultiplier));", + " const effectiveRPM = Math.max(1, rpm * (isADS ? adsRateMul : 1));", + " const fireInterval = 60 / effectiveRPM;", + " let lastShot = Number(vars.get(\"ShootTimer\").getAsNumber() || 0);", + " if (lastShot \u003e now) lastShot = 0;", + " const cooldownRemaining = Math.max(0, fireInterval - (now - lastShot));", + " vars.get(\"CooldownRemaining\").setNumber(cooldownRemaining);", + " const clipSizeDbg = Math.max(1, Number(d.ClipSize || 30));", + " const pelletsDbg = Math.max(1, Number(d.BulletsPerShot || 1));", + " const infiniteAmmoDbg = !!d.InfiniteAmmo;", + " const ammoCostPerShotDbg = Math.max(0, Number(d.AmmoCostPerShot || 1));", + " const ammoCostModeDbg = String(d.AmmoCostMode || \"PerTrigger\").toLowerCase();", + " const ammoCostDbg = infiniteAmmoDbg ? 0 : (ammoCostModeDbg === \"perprojectile\" ? ammoCostPerShotDbg * pelletsDbg : ammoCostPerShotDbg);", + " const currentAmmoDbg = vars.has(\"CurrentAmmo\") ? Number(vars.get(\"CurrentAmmo\").getAsNumber() || 0) : Number((d.CurrentAmmo !== undefined) ? d.CurrentAmmo : clipSizeDbg);", + " const reserveAmmoDbg = vars.has(\"ReserveAmmo\") ? Number(vars.get(\"ReserveAmmo\").getAsNumber() || 0) : Number((d.ReserveAmmo !== undefined) ? d.ReserveAmmo : 90);", + " const rayHitDbg = vars.has(\"RayHitFound\") ? vars.get(\"RayHitFound\").getAsBoolean() : false;", + " const showDebugPanel = !!d.ShowDebugPanel;", + " const debugPanelName = String(d.DebugPanelObjectName || \"\").trim();", + " if (showDebugPanel \u0026\u0026 debugPanelName !== \"\" \u0026\u0026 typeof runtimeScene.getObjects === \"function\") {", + " const panels = runtimeScene.getObjects(debugPanelName) || [];", + " if (panels.length \u003e 0) {", + " const text = \"FM:\" + String(d.FireMode || \"Semi\") + \" ADS:\" + (isADS ? \"Y\" : \"N\") + \" Ammo:\" + Math.round(currentAmmoDbg) + \"/\" + Math.round(reserveAmmoDbg) + \" Cost:\" + ammoCostDbg.toFixed(2) + \"\\nCD:\" + cooldownRemaining.toFixed(3) + \" Spread:\" + currentSpread.toFixed(3) + \" Heat:\" + currentHeat.toFixed(1) + \"/\" + maxHeat.toFixed(1) + \"\\nRay:\" + (rayHitDbg ? \"HIT\" : \"MISS\") + \" Jam:\" + (vars.get(\"IsJammed\").getAsBoolean() ? \"Y\" : \"N\") + \" OHeat:\" + (isOverheated ? \"Y\" : \"N\");", + " for (let p = 0; p \u003c panels.length; p++) {", + " const panel = panels[p]; if (!panel) continue;", + " if (typeof panel.setString === \"function\") panel.setString(text);", + " else if (typeof panel.setText === \"function\") panel.setText(text);", + " }", + " }", + " }", + " const isReloading = vars.get(\"IsReloading\").getAsBoolean();", + " if (!isReloading) {", + " vars.get(\"ReloadProgress\").setNumber(0);", + " continue;", + " }", + " const reloadDuration = Math.max(0.01, Number(d.ReloadDuration || 1));", + " let timer = Number(vars.get(\"ReloadTimer\").getAsNumber() || 0);", + " timer -= dt;", + " vars.get(\"ReloadTimer\").setNumber(timer);", + " vars.get(\"ReloadProgress\").setNumber(Math.max(0, Math.min(1, 1 - (timer / reloadDuration))));", + " if (timer \u003e 0) continue;", + " vars.get(\"IsReloading\").setBoolean(false);", + " if (vars.has(\"IsJammed\")) vars.get(\"IsJammed\").setBoolean(false);", + " const clipSize = Math.max(1, Number(d.ClipSize || 30));", + " const infiniteAmmo = !!d.InfiniteAmmo;", + " const startReserve = (d.ReserveAmmo !== undefined) ? Number(d.ReserveAmmo) : 90;", + " const useChambered = !!d.UseChamberedRound;", + " if (!vars.has(\"CurrentAmmo\")) vars.get(\"CurrentAmmo\").setNumber((d.CurrentAmmo !== undefined) ? Number(d.CurrentAmmo) : clipSize);", + " if (!vars.has(\"ReserveAmmo\")) vars.get(\"ReserveAmmo\").setNumber(startReserve);", + " let current = Number(vars.get(\"CurrentAmmo\").getAsNumber() || 0);", + " let reserve = Number(vars.get(\"ReserveAmmo\").getAsNumber() || 0);", + " let targetClip = clipSize;", + " if (vars.has(\"ReloadTargetClip\")) {", + " targetClip = Math.max(clipSize, Number(vars.get(\"ReloadTargetClip\").getAsNumber() || clipSize));", + " } else if (useChambered \u0026\u0026 current \u003e 0) {", + " targetClip = clipSize + 1;", + " }", + " if (current \u003c targetClip) {", + " if (infiniteAmmo) {", + " current = targetClip;", + " } else if (reserve \u003e 0) {", + " const needed = targetClip - current;", + " const moved = Math.min(needed, reserve);", + " current += moved;", + " reserve -= moved;", + " }", + " }", + " vars.get(\"CurrentAmmo\").setNumber(current);", + " vars.get(\"ReserveAmmo\").setNumber(Math.max(0, reserve));", + " vars.get(\"ReloadProgress\").setNumber(0);", + " vars.get(\"ReloadTargetClip\").setNumber(0);", + " }", + "})(runtimeScene, eventsFunctionContext.getObjects(\"Object\"), eventsFunctionContext.getBehaviorName(\"Behavior\"));" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "FireBullet3D::Firebullet3D", + "type": "behavior" + } + ], + "objectGroups": [ + + ] + }, + { + "description": "Returns true if the backpack is full (Max Capacity).", + "fullName": "Is reserve ammo full", + "functionType": "Condition", + "name": "IsReserveFull", + "sentence": "_PARAM0_ reserve ammo is full", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "// 1. Get Object", + "const objects = eventsFunctionContext.getObjects(\"Object\");", + "if (objects.length === 0) {", + " eventsFunctionContext.returnValue = false;", + " return false;", + "}", + "const gunObj = objects[0];", + "", + "// 2. Get Settings", + "const behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");", + "const behavior = gunObj.getBehavior(behaviorName);", + "", + "// Get Max Limit (Property) - Default to 210 if missing", + "const maxReserve = (behavior._behaviorData.MaxReserveAmmo !== undefined) ? Number(behavior._behaviorData.MaxReserveAmmo) : 210;", + "// Get Start Reserve (Property) - Default to 90", + "const startReserve = (behavior._behaviorData.ReserveAmmo !== undefined) ? Number(behavior._behaviorData.ReserveAmmo) : 90;", + "", + "// 3. Get Current Value from Variable", + "const variables = gunObj.getVariables();", + "let currentReserve = startReserve;", + "", + "if (variables.has(\"ReserveAmmo\")) {", + " currentReserve = variables.get(\"ReserveAmmo\").getAsNumber();", + "}", + "", + "// 4. COMPARE", + "// If current reserve is Equal to (or somehow greater than) Max, we are FULL.", + "if (currentReserve \u003e= maxReserve) {", + " eventsFunctionContext.returnValue = true;", + " return true;", + "}", + "", + "// Otherwise, not full.", + "eventsFunctionContext.returnValue = false;", + "return false;" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "FireBullet3D::Firebullet3D", + "type": "behavior" + } + ], + "objectGroups": [ + + ] + }, + { + "description": "Returns true if the gun is currently performing a reload.", + "fullName": "Is reloading", + "functionType": "Condition", + "name": "IsReloading", + "sentence": "_PARAM0_ is reloading", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "const objects = eventsFunctionContext.getObjects(\"Object\");", + "if (objects.length === 0) { eventsFunctionContext.returnValue = false; return false; }", + "", + "const gunObj = objects[0];", + "const variables = gunObj.getVariables();", + "", + "if (variables.has(\"IsReloading\")) {", + " const isReloading = variables.get(\"IsReloading\").getAsBoolean();", + " eventsFunctionContext.returnValue = isReloading;", + " return isReloading;", + "}", + "", + "eventsFunctionContext.returnValue = false;", + "return false;", + "" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "FireBullet3D::Firebullet3D", + "type": "behavior" + } + ], + "objectGroups": [ + + ] + }, + { + "description": "Checks fire cooldown, reload state, and available ammo cost for the current fire mode.", + "fullName": "Is Gun Ready To Fire", + "functionType": "Condition", + "name": "IsReadyToFire", + "sentence": "_PARAM0_ is ready to fire", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "const objects = eventsFunctionContext.getObjects(\"Object\");", + "if (!objects || objects.length === 0) { eventsFunctionContext.returnValue = false; return false; }", + "const gunObj = objects[0];", + "const behavior = gunObj.getBehavior(eventsFunctionContext.getBehaviorName(\"Behavior\"));", + "if (!behavior) { eventsFunctionContext.returnValue = false; return false; }", + "const d = behavior._behaviorData || {};", + "const vars = gunObj.getVariables();", + "if (vars.has(\"IsReloading\") \u0026\u0026 vars.get(\"IsReloading\").getAsBoolean()) { eventsFunctionContext.returnValue = false; return false; }", + "if (vars.has(\"IsOverheated\") \u0026\u0026 vars.get(\"IsOverheated\").getAsBoolean()) { eventsFunctionContext.returnValue = false; return false; }", + "if (vars.has(\"IsJammed\") \u0026\u0026 vars.get(\"IsJammed\").getAsBoolean()) { eventsFunctionContext.returnValue = false; return false; }", + "const clipSize = Math.max(1, Number(d.ClipSize || 30));", + "const bulletsPerShot = Math.max(1, Number(d.BulletsPerShot || 1));", + "const infiniteAmmo = !!d.InfiniteAmmo;", + "const ammoCostPerShot = Math.max(0, Number(d.AmmoCostPerShot || 1));", + "const ammoCostMode = String(d.AmmoCostMode || \"PerTrigger\").toLowerCase();", + "const ammoCost = infiniteAmmo ? 0 : (ammoCostMode === \"perprojectile\" ? ammoCostPerShot * bulletsPerShot : ammoCostPerShot);", + "let currentAmmo = (d.CurrentAmmo !== undefined) ? Number(d.CurrentAmmo) : clipSize;", + "if (vars.has(\"CurrentAmmo\")) currentAmmo = Number(vars.get(\"CurrentAmmo\").getAsNumber() || 0);", + "if (!infiniteAmmo \u0026\u0026 currentAmmo \u003c ammoCost) { eventsFunctionContext.returnValue = false; return false; }", + "const fireMode = String(d.FireMode || \"Semi\").toLowerCase();", + "const semiRPM = Math.max(1, Number(d.SemiRPM || 420));", + "const autoRPM = Math.max(1, Number(d.AutoRPM || 600));", + "const burstRPM = Math.max(1, Number(d.BurstRPM || 900));", + "const rpm = (fireMode === \"auto\") ? autoRPM : (fireMode === \"burst\" ? burstRPM : semiRPM);", + "const isADS = vars.has(\"IsADS\") ? vars.get(\"IsADS\").getAsBoolean() : false;", + "const adsRateMul = Math.max(0.05, Number((d.ADSFireRateMultiplier === undefined) ? 1 : d.ADSFireRateMultiplier));", + "const effectiveRPM = Math.max(1, rpm * (isADS ? adsRateMul : 1));", + "const fireInterval = 60 / effectiveRPM;", + "const currentTime = runtimeScene.getTimeManager().getTimeFromStart() / 1000;", + "let lastShotTime = vars.has(\"ShootTimer\") ? Number(vars.get(\"ShootTimer\").getAsNumber() || 0) : 0;", + "if (lastShotTime \u003e currentTime) lastShotTime = 0;", + "const ready = (currentTime - lastShotTime) \u003e= fireInterval;", + "eventsFunctionContext.returnValue = ready;", + "return ready;" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "FireBullet3D::Firebullet3D", + "type": "behavior" + } + ], + "objectGroups": [ + + ] + }, + { + "description": "Returns true when auto-reload should begin (empty clip, reserve available, and not already reloading).", + "fullName": "Should Gun Auto Reload", + "functionType": "Condition", + "name": "ShouldAutoReload", + "sentence": "_PARAM0_ should auto-reload", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "const objects = eventsFunctionContext.getObjects(\"Object\");", + "if (!objects || objects.length === 0) { eventsFunctionContext.returnValue = false; return false; }", + "const gunObj = objects[0];", + "const behavior = gunObj.getBehavior(eventsFunctionContext.getBehaviorName(\"Behavior\"));", + "if (!behavior) { eventsFunctionContext.returnValue = false; return false; }", + "const d = behavior._behaviorData || {};", + "const autoReload = !!d.AutoReload;", + "const infiniteAmmo = !!d.InfiniteAmmo;", + "if (!autoReload || infiniteAmmo) { eventsFunctionContext.returnValue = false; return false; }", + "const clipSize = Math.max(1, Number(d.ClipSize || 30));", + "const startReserve = (d.ReserveAmmo !== undefined) ? Number(d.ReserveAmmo) : 90;", + "const vars = gunObj.getVariables();", + "if (vars.has(\"IsReloading\") \u0026\u0026 vars.get(\"IsReloading\").getAsBoolean()) { eventsFunctionContext.returnValue = false; return false; }", + "let currentAmmo = clipSize;", + "if (vars.has(\"CurrentAmmo\")) currentAmmo = Number(vars.get(\"CurrentAmmo\").getAsNumber() || 0);", + "let reserveAmmo = startReserve;", + "if (vars.has(\"ReserveAmmo\")) reserveAmmo = Number(vars.get(\"ReserveAmmo\").getAsNumber() || 0);", + "const shouldReload = currentAmmo \u003c= 0 \u0026\u0026 reserveAmmo \u003e 0;", + "eventsFunctionContext.returnValue = shouldReload;", + "return shouldReload;" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "FireBullet3D::Firebullet3D", + "type": "behavior" + } + ], + "objectGroups": [ + + ] + }, + { + "description": "Spawns physical bullets from the gun with smart aiming, pooling, ammo rules, and bloom spread.", + "fullName": "Spawn bullet", + "functionType": "Action", + "name": "Spawn_Bullet_at_Gun_Position", + "sentence": "Spawn _PARAM2_ from _PARAM0_ aimed at _PARAM6_ (Offset: _PARAM3_, _PARAM4_, _PARAM5_) (Speed: _PARAM7_, Spread: _PARAM8_) (RotFix: _PARAM9_, _PARAM10_, _PARAM11_)", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + + ], + "actions": [ + + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "(function(runtimeScene, eventsFunctionContext) {", + " const gunObjects = eventsFunctionContext.getObjects(\"Object\");", + " const bulletResource = eventsFunctionContext.getObjects(\"The_Bullet\");", + " const bulletLists = (typeof eventsFunctionContext.getObjectsLists === \"function\") ? eventsFunctionContext.getObjectsLists(\"The_Bullet\") : null;", + " const crosshairObjects = eventsFunctionContext.getObjects(\"The_Crosshair\");", + " if (!gunObjects || gunObjects.length === 0) return;", + " const gunObj = gunObjects[0];", + " const behavior = gunObj.getBehavior(eventsFunctionContext.getBehaviorName(\"Behavior\"));", + " if (!behavior) return;", + " const d = behavior._behaviorData || {};", + " const vars = gunObj.getVariables();", + " if (vars.has(\"IsReloading\") \u0026\u0026 vars.get(\"IsReloading\").getAsBoolean()) return;", + " if (!vars.has(\"IsADS\")) vars.get(\"IsADS\").setBoolean(false);", + " if (!vars.has(\"CurrentHeat\")) vars.get(\"CurrentHeat\").setNumber(0);", + " if (!vars.has(\"IsOverheated\")) vars.get(\"IsOverheated\").setBoolean(false);", + " if (!vars.has(\"IsJammed\")) vars.get(\"IsJammed\").setBoolean(false);", + " if (!vars.has(\"LastRecoilPitch\")) vars.get(\"LastRecoilPitch\").setNumber(0);", + " if (!vars.has(\"LastRecoilYaw\")) vars.get(\"LastRecoilYaw\").setNumber(0);", + " if (vars.get(\"IsOverheated\").getAsBoolean() || vars.get(\"IsJammed\").getAsBoolean()) return;", + " const offX = Number(eventsFunctionContext.getArgument(\"OffsetX\") || 0);", + " const offY = Number(eventsFunctionContext.getArgument(\"OffsetY\") || 0);", + " const offZ = Number(eventsFunctionContext.getArgument(\"OffsetZ\") || 0);", + " const speed = Number(eventsFunctionContext.getArgument(\"BulletSpeed\") || 0);", + " const spreadArg = Number(eventsFunctionContext.getArgument(\"Spread\") || 0);", + " const fixRotX = Number(eventsFunctionContext.getArgument(\"ModelRotX\") || 0);", + " const fixRotY = Number(eventsFunctionContext.getArgument(\"ModelRotY\") || 0);", + " const fixRotZ = Number(eventsFunctionContext.getArgument(\"ModelRotZ\") || 0);", + " let bulletName = (bulletResource \u0026\u0026 bulletResource.length \u003e 0) ? String(bulletResource[0].getName() || \"\") : \"\";", + " if (!bulletName \u0026\u0026 bulletLists \u0026\u0026 bulletLists.items) {", + " for (const objName in bulletLists.items) {", + " if (Object.prototype.hasOwnProperty.call(bulletLists.items, objName)) { bulletName = objName; break; }", + " }", + " }", + " if (!bulletName) {", + " console.warn(\"FireBullet3D: Could not resolve bullet object name from The_Bullet parameter.\");", + " return;", + " }", + " vars.get(\"ManagedBulletObjectName\").setString(bulletName);", + " const baseDamage = Math.max(0, Number((d.Damage === undefined) ? 25 : d.Damage));", + " const fireMode = String(d.FireMode || \"Semi\").toLowerCase();", + " const semiRPM = Math.max(1, Number(d.SemiRPM || 420));", + " const autoRPM = Math.max(1, Number(d.AutoRPM || 600));", + " const burstRPM = Math.max(1, Number(d.BurstRPM || 900));", + " const rpm = (fireMode === \"auto\") ? autoRPM : (fireMode === \"burst\" ? burstRPM : semiRPM);", + " const isADS = vars.get(\"IsADS\").getAsBoolean();", + " const adsRateMul = Math.max(0.05, Number((d.ADSFireRateMultiplier === undefined) ? 1 : d.ADSFireRateMultiplier));", + " const effectiveRPM = Math.max(1, rpm * (isADS ? adsRateMul : 1));", + " const fireInterval = 60 / effectiveRPM;", + " const now = runtimeScene.getTimeManager().getTimeFromStart() / 1000;", + " if (!vars.has(\"ShootTimer\")) vars.get(\"ShootTimer\").setNumber(0);", + " if (!vars.has(\"LastShotSyncTime\")) vars.get(\"LastShotSyncTime\").setNumber(-1);", + " if (!vars.has(\"LastShotSyncSource\")) vars.get(\"LastShotSyncSource\").setString(\"\");", + " if (!vars.has(\"LastShotSyncLinkedUsed\")) vars.get(\"LastShotSyncLinkedUsed\").setBoolean(false);", + " const syncWindow = 0.03;", + " const lastSyncTime = Number(vars.get(\"LastShotSyncTime\").getAsNumber() || -1);", + " const lastSyncSource = String(vars.get(\"LastShotSyncSource\").getAsString() || \"\");", + " const lastSyncLinkedUsed = vars.get(\"LastShotSyncLinkedUsed\").getAsBoolean();", + " const callSource = \"projectile\";", + " const linkedDualShot = (lastSyncSource !== \"\" \u0026\u0026 lastSyncSource !== callSource \u0026\u0026 now \u003e= lastSyncTime \u0026\u0026 (now - lastSyncTime) \u003c= syncWindow \u0026\u0026 !lastSyncLinkedUsed);", + " let lastShot = Number(vars.get(\"ShootTimer\").getAsNumber() || 0);", + " if (lastShot \u003e now) lastShot = 0;", + " if (!linkedDualShot \u0026\u0026 (now - lastShot) \u003c fireInterval) return;", + " const clipSize = Math.max(1, Number(d.ClipSize || 30));", + " const bulletsPerShot = Math.max(1, Number(d.BulletsPerShot || 1));", + " const infiniteAmmo = !!d.InfiniteAmmo;", + " const ammoCostPerShot = Math.max(0, Number(d.AmmoCostPerShot || 1));", + " const ammoCostMode = String(d.AmmoCostMode || \"PerTrigger\").toLowerCase();", + " const ammoCost = infiniteAmmo ? 0 : (ammoCostMode === \"perprojectile\" ? ammoCostPerShot * bulletsPerShot : ammoCostPerShot);", + " const startAmmo = (d.CurrentAmmo !== undefined) ? Number(d.CurrentAmmo) : clipSize;", + " if (!vars.has(\"CurrentAmmo\")) vars.get(\"CurrentAmmo\").setNumber(startAmmo);", + " let currentAmmo = Number(vars.get(\"CurrentAmmo\").getAsNumber() || 0);", + " const enableJam = !!d.EnableJam;", + " const maxHeat = Math.max(0.01, Number((d.MaxHeat === undefined) ? 100 : d.MaxHeat));", + " const jamMinRatio = Math.max(0, Math.min(1, Number((d.JamMinHeatRatio === undefined) ? 0.5 : d.JamMinHeatRatio)));", + " const jamChanceAtMax = Math.max(0, Math.min(1, Number((d.JamChanceAtMaxHeat === undefined) ? 0.08 : d.JamChanceAtMaxHeat)));", + " const currentHeat = Number(vars.get(\"CurrentHeat\").getAsNumber() || 0);", + " if (!linkedDualShot \u0026\u0026 enableJam \u0026\u0026 jamChanceAtMax \u003e 0) {", + " const heatRatio = Math.max(0, Math.min(1, currentHeat / maxHeat));", + " if (heatRatio \u003e= jamMinRatio) {", + " const norm = (1 - jamMinRatio) \u003e 0.0001 ? ((heatRatio - jamMinRatio) / (1 - jamMinRatio)) : 1;", + " const jamChance = jamChanceAtMax * Math.max(0, Math.min(1, norm));", + " if (Math.random() \u003c jamChance) { vars.get(\"IsJammed\").setBoolean(true); return; }", + " }", + " }", + " if (!linkedDualShot \u0026\u0026 !infiniteAmmo \u0026\u0026 currentAmmo \u003c ammoCost) return;", + " if (!linkedDualShot \u0026\u0026 !infiniteAmmo) vars.get(\"CurrentAmmo\").setNumber(Math.max(0, currentAmmo - ammoCost));", + " if (!linkedDualShot) {", + " vars.get(\"ShootTimer\").setNumber(now);", + " vars.get(\"LastShotSyncTime\").setNumber(now);", + " vars.get(\"LastShotSyncSource\").setString(callSource);", + " vars.get(\"LastShotSyncLinkedUsed\").setBoolean(false);", + " } else {", + " vars.get(\"LastShotSyncLinkedUsed\").setBoolean(true);", + " }", + " if (!linkedDualShot) {", + " const enableOverheat = !!d.EnableOverheat;", + " const heatPerShot = Math.max(0, Number((d.HeatPerShot === undefined) ? 8 : d.HeatPerShot));", + " let heat = Number(vars.get(\"CurrentHeat\").getAsNumber() || 0);", + " if (enableOverheat \u0026\u0026 heatPerShot \u003e 0) {", + " const maxH = Math.max(0.01, Number((d.MaxHeat === undefined) ? 100 : d.MaxHeat));", + " heat = Math.min(maxH, heat + heatPerShot);", + " vars.get(\"CurrentHeat\").setNumber(heat);", + " if (heat \u003e= maxH) vars.get(\"IsOverheated\").setBoolean(true);", + " }", + " const adsRecoilMul = Math.max(0, Number((d.ADSRecoilMultiplier === undefined) ? 0.7 : d.ADSRecoilMultiplier));", + " const recoilMul = isADS ? adsRecoilMul : 1;", + " const rPitchMin = Number((d.RecoilPitchMin === undefined) ? 0.25 : d.RecoilPitchMin);", + " const rPitchMax = Number((d.RecoilPitchMax === undefined) ? 0.65 : d.RecoilPitchMax);", + " const rYawMin = Number((d.RecoilYawMin === undefined) ? -0.3 : d.RecoilYawMin);", + " const rYawMax = Number((d.RecoilYawMax === undefined) ? 0.3 : d.RecoilYawMax);", + " const recoilPitch = (Math.min(rPitchMin, rPitchMax) + Math.random() * Math.abs(rPitchMax - rPitchMin)) * recoilMul;", + " const recoilYaw = (Math.min(rYawMin, rYawMax) + Math.random() * Math.abs(rYawMax - rYawMin)) * recoilMul;", + " vars.get(\"LastRecoilPitch\").setNumber(recoilPitch);", + " vars.get(\"LastRecoilYaw\").setNumber(recoilYaw);", + " }", + " const useSpreadBloom = (d.UseSpreadBloom === undefined) ? false : !!d.UseSpreadBloom;", + " const baseSpread = useSpreadBloom ? Number(d.BaseSpread || 0) : 0;", + " const maxSpread = useSpreadBloom ? Math.max(baseSpread, Number(d.MaxSpread || 8)) : 0;", + " const spreadPerShot = useSpreadBloom ? Math.max(0, Number(d.SpreadPerShot || 0)) : 0;", + " if (!vars.has(\"CurrentSpread\")) vars.get(\"CurrentSpread\").setNumber(0);", + " let currentSpread = useSpreadBloom ? Number(vars.get(\"CurrentSpread\").getAsNumber() || 0) : 0;", + " const adsSpreadMul = Math.max(0, Number((d.ADSSpreadMultiplier === undefined) ? 0.7 : d.ADSSpreadMultiplier));", + " const spreadMul = isADS ? adsSpreadMul : 1;", + " const totalSpread = Math.max(0, (spreadArg + baseSpread + currentSpread) * spreadMul);", + " if (useSpreadBloom \u0026\u0026 !linkedDualShot) vars.get(\"CurrentSpread\").setNumber(Math.min(maxSpread, currentSpread + spreadPerShot));", + " const gunRenderer = gunObj.get3DRendererObject();", + " if (!gunRenderer) return;", + " const layerName = gunObj.getLayer();", + " const layer = runtimeScene.getLayer(layerName);", + " const layerRenderer = layer.getRenderer();", + " if (!layerRenderer || !layerRenderer.getThreeScene || !layerRenderer.getThreeCamera) return;", + " const threeScene = layerRenderer.getThreeScene();", + " const camera = layerRenderer.getThreeCamera();", + " const gunPosWorld = new THREE.Vector3();", + " const gunQuat = new THREE.Quaternion();", + " gunRenderer.getWorldPosition(gunPosWorld);", + " gunRenderer.getWorldQuaternion(gunQuat);", + " const offsetWorld = new THREE.Vector3(offX, offY, offZ).applyQuaternion(gunQuat);", + " gunPosWorld.add(offsetWorld);", + " const camRay = new THREE.Raycaster();", + " camRay.far = 5000;", + " if (crosshairObjects \u0026\u0026 crosshairObjects.length \u003e 0) {", + " const crosshair = crosshairObjects[0];", + " const uiLayer = runtimeScene.getLayer(crosshair.getLayer());", + " const width = uiLayer.getCameraWidth();", + " const height = uiLayer.getCameraHeight();", + " const cX = crosshair.getCenterXInScene();", + " const cY = crosshair.getCenterYInScene();", + " const pointer = new THREE.Vector2((cX / width) * 2 - 1, - (cY / height) * 2 + 1);", + " camRay.setFromCamera(pointer, camera);", + " } else {", + " const camPos = new THREE.Vector3();", + " const camDir = new THREE.Vector3();", + " camera.getWorldPosition(camPos);", + " camera.getWorldDirection(camDir);", + " camRay.set(camPos, camDir);", + " }", + " const cameraIgnoreDist = Math.max(0, Number(d.CameraSelfIgnoreDistance || 150));", + " const camHits = camRay.intersectObjects(threeScene.children, true);", + " let targetWorld = new THREE.Vector3();", + " camRay.ray.at(5000, targetWorld);", + " for (let i = 0; i \u003c camHits.length; i++) {", + " const h = camHits[i]; const o = h.object;", + " if (!o || !o.visible || o.isLine || o.isPoints) continue;", + " if (o === gunRenderer || o.parent === gunRenderer) continue;", + " if (h.distance \u003c cameraIgnoreDist) continue;", + " targetWorld.copy(h.point); break;", + " }", + " const gunPos = gunPosWorld.clone();", + " threeScene.worldToLocal(gunPos);", + " threeScene.worldToLocal(targetWorld);", + " const gravityScale = (d.GravityScale !== undefined) ? Number(d.GravityScale) : 0;", + " const enablePooling = (d.EnablePooling === undefined) ? true : !!d.EnablePooling;", + " const setObjectVisible = (bulletObj, bulletRenderer) =\u003e {", + " if (typeof bulletObj.hide === \"function\") bulletObj.hide(false);", + " if (typeof bulletObj.setHidden === \"function\") bulletObj.setHidden(false);", + " if (typeof bulletObj.setVisible === \"function\") bulletObj.setVisible(true);", + " if (bulletRenderer) {", + " bulletRenderer.visible = true;", + " if (typeof bulletRenderer.traverse === \"function\") bulletRenderer.traverse((node) =\u003e { if (node) node.visible = true; });", + " }", + " };", + " const tryApplyPhysicsBehavior = (pb, velocityVec) =\u003e {", + " if (!pb) return false;", + " if (typeof pb.setGravityScale === \"function\") pb.setGravityScale(gravityScale);", + " if (typeof pb.setLinearDamping === \"function\") pb.setLinearDamping(0);", + " if (typeof pb.setAngularDamping === \"function\") pb.setAngularDamping(0);", + " if (typeof pb.activate === \"function\") pb.activate();", + " if (typeof pb.setLinearVelocityX === \"function\") { pb.setLinearVelocityX(velocityVec.x); pb.setLinearVelocityY(velocityVec.y); if (typeof pb.setLinearVelocityZ === \"function\") pb.setLinearVelocityZ(velocityVec.z); return true; }", + " if (typeof pb.setLinearVelocity === \"function\") { try { pb.setLinearVelocity(velocityVec.x, velocityVec.y, velocityVec.z); } catch (e) { pb.setLinearVelocity(velocityVec.x, velocityVec.y); } return true; }", + " if (typeof pb.setVelocity === \"function\") { pb.setVelocity(velocityVec.x, velocityVec.y); return true; }", + " return false;", + " };", + " const applyPhysics = (bulletObj, velocityVec) =\u003e {", + " const names = [\"Physics2\",\"Physics3D\",\"Physics3\",\"Physics\"];", + " for (let i = 0; i \u003c names.length; i++) {", + " let pb = null; try { pb = bulletObj.getBehavior(names[i]); } catch (e) { pb = null; }", + " if (tryApplyPhysicsBehavior(pb, velocityVec)) return true;", + " }", + " const bag = bulletObj._behaviors;", + " if (bag) {", + " if (Array.isArray(bag)) {", + " for (let i = 0; i \u003c bag.length; i++) { if (tryApplyPhysicsBehavior(bag[i], velocityVec)) return true; }", + " } else {", + " for (const key in bag) { if (Object.prototype.hasOwnProperty.call(bag, key) \u0026\u0026 tryApplyPhysicsBehavior(bag[key], velocityVec)) return true; }", + " }", + " }", + " return false;", + " };", + " for (let i = 0; i \u003c bulletsPerShot; i++) {", + " const direction = new THREE.Vector3().subVectors(targetWorld, gunPos).normalize();", + " if (totalSpread \u003e 0) {", + " const spreadScale = totalSpread * 0.01;", + " direction.x += (Math.random() - 0.5) * spreadScale;", + " direction.y += (Math.random() - 0.5) * spreadScale;", + " direction.z += (Math.random() - 0.5) * spreadScale;", + " direction.normalize();", + " }", + " const lookTarget = new THREE.Vector3().copy(gunPos).add(direction.clone().multiplyScalar(2000));", + " const lookMatrix = new THREE.Matrix4().lookAt(gunPos, lookTarget, new THREE.Vector3(0, 1, 0));", + " const finalQuat = new THREE.Quaternion().setFromRotationMatrix(lookMatrix);", + " if (fixRotX !== 0 || fixRotY !== 0 || fixRotZ !== 0) {", + " const correctionEuler = new THREE.Euler(THREE.MathUtils.degToRad(fixRotX), THREE.MathUtils.degToRad(fixRotY), THREE.MathUtils.degToRad(fixRotZ), \"XYZ\");", + " finalQuat.multiply(new THREE.Quaternion().setFromEuler(correctionEuler));", + " }", + " let newBullet = null;", + " if (enablePooling \u0026\u0026 typeof runtimeScene.getObjects === \"function\") {", + " const existing = runtimeScene.getObjects(bulletName) || [];", + " for (let pIdx = 0; pIdx \u003c existing.length; pIdx++) {", + " const candidate = existing[pIdx]; if (!candidate) continue;", + " const cv = candidate.getVariables();", + " if (cv.has(\"IsActive\") \u0026\u0026 !cv.get(\"IsActive\").getAsBoolean()) { newBullet = candidate; break; }", + " }", + " }", + " if (!newBullet) {", + " newBullet = runtimeScene.createObject(bulletName);", + " if (!newBullet) {", + " console.warn(\"FireBullet3D: createObject failed for bullet \u0027\" + bulletName + \"\u0027.\");", + " continue;", + " }", + " }", + " newBullet.setLayer(layerName);", + " const bVars = newBullet.getVariables();", + " if (bVars.has(\"IsActive\")) bVars.get(\"IsActive\").setBoolean(true);", + " bVars.get(\"Damage\").setNumber(baseDamage);", + " bVars.get(\"FireBullet3DDamage\").setNumber(baseDamage);", + " bVars.get(\"FireBullet3DSpawnTime\").setNumber(now);", + " bVars.get(\"FireBullet3DStartX\").setNumber(gunPos.x);", + " bVars.get(\"FireBullet3DStartY\").setNumber(gunPos.y);", + " bVars.get(\"FireBullet3DStartZ\").setNumber(gunPos.z);", + " if (typeof newBullet.hide === \"function\") newBullet.hide(false);", + " newBullet.setX(gunPos.x);", + " newBullet.setY(gunPos.y);", + " if (typeof newBullet.setZ === \"function\") newBullet.setZ(gunPos.z); else if (typeof newBullet.setElevation === \"function\") newBullet.setElevation(gunPos.z);", + " const euler = new THREE.Euler().setFromQuaternion(finalQuat, \"YXZ\");", + " const rotX = THREE.MathUtils.radToDeg(euler.x); const rotY = THREE.MathUtils.radToDeg(euler.y); const rotZ = THREE.MathUtils.radToDeg(euler.z);", + " if (typeof newBullet.setRotationX === \"function\") { newBullet.setRotationX(rotX); newBullet.setRotationY(rotY); if (typeof newBullet.setRotationZ === \"function\") newBullet.setRotationZ(rotZ); else newBullet.setAngle(rotZ); } else { newBullet.setAngle(rotY); }", + " const bulletRenderer = newBullet.get3DRendererObject();", + " if (bulletRenderer) { bulletRenderer.position.copy(gunPos); bulletRenderer.quaternion.copy(finalQuat); bulletRenderer.updateMatrix(); bulletRenderer.updateMatrixWorld(true); }", + " setObjectVisible(newBullet, bulletRenderer);", + " if (speed \u003e 0) {", + " const velocityVec = direction.clone().multiplyScalar(speed);", + " applyPhysics(newBullet, velocityVec);", + " }", + " }", + "})(runtimeScene, eventsFunctionContext);" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "FireBullet3D::Firebullet3D", + "type": "behavior" + }, + { + "description": "The bullet object", + "name": "The_Bullet", + "supplementaryInformation": "Scene3D::Model3DObject", + "type": "objectList" + }, + { + "description": "(Forward/Back)", + "name": "OffsetX", + "type": "expression" + }, + { + "description": "(Right/Left)", + "name": "OffsetY", + "type": "expression" + }, + { + "description": "(Up/Down)", + "name": "OffsetZ", + "type": "expression" + }, + { + "description": "The UI crosshair sprite", + "name": "The_Crosshair", + "type": "objectList" + }, + { + "description": "Bullet Speed (Physics)", + "name": "BulletSpeed", + "type": "expression" + }, + { + "description": "Accuracy Spread (0 = Perfect)", + "name": "Spread", + "type": "expression" + }, + { + "description": "ModelRotX", + "name": "ModelRotX", + "type": "expression" + }, + { + "description": "ModelRotY", + "name": "ModelRotY", + "type": "expression" + }, + { + "description": "ModelRotZ", + "name": "ModelRotZ", + "type": "expression" + } + ], + "objectGroups": [ + + ] + }, + { + "description": "Saves the Current and Reserve ammo to the device storage.", + "fullName": "Save Weapon Ammo", + "functionType": "Action", + "name": "SaveWeaponData", + "sentence": "Save _PARAM0_ ammo to slot _PARAM2_ for profile _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + + ], + "actions": [ + + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "const objects = eventsFunctionContext.getObjects(\"Object\");", + "if (!objects || objects.length === 0) return;", + "const gunObj = objects[0];", + "const behavior = gunObj.getBehavior(eventsFunctionContext.getBehaviorName(\"Behavior\"));", + "if (!behavior) return;", + "const d = behavior._behaviorData || {};", + "const saveSlot = eventsFunctionContext.getArgument(\"SaveSlotName\") || \"GameData\";", + "const profile = String(eventsFunctionContext.getArgument(\"ProfileId\") || d.DefaultProfileId || \"Player1\").trim();", + "const weaponId = String(d.WeaponId || \"\").trim() || gunObj.getName();", + "const keyRoot = profile + \"_\" + weaponId;", + "const vars = gunObj.getVariables();", + "const current = vars.has(\"CurrentAmmo\") ? Number(vars.get(\"CurrentAmmo\").getAsNumber() || 0) : Number(d.CurrentAmmo || d.ClipSize || 30);", + "const reserve = vars.has(\"ReserveAmmo\") ? Number(vars.get(\"ReserveAmmo\").getAsNumber() || 0) : Number(d.ReserveAmmo || 90);", + "gdjs.evtTools.storage.writeNumberInJSONFile(saveSlot, keyRoot + \"_Current\", current);", + "gdjs.evtTools.storage.writeNumberInJSONFile(saveSlot, keyRoot + \"_Reserve\", reserve);" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "FireBullet3D::Firebullet3D", + "type": "behavior" + }, + { + "description": "Save Slot Name (e.g. Save1)", + "name": "SaveSlotName", + "type": "string" + }, + { + "description": "Profile Id (optional)", + "name": "ProfileId", + "type": "string" + } + ], + "objectGroups": [ + + ] + }, + { + "description": "Removes the saved ammo data for this specific weapon.", + "fullName": "Delete Weapon Data", + "functionType": "Action", + "name": "DeleteWeaponData", + "sentence": "Delete _PARAM0_ save data from slot _PARAM2_ for profile _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "const objects = eventsFunctionContext.getObjects(\"Object\");", + "if (!objects || objects.length === 0) return;", + "const gunObj = objects[0];", + "const behavior = gunObj.getBehavior(eventsFunctionContext.getBehaviorName(\"Behavior\"));", + "if (!behavior) return;", + "const d = behavior._behaviorData || {};", + "const saveSlot = eventsFunctionContext.getArgument(\"SaveSlotName\") || \"GameData\";", + "const profile = String(eventsFunctionContext.getArgument(\"ProfileId\") || d.DefaultProfileId || \"Player1\").trim();", + "const weaponId = String(d.WeaponId || \"\").trim() || gunObj.getName();", + "const keyRoot = profile + \"_\" + weaponId;", + "gdjs.evtTools.storage.deleteElementFromJSONFile(saveSlot, keyRoot + \"_Current\");", + "gdjs.evtTools.storage.deleteElementFromJSONFile(saveSlot, keyRoot + \"_Reserve\");" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "FireBullet3D::Firebullet3D", + "type": "behavior" + }, + { + "description": "Save slot name", + "name": "SaveSlotName", + "type": "string" + }, + { + "description": "Profile Id (optional)", + "name": "ProfileId", + "type": "string" + } + ], + "objectGroups": [ + + ] + }, + { + "description": "Loads the ammo counts from storage (if they exist).", + "fullName": "Load Weapon Ammo", + "functionType": "Action", + "name": "LoadWeaponData", + "sentence": "Load _PARAM0_ ammo from slot _PARAM2_ for profile _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "const objects = eventsFunctionContext.getObjects(\"Object\");", + "if (!objects || objects.length === 0) return;", + "const gunObj = objects[0];", + "const behavior = gunObj.getBehavior(eventsFunctionContext.getBehaviorName(\"Behavior\"));", + "if (!behavior) return;", + "const d = behavior._behaviorData || {};", + "const clipSize = Math.max(1, Number(d.ClipSize || 30));", + "const startAmmo = (d.CurrentAmmo !== undefined) ? Number(d.CurrentAmmo) : clipSize;", + "const defaultReserve = (d.ReserveAmmo !== undefined) ? Number(d.ReserveAmmo) : 90;", + "const saveSlot = eventsFunctionContext.getArgument(\"SaveSlotName\") || \"GameData\";", + "const profile = String(eventsFunctionContext.getArgument(\"ProfileId\") || d.DefaultProfileId || \"Player1\").trim();", + "const weaponId = String(d.WeaponId || \"\").trim() || gunObj.getName();", + "const keyRoot = profile + \"_\" + weaponId;", + "const vars = gunObj.getVariables();", + "if (!vars.has(\"CurrentAmmo\")) vars.get(\"CurrentAmmo\").setNumber(startAmmo);", + "if (!vars.has(\"ReserveAmmo\")) vars.get(\"ReserveAmmo\").setNumber(defaultReserve);", + "const tempVar = new gdjs.Variable({ type: \"number\", value: -9999 });", + "gdjs.evtTools.storage.readNumberFromJSONFile(saveSlot, keyRoot + \"_Current\", runtimeScene, tempVar);", + "if (tempVar.getAsNumber() !== -9999) vars.get(\"CurrentAmmo\").setNumber(tempVar.getAsNumber());", + "tempVar.setNumber(-9999);", + "gdjs.evtTools.storage.readNumberFromJSONFile(saveSlot, keyRoot + \"_Reserve\", runtimeScene, tempVar);", + "if (tempVar.getAsNumber() !== -9999) vars.get(\"ReserveAmmo\").setNumber(tempVar.getAsNumber());" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "FireBullet3D::Firebullet3D", + "type": "behavior" + }, + { + "description": "Save Slot Name", + "name": "SaveSlotName", + "type": "string" + }, + { + "description": "Profile Id (optional)", + "name": "ProfileId", + "type": "string" + } + ], + "objectGroups": [ + + ] + }, + { + "description": "Spawns a visual effect at the gun barrel.", + "fullName": "Spawn muzzle flash", + "functionType": "Action", + "name": "Spawn_Muzzle_Flash", + "sentence": "Spawn muzzle flash _PARAM2_ on _PARAM0_ (Scale: _PARAM3_)", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "(function(runtimeScene, eventsFunctionContext) {", + " // 1. GET OBJECTS", + " // Use \"Object\" because this is a behavior function attached to the Gun", + " const gunObjects = eventsFunctionContext.getObjects(\"Object\"); ", + " const flashResource = eventsFunctionContext.getObjects(\"Flash_Object\"); ", + " ", + " // ARGS", + " const offX = eventsFunctionContext.getArgument(\"OffsetX\") || 0;", + " const offY = eventsFunctionContext.getArgument(\"OffsetY\") || 0;", + " const offZ = eventsFunctionContext.getArgument(\"OffsetZ\") || 0;", + " const scale = eventsFunctionContext.getArgument(\"Scale\") || 1;", + "", + " if (gunObjects.length === 0 || flashResource.length === 0) return;", + "", + " const gunObj = gunObjects[0];", + " const flashName = flashResource[0].getName(); ", + "", + " // 2. GET POSITIONS", + " const gunRenderer = gunObj.get3DRendererObject();", + " if (!gunRenderer) return;", + "", + " const layerName = gunObj.getLayer();", + " const layer = runtimeScene.getLayer(layerName);", + " const layerRenderer = layer.getRenderer();", + " ", + " if (!layerRenderer || !layerRenderer.getThreeScene) return;", + " const threeScene = layerRenderer.getThreeScene();", + "", + " const gunPos = new THREE.Vector3();", + " const gunQuat = new THREE.Quaternion();", + "", + " gunRenderer.getWorldPosition(gunPos);", + " gunRenderer.getWorldQuaternion(gunQuat);", + "", + " // Apply Offsets", + " const offsetVector = new THREE.Vector3(offX, offY, offZ);", + " offsetVector.applyQuaternion(gunQuat);", + " gunPos.add(offsetVector);", + "", + " // 3. CONVERT TO LOCAL", + " threeScene.worldToLocal(gunPos);", + "", + " // 4. SPAWN FLASH", + " const newFlash = runtimeScene.createObject(flashName);", + "", + " if (newFlash) {", + " newFlash.setLayer(layerName);", + " ", + " // Position", + " newFlash.setX(gunPos.x);", + " newFlash.setY(gunPos.y);", + " if (typeof newFlash.setZ === \u0027function\u0027) newFlash.setZ(gunPos.z);", + " else if (typeof newFlash.setElevation === \u0027function\u0027) newFlash.setElevation(gunPos.z);", + "", + " // Rotation: Match Gun + Random Roll", + " const randomRoll = Math.random() * Math.PI * 2; ", + " const rollQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), randomRoll);", + " const finalQuat = gunQuat.multiply(rollQuat);", + "", + " // Apply Logic Rotation (THE FIX IS HERE)", + " const euler = new THREE.Euler().setFromQuaternion(finalQuat, \u0027YXZ\u0027);", + " const rotX = THREE.MathUtils.radToDeg(euler.x);", + " const rotY = THREE.MathUtils.radToDeg(euler.y);", + " const rotZ = THREE.MathUtils.radToDeg(euler.z);", + "", + " if (typeof newFlash.setRotationX === \u0027function\u0027) {", + " newFlash.setRotationX(rotX);", + " newFlash.setRotationY(rotY);", + " ", + " // SAFE CHECK: Does setRotationZ exist?", + " if (typeof newFlash.setRotationZ === \u0027function\u0027) {", + " newFlash.setRotationZ(rotZ);", + " } else {", + " // Fallback for Emitters/Sprites", + " newFlash.setAngle(rotZ);", + " }", + " } else {", + " // Standard 2D object fallback", + " newFlash.setAngle(rotY);", + " }", + "", + " // Apply Scale", + " if (typeof newFlash.setScale === \u0027function\u0027) newFlash.setScale(scale);", + "", + " // FORCE VISUAL UPDATE", + " const flashRenderer = newFlash.get3DRendererObject();", + " if (flashRenderer) {", + " flashRenderer.position.copy(gunPos);", + " flashRenderer.quaternion.copy(finalQuat);", + " flashRenderer.updateMatrix();", + " flashRenderer.updateMatrixWorld(true);", + " }", + "", + " // 5. LIFETIME TAG", + " // Increase to 0.1 or 0.2 if the flash is too quick to see", + " newFlash.getVariables().get(\"LifeTime\").setNumber(0.1); ", + " }", + "", + "})(runtimeScene, eventsFunctionContext);" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "FireBullet3D::Firebullet3D", + "type": "behavior" + }, + { + "description": "3D emitter", + "name": "Flash_Object", + "supplementaryInformation": "ParticleEmitter3D::ParticleEmitter3D", + "type": "objectList" + }, + { + "description": "Scale (Default 1.0)", + "name": "Scale", + "type": "expression" + }, + { + "description": "Offset X (Forward)", + "name": "OffsetX", + "type": "expression" + }, + { + "description": "Offset Y (Right)", + "name": "OffsetY", + "type": "expression" + }, + { + "description": "Offset Z (Up)", + "name": "OffsetZ", + "type": "expression" + } + ], + "objectGroups": [ + + ] + }, + { + "description": "Fires hitscan rays with smart aim, spread bloom, filtering, optional penetration, and optional ricochet.", + "fullName": "Shoot hitscan ray", + "functionType": "Action", + "name": "Shoot_Hitscan_Ray", + "sentence": "Shoot hitscan ray from _PARAM0_ (Max Dist: _PARAM2_) aimed at _PARAM3_ (Spread: _PARAM4_)", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + + ], + "actions": [ + + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "(function(runtimeScene, eventsFunctionContext) {", + " const gunObjects = eventsFunctionContext.getObjects(\"Object\");", + " const crosshairObjects = eventsFunctionContext.getObjects(\"The_Crosshair\");", + " if (!gunObjects || gunObjects.length === 0) return;", + " const gunObj = gunObjects[0];", + " const behavior = gunObj.getBehavior(eventsFunctionContext.getBehaviorName(\"Behavior\"));", + " if (!behavior) return;", + " const d = behavior._behaviorData || {};", + " const vars = gunObj.getVariables();", + " if (vars.has(\"IsReloading\") \u0026\u0026 vars.get(\"IsReloading\").getAsBoolean()) return;", + " if (!vars.has(\"IsADS\")) vars.get(\"IsADS\").setBoolean(false);", + " if (!vars.has(\"CurrentHeat\")) vars.get(\"CurrentHeat\").setNumber(0);", + " if (!vars.has(\"IsOverheated\")) vars.get(\"IsOverheated\").setBoolean(false);", + " if (!vars.has(\"IsJammed\")) vars.get(\"IsJammed\").setBoolean(false);", + " if (!vars.has(\"LastRecoilPitch\")) vars.get(\"LastRecoilPitch\").setNumber(0);", + " if (!vars.has(\"LastRecoilYaw\")) vars.get(\"LastRecoilYaw\").setNumber(0);", + " if (vars.get(\"IsOverheated\").getAsBoolean() || vars.get(\"IsJammed\").getAsBoolean()) return;", + " const maxDist = Math.max(1, Number(eventsFunctionContext.getArgument(\"MaxDistance\") || 5000));", + " const spreadArg = Number(eventsFunctionContext.getArgument(\"Spread\") || 0);", + " const debugMode = !!d.ShowDebugRay;", + " const debugRayLifetime = Math.max(0, Number((d.DebugRayLifetime === undefined) ? 0.06 : d.DebugRayLifetime));", + " const fireMode = String(d.FireMode || \"Semi\").toLowerCase();", + " const semiRPM = Math.max(1, Number(d.SemiRPM || 420));", + " const autoRPM = Math.max(1, Number(d.AutoRPM || 600));", + " const burstRPM = Math.max(1, Number(d.BurstRPM || 900));", + " const rpm = (fireMode === \"auto\") ? autoRPM : (fireMode === \"burst\" ? burstRPM : semiRPM);", + " const isADS = vars.get(\"IsADS\").getAsBoolean();", + " const adsRateMul = Math.max(0.05, Number((d.ADSFireRateMultiplier === undefined) ? 1 : d.ADSFireRateMultiplier));", + " const effectiveRPM = Math.max(1, rpm * (isADS ? adsRateMul : 1));", + " const fireInterval = 60 / effectiveRPM;", + " const now = runtimeScene.getTimeManager().getTimeFromStart() / 1000;", + " if (!vars.has(\"ShootTimer\")) vars.get(\"ShootTimer\").setNumber(0);", + " if (!vars.has(\"LastShotSyncTime\")) vars.get(\"LastShotSyncTime\").setNumber(-1);", + " if (!vars.has(\"LastShotSyncSource\")) vars.get(\"LastShotSyncSource\").setString(\"\");", + " if (!vars.has(\"LastShotSyncLinkedUsed\")) vars.get(\"LastShotSyncLinkedUsed\").setBoolean(false);", + " const syncWindow = 0.03;", + " const lastSyncTime = Number(vars.get(\"LastShotSyncTime\").getAsNumber() || -1);", + " const lastSyncSource = String(vars.get(\"LastShotSyncSource\").getAsString() || \"\");", + " const lastSyncLinkedUsed = vars.get(\"LastShotSyncLinkedUsed\").getAsBoolean();", + " const callSource = \"hitscan\";", + " const linkedDualShot = (lastSyncSource !== \"\" \u0026\u0026 lastSyncSource !== callSource \u0026\u0026 now \u003e= lastSyncTime \u0026\u0026 (now - lastSyncTime) \u003c= syncWindow \u0026\u0026 !lastSyncLinkedUsed);", + " let lastShot = Number(vars.get(\"ShootTimer\").getAsNumber() || 0);", + " if (lastShot \u003e now) lastShot = 0;", + " if (!linkedDualShot \u0026\u0026 (now - lastShot) \u003c fireInterval) return;", + " const clipSize = Math.max(1, Number(d.ClipSize || 30));", + " const pellets = Math.max(1, Number(d.BulletsPerShot || 1));", + " const infiniteAmmo = !!d.InfiniteAmmo;", + " const ammoCostPerShot = Math.max(0, Number(d.AmmoCostPerShot || 1));", + " const ammoCostMode = String(d.AmmoCostMode || \"PerTrigger\").toLowerCase();", + " const ammoCost = infiniteAmmo ? 0 : (ammoCostMode === \"perprojectile\" ? ammoCostPerShot * pellets : ammoCostPerShot);", + " if (!vars.has(\"CurrentAmmo\")) vars.get(\"CurrentAmmo\").setNumber((d.CurrentAmmo !== undefined) ? Number(d.CurrentAmmo) : clipSize);", + " let currentAmmo = Number(vars.get(\"CurrentAmmo\").getAsNumber() || 0);", + " const enableJam = !!d.EnableJam;", + " const maxHeat = Math.max(0.01, Number((d.MaxHeat === undefined) ? 100 : d.MaxHeat));", + " const jamMinRatio = Math.max(0, Math.min(1, Number((d.JamMinHeatRatio === undefined) ? 0.5 : d.JamMinHeatRatio)));", + " const jamChanceAtMax = Math.max(0, Math.min(1, Number((d.JamChanceAtMaxHeat === undefined) ? 0.08 : d.JamChanceAtMaxHeat)));", + " const currentHeat = Number(vars.get(\"CurrentHeat\").getAsNumber() || 0);", + " if (!linkedDualShot \u0026\u0026 enableJam \u0026\u0026 jamChanceAtMax \u003e 0) {", + " const heatRatio = Math.max(0, Math.min(1, currentHeat / maxHeat));", + " if (heatRatio \u003e= jamMinRatio) {", + " const norm = (1 - jamMinRatio) \u003e 0.0001 ? ((heatRatio - jamMinRatio) / (1 - jamMinRatio)) : 1;", + " const jamChance = jamChanceAtMax * Math.max(0, Math.min(1, norm));", + " if (Math.random() \u003c jamChance) { vars.get(\"IsJammed\").setBoolean(true); return; }", + " }", + " }", + " if (!linkedDualShot \u0026\u0026 !infiniteAmmo \u0026\u0026 currentAmmo \u003c ammoCost) return;", + " if (!linkedDualShot \u0026\u0026 !infiniteAmmo) vars.get(\"CurrentAmmo\").setNumber(Math.max(0, currentAmmo - ammoCost));", + " if (!linkedDualShot) {", + " vars.get(\"ShootTimer\").setNumber(now);", + " vars.get(\"LastShotSyncTime\").setNumber(now);", + " vars.get(\"LastShotSyncSource\").setString(callSource);", + " vars.get(\"LastShotSyncLinkedUsed\").setBoolean(false);", + " } else {", + " vars.get(\"LastShotSyncLinkedUsed\").setBoolean(true);", + " }", + " if (!linkedDualShot) {", + " const enableOverheat = !!d.EnableOverheat;", + " const heatPerShot = Math.max(0, Number((d.HeatPerShot === undefined) ? 8 : d.HeatPerShot));", + " let heat = Number(vars.get(\"CurrentHeat\").getAsNumber() || 0);", + " if (enableOverheat \u0026\u0026 heatPerShot \u003e 0) {", + " const maxH = Math.max(0.01, Number((d.MaxHeat === undefined) ? 100 : d.MaxHeat));", + " heat = Math.min(maxH, heat + heatPerShot);", + " vars.get(\"CurrentHeat\").setNumber(heat);", + " if (heat \u003e= maxH) vars.get(\"IsOverheated\").setBoolean(true);", + " }", + " const adsRecoilMul = Math.max(0, Number((d.ADSRecoilMultiplier === undefined) ? 0.7 : d.ADSRecoilMultiplier));", + " const recoilMul = isADS ? adsRecoilMul : 1;", + " const rPitchMin = Number((d.RecoilPitchMin === undefined) ? 0.25 : d.RecoilPitchMin);", + " const rPitchMax = Number((d.RecoilPitchMax === undefined) ? 0.65 : d.RecoilPitchMax);", + " const rYawMin = Number((d.RecoilYawMin === undefined) ? -0.3 : d.RecoilYawMin);", + " const rYawMax = Number((d.RecoilYawMax === undefined) ? 0.3 : d.RecoilYawMax);", + " const recoilPitch = (Math.min(rPitchMin, rPitchMax) + Math.random() * Math.abs(rPitchMax - rPitchMin)) * recoilMul;", + " const recoilYaw = (Math.min(rYawMin, rYawMax) + Math.random() * Math.abs(rYawMax - rYawMin)) * recoilMul;", + " vars.get(\"LastRecoilPitch\").setNumber(recoilPitch);", + " vars.get(\"LastRecoilYaw\").setNumber(recoilYaw);", + " }", + " const useSpreadBloom = (d.UseSpreadBloom === undefined) ? false : !!d.UseSpreadBloom;", + " const baseSpread = useSpreadBloom ? Number(d.BaseSpread || 0) : 0;", + " const maxSpread = useSpreadBloom ? Math.max(baseSpread, Number(d.MaxSpread || 8)) : 0;", + " const spreadPerShot = useSpreadBloom ? Math.max(0, Number(d.SpreadPerShot || 0)) : 0;", + " if (!vars.has(\"CurrentSpread\")) vars.get(\"CurrentSpread\").setNumber(0);", + " let currentSpread = useSpreadBloom ? Number(vars.get(\"CurrentSpread\").getAsNumber() || 0) : 0;", + " const adsSpreadMul = Math.max(0, Number((d.ADSSpreadMultiplier === undefined) ? 0.7 : d.ADSSpreadMultiplier));", + " const spreadMul = isADS ? adsSpreadMul : 1;", + " const totalSpread = Math.max(0, (spreadArg + baseSpread + currentSpread) * spreadMul);", + " if (useSpreadBloom \u0026\u0026 !linkedDualShot) vars.get(\"CurrentSpread\").setNumber(Math.min(maxSpread, currentSpread + spreadPerShot));", + " const gunRenderer = gunObj.get3DRendererObject();", + " if (!gunRenderer) return;", + " const layer = runtimeScene.getLayer(gunObj.getLayer());", + " const layerRenderer = layer.getRenderer();", + " if (!layerRenderer || !layerRenderer.getThreeScene || !layerRenderer.getThreeCamera) return;", + " const threeScene = layerRenderer.getThreeScene();", + " const camera = layerRenderer.getThreeCamera();", + " const camRay = new THREE.Raycaster(); camRay.far = maxDist;", + " if (crosshairObjects \u0026\u0026 crosshairObjects.length \u003e 0) {", + " const crosshair = crosshairObjects[0];", + " const uiLayer = runtimeScene.getLayer(crosshair.getLayer());", + " const width = uiLayer.getCameraWidth(); const height = uiLayer.getCameraHeight();", + " const cX = crosshair.getCenterXInScene(); const cY = crosshair.getCenterYInScene();", + " camRay.setFromCamera(new THREE.Vector2((cX / width) * 2 - 1, - (cY / height) * 2 + 1), camera);", + " } else {", + " const cp = new THREE.Vector3(); const cd = new THREE.Vector3();", + " camera.getWorldPosition(cp); camera.getWorldDirection(cd);", + " camRay.set(cp, cd);", + " }", + " const cameraIgnoreDist = Math.max(0, Number(d.CameraSelfIgnoreDistance || 150));", + " const gunIgnoreDist = Math.max(0, Number(d.GunSelfIgnoreDistance || 100));", + " const muzzleOffset = Math.max(0, Number(d.MuzzleStartOffset || 80));", + " const filterTag = String(d.FilterTag || \"\").trim();", + " const tagKey = String(d.TagUserDataKey || \"Tag\");", + " const passesFilters = (obj) =\u003e {", + " const ud = (obj \u0026\u0026 obj.userData) ? obj.userData : {};", + " if (filterTag \u0026\u0026 String(ud[tagKey] === undefined ? \"\" : ud[tagKey]) !== filterTag) return false;", + " return true;", + " };", + " const camHits = camRay.intersectObjects(threeScene.children, true);", + " let aimPoint = new THREE.Vector3(); camRay.ray.at(maxDist, aimPoint);", + " for (let i = 0; i \u003c camHits.length; i++) {", + " const hit = camHits[i]; const obj = hit.object;", + " if (!obj || !obj.visible || obj.isLine || obj.isPoints) continue;", + " if (obj === gunRenderer || obj.parent === gunRenderer) continue;", + " if (hit.distance \u003c cameraIgnoreDist) continue;", + " if (!passesFilters(obj)) continue;", + " aimPoint.copy(hit.point); break;", + " }", + " const muzzlePos = new THREE.Vector3(); gunRenderer.getWorldPosition(muzzlePos);", + " const baseDir = new THREE.Vector3().subVectors(aimPoint, muzzlePos).normalize();", + " const enablePen = !!d.EnablePenetration;", + " const maxPen = Math.max(0, Number(d.MaxPenetrationCount || 0));", + " const enableRic = !!d.EnableRicochet;", + " const maxRic = Math.max(0, Number(d.MaxRicochetBounces || 0));", + " const ricLoss = Math.min(0.95, Math.max(0, Number(d.RicochetEnergyLoss || 0.2)));", + " let bestDistance = Number.POSITIVE_INFINITY; let bestPoint = aimPoint.clone(); let bestObj = null; let bestPen = 0; let bestRic = 0; let bestNormal = new THREE.Vector3(0, 0, 0); let bestBone = \"\";", + " for (let pIdx = 0; pIdx \u003c pellets; pIdx++) {", + " let dir = baseDir.clone();", + " if (totalSpread \u003e 0) {", + " const spreadScale = totalSpread * 0.01;", + " dir.x += (Math.random() - 0.5) * spreadScale; dir.y += (Math.random() - 0.5) * spreadScale; dir.z += (Math.random() - 0.5) * spreadScale;", + " dir.normalize();", + " }", + " let origin = muzzlePos.clone().add(dir.clone().multiplyScalar(muzzleOffset));", + " let remaining = maxDist; let penCount = 0; let ricCount = 0; let point = origin.clone().add(dir.clone().multiplyScalar(remaining)); let hitObj = null; let hitNormal = new THREE.Vector3(0, 0, 0); let hitBone = \"\";", + " while (remaining \u003e 0.01) {", + " const ray = new THREE.Raycaster(origin, dir, 0, remaining);", + " const hits = ray.intersectObjects(threeScene.children, true);", + " let found = null;", + " for (let h = 0; h \u003c hits.length; h++) {", + " const candidate = hits[h]; const o = candidate.object;", + " if (!o || !o.visible || o.isLine || o.isPoints) continue;", + " if (o === gunRenderer || o.parent === gunRenderer) continue;", + " if (candidate.distance \u003c gunIgnoreDist) continue;", + " if (!passesFilters(o)) continue;", + " found = candidate; break;", + " }", + " if (!found) { point = origin.clone().add(dir.clone().multiplyScalar(remaining)); break; }", + " point = found.point.clone(); hitObj = found.object;", + " if (found.face \u0026\u0026 found.face.normal) hitNormal = found.face.normal.clone().transformDirection(found.object.matrixWorld).normalize();", + " hitBone = (found.bone \u0026\u0026 found.bone.name) ? String(found.bone.name) : \"\";", + " const traveled = Math.max(0.05, Number(found.distance) || 0.05);", + " remaining -= traveled;", + " if (enablePen \u0026\u0026 penCount \u003c maxPen) { penCount++; origin = point.clone().add(dir.clone().multiplyScalar(0.05)); continue; }", + " if (enableRic \u0026\u0026 ricCount \u003c maxRic \u0026\u0026 found.face \u0026\u0026 found.face.normal) {", + " const normal = found.face.normal.clone().transformDirection(found.object.matrixWorld).normalize();", + " const reflectDir = dir.clone().reflect(normal).normalize();", + " if (!isFinite(reflectDir.x + reflectDir.y + reflectDir.z)) break;", + " ricCount++; remaining *= (1 - ricLoss); origin = point.clone().add(reflectDir.clone().multiplyScalar(0.05)); dir = reflectDir; continue;", + " }", + " break;", + " }", + " const dHit = muzzlePos.distanceTo(point);", + " if (dHit \u003c bestDistance) { bestDistance = dHit; bestPoint = point.clone(); bestObj = hitObj; bestPen = penCount; bestRic = ricCount; bestNormal = hitNormal.clone(); bestBone = hitBone; }", + " }", + " vars.get(\"RayHitX\").setNumber(bestPoint.x);", + " vars.get(\"RayHitY\").setNumber(bestPoint.y);", + " vars.get(\"RayHitZ\").setNumber(bestPoint.z);", + " vars.get(\"RayHitFound\").setBoolean(!!bestObj);", + " vars.get(\"RayPenetrationCount\").setNumber(bestPen);", + " vars.get(\"RayRicochetCount\").setNumber(bestRic);", + " vars.get(\"RayHitDistance\").setNumber(bestDistance === Number.POSITIVE_INFINITY ? 0 : bestDistance);", + " vars.get(\"RayHitObjectName\").setString(bestObj ? String(bestObj.name || \"\") : \"\");", + " vars.get(\"RayHitObjectUuid\").setString(bestObj ? String(bestObj.uuid || \"\") : \"\");", + " const baseDamage = Math.max(0, Number((d.Damage === undefined) ? 25 : d.Damage));", + " const headshotMultiplier = Math.max(1, Number((d.HeadshotMultiplier === undefined) ? 2 : d.HeadshotMultiplier));", + " const headshotKeyword = String((d.HeadshotBoneKeyword === undefined) ? \"head\" : d.HeadshotBoneKeyword).toLowerCase();", + " let finalDamage = bestObj ? baseDamage : 0;", + " if (bestObj \u0026\u0026 bestBone \u0026\u0026 headshotKeyword !== \"\" \u0026\u0026 bestBone.toLowerCase().indexOf(headshotKeyword) !== -1) finalDamage *= headshotMultiplier;", + " vars.get(\"LastHitFound\").setBoolean(!!bestObj);", + " vars.get(\"LastHitObject\").setString(bestObj ? String(bestObj.name || \"\") : \"\");", + " vars.get(\"LastHitObjectUuid\").setString(bestObj ? String(bestObj.uuid || \"\") : \"\");", + " vars.get(\"LastHitPointX\").setNumber(bestPoint.x);", + " vars.get(\"LastHitPointY\").setNumber(bestPoint.y);", + " vars.get(\"LastHitPointZ\").setNumber(bestPoint.z);", + " vars.get(\"LastHitNormalX\").setNumber(bestNormal.x);", + " vars.get(\"LastHitNormalY\").setNumber(bestNormal.y);", + " vars.get(\"LastHitNormalZ\").setNumber(bestNormal.z);", + " vars.get(\"LastHitBone\").setString(bestBone);", + " vars.get(\"LastHitDamage\").setNumber(finalDamage);", + " const laserName = \"RayDebug_\" + gunObj.id;", + " let laserLine = threeScene.getObjectByName(laserName);", + " const removeDebugLine = (line) =\u003e {", + " if (!line) return;", + " if (line.geometry \u0026\u0026 typeof line.geometry.dispose === \"function\") line.geometry.dispose();", + " if (line.material) {", + " if (Array.isArray(line.material)) {", + " for (let i = 0; i \u003c line.material.length; i++) {", + " if (line.material[i] \u0026\u0026 typeof line.material[i].dispose === \"function\") line.material[i].dispose();", + " }", + " } else if (typeof line.material.dispose === \"function\") {", + " line.material.dispose();", + " }", + " }", + " if (line.parent) line.parent.remove(line);", + " };", + " if (debugMode \u0026\u0026 debugRayLifetime \u003e 0) {", + " const visualStart = muzzlePos.clone(); const visualEnd = bestPoint.clone();", + " threeScene.worldToLocal(visualStart); threeScene.worldToLocal(visualEnd);", + " if (!laserLine) {", + " const mat = new THREE.LineBasicMaterial({ color: 0x00FFFF, linewidth: 2, depthTest: false, transparent: true });", + " const geo = new THREE.BufferGeometry().setFromPoints([visualStart, visualEnd]);", + " laserLine = new THREE.Line(geo, mat); laserLine.name = laserName; laserLine.frustumCulled = false; laserLine.renderOrder = 999; threeScene.add(laserLine);", + " } else {", + " const pos = laserLine.geometry.attributes.position.array;", + " pos[0]=visualStart.x; pos[1]=visualStart.y; pos[2]=visualStart.z; pos[3]=visualEnd.x; pos[4]=visualEnd.y; pos[5]=visualEnd.z;", + " laserLine.geometry.attributes.position.needsUpdate = true; laserLine.visible = true;", + " }", + " laserLine.userData = laserLine.userData || {};", + " laserLine.userData.fireBullet3DDeleteAt = now + debugRayLifetime;", + " } else if (laserLine) {", + " removeDebugLine(laserLine);", + " }", + "})(runtimeScene, eventsFunctionContext);" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "FireBullet3D::Firebullet3D", + "type": "behavior" + }, + { + "description": "Max Distance (e.g. 5000)", + "name": "MaxDistance", + "type": "expression" + }, + { + "description": "The UI crosshair sprite", + "name": "The_Crosshair", + "type": "objectList" + }, + { + "description": "Additional spread override (0 = none)", + "name": "Spread", + "type": "expression" + } + ], + "objectGroups": [ + + ] + }, + { + "description": "Returns true when the last hitscan ray hit one of the supplied objects.", + "fullName": "Is hitscan ray colliding", + "functionType": "Condition", + "name": "IsHitscanRayColliding", + "sentence": "_PARAM0_ hitscan ray collided with _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "const objects = eventsFunctionContext.getObjects(\"Object\");", + "if (!objects || objects.length === 0) { eventsFunctionContext.returnValue = false; return false; }", + "const gunObj = objects[0];", + "const behavior = gunObj.getBehavior(eventsFunctionContext.getBehaviorName(\"Behavior\"));", + "if (!behavior) { eventsFunctionContext.returnValue = false; return false; }", + "const vars = gunObj.getVariables();", + "const hit = vars.has(\"RayHitFound\") ? vars.get(\"RayHitFound\").getAsBoolean() : false;", + "if (!hit) { eventsFunctionContext.returnValue = false; return false; }", + "const hitUuid = vars.has(\"RayHitObjectUuid\") ? String(vars.get(\"RayHitObjectUuid\").getAsString() || \"\") : \"\";", + "const hitName = vars.has(\"RayHitObjectName\") ? String(vars.get(\"RayHitObjectName\").getAsString() || \"\") : \"\";", + "const targetObjects = eventsFunctionContext.getObjects(\"HitObject\");", + "if (!targetObjects || targetObjects.length === 0) { eventsFunctionContext.returnValue = hit; return hit; }", + "const objectMatchesHit = (runtimeObject) =\u003e {", + " if (!runtimeObject) return false;", + " const renderer = (typeof runtimeObject.get3DRendererObject === \"function\") ? runtimeObject.get3DRendererObject() : null;", + " if (!renderer) return false;", + " if (hitUuid !== \"\") {", + " if (renderer.uuid === hitUuid) return true;", + " let foundByUuid = false;", + " if (typeof renderer.traverse === \"function\") {", + " renderer.traverse((node) =\u003e { if (!foundByUuid \u0026\u0026 node \u0026\u0026 node.uuid === hitUuid) foundByUuid = true; });", + " }", + " if (foundByUuid) return true;", + " }", + " if (hitName !== \"\") {", + " if (String(renderer.name || \"\") === hitName) return true;", + " let foundByName = false;", + " if (typeof renderer.traverse === \"function\") {", + " renderer.traverse((node) =\u003e { if (!foundByName \u0026\u0026 node \u0026\u0026 String(node.name || \"\") === hitName) foundByName = true; });", + " }", + " if (foundByName) return true;", + " }", + " return false;", + "};", + "for (let i = 0; i \u003c targetObjects.length; i++) {", + " if (objectMatchesHit(targetObjects[i])) { eventsFunctionContext.returnValue = true; return true; }", + "}", + "eventsFunctionContext.returnValue = false;", + "return false;" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "FireBullet3D::Firebullet3D", + "type": "behavior" + }, + { + "description": "Object to compare against the last hitscan hit", + "name": "HitObject", + "type": "objectList" + } + ], + "objectGroups": [ + + ] + }, + { + "description": "Starts reload when valid, with tactical/chambered options and reserve checks.", + "fullName": "Reload gun", + "functionType": "Action", + "name": "Reload_Gun", + "sentence": "Reload _PARAM0_", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + + ], + "actions": [ + + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "(function(runtimeScene, eventsFunctionContext) {", + " const gunObjects = eventsFunctionContext.getObjects(\"Object\");", + " if (!gunObjects || gunObjects.length === 0) return;", + " const gunObj = gunObjects[0];", + " const vars = gunObj.getVariables();", + " if (vars.has(\"IsReloading\") \u0026\u0026 vars.get(\"IsReloading\").getAsBoolean()) return;", + " const behavior = gunObj.getBehavior(eventsFunctionContext.getBehaviorName(\"Behavior\"));", + " if (!behavior) return;", + " const d = behavior._behaviorData || {};", + " const clipSize = Math.max(1, Number(d.ClipSize || 30));", + " const reloadTime = Math.max(0.01, Number(d.ReloadDuration || 1));", + " const allowTactical = (d.AllowTacticalReload === undefined) ? true : !!d.AllowTacticalReload;", + " const useChambered = !!d.UseChamberedRound;", + " const infiniteAmmo = !!d.InfiniteAmmo;", + " const startReserve = (d.ReserveAmmo !== undefined) ? Number(d.ReserveAmmo) : 90;", + " if (!vars.has(\"CurrentAmmo\")) vars.get(\"CurrentAmmo\").setNumber((d.CurrentAmmo !== undefined) ? Number(d.CurrentAmmo) : clipSize);", + " if (!vars.has(\"ReserveAmmo\")) vars.get(\"ReserveAmmo\").setNumber(startReserve);", + " const current = Number(vars.get(\"CurrentAmmo\").getAsNumber() || 0);", + " const reserve = Number(vars.get(\"ReserveAmmo\").getAsNumber() || 0);", + " if (!allowTactical \u0026\u0026 current \u003e 0) return;", + " const targetClip = clipSize + ((useChambered \u0026\u0026 current \u003e 0) ? 1 : 0);", + " if (current \u003e= targetClip) return;", + " if (!infiniteAmmo \u0026\u0026 reserve \u003c= 0) return;", + " vars.get(\"ReloadTargetClip\").setNumber(targetClip);", + " vars.get(\"IsReloading\").setBoolean(true);", + " vars.get(\"ReloadTimer\").setNumber(reloadTime);", + " vars.get(\"ReloadProgress\").setNumber(0);", + "})(runtimeScene, eventsFunctionContext);" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "FireBullet3D::Firebullet3D", + "type": "behavior" + } + ], + "objectGroups": [ + + ] + }, + { + "description": "Adds ammo to reserve and clamps to max reserve limit.", + "fullName": "Add reserve ammo", + "functionType": "Action", + "name": "Add_Reserve_Ammo", + "sentence": "Add _PARAM2_ to _PARAM0_ reserve ammo", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + + ], + "actions": [ + + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "(function(runtimeScene, eventsFunctionContext) {", + " const gunObjects = eventsFunctionContext.getObjects(\"Object\");", + " if (!gunObjects || gunObjects.length === 0) return;", + " const gunObj = gunObjects[0];", + " const amountToAdd = Number(eventsFunctionContext.getArgument(\"Amount\") || 0);", + " if (amountToAdd === 0) return;", + " const behavior = gunObj.getBehavior(eventsFunctionContext.getBehaviorName(\"Behavior\"));", + " if (!behavior) return;", + " const d = behavior._behaviorData || {};", + " const maxReserve = (d.MaxReserveAmmo !== undefined) ? Number(d.MaxReserveAmmo) : 210;", + " const startReserve = (d.ReserveAmmo !== undefined) ? Number(d.ReserveAmmo) : 90;", + " const vars = gunObj.getVariables();", + " if (!vars.has(\"ReserveAmmo\")) vars.get(\"ReserveAmmo\").setNumber(startReserve);", + " let reserve = Number(vars.get(\"ReserveAmmo\").getAsNumber() || 0);", + " reserve += amountToAdd;", + " if (reserve \u003e maxReserve) reserve = maxReserve;", + " if (reserve \u003c 0) reserve = 0;", + " vars.get(\"ReserveAmmo\").setNumber(reserve);", + "})(runtimeScene, eventsFunctionContext);" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "FireBullet3D::Firebullet3D", + "type": "behavior" + }, + { + "description": "Amount to Add", + "name": "Amount", + "type": "expression" + } + ], + "objectGroups": [ + + ] + }, + { + "description": "Forces GDevelop to load the Storage module.", + "fullName": "", + "functionType": "Action", + "name": "_Internal_ForceStorage", + "private": true, + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + + ], + "actions": [ + { + "type": { + "value": "ReadNumberFromStorage" + }, + "parameters": [ + "\"Dummy\"", + "\"Dummy\"", + "", + "Temp" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "FireBullet3D::Firebullet3D", + "type": "behavior" + } + ], + "objectGroups": [ + + ] + }, + { + "description": "Returns the number of bullets currently in the clip.", + "fullName": "Current ammo", + "functionType": "Expression", + "name": "CurrentAmmo", + "sentence": "Current ammo", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + + ], + "actions": [ + + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "// 1. Get Objects", + "const objects = eventsFunctionContext.getObjects(\"Object\");", + "if (objects.length === 0) {", + " eventsFunctionContext.returnValue = 0;", + " return 0;", + "}", + "", + "const gunObj = objects[0];", + "let finalAmmo = 0;", + "", + "// 2. Check if the Game Variable exists (Has the game started tracking ammo?)", + "if (gunObj.getVariables().has(\"CurrentAmmo\")) {", + " finalAmmo = gunObj.getVariables().get(\"CurrentAmmo\").getAsNumber();", + "} ", + "else {", + " // 3. Fallback: Read the \"CurrentAmmo\" PROPERTY from the Behavior Settings", + " const behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");", + " const behavior = gunObj.getBehavior(behaviorName);", + " ", + " // Check if you typed a number in the \"CurrentAmmo\" property box", + " if (behavior._behaviorData.CurrentAmmo !== undefined) {", + " finalAmmo = Number(behavior._behaviorData.CurrentAmmo);", + " } else {", + " // Safety fallback if property is missing", + " finalAmmo = Number(behavior._behaviorData.ClipSize) || 30;", + " }", + "}", + "", + "eventsFunctionContext.returnValue = finalAmmo;", + "return finalAmmo;" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "FireBullet3D::Firebullet3D", + "type": "behavior" + } + ], + "objectGroups": [ + + ] + }, + { + "description": "Returns the maximum ammo of the gun.", + "fullName": "Get Clip Size", + "functionType": "Expression", + "name": "ClipSize", + "sentence": "Clip size", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "// 1. Get Objects", + "const objects = eventsFunctionContext.getObjects(\"Object\");", + "if (objects.length === 0) return 0;", + "", + "const gunObj = objects[0];", + "const behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");", + "const behavior = gunObj.getBehavior(behaviorName);", + "", + "// 2. Get the Value safely", + "let finalValue = 30; // Default", + "", + "if (behavior._behaviorData \u0026\u0026 behavior._behaviorData.ClipSize !== undefined) {", + " finalValue = Number(behavior._behaviorData.ClipSize);", + "}", + "", + "// 3. FORCE RETURN VALUE (The Fix)", + "// This explicitly tells GDevelop\u0027s engine \"This is the result\".", + "eventsFunctionContext.returnValue = finalValue;", + "", + "// 4. Standard Return (Backup)", + "return finalValue;" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "FireBullet3D::Firebullet3D", + "type": "behavior" + } + ], + "objectGroups": [ + + ] + }, + { + "description": "Returns the number of bullets remaining in the reserve.", + "fullName": "Reserve ammo", + "functionType": "Expression", + "name": "ReserveAmmo", + "sentence": "Reserve ammo", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + + ], + "actions": [ + + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "const objects = eventsFunctionContext.getObjects(\"Object\");", + "if (objects.length === 0) { eventsFunctionContext.returnValue = 0; return 0; }", + "", + "const gunObj = objects[0];", + "let finalVal = 0;", + "", + "// 1. Check Object Variable", + "if (gunObj.getVariables().has(\"ReserveAmmo\")) {", + " finalVal = gunObj.getVariables().get(\"ReserveAmmo\").getAsNumber();", + "} else {", + " // 2. Fallback to Property", + " const behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");", + " const behavior = gunObj.getBehavior(behaviorName);", + " // Note: If property is missing, default to 90", + " finalVal = (behavior._behaviorData.ReserveAmmo !== undefined) ? Number(behavior._behaviorData.ReserveAmmo) : 90;", + "}", + "", + "eventsFunctionContext.returnValue = finalVal;", + "return finalVal;" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "FireBullet3D::Firebullet3D", + "type": "behavior" + } + ], + "objectGroups": [ + + ] + }, + { + "description": "Returns the time it takes to reload in seconds.", + "fullName": "Reload duration", + "functionType": "Expression", + "name": "ReloadTime", + "sentence": "Reload duration", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + + ], + "actions": [ + + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "const objects = eventsFunctionContext.getObjects(\"Object\");", + "if (!objects || objects.length === 0) { eventsFunctionContext.returnValue = 0; return 0; }", + "const gunObj = objects[0];", + "const behavior = gunObj.getBehavior(eventsFunctionContext.getBehaviorName(\"Behavior\"));", + "const d = behavior ? behavior._behaviorData || {} : {};", + "const val = Number(d.ReloadDuration) || 2.0;", + "eventsFunctionContext.returnValue = val;", + "return val;" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "FireBullet3D::Firebullet3D", + "type": "behavior" + } + ], + "objectGroups": [ + + ] + }, + { + "description": "Cancels current reload without refilling ammo.", + "fullName": "Cancel reload", + "functionType": "Action", + "name": "CancelReload", + "sentence": "Cancel reload on _PARAM0_", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + + ], + "actions": [ + + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "const objs = eventsFunctionContext.getObjects(\"Object\");", + "if (!objs || objs.length === 0) return;", + "const vars = objs[0].getVariables();", + "vars.get(\"IsReloading\").setBoolean(false);", + "vars.get(\"ReloadTimer\").setNumber(0);", + "vars.get(\"ReloadProgress\").setNumber(0);" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "FireBullet3D::Firebullet3D", + "type": "behavior" + } + ], + "objectGroups": [ + + ] + }, + { + "description": "Sets the fire mode at runtime.", + "fullName": "Set fire mode", + "functionType": "Action", + "name": "SetFireMode", + "sentence": "Set _PARAM0_ fire mode to _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + + ], + "actions": [ + + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "const objs = eventsFunctionContext.getObjects(\"Object\");", + "if (!objs || objs.length === 0) return;", + "const behavior = objs[0].getBehavior(eventsFunctionContext.getBehaviorName(\"Behavior\"));", + "if (!behavior) return;", + "const mode = String(eventsFunctionContext.getArgument(\"Mode\") || \"Semi\").trim().toLowerCase();", + "if (mode === \"semi\") behavior._behaviorData.FireMode = \"Semi\";", + "else if (mode === \"auto\") behavior._behaviorData.FireMode = \"Auto\";", + "else if (mode === \"burst\") behavior._behaviorData.FireMode = \"Burst\";" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "FireBullet3D::Firebullet3D", + "type": "behavior" + }, + { + "description": "Mode: Semi, Auto, or Burst", + "name": "Mode", + "supplementaryInformation": "[\"Semi\",\"Auto\",\"Burst\"]", + "type": "stringWithSelector" + } + ], + "objectGroups": [ + + ] + }, + { + "description": "Marks a bullet as inactive so pooling can reuse it.", + "fullName": "Recycle bullet", + "functionType": "Action", + "name": "RecycleBulletToPool", + "sentence": "Recycle bullet _PARAM2_ into pool", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + + ], + "actions": [ + + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "const bullets = eventsFunctionContext.getObjects(\"BulletObject\");", + "if (!bullets || bullets.length === 0) return;", + "for (let i = 0; i \u003c bullets.length; i++) {", + " const b = bullets[i]; if (!b) continue;", + " const v = b.getVariables();", + " if (v.has(\"IsActive\")) v.get(\"IsActive\").setBoolean(false);", + " if (typeof b.hide === \"function\") b.hide(true);", + " if (typeof b.setX === \"function\") b.setX(-999999);", + " if (typeof b.setY === \"function\") b.setY(-999999);", + " if (typeof b.setZ === \"function\") b.setZ(-999999); else if (typeof b.setElevation === \"function\") b.setElevation(-999999);", + "}" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "FireBullet3D::Firebullet3D", + "type": "behavior" + }, + { + "description": "Bullet object to recycle", + "name": "BulletObject", + "type": "objectList", + "supplementaryInformation": "Scene3D::Model3DObject" + } + ], + "objectGroups": [ + + ] + }, + { + "description": "Returns the max reserve ammo limit from behavior settings.", + "fullName": "Max reserve ammo", + "functionType": "Expression", + "name": "MaxReserveAmmo", + "sentence": "Max reserve ammo", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + + ], + "actions": [ + + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "const objects = eventsFunctionContext.getObjects(\"Object\");", + "if (!objects || objects.length === 0) { eventsFunctionContext.returnValue = 0; return 0; }", + "const behavior = objects[0].getBehavior(eventsFunctionContext.getBehaviorName(\"Behavior\"));", + "const d = behavior ? behavior._behaviorData || {} : {};", + "const val = (d.MaxReserveAmmo !== undefined) ? Number(d.MaxReserveAmmo) : 210;", + "eventsFunctionContext.returnValue = val;", + "return val;" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "FireBullet3D::Firebullet3D", + "type": "behavior" + } + ], + "objectGroups": [ + + ], + "expressionType": { + "type": "expression" + } + }, + { + "description": "Returns remaining fire cooldown time in seconds.", + "fullName": "Cooldown remaining", + "functionType": "Expression", + "name": "CooldownRemaining", + "sentence": "Cooldown remaining", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + + ], + "actions": [ + + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "const objects = eventsFunctionContext.getObjects(\"Object\");", + "if (!objects || objects.length === 0) { eventsFunctionContext.returnValue = 0; return 0; }", + "const gunObj = objects[0];", + "const behavior = gunObj.getBehavior(eventsFunctionContext.getBehaviorName(\"Behavior\"));", + "if (!behavior) { eventsFunctionContext.returnValue = 0; return 0; }", + "const d = behavior._behaviorData || {};", + "const vars = gunObj.getVariables();", + "const fireMode = String(d.FireMode || \"Semi\").toLowerCase();", + "const semiRPM = Math.max(1, Number(d.SemiRPM || 420));", + "const autoRPM = Math.max(1, Number(d.AutoRPM || 600));", + "const burstRPM = Math.max(1, Number(d.BurstRPM || 900));", + "const rpm = (fireMode === \"auto\") ? autoRPM : (fireMode === \"burst\" ? burstRPM : semiRPM);", + "const interval = 60 / rpm;", + "const now = runtimeScene.getTimeManager().getTimeFromStart() / 1000;", + "let last = vars.has(\"ShootTimer\") ? Number(vars.get(\"ShootTimer\").getAsNumber() || 0) : 0;", + "if (last \u003e now) last = 0;", + "const remain = Math.max(0, interval - (now - last));", + "eventsFunctionContext.returnValue = remain;", + "return remain;" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "FireBullet3D::Firebullet3D", + "type": "behavior" + } + ], + "objectGroups": [ + + ], + "expressionType": { + "type": "expression" + } + }, + { + "description": "Returns reload progress from 0 to 1.", + "fullName": "Reload progress", + "functionType": "Expression", + "name": "ReloadProgress", + "sentence": "Reload progress", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + + ], + "actions": [ + + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "const objects = eventsFunctionContext.getObjects(\"Object\");", + "if (!objects || objects.length === 0) { eventsFunctionContext.returnValue = 0; return 0; }", + "const vars = objects[0].getVariables();", + "const val = vars.has(\"ReloadProgress\") ? Number(vars.get(\"ReloadProgress\").getAsNumber() || 0) : 0;", + "eventsFunctionContext.returnValue = val;", + "return val;" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "FireBullet3D::Firebullet3D", + "type": "behavior" + } + ], + "objectGroups": [ + + ], + "expressionType": { + "type": "expression" + } + }, + { + "description": "Returns 1 when clip is full, otherwise 0.", + "fullName": "Is clip full", + "functionType": "Expression", + "name": "IsClipFull", + "sentence": "Is clip full", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + + ], + "actions": [ + + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "const objects = eventsFunctionContext.getObjects(\"Object\");", + "if (!objects || objects.length === 0) { eventsFunctionContext.returnValue = 0; return 0; }", + "const gunObj = objects[0];", + "const behavior = gunObj.getBehavior(eventsFunctionContext.getBehaviorName(\"Behavior\"));", + "if (!behavior) { eventsFunctionContext.returnValue = 0; return 0; }", + "const d = behavior._behaviorData || {};", + "const clipSize = Math.max(1, Number(d.ClipSize || 30));", + "const vars = gunObj.getVariables();", + "const current = vars.has(\"CurrentAmmo\") ? Number(vars.get(\"CurrentAmmo\").getAsNumber() || 0) : Number(d.CurrentAmmo || clipSize);", + "const out = (current \u003e= clipSize) ? 1 : 0;", + "eventsFunctionContext.returnValue = out;", + "return out;" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "FireBullet3D::Firebullet3D", + "type": "behavior" + } + ], + "objectGroups": [ + + ], + "expressionType": { + "type": "expression" + } + }, + { + "description": "Sets ADS state for this weapon.", + "fullName": "Set ADS state", + "functionType": "Action", + "name": "SetADSState", + "sentence": "Set _PARAM0_ ADS to _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "const objs = eventsFunctionContext.getObjects(\"Object\");", + "if (!objs || objs.length === 0) return;", + "const vars = objs[0].getVariables();", + "const enabled = !!eventsFunctionContext.getArgument(\"Enabled\");", + "vars.get(\"IsADS\").setBoolean(enabled);" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "FireBullet3D::Firebullet3D", + "type": "behavior" + }, + { + "description": "1 = true, 0 = false", + "name": "Enabled", + "type": "expression" + } + ], + "objectGroups": [ + + ] + }, + { + "description": "Clears jam state manually.", + "fullName": "Clear jam", + "functionType": "Action", + "name": "ClearJam", + "sentence": "Clear jam on _PARAM0_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "const objs = eventsFunctionContext.getObjects(\"Object\");", + "if (!objs || objs.length === 0) return;", + "const vars = objs[0].getVariables();", + "vars.get(\"IsJammed\").setBoolean(false);" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "FireBullet3D::Firebullet3D", + "type": "behavior" + } + ], + "objectGroups": [ + + ] + }, + { + "description": "Checks if ADS mode is active.", + "fullName": "Is ADS", + "functionType": "Condition", + "name": "IsADS", + "sentence": "_PARAM0_ is ADS", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "const objs = eventsFunctionContext.getObjects(\"Object\");", + "if (!objs || objs.length === 0) { eventsFunctionContext.returnValue = false; return false; }", + "const vars = objs[0].getVariables();", + "const out = vars.has(\"IsADS\") ? vars.get(\"IsADS\").getAsBoolean() : false;", + "eventsFunctionContext.returnValue = out;", + "return out;" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "FireBullet3D::Firebullet3D", + "type": "behavior" + } + ], + "objectGroups": [ + + ] + }, + { + "description": "Checks if weapon is jammed.", + "fullName": "Is jammed", + "functionType": "Condition", + "name": "IsJammed", + "sentence": "_PARAM0_ is jammed", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "const objs = eventsFunctionContext.getObjects(\"Object\");", + "if (!objs || objs.length === 0) { eventsFunctionContext.returnValue = false; return false; }", + "const vars = objs[0].getVariables();", + "const out = vars.has(\"IsJammed\") ? vars.get(\"IsJammed\").getAsBoolean() : false;", + "eventsFunctionContext.returnValue = out;", + "return out;" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "FireBullet3D::Firebullet3D", + "type": "behavior" + } + ], + "objectGroups": [ + + ] + }, + { + "description": "Checks if weapon is overheated.", + "fullName": "Is overheated", + "functionType": "Condition", + "name": "IsOverheated", + "sentence": "_PARAM0_ is overheated", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "const objs = eventsFunctionContext.getObjects(\"Object\");", + "if (!objs || objs.length === 0) { eventsFunctionContext.returnValue = false; return false; }", + "const vars = objs[0].getVariables();", + "const out = vars.has(\"IsOverheated\") ? vars.get(\"IsOverheated\").getAsBoolean() : false;", + "eventsFunctionContext.returnValue = out;", + "return out;" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "FireBullet3D::Firebullet3D", + "type": "behavior" + } + ], + "objectGroups": [ + + ] + }, + { + "description": "Returns current heat value.", + "fullName": "Current heat", + "functionType": "Expression", + "name": "CurrentHeat", + "sentence": "Current heat", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "const objs = eventsFunctionContext.getObjects(\"Object\");", + "if (!objs || objs.length === 0) { eventsFunctionContext.returnValue = 0; return 0; }", + "const vars = objs[0].getVariables();", + "const out = vars.has(\"CurrentHeat\") ? Number(vars.get(\"CurrentHeat\").getAsNumber() || 0) : 0;", + "eventsFunctionContext.returnValue = out;", + "return out;" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "FireBullet3D::Firebullet3D", + "type": "behavior" + } + ], + "objectGroups": [ + + ], + "expressionType": { + "type": "expression" + } + }, + { + "description": "Returns last hit damage value from hitscan callback.", + "fullName": "Last hit damage", + "functionType": "Expression", + "name": "LastHitDamage", + "sentence": "Last hit damage", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "const objs = eventsFunctionContext.getObjects(\"Object\");", + "if (!objs || objs.length === 0) { eventsFunctionContext.returnValue = 0; return 0; }", + "const vars = objs[0].getVariables();", + "const out = vars.has(\"LastHitDamage\") ? Number(vars.get(\"LastHitDamage\").getAsNumber() || 0) : 0;", + "eventsFunctionContext.returnValue = out;", + "return out;" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "FireBullet3D::Firebullet3D", + "type": "behavior" + } + ], + "objectGroups": [ + + ], + "expressionType": { + "type": "expression" + } + } + ], + "propertyDescriptors": [ + { + "value": "false", + "type": "Boolean", + "label": "Show Debug Raycast (Visual Line)", + "description": "Draws a cyan debug line for hitscan rays.", + "group": "debug", + "name": "ShowDebugRay" + }, + { + "value": "0.06", + "type": "Number", + "label": "Debug Ray Lifetime", + "description": "Seconds before hitscan debug ray is removed.", + "group": "advanced raycast", + "name": "DebugRayLifetime" + }, + { + "value": "false", + "type": "Boolean", + "label": "Show Debug Panel", + "description": "If enabled, writes runtime weapon stats to a text object.", + "group": "debug", + "name": "ShowDebugPanel" + }, + { + "value": "", + "type": "String", + "label": "Debug Panel Object Name", + "description": "Name of a Text object to receive the debug panel output.", + "group": "debug", + "name": "DebugPanelObjectName" + }, + { + "value": "30", + "type": "Number", + "label": "Clip Size", + "description": "Maximum rounds in magazine (without chamber bonus).", + "group": "ammo", + "name": "ClipSize" + }, + { + "value": "Semi", + "type": "Choice", + "label": "Fire Mode", + "description": "Select how this weapon fires: Semi, Auto, or Burst.", + "group": "fire mode", + "choices": [ + { + "label": "", + "value": "Semi" + }, + { + "label": "", + "value": "Auto" + }, + { + "label": "", + "value": "Burst" + } + ], + "name": "FireMode" + }, + { + "value": "420", + "type": "Number", + "label": "Semi RPM", + "description": "Rounds per minute in semi mode.", + "group": "fire mode", + "name": "SemiRPM" + }, + { + "value": "600", + "type": "Number", + "label": "Auto RPM", + "description": "Rounds per minute in auto mode.", + "group": "fire mode", + "name": "AutoRPM" + }, + { + "value": "900", + "type": "Number", + "label": "Burst RPM", + "description": "Rounds per minute while burst sequence is active.", + "group": "fire mode", + "name": "BurstRPM" + }, + { + "value": "3", + "type": "Number", + "label": "Burst Count", + "description": "Shots fired per burst trigger.", + "group": "fire mode", + "name": "BurstCount" + }, + { + "value": "1", + "type": "Number", + "label": "ADS Fire Rate Multiplier", + "description": "Fire rate multiplier while ADS is active.", + "group": "ads", + "name": "ADSFireRateMultiplier" + }, + { + "value": "0.7", + "type": "Number", + "label": "ADS Spread Multiplier", + "description": "Spread multiplier while ADS is active.", + "group": "ads", + "name": "ADSSpreadMultiplier" + }, + { + "value": "0.7", + "type": "Number", + "label": "ADS Recoil Multiplier", + "description": "Recoil multiplier while ADS is active.", + "group": "ads", + "name": "ADSRecoilMultiplier" + }, + { + "value": "30", + "type": "Number", + "label": "Start Current Ammo", + "description": "Initial magazine ammo if runtime var is missing.", + "group": "ammo", + "name": "CurrentAmmo" + }, + { + "value": "90", + "type": "Number", + "label": "Start Reserve Ammo", + "description": "Initial reserve ammo if runtime var is missing.", + "group": "ammo", + "name": "ReserveAmmo" + }, + { + "value": "210", + "type": "Number", + "label": "Max Reserve Ammo", + "description": "Maximum reserve capacity.", + "group": "ammo", + "name": "MaxReserveAmmo" + }, + { + "value": "false", + "type": "Boolean", + "label": "Infinite Ammo", + "description": "If enabled, ammo is never consumed.", + "group": "ammo", + "name": "InfiniteAmmo" + }, + { + "value": "PerTrigger", + "type": "Choice", + "label": "Ammo Cost Mode", + "description": "PerTrigger or PerProjectile.", + "group": "ammo", + "extraInformation": [ + "PerTrigger", + "PerProjectile" + ], + "name": "AmmoCostMode" + }, + { + "value": "1", + "type": "Number", + "label": "Ammo Cost Per Shot", + "description": "Ammo consumed per trigger or per projectile depending on mode.", + "group": "ammo", + "name": "AmmoCostPerShot" + }, + { + "value": "true", + "type": "Boolean", + "label": "Auto Reload", + "description": "Automatically reload when clip is empty and reserve is available.", + "group": "reload", + "name": "AutoReload" + }, + { + "value": "true", + "type": "Boolean", + "label": "Allow Tactical Reload", + "description": "Allow reloading while clip still has ammo.", + "group": "reload", + "name": "AllowTacticalReload" + }, + { + "value": "false", + "type": "Boolean", + "label": "Use Chambered Round", + "description": "Allow clip to fill to ClipSize + 1 when tactical reloading.", + "group": "reload", + "name": "UseChamberedRound" + }, + { + "value": "1", + "type": "Number", + "label": "Reload Duration (sec)", + "description": "Time to complete reload.", + "group": "reload", + "name": "ReloadDuration" + }, + { + "value": "1", + "type": "Number", + "label": "Bullets Per Shot", + "description": "Projectile count per trigger (shotgun pellets).", + "group": "projectile", + "name": "BulletsPerShot" + }, + { + "value": "0", + "type": "Number", + "label": "Gravity Scale", + "description": "Bullet gravity multiplier for physics bullets.", + "group": "projectile", + "name": "GravityScale" + }, + { + "value": "0", + "type": "Number", + "label": "Bullet Max Lifetime", + "description": "Seconds before projectile bullets are auto-despawned (0 = disabled).", + "group": "projectile", + "name": "BulletMaxLifetime" + }, + { + "value": "0", + "type": "Number", + "label": "Bullet Max Distance", + "description": "Distance before projectile bullets are auto-despawned (0 = disabled).", + "group": "projectile", + "name": "BulletMaxDistance" + }, + { + "value": "25", + "type": "Number", + "label": "Damage", + "description": "Base weapon damage for hitscan and spawned projectile metadata.", + "group": "damage", + "name": "Damage" + }, + { + "value": "2", + "type": "Number", + "label": "Headshot Multiplier", + "description": "Damage multiplier when hit bone matches headshot keyword.", + "group": "damage", + "name": "HeadshotMultiplier" + }, + { + "value": "head", + "type": "String", + "label": "Headshot Bone Keyword", + "description": "Case-insensitive keyword used to detect headshot bones.", + "group": "damage", + "name": "HeadshotBoneKeyword" + }, + { + "value": "0.25", + "type": "Number", + "label": "Recoil Pitch Min", + "description": "Minimum pitch recoil output per shot.", + "group": "recoil", + "name": "RecoilPitchMin" + }, + { + "value": "0.65", + "type": "Number", + "label": "Recoil Pitch Max", + "description": "Maximum pitch recoil output per shot.", + "group": "recoil", + "name": "RecoilPitchMax" + }, + { + "value": "-0.3", + "type": "Number", + "label": "Recoil Yaw Min", + "description": "Minimum yaw recoil output per shot.", + "group": "recoil", + "name": "RecoilYawMin" + }, + { + "value": "0.3", + "type": "Number", + "label": "Recoil Yaw Max", + "description": "Maximum yaw recoil output per shot.", + "group": "recoil", + "name": "RecoilYawMax" + }, + { + "value": "false", + "type": "Boolean", + "label": "Enable Overheat", + "description": "Enable heat gain and overheat lockout while firing.", + "group": "heat", + "name": "EnableOverheat" + }, + { + "value": "100", + "type": "Number", + "label": "Max Heat", + "description": "Heat ceiling used for overheat and jam calculations.", + "group": "heat", + "name": "MaxHeat" + }, + { + "value": "8", + "type": "Number", + "label": "Heat Per Shot", + "description": "Heat added for each fired shot.", + "group": "heat", + "name": "HeatPerShot" + }, + { + "value": "20", + "type": "Number", + "label": "Heat Cooldown/sec", + "description": "Heat removed per second.", + "group": "heat", + "name": "HeatCooldownPerSecond" + }, + { + "value": "35", + "type": "Number", + "label": "Overheat Recover Threshold", + "description": "Heat value needed to clear overheat state.", + "group": "heat", + "name": "OverheatRecoverThreshold" + }, + { + "value": "false", + "type": "Boolean", + "label": "Enable Jam", + "description": "Allow jams based on heat level.", + "group": "jam", + "name": "EnableJam" + }, + { + "value": "0.08", + "type": "Number", + "label": "Jam Chance At Max Heat", + "description": "Chance to jam per shot at maximum heat (0-1).", + "group": "jam", + "name": "JamChanceAtMaxHeat" + }, + { + "value": "0.5", + "type": "Number", + "label": "Jam Min Heat Ratio", + "description": "Heat ratio (0-1) where jam checks begin.", + "group": "jam", + "name": "JamMinHeatRatio" + }, + { + "value": "false", + "type": "Boolean", + "label": "Use Spread Bloom", + "description": "If enabled, Base/Max/PerShot/Recovery spread settings are applied. If disabled, shots stay straight unless action Spread is used.", + "group": "accuracy", + "name": "UseSpreadBloom" + }, + { + "value": "0", + "type": "Number", + "label": "Base Spread", + "description": "Baseline spread added to all shots.", + "group": "accuracy", + "name": "BaseSpread" + }, + { + "value": "8", + "type": "Number", + "label": "Max Spread", + "description": "Maximum bloom spread cap.", + "group": "accuracy", + "name": "MaxSpread" + }, + { + "value": "0.35", + "type": "Number", + "label": "Spread Per Shot", + "description": "Bloom increase per fired shot.", + "group": "accuracy", + "name": "SpreadPerShot" + }, + { + "value": "6", + "type": "Number", + "label": "Spread Recovery/sec", + "description": "Bloom recovery speed per second.", + "group": "accuracy", + "name": "SpreadRecoveryPerSecond" + }, + { + "value": "150", + "type": "Number", + "label": "Camera Self Ignore Dist", + "description": "Ignore camera ray hits nearer than this distance.", + "group": "raycast", + "name": "CameraSelfIgnoreDistance" + }, + { + "value": "100", + "type": "Number", + "label": "Gun Self Ignore Dist", + "description": "Ignore gun ray hits nearer than this distance.", + "group": "raycast", + "name": "GunSelfIgnoreDistance" + }, + { + "value": "80", + "type": "Number", + "label": "Muzzle Start Offset", + "description": "Start gun ray this many units forward from muzzle.", + "group": "raycast", + "name": "MuzzleStartOffset" + }, + { + "value": "", + "type": "String", + "label": "Filter Tag", + "description": "Optional userData tag that targets must match.", + "group": "filtering", + "name": "FilterTag" + }, + { + "value": "Tag", + "type": "String", + "label": "Tag userData Key", + "description": "userData key used for tag filtering.", + "group": "filtering", + "name": "TagUserDataKey" + }, + { + "value": "false", + "type": "Boolean", + "label": "Enable Penetration", + "description": "Allow hitscan rays to continue through valid targets.", + "group": "advanced raycast", + "name": "EnablePenetration" + }, + { + "value": "0", + "type": "Number", + "label": "Max Penetration Count", + "description": "Maximum number of penetrations per shot.", + "group": "advanced raycast", + "name": "MaxPenetrationCount" + }, + { + "value": "false", + "type": "Boolean", + "label": "Enable Ricochet", + "description": "Allow hitscan rays to bounce on surfaces.", + "group": "advanced raycast", + "name": "EnableRicochet" + }, + { + "value": "0", + "type": "Number", + "label": "Max Ricochet Bounces", + "description": "Maximum ricochet bounces per shot.", + "group": "advanced raycast", + "name": "MaxRicochetBounces" + }, + { + "value": "0.2", + "type": "Number", + "label": "Ricochet Energy Loss", + "description": "0-1 distance loss multiplier applied each bounce.", + "group": "advanced raycast", + "name": "RicochetEnergyLoss" + }, + { + "value": "true", + "type": "Boolean", + "label": "Enable Bullet Pooling", + "description": "Reuse inactive bullets before creating new ones.", + "group": "performance", + "name": "EnablePooling" + }, + { + "value": "", + "type": "String", + "label": "Weapon Id", + "description": "Stable identifier used for save/load keys.", + "group": "save", + "name": "WeaponId" + }, + { + "value": "Player1", + "type": "String", + "label": "Default Profile Id", + "description": "Default profile prefix for save/load operations.", + "group": "save", + "name": "DefaultProfileId" + } + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "propertyName": "ShowDebugRay" + }, + { + "propertyName": "DebugRayLifetime" + }, + { + "propertyName": "ShowDebugPanel" + }, + { + "propertyName": "DebugPanelObjectName" + }, + { + "propertyName": "ClipSize" + }, + { + "propertyName": "FireMode" + }, + { + "propertyName": "SemiRPM" + }, + { + "propertyName": "AutoRPM" + }, + { + "propertyName": "BurstRPM" + }, + { + "propertyName": "BurstCount" + }, + { + "propertyName": "ADSFireRateMultiplier" + }, + { + "propertyName": "ADSSpreadMultiplier" + }, + { + "propertyName": "ADSRecoilMultiplier" + }, + { + "propertyName": "CurrentAmmo" + }, + { + "propertyName": "ReserveAmmo" + }, + { + "propertyName": "MaxReserveAmmo" + }, + { + "propertyName": "InfiniteAmmo" + }, + { + "propertyName": "AmmoCostMode" + }, + { + "propertyName": "AmmoCostPerShot" + }, + { + "propertyName": "AutoReload" + }, + { + "propertyName": "AllowTacticalReload" + }, + { + "propertyName": "UseChamberedRound" + }, + { + "propertyName": "ReloadDuration" + }, + { + "propertyName": "BulletsPerShot" + }, + { + "propertyName": "GravityScale" + }, + { + "propertyName": "BulletMaxLifetime" + }, + { + "propertyName": "BulletMaxDistance" + }, + { + "propertyName": "Damage" + }, + { + "propertyName": "HeadshotMultiplier" + }, + { + "propertyName": "HeadshotBoneKeyword" + }, + { + "propertyName": "RecoilPitchMin" + }, + { + "propertyName": "RecoilPitchMax" + }, + { + "propertyName": "RecoilYawMin" + }, + { + "propertyName": "RecoilYawMax" + }, + { + "propertyName": "EnableOverheat" + }, + { + "propertyName": "MaxHeat" + }, + { + "propertyName": "HeatPerShot" + }, + { + "propertyName": "HeatCooldownPerSecond" + }, + { + "propertyName": "OverheatRecoverThreshold" + }, + { + "propertyName": "EnableJam" + }, + { + "propertyName": "JamChanceAtMaxHeat" + }, + { + "propertyName": "JamMinHeatRatio" + }, + { + "propertyName": "UseSpreadBloom" + }, + { + "propertyName": "BaseSpread" + }, + { + "propertyName": "MaxSpread" + }, + { + "propertyName": "SpreadPerShot" + }, + { + "propertyName": "SpreadRecoveryPerSecond" + }, + { + "propertyName": "CameraSelfIgnoreDistance" + }, + { + "propertyName": "GunSelfIgnoreDistance" + }, + { + "propertyName": "MuzzleStartOffset" + }, + { + "propertyName": "FilterTag" + }, + { + "propertyName": "TagUserDataKey" + }, + { + "propertyName": "EnablePenetration" + }, + { + "propertyName": "MaxPenetrationCount" + }, + { + "propertyName": "EnableRicochet" + }, + { + "propertyName": "MaxRicochetBounces" + }, + { + "propertyName": "RicochetEnergyLoss" + }, + { + "propertyName": "EnablePooling" + }, + { + "folderName": "Save / Load", + "children": [ + { + "propertyName": "WeaponId" + }, + { + "propertyName": "DefaultProfileId" + } + ] + } + ] + } + } + ], + "eventsBasedObjects": [ + + ] +}