Battlefield 6 Mod: Gunfight Mode
Gunfight Mode is a custom game mode for Battlefield 6 built with the Battlefield Portal SDK, recreating Modern Warfare's Gunfight inside Battlefield.
The mode features random loadouts each round, a round timer, round-based scoring, and support for multiple maps. It is published and playable through the in-game Portal browser.
Code
Randomized Loadout Generationtypescript
Both teams roll identical random kits. The Portal SDK exposes weapons, attachments, and gadgets as TypeScript enums, so the generator reflects over the enum keys to pull from everything in the game dynamically.
static Init()
{
// Object.keys on a numeric enum returns names AND values; filter to names
const primaryWeaponKeys = Object.keys(mod.Weapons).filter(k => isNaN(Number(k)));
const primaryWeaponIndex = GetRandomInt(primaryWeaponKeys.length);
const primaryWeaponKey = primaryWeaponKeys[primaryWeaponIndex] as keyof typeof mod.Weapons;
const primaryAttachmentKeys = Object.keys(mod.WeaponAttachments).filter(k => isNaN(Number(k)));
const primaryAttachmentIndex = GetRandomInt(primaryAttachmentKeys.length);
const primaryAttachmentKey = primaryAttachmentKeys[primaryAttachmentIndex] as keyof typeof mod.WeaponAttachments;
// ...same random roll for the secondary weapon, its attachment, and both gadgets
const currentPrimaryWeapon = mod.Weapons[primaryWeaponKey];
const currentSecondaryWeapon = mod.Weapons[secondaryWeaponKey];
const currentPrimaryAttachment = mod.WeaponAttachments[primaryAttachmentKey];
const currentSecondaryAttachment = mod.WeaponAttachments[secondaryAttachmentKey];
const currentGadgetOne = mod.Gadgets[gadgetOneKey];
const currentGadgetTwo = mod.Gadgets[gadgetTwoKey];
this.currentLoadout = new Loadout(
currentPrimaryWeapon, currentSecondaryWeapon,
currentPrimaryAttachment, currentSecondaryAttachment,
currentGadgetOne, currentGadgetTwo
);
this.currentLoadout.weaponPackagePrimary = mod.CreateNewWeaponPackage();
this.currentLoadout.weaponPackageSecondary = mod.CreateNewWeaponPackage();
}Overtime Capture Point State Machinetypescript
In the final 10 seconds of a round, a capture point activates as the tiebreaker. Area-trigger events feed players into this state machine, which handles clean captures, contested standoffs, and one team stealing the point mid-capture.
async StartCaptureTimer() {
while (!HasBeenCaptured && roundEnded == false) {
const teamsPresent = this.GetUniqueTeams(this.CollidingPlayers);
if (teamsPresent.length === 0) {
CurrentCaptureState = CaptureState.Idle;
} else if (teamsPresent.length === 1) {
this.CurrentTeam = mod.GetObjId(mod.GetTeam(this.CollidingPlayers[0]));
if (this.CapturingTeam === null) {
this.CapturingTeam = this.CurrentTeam;
CurrentCaptureState = CaptureState.Capturing;
} else if (this.CapturingTeam === this.CurrentTeam) {
CurrentCaptureState = CaptureState.Capturing;
} else {
// Point stolen, progress resets for the new team
this.CapturingTeam = this.CurrentTeam;
CaptureProgress = captureTime;
CurrentCaptureState = CaptureState.Capturing;
}
} else {
CurrentCaptureState = CaptureState.Contested;
}
switch (CurrentCaptureState) {
case CaptureState.Idle:
CaptureProgress = captureTime;
this.CapturingTeam = null;
this.IsBeingCaptured = false;
break;
case CaptureState.Capturing:
CaptureProgress--;
this.IsBeingCaptured = true;
this.CurrentTeam = mod.GetObjId(mod.GetTeam(this.CollidingPlayers[0]));
MessageAllUI(MakeMessage(mod.stringkeys.CaptureTimerCountdown, this.CurrentTeam, CaptureProgress), REDCOLOR);
break;
case CaptureState.Contested:
MessageAllUI(MakeMessage(mod.stringkeys.CaptureTimerInterrupted), REDCOLOR);
break;
}
if (CaptureProgress <= 0) {
HasBeenCaptured = true;
this.IsBeingCaptured = false;
CurrentCaptureState = CaptureState.Idle;
EndRound(mod.GetTeam(this.CollidingPlayers[0]));
}
await mod.Wait(1);
}
}Round Setup & Loadout Rotationtypescript
Rounds alternate loadouts every two rounds. Players are healed and teleported back to their HQ during intermission.
async function SetupRound()
{
CaptureM.ResetCapturePoint();
CapturePoint.Deactivate();
roundEnded = false;
isInIntermission = true;
let loadoutSwitch: boolean = roundNum % 2 == 0;
if(loadoutSwitch || roundNum == 0)
{
Loadout.ResetLoadout();
Loadout.Init();
await Loadout.SwitchLoadout();
}
for(const player of GFPlayer.playerInstances)
{
let teamID = mod.GetObjId(mod.GetTeam(player));
let gfPlayer = GFPlayer.get(player);
let isAI = mod.GetSoldierState(player, mod.SoldierStateBool.IsAISoldier);
if(isAI)
{
mod.Kill(player);
}
else
{
if (gfPlayer && gfPlayer.isDeployed)
{
let hqPos: mod.Vector = mod.GetObjectPosition(
teamID == mod.GetObjId(attackingTeam) ? attackersHQ : defendersHQ
);
mod.Teleport(player, hqPos, 0);
}
mod.Heal(player, 150);
}
}
}What I Did
Game Mode Design & Scripting
- Built the full game mode loop in TypeScript using the Battlefield Portal SDK: round flow, win conditions, and round-based scoring.
- Implemented randomized loadout assignment so both teams receive matching weapons each round.
- Added a round timer to keep rounds fast-paced and decisive.
- Configured the mode to run across multiple maps.