Skip to main content

Introduction to HScript

Friday Night Funkin’ uses HScript for mod scripting - a scripting language that looks and works like Haxe. Scripts can extend classes, react to events, and modify gameplay in real-time.
HScript syntax is nearly identical to Haxe. If you know Haxe, you already know HScript!

Script Types

There are three main types of scripts in FNF:

Modules

Global scripts that receive events from any game state

Scripted Classes

Scripts that extend base Flixel classes

State Scripts

Scripts attached to specific game states or objects

Creating a Module

Modules are the most powerful script type - they receive events globally across all game states.

Basic Module Structure

1

Create script file

In your mod folder, create a .hxc file:
mymod/
  └── scripts/
      └── MyModule.hxc
2

Import the Module class

MyModule.hxc
import funkin.modding.module.Module;

class MyModule extends Module
{
  public function new()
  {
    super('mymodule', 100);
  }
}
3

Add event handlers

Override methods to handle events:
public function onNoteHit(event:HitNoteScriptEvent)
{
  trace('Note hit! Judgement: ' + event.judgement);
}

Module Constructor

The Module constructor takes these parameters:
Module.hx
public function new(moduleId:String, priority:Int = 1000, ?params:ModuleParams):Void
{
  this.moduleId = moduleId;
  this.priority = priority;
  
  if (params != null)
  {
    this.state = params.state ?? null;
  }
}
  • moduleId - Unique identifier for your module
  • priority - Event priority (lower = earlier, default 1000)
  • params - Optional parameters (can restrict to specific state)
Use priority to control when your module receives events relative to others. Priority 1 runs before Priority 1000.

Module Properties

// Control whether module receives events
this.active = true;  // or false

// Change event priority
this.priority = 500;

// Restrict to specific state
this.state = flixel.FlxState;  // Only active in this state

Available Events

Lifecycle Events

public function onCreate(event:ScriptEvent)
{
  // Called when module is first created (before title screen)
}

public function onDestroy(event:ScriptEvent)
{
  // Called when module is destroyed (F5 reload)
}

public function onUpdate(event:UpdateScriptEvent)
{
  // Called every frame
  // event.elapsed = time since last frame
}

Gameplay Events

public function onSongLoaded(event:SongLoadScriptEvent)
{
  // Song chart parsed, before notes placed
  // Modify event.notes or event.events to change chart
}

public function onSongStart(event:ScriptEvent)
{
  // Song started (conductor time = 0)
}

public function onSongEnd(event:ScriptEvent)
{
  // Song ended, about to unload
}

public function onSongRetry(event:SongRetryEvent)
{
  // Player restarted song
  // event.difficulty = new difficulty
}

UI Events

public function onCapsuleSelected(event:CapsuleScriptEvent)
{
  // Song capsule selected in Freeplay
}

public function onDifficultySwitch(event:CapsuleScriptEvent)
{
  // Difficulty changed
}

public function onSongSelected(event:CapsuleScriptEvent)
{
  // Song confirmed
}

Event Manipulation

Events can be cancelled or stopped:
public function onCountdownStart(event:CountdownScriptEvent)
{
  // Prevent countdown from starting
  event.cancel();
  
  // Stop other scripts from receiving this event
  event.stopPropagation();
}
Only cancelable events can be cancelled. Check event.cancelable before calling cancel().

Scripted Classes

Create custom classes that extend Flixel base classes:

Available Base Classes

From funkin/modding/base/:
  • ScriptedFlxBasic
  • ScriptedFlxObject
  • ScriptedFlxSprite
  • ScriptedFlxSpriteGroup
  • ScriptedFlxState
  • ScriptedFlxSubState
  • ScriptedFlxTypedGroup

Example: Custom Sprite

CustomSprite.hxc
import flixel.FlxSprite;

class CustomSprite extends FlxSprite
{
  public function new(x:Float, y:Float)
  {
    super(x, y);
    makeGraphic(64, 64, 0xFFFF0000); // Red square
  }
  
  public override function update(elapsed:Float)
  {
    super.update(elapsed);
    
    // Rotate sprite
    angle += 90 * elapsed;
  }
}
Usage in another script:
var sprite = new CustomSprite(100, 100);
FlxG.state.add(sprite);

Available Imports

The following classes are automatically imported:
static final DEFAULT_IMPORTS:Array<Class<Dynamic>> = [
  funkin.Assets,
  funkin.Paths,
  funkin.Preferences,
  funkin.util.Constants,
  flixel.FlxG
];
You can use these directly without importing:
// Access game assets
var sprite = Assets.getSprite('myImage');

// Get paths
var soundPath = Paths.sound('mySound');

// Access preferences
var volume = Preferences.volume;

// Access Flixel globally
FlxG.camera.flash();

Manual Imports

For other classes, import them explicitly:
import flixel.FlxSprite;
import flixel.tweens.FlxTween;
import flixel.util.FlxColor;

class MyScript
{
  public function flashScreen()
  {
    FlxG.camera.flash(FlxColor.WHITE, 0.5);
  }
}

Script Event Interface

Scripts can implement the IScriptedClass interface:
interface IScriptedClass
{
  public function onScriptEvent(event:ScriptEvent):Void;
  public function onCreate(event:ScriptEvent):Void;
  public function onDestroy(event:ScriptEvent):Void;
  public function onUpdate(event:UpdateScriptEvent):Void;
}
Specialized interfaces add more events:
  • IPlayStateScriptedClass - All gameplay events
  • IStateChangingScriptedClass - State transition events
  • IFreeplayScriptedClass - Freeplay menu events
  • IBPMSyncedScriptedClass - Beat and step events
  • INoteScriptedClass - Note-related events

Practical Examples

Example 1: Score Multiplier

ScoreMultiplier.hxc
import funkin.modding.module.Module;

class ScoreMultiplier extends Module
{
  public function new()
  {
    super('scoremultiplier');
  }
  
  public function onNoteHit(event:HitNoteScriptEvent)
  {
    // Double the score for sick notes
    if (event.judgement == 'sick')
    {
      event.score *= 2;
      trace('Score doubled!');
    }
  }
}

Example 2: Custom Countdown Sound

CustomCountdown.hxc
import funkin.modding.module.Module;
import flixel.FlxG;

class CustomCountdown extends Module
{
  public function new()
  {
    super('customcountdown');
  }
  
  public function onCountdownStep(event:CountdownScriptEvent)
  {
    // Play custom sound on "Go!"
    if (event.step == CountdownStep.THREE)
    {
      FlxG.sound.play(Paths.sound('customGo'));
    }
  }
}

Example 3: Health Drain

HealthDrain.hxc
import funkin.modding.module.Module;
import funkin.play.PlayState;

class HealthDrain extends Module
{
  var drainRate:Float = 0.01;
  
  public function new()
  {
    super('healthdrain');
  }
  
  public function onUpdate(event:UpdateScriptEvent)
  {
    // Only drain in PlayState
    if (Std.is(FlxG.state, PlayState))
    {
      var playState = cast(FlxG.state, PlayState);
      playState.health -= drainRate * event.elapsed;
    }
  }
}

Example 4: Camera Flash on Beat

BeatFlash.hxc
import funkin.modding.module.Module;
import flixel.util.FlxColor;

class BeatFlash extends Module
{
  public function new()
  {
    super('beatflash');
  }
  
  public function onBeatHit(event:SongTimeScriptEvent)
  {
    // Flash every 4 beats
    if (event.beat % 4 == 0)
    {
      FlxG.camera.flash(FlxColor.WHITE, 0.15);
    }
  }
}

Example 5: Chart Modifier

ChartRandomizer.hxc
import funkin.modding.module.Module;

class ChartRandomizer extends Module
{
  public function new()
  {
    super('chartrandomizer');
  }
  
  public function onSongLoaded(event:SongLoadScriptEvent)
  {
    // Randomize note directions
    for (note in event.notes)
    {
      note.data = FlxG.random.int(0, 3);
    }
    trace('Randomized ' + event.notes.length + ' notes!');
  }
}

ModStore - Persistent Data

Share data between scripts using ModStore:
public static function register(id:String, ?data:Dynamic):Dynamic
public static function get(id:String):Null<Dynamic>
public static function remove(id:String):Null<Dynamic>
Usage:
import funkin.modding.ModStore;

// Register a store
var myData = ModStore.register('mymod', { score: 0 });

// Access from any script
var data = ModStore.get('mymod');
data.score += 100;

// Clean up when done
ModStore.remove('mymod');

Security & Limitations

For security, certain classes are blacklisted or sandboxed:
Blacklisted (not accessible):
  • Sys - System commands
  • sys.* - File system access
  • cpp.Lib - Native libraries
  • polymod.* - Polymod internals
  • hscript.* - Script parser
Sandboxed (limited access):
  • FileUtilFileUtilSandboxed
  • ReflectReflectUtil (safe subset)
  • TypeReflectUtil (safe subset)
  • Newgrounds API (read-only)

Debugging Scripts

Use trace() to log debug information:
trace('Hello from my script!');
trace('Current beat: ' + event.beat);
trace('Health: ' + playState.health);
Traces appear in the game console (enable with F5 or compile flags).

Best Practices

  • Avoid heavy operations in onUpdate()
  • Cache expensive calculations
  • Use module active property to disable when not needed
  • Set appropriate priority to avoid redundant processing
  • Check for null before accessing properties
  • Validate event data before using
  • Use try-catch for risky operations
  • Test scripts thoroughly before distribution
  • One class per file
  • Use descriptive class and variable names
  • Comment complex logic
  • Break large scripts into smaller modules
  • Don’t assume specific class internals
  • Use public APIs when available
  • Test with multiple game versions
  • Document required API version

Next Steps

Polymod Deep Dive

Learn advanced asset manipulation and merging

Modding Overview

Return to the modding overview

Build docs developers (and LLMs) love