NEON STRUCT

NEON STRUCT

36 ratings
NEON STRUCT Level Editor Documentation
By dphrygian
This is the official documentation for the NEON STRUCT level editor and mod tools. It introduces the reader to the editor interface and configuration file format, and describes how to publish a mod as a standalone package or for Steam Workshop.
   
Award
Favorite
Favorited
Unfavorite
Introduction
Custom levels for NEON STRUCT are constructed using the game's built-in level editor. Custom objects and scripts are authored separately in a text file, using an ad hoc format similar to INI files.

Mods may be published as standalone packages, or uploaded to Steam Workshop using the Steam Console Client.

If you have specific questions that are not answered in this document, please ask in the forums. If necessary, you may contact me for further support at support@neonstruct.com.
Level Editor Interface: Basics
Note: For Mac OS X, editor controls are slightly different. Use Command instead of Ctrl in all instances, and use the number row instead of function keys (e.g., 1 instead of F1).

Launching the Level Editor
Run NEON STRUCT as usual, and continue past the title screen into normal gameplay. Press 0 on the numpad to switch to the level editor. Press it again to switch back to gameplay.

Note: If numpad 0 does not work, or if you want to use a different key, check the game's Controls options. This can be rebound to any other key.

Note: It is recommended to run the editor at a display resolution of at least 1280x720 pixels. Smaller displays may crop some elements off the screen.

Moving Around in the Editor
Controlling the camera in the editor is much like moving in a first-person game. Use the WASD cluster to move and the mouse to look around. Hold Shift to move the camera faster.

To very quickly move around the level, you can teleport the camera to the location beneath the reticle by pressing X.

Note: The editor's controls are not bindable.

Adding and Removing Voxels
Levels in NEON STRUCT are formed primarily of voxels: cubic meter chunks of solid geometry.

Left-click to add a voxel beneath the reticle.

While aiming at a voxel, Ctrl + left-click to replace it with the currently selected voxel type (see next section).

While aiming at a voxel, right-click to remove it.

While aiming at a voxel, Ctrl + right-click to "eyedropper" it (make its type the currently selected type).

Selecting the Voxel Type
Press F3 to open the voxel palette. Left-click on a voxel type to select it; that type of voxel will now be used whenever you add a voxel to the world.

Use Shift + left-click in the voxel palette to select and deselect multiple types. Now when you add voxels to the world, they will be chosen at random from the selection.

Press F2 to exit the voxel palette and return to the main editor mode.

Manipulating the Brush
Building a world one voxel at a time would be very tedious. Instead, you can add or remove large chunks of voxels at once using the brush.

To move the brush to the location beneath the reticle, press the middle mouse button. You will see a red wireframe box appear there; this is the brush.

Ctrl + middle-click to move the brush into the targeted voxel instead of onto it.

Shift + middle-click to expand the brush to another location beneath the reticle. The brush will expand to include this location. You may continue to expand the brush in this way by Shift + middle-clicking around the world.

Ctrl + Shift + middle-click to expand the brush into the targeted voxel.

Note: The B key can be used instead of the middle mouse button, for users without a three-button mouse.

Applying the Brush
Press F to fill the brush with the selected voxel type. If multiple voxel types are selected, the brush will be filled with a random pattern from that selection.

Press Shift + F to erase all voxels within the brush.

Applying Veneers
Alt + left-click to paint the selected voxel type as a veneer on one or more surfaces of a voxel.

Alt + right-click to remove the veneer.

Note: Veneers do not change the surface type of a voxel; a wood veneer painted over a metal voxel will play the metallic footstep sound when walked upon.

Undo and Redo
Press Ctrl + Z to undo your last action(s). You can undo up to 100 actions.

Press Ctrl + Y to redo the last change(s) you undid.

Hiding Voxels
Press H to hide all voxels inside the brush. Hidden voxels are only invisible in the editor; they will be visible as normal during gameplay.

Press Shift + H to hide all voxels outside the brush.

Press Alt + H to unhide all hidden voxels.

Press Alt + Shift + H to unhide hidden voxels inside the brush.

Resizing the Level
At startup, the editor creates a level with dimensions of 60m x 60m x 30m, but the bounds can be adjusted to any size.

Press [ or ] to contract or expand the bounds on the X axis.
Press ; or ' to contract or expand the bounds on the Y axis.
Press . or / to contract or expand the bounds on the Z axis.

Note: Large, complex levels may suffer from poor performance. A simple rule of thumb is to keep the level's volume less than 150,000 voxels. For example, the default size is 60*60*30 = 108,000 voxels.

Note: The extents of a level can be adjusted at any time, but they can only expand or contract in one direction on each axis. If more space is needed on the other side, the level will need to be rebuilt by hand.

Saving and Loading
Press F6 to save a level as a new file.

Press Ctrl + S to save the level with its current filename.

Press F9 to load a level.

Note: NEON STRUCT levels are customarily saved with a .neonroom extension, but the game does not require it.

Clearing and Filling
Press F7 to clear the world of all voxels and placed entities. (Effectively, begin a new map.)

Press Alt + Shift + F8 to fill any empty spaces on the outer surfaces of the world with the selected voxel type. This is mainly used to "waterproof" a level with invisible voxels and ensure the player and other entities cannot fall out of the world.
Level Editor Interface: Entities
Selecting an Entity Type
Press F4 to open the entity palette. Left-click in the left column to select a category of entities, then left-click in the right column to select an entity from that category.

Return to the main editor mode by pressing F2.

Placing Entities
Press R to place an entity of the selected type beneath the reticle.

While aiming at a placed entity, press Ctrl + R to rotate it counter-clockwise by 90 degrees.

While aiming at a placed entity, press Shift + R to remove it.

Entity Paths and Links
Certain kinds of entities may use or require paths as additional information about how to behave in the world. For example, AIs can follow a looping patrol path, and light switches can be linked to the lights they control by paths.

To give an entity a path, first put the brush on the entity by middle-clicking on it.

Press T to add a new path node beneath the reticle.

While aiming at a path node, press Ctrl + T to rotate it counter-clockwise by 90 degrees.

While aiming at a path node, press Shift + T to remove it.

While aiming at a path node, press Alt + T to select it. The next node added will be inserted after this one.
Custom Mission 101: Basics
This section will walk you through the steps to create a simple custom mission.

A custom mission for NEON STRUCT requires at least three files:
  1. A map file created in the level editor
  2. A .config text file defining the level's name and other properties
  3. A .neonmission text file defining the mission's entry point level, to appear in the Custom Missions menu.
Custom missions may consist of a single level, or they may span multiple levels.

Make a Mods Folder
If it does not exist yet, create a Mods subfolder in the folder where the NEON STRUCT executable is located. (For example, if you are using Windows, this may be C:\Program Files (x86)\Steam\steamapps\common\NEON STRUCT\)

Within the NEON STRUCT/Mods/ folder, create another subfolder called Mission101. This will be the working directory for all custom content related to this mission.

Creating the Map
Open the level editor and place a Spawner_Player entity anywhere on the floor.

Save this map in your Mission101 folder as mission101.neonroom, then close NEON STRUCT.

Writing the Level Config File
Create a new text file in the Mission101 folder called mission101.config, and paste the following into it:
[Level_Mission101] WorldDef = "WorldDef_Mission101" Name = "Level_Mission101" [WorldDef_Mission101] WorldSizeX = 60 WorldSizeY = 60 WorldSizeZ = 30 WorldGenDef = "WorldGenDef_Mission101" VoxelSetDef = "VoxelSet_Default" SkyDef = "Sky_Default" LightDef = "Light_Default" FogDef = "Fog_Default" BloomDef = "Bloom_Default" PostDef = "Post_Default" [WorldGenDef_Mission101] FixedRoomDef = "mission101.neonroom" [English] Level_Mission101 = "Mission 101"
The config file format will be explained in more detail later. In brief, this tells the game what your level is called, how big it is, where its map file is located, and how to render it.

Writing the Mission File
Make another new text file in the Mission101 folder called mission101.neonmission, and paste the following into it:
NumCustomMissions = 1 CustomMission0 = "Level_Mission101"
This file is used to populate the Custom Missions menu on the NEON STRUCT title screen.

Playing the Custom Mission
Run NEON STRUCT again. Select Custom Missions on the title screen.

"Mission 101" should now appear in the Custom Missions menu. Select it, and you will load into your empty custom level.
Custom Mission 101: Custom Entities
Now that you can load into an empty world, let's add a way to complete the level and return to the title screen.

Defining a Custom Entity
Open mission101.config again. Paste the following into it:
[Mission101_EndLevelFrob] EldTransform = "ImmobileTransform" EldMesh = "Mission101_EndLevelFrob_Mesh" EldFrobbable = "Mission101_EndLevelFrob_Frobbable" Reactions = "Mission101_EndLevelFrob_Reactions" [Mission101_EndLevelFrob_Mesh] Mesh = "Meshes/box.cms" [Mission101_EndLevelFrob_Frobbable] UseMeshExtents = true FrobVerb = "Use" [Mission101_EndLevelFrob_Reactions] NumReactions = 1 Reaction0 = "Mission101_EndLevelFrob_OnFrobbedReaction" [Mission101_EndLevelFrob_OnFrobbedReaction] Rule = "OnFrobbedRule" NumActions = 1 Action0 = "Mission101_EndLevelFrob_ReturnToTitle" [Mission101_EndLevelFrob_ReturnToTitle] ActionType = "SendEvent" EventName = "GoToTitleLevel" Recipient = "PlayerPE" [English] Mission101_EndLevelFrob = "End Level Trigger"
This defines a new kind of entity that will return the player to the title screen when it is "frobbed", or used.

Exposing the Entity to the Editor
Finally, you need to describe how to spawn the entity, and tell the editor that it exists for use in your level. Paste the following into mission101.config as well:
[NeonTools_AppendCategories] Level_Mission101 = "SpawnerCategory_Level_Mission101" [SpawnerCategory_Level_Mission101] NumSpawnerDefs = 1 SpawnerDef0 = "Spawner_Mission101_EndLevelFrob" [Spawner_Mission101_EndLevelFrob] Entity = "Mission101_EndLevelFrob"

Placing the New Entity
Run NEON STRUCT and enter Mission 101, then open the editor. You must be in Mission 101, or the editor will not allow you to place the new entity.

Press F9 and load mission101.neonroom.

Open the spawner palette (F4) and notice that the category "SpawnerCategory_Level_Mission101" has appeared at the bottom of the left column. Select this category and select the Spawner_Mission101_EndLevelFrob from the right column.

Place the Spawner_Mission101_EndLevelFrob somewhere in the level in front of the Spawner_Player.

You may also wish to place a Spawner_NeonLight (from SpawnerCategory_Lights) nearby so you can see the object clearly.

Save mission101.neonroom again and exit the editor.

Playing the Mission
Pause the game, return to the title screen, and re-enter Mission 101 from the Custom Missions menu.

The world will be reloaded and the new entity should now appear in front of you. Use it to complete the level and return to the title screen.
Publishing
There are two ways to publish your custom mission or mod. You may package the files and let users unpack them into their Mods directory manually, or you may upload the files to Steam Workshop.

Manual Packages
If you wish to make your custom mission or mod available to users who do not use Steam Workshop, you may simply pack your mod's folder (e.g., NEON STRUCT/Mods/Your Mission/) as a ZIP file or compressed tarball.

Provide directions to the user to unpack the archive into their NEON STRUCT/Mods folder (creating it if it does not yet exist). When the user runs NEON STRUCT, your custom mission or mod will be available.

Steam Workshop
NEON STRUCT does not have built-in support for publishing to Steam Workshop. Instead, you should use the Steam Console Client (or SteamCMD).

First, download and run SteamCMD once; it may need to update itself and verify your two-factor authentication the first time.

SteamCMD requires a small definition file that describes the item to be uploaded to Workshop. Create a file (e.g. mission101.vdf) with the following contents:
"workshopitem" { // This is NEON STRUCT's app id, it must always be 310740 "appid" "310740" // Paths can be either absolute, or relative to the location of steamcmd.exe "contentfolder" "(path to NEON STRUCT/Mods/Your Mod/)" "previewfile" "(path to a 16:9 preview JPG)" // 0 means publicly visible, 1 means friends-only, 2 means hidden "visibility" "0" "title" "(your mission/mod's title)" "description" "(a brief description of your mission/mod)" "changenote" "(optional description of recent changes)" }

Then run SteamCMD with your login information, the workshop_build_item instruction, and the path to your VDF file. (This path can be absolute, or relative to the location of steamcmd.exe.)
steamcmd.exe +login [your user name] [your password] +workshop_build_item [path to your .vdf file] +quit

After the item is published, your VDF file will be updated to include a publishedfileid property, which is your item's unique identifier in the Workshop. Running SteamCMD again with this property in the VDF file will update the existing item instead of creating a new item.
Config File Format
Configuration files for NEON STRUCT (as seen in prefs.cfg or the Mission 101 examples above) use a bespoke format similar to INI files.

Key-Value Pairs
The fundamental statements in a config file are key-value pairs. These are written in the form:
Key = Value

Keys are unique, case-sensitive identifiers with no whitespace in them. For example,
NeonStruct neonstruct NEONSTRUCT Neon_Struct Neon-Struct
are all valid and unique keys, but
Neon Struct
is not.

Values can be Booleans (true or false), numbers, or string literals (text). For example,
5 -1 3.2 true "Hello World!"
are all values. Complex expressions (e.g., 5 + 2) are not allowed.

Contexts
A context (also called a "section") is a named group of key-value pairs. A context is declared in the form:
[Context] KeyA = ValueA KeyB = ValueB

All key-value pairs following the context declaration belong to that context, until the next context declaration. Like keys, context names are unique, case-sensitive identifiers with no whitespace.

A key's "full name" includes its context; in this way, a key may be reused in different contexts without overwriting its paired value. In this example,
[OldMan] Name = "David" [YoungWoman] Name = "Jillian"
the key "Name" is used in two contexts. The fully-qualified keys OldMan/Name and YoungWoman/Name are still unique, so this usage is permitted.

Comments
Single-line comments are started with #, and multi-line comments are opened with #! and closed with !#. Comments are ignored when parsing the config file. For example,
[Player] Health = 500 # Increased player's health for testing, don't forget to change back to 100 #! Commenting this out entirely, no one likes escort quests! [EscortQuestCharacter] Health = 50 !#

Macros and Counters
The config file format has no support for arrays or sequences of values. To work around this, the common pattern (as seen in the Mission 101 examples) is to declare the number of items in a sequence and give each one an ordered, 0-based key. For example:
NumNames = 3 Name0 = "David" Name1 = "Jillian" Name2 = "Vinod"

But this pattern is prone to error; when adding or removing items, the author may forget to set NumNames correctly, or the indices might get mixed up.

The config file format introduces a pair of syntactical helpers—macros and counters—to simplify this.

A macro is a short string declared with @ that can be inserted into subsequent keys with @@. For example:
@ Player # Declare a macro "Player" @@Health = 50 # Creates a key "PlayerHealth" @@Speed = 10.0 # Creates a key "PlayerSpeed"

A counter is a numeric value determined at parse time based on the number of items in a sequence. For example:
NumNames = & # Declare a counter for NumNames Name& = "David" # Creates a key "Name0" and increments NumNames to 1 Name& = "Jillian" # Creates a key "Name1" and increments NumNames to 2 Name& = "Vinod" # Creates a key "Name2" and increments NumNames to 3

Counter values can be queried in a key without incrementing by using ^ instead of &. For example:
NumPeople = & # Declare a counter for NumPeople Name& = "David" # Creates a key "Name0" and increments NumPeople to 1 Height^ = 69 # Creates a key "Height0", doesn't change NumPeople Name& = "Jillian" # Creates a key "Name1" and increments NumPeople to 2 Height^ = 62 # Creates a key "Height1", doesn't change NumPeople

Using macros and counters together, the original sequence example can be simplified like so:
NumNames = & @ Name @@& = "David" @@& = "Jillian" @@& = "Vinod"

Now, names can be added and removed from the sequence without risk of breaking NumNames or the indices of each key.
Entities and Components
All objects in NEON STRUCT—the player, guards, security cameras, desks and chairs, etc.—are called entities.

Many entities are provided by default within NEON STRUCT's core files, but custom missions will often require new entities (such as the box in the Mission 101 example).

An entity is composed of one or more components. Components describe the properties and behavior of the entity.

Entities and components are defined in config files like so:
# First, make a context for the entity. This is the object's name. [Box] # Within this context, list the types of components the entity uses. # The definitions for these components are referenced by name. EldTransform = "Box_Transform" EldMesh = "Box_Mesh" # Next, define properties of the components in their own named contexts. [Box_Transform] CanMove = false [Box_Mesh] Mesh = "Meshes/box.cms"
Tutorial: Keycard Door
Locked doors that can be opened with a keycard must be set up with a custom entity. This section describes how to create a custom keycarded door.

Spawner Definition
Your custom door will require a spawner. A spawner describes what entity to spawn, but also where to spawn it relative to the voxel center where it is placed. Conventionally, doors in NEON STRUCT are positioned 0.4375m off-grid.

[Spawner_Door_Tutorial] Entity = "Door_Tutorial" OffsetY = -0.4375 OffsetZ = 0.5

Adding the Spawner to a Level
A spawner must be exposed to the editor for each level in which it will be used. This requires two parts: first, a key-value pair defining a spawner category for the level should be added to the NeonTools_AppendCategories context; then, the spawner definition should be added to a NumSpawnerDefs sequences in the named category.

For example, to add this spawner to the Mission 101 level:
[NeonTools_AppendCategories] Level_Mission101 = "SpawnerCategory_Level_Mission101" [SpawnerCategory_Level_Mission101] NumSpawnerDefs = & @ SpawnerDef @@& = "Spawner_Mission101_EndLevelFrob" @@& = "Spawner_Door_Tutorial"

Door Entity Definition
A custom keycarded door should begin with an Extends statement, which makes it inherit properties from a base entity definition. Depending on whether the door is hinged on the left or right, it should extend either LockedDoorLeft or LockedDoorRight.

The keycard that unlocks the door will be provided in a custom EldDoor component definition. New or overridden components are referenced by name beneath the Extends statement.

[Door_Tutorial] Extends = "LockedDoorLeft" EldDoor = "Door_Tutorial_DoorComponent"

Door Component Definition
Finally, the EldDoor component named Door_Tutorial_DoorComponent must be defined. Like the door entity, this extends from a base definition: either LockedDoorLeftDoor or LockedDoorRightDoor depending on the use case.

The keycard to unlock the door is referenced by name. Elsewhere, this same name will be used to define the item that goes into the player's inventory, and the localized string that is displayed when the keycard is obtained or used.

[Door_Tutorial_DoorComponent] Extends = "LockedDoorLeftDoor" Keycard = "Keycard_Tutorial"
Tutorial: Keycard Guard
To unlock a keycarded door, the player needs a way to obtain the keycard. There are various ways to do this. The player may simply find the keycard lying around, or it might be locked inside a container. In this case, it will be carried by a guard for the player to pickpocket.

Guard Spawner Definition
Human AIs are typically spawned 0.32m above the voxel grid. Be sure to add this spawner definition to the level's spawner category (see the Keycard Door tutorial for more information).

[Spawner_AI_Tutorial] Entity = "AI_Tutorial" OffsetZ = 0.32

Guard Entity and Component Definitions
For this example, we are simply inheriting from a BaseRangedGuard class which gives us a randomized character wearing an overbright white outfit. (Later, we will examine how to configure an AI's appearance.)

The guard needs the keycard defined in two places: first, a NeonPockets component defines what the player will receive when pickpocketing the guard; second, an EldKeyRing component defines what locked doors the guard is able to open.

Note: Pickpocketing the guard does not remove the keycard from its keyring component; so a guard who has been pickpocketed will still be able to follow the player through the locked door.

[AI_Tutorial] Extends = "BaseRangedGuard" NeonPockets = "AI_Tutorial_Pockets" EldKeyRing = "AI_Tutorial_KeyRing" [AI_Tutorial_Pockets] Keycard = "Keycard_Tutorial" Money = 10 # Let's give the player a bit of cash, too [AI_Tutorial_KeyRing] NumKeycards = & @ Keycard @@& = "Keycard_Tutorial"

Localized Keycard Name
The keycard's name is displayed on screen when it is picked up and used. Instead of showing the player the literal string "Keycard_Tutorial", that name is used to lookup a localized (or translated) string in the string table.

A localized string must be provided for English, at least; if a string does not exist in the user's chosen language, the English string will always be used instead. Localized strings are defined by key-value pairs in the appropriate language's section.

For example, to give the tutorial keycard an English name:
[English] Keycard_Tutorial = "A Tutorial Keycard"
Tutorial: Keycode Door
Locked doors that can be opened with a keycode are set up almost the same as keycard doors. The only difference is in the door component:
[Door_Tutorial_DoorComponent] Extends = "LockedDoorLeftDoor" Keycode = 0451

The keycode may be any 4-digit number except 0000.

Doors may use both keycards and keycodes:
[Door_Tutorial_DoorComponent] Extends = "LockedDoorLeftDoor" Keycard = "Keycard_Tutorial" Keycode = 0451
Tutorial: Notes
Notes are electronic tablets that the player can read. They are typically used to deliver local narrative or provide the keycode for a nearby door.

A note should extend from the NoteBase entity and implement a NeonReadable component with a String property:
[Spawner_Note_Tutorial] Entity = "Note_Tutorial" OffsetZ = -0.4921875 OffsetYaw = -30.0 [Note_Tutorial] Extends = "NoteBase" NeonReadable = "Note_Tutorial_Readable" [Note_Tutorial_Readable] String = "Note_Tutorial" [English] Note_Tutorial = "The keycode is 0451."
Tutorial: Hackable Doors / Hack Boards
Hackable doors are configured similarly to the keycard and keycode doors previously described, but they require two additional components: a preconfigured EldFrobbable component called "HackableDoorFrobbable", and a NeonHackable component that specifies what hacking minigame board will be used.

[Door_Tutorial] Extends = "LockedDoorLeft" EldDoor = "Door_Tutorial_DoorComponent" EldFrobbable = "HackableDoorFrobbable" NeonHackable = "Door_Tutorial_Hackable" [Door_Tutorial_Hackable] BoardDef = "Board_Tutorial"

Five default hack boards are available:
Board_VeryEasy Board_Easy Board_Medium Board_Hard Board_VeryHard

But if you wish to define your own custom hack board, you may do that too:
[Board_Tutorial] NumBricksX = 5 # How many bricks there are across the width of the board NumBricksY = 3 # How many bricks there are vertically BrickType1,1 = "Null" # The second brick on the second row will be missing BrickType1,3 = "Barrier" # The fourth brick on the second row will be unbreakable BoardHeight = 9 # The height of the board, in brick units PaddleSpeed = 0.15 BallSpeed = 1.0 PaddleExtentsX = 0.1

The BrickType#,# definitions are optional properties to give the board a more interesting structure. Brick coordinates are row-major and 0-based, with the origin at the top left of the board. (In other words, BrickType0,0 is the top left brick.) BrickTypes may be "Null" or "Barrier"; any other string will be ignored and a normal brick will be used instead.
Tutorial: Containers
Containers are chests which give the player items or money when opened. Chests may be unlocked, or they may require hacking to unlock and open.

Unlocked Container
Unlocked containers should inherit from the Container_Base entity. Containers are set up with a NeonPockets component, similar to how guards are configured to carry keycards or money.

[Spawner_TutorialContainerA] Extends = "Spawner_Container_Base" Entity = "TutorialContainerA" [TutorialContainerA] Extends = "Container_Base" NeonPockets = "TutorialContainerA_Pockets" [TutorialContainerA_Pockets] Money = 100 NumItemUses = & @ ItemUses @@&Slot = "StimsHealth" @@^NumUses = 2 @@&Slot = "Scramblers"

Note: If NumUses is not defined for a slot, one use of that item will be given by default. Items that may be given include:
Tokens Scramblers Subtractors Displacers StimsSpeed StimsHealth StimsQuiet StimsHack StimsCloak

Locked Container
Locked containers should extend the Container_Locked_Base entity.

[Spawner_TutorialContainerB] Extends = "Spawner_Container_Base" Entity = "TutorialContainerB" [TutorialContainerB] Extends = "Container_Locked_Base" NeonPockets = "TutorialContainerB_Pockets" [TutorialContainerB_Pockets] NumKeycards = & @ Keycard @@& = "Keycard_Tutorial"

By default, a locked container will use a Board_Medium hack board. To use a different board (including a custom board, as described in the Hackable Doors tutorial), implement a NeonHackable component:
[TutorialContainerB] Extends = "Container_Locked_Base" NeonPockets = "TutorialContainerB_Pockets" NeonHackable = "TutorialContainerB_Hackable" [TutorialContainerB_Hackable] BoardDef = "Board_VeryEasy"
Tutorial: AI Patrols
AI characters have two default behaviors. An AI without a patrol path will simply stand in place until it sees or hears a disturbance. An AI with a patrol path will walk in a simple loop until disturbed.

To assign a patrol path to an AI in the editor, place the brush on its spawner (middle-click). Then place path nodes (T) along the path the AI should follow.

AIs will turn to face the direction of each path node as they reach them. Rotate path nodes (Ctrl+T) to achieve the desired patrol behavior.

Patrol paths are always looping. To create a back-and-forth path, place path nodes along the path in both directions. There is no support for random/branching paths in NEON STRUCT.
Tutorial: Security Systems
Security systems consist of alarm boxes and security cameras. Each security camera is connected to an alarm box, and sounds that alarm if it spots the player. Each alarm box may be connected to multiple security cameras.

To link alarm boxes and cameras in the editor, place the brush on the alarm box (middle-click), then draw paths to the locations of each camera in the system (T).

Security cameras sweep back and forth periodically. Cameras with arcs of 15 and 60 degrees are provided by default; custom cameras with different sweeping angles can be created if desired:

[Spawner_SecurityCamera_Tutorial] Entity = "SecurityCamera_Tutorial" OffsetY = -0.25 [SecurityCamera_Tutorial] Extends = "SecurityCamera" NeonSecCam = "SecurityCamera_Tutorial_SecCam" [SecurityCamera_Tutorial_SecCam] Extends = "SecurityCameraSecCam" FixedPitch = -25.0 MaxYaw = 30.0
Tutorial: Signs
Signs in NEON STRUCT come in two forms.

The first is a small plaque usually posted mounted to a door to describe the interior room. These signs use a NeonSign component, and contain a short string that is shown when the player is near and looks directly at the sign.
[Spawner_SignSmall_Tutorial] Entity = "SignSmall_Tutorial" OffsetY = -0.4921875 OffsetZ = -0.1 [SignSmall_Tutorial] Extends = "SignSmallBase" NeonSign = "SignSmall_Tutorial_Sign" [SignSmall_Tutorial_Sign] Extends = "SignSmallBaseSign" SignText = "Sign_TutorialA" [English] Sign_TutorialA = "Here be dragons."

The second kind of sign is a larger sign that the player can frob (interact with) to read. These signs use a NeonReadable component, which opens a UI screen and pauses the game. This is mainly intended for signs that contain more information than would fit on a single line.
[Spawner_SignMedium_Tutorial] Entity = "SignMedium_Tutorial" OffsetY = -0.484375 [SignMedium_Tutorial] Extends = "SignMediumBase" NeonReadable = "SignMedium_Tutorial_Readable" [SignMedium_Tutorial_Readable] String = "Sign_TutorialB" [English] Sign_TutorialB = "Here be dragons.\nThere be dragons.\nEverywhere be dragons."
Tutorial: Radios
Radios are interactive entities that play one or more songs on a loop. A radio should extend the RadioBase entity and implement a NeonJukebox component that lists the songs to be played.

Songs should inherit from the SoundArchetypeDiegeticMusic archetype and specify a source file. (Note: NEON STRUCT uses the Ogg Vorbis format for its music due to MP3 licensing issues.)

[Spawner_Radio_Tutorial] Entity = "Radio_Tutorial" OffsetZ = -0.3125 OffsetYaw = -15.0 [Radio_Tutorial] Extends = "RadioBase" NeonJukebox = "Radio_Tutorial_Jukebox" [Radio_Tutorial_Jukebox] NumTracks = & @ Track @@& = "Music_Tutorial" [Music_Tutorial] Extends = "SoundArchetypeDiegeticMusic" Source = "Audio/Music/tutorial.ogg" Volume = 1.0
Tutorial: Traveling Between Levels
Traveling between levels in multi-level missions may be strictly linear (the player can never return to a previous level), or the state of previous levels may be saved so the player can freely roam between two or more levels that comprise a larger, persistent world.

Linear Travel
The simplest way to progress to the next level is to define a NextLevel property in the current level's context:
[Level_TutorialA] WorldDef = "WorldDef_TutorialA" Name = "Level_TutorialA" NextLevel = "Level_TutorialB"

Then invoke the predefined action "GoToNextLevelAction" in any script:
[Tutorial_OnFrobbedReaction] Rule = "OnFrobbedRule" NumActions = & @ Action @@& = "GoToNextLevelAction"

Persistent Travel
When traveling between levels that comprise a larger, persistent space, it is necessary to specify which level to load and where in that level to enter.
[Tutorial_OnFrobbedReaction] Rule = "OnFrobbedRule" NumActions = & @ Action @@& = "Tutorial_GoToLevelB2" [Tutorial_GoToLevelB2] ActionType = "EldGoToLevel" Level = "Level_TutorialB" TeleportLabel = "TutorialB_TeleportMarker_2"

Here, the EldGoToLevel action is used with a Level property declaring the target level and a TeleportLabel declaring where in that level to appear. In Level_TutorialB, there should be an invisible entity matching this label:
[TutorialB_TeleportMarker_2] EldTransform = "ImmobileTransform" Label = "."

Note: The "." syntax declares an undefined, anonymous component. An undefined Label component automatically takes its entity name as its label.
Tutorial: Level Start Script
Although not strictly necessary, most NEON STRUCT levels should begin with a script that introduces the level, initializes player objectives, and schedules a phone call to provide information about the mission.

Intro Script Entity
Scripts are entities that typically only have a Reactions component; they do not physically exist in the world, but sit in the background to handle various events.

[Spawner_Script_Tutorial_Intro] Entity = "Script_Tutorial_Intro" [Script_Tutorial_Intro] Reactions = "Script_Tutorial_Intro_Reactions"

Reactions Component
A Reactions component defines how an entity reacts when it receives certain events (or messages). It comprises a sequence of named reactions:
[Script_Tutorial_Intro_Reactions] NumReactions = & @ Reaction @@& = "Script_Tutorial_Intro_OnSpawnedReaction" @@& = "Script_Tutorial_Intro_TryCallReaction"

Each reaction is described by a rule and a sequence of actions. The rule defines what event triggers the reaction, and an optional sequence of conditions that determine whether the reaction is appropriate at this time.

[Script_Tutorial_Intro_OnSpawnedReaction] Rule = "Script_Tutorial_Intro_OnSpawnedRule" NumActions = & @ Action @@& = "Script_Common_FlushWorldFilesAction" @@& = "Script_Common_ResetResults" @@& = "Script_Common_ClearObjectivesAction" @@& = "Script_Tutorial_Intro_AddObjective" @@& = "Script_Tutorial_Intro_TryCall" @@& = "Script_Common_ShowTitleCard" @@& = "Script_Tutorial_Intro_SetTitleCard" @@& = "Script_Tutorial_Intro_SetTitleCardSub" @@& = "Script_Common_QueueHideTitleCard" [Script_Tutorial_Intro_OnSpawnedRule] Event = "OnSpawned"

Here, we use a simple rule: this reaction is triggered when the "OnSpawned" event is received, which occurs when this entity is spawned in the world.

The sequence of actions includes a mix of predefined common actions and custom actions.

"Script_Common_FlushWorldFilesAction" flushes the state of any level the player has previously visited. This is used to minimize the size of save files. If the player is expected to backtrack to a previous level, this should be removed from the script.

"Script_Common_ResetResults" resets various counters for the Mission Results screen. If a mission spans multiple levels, this should only be invoked in the first level.

"Script_Common_ClearObjectivesAction" removes all player objectives, and is typically following by adding one or more new objectives so the player always has an objective.

"Script_Common_ShowTitleCard" displays the title card UI elements (typically, the location and date/time of the mission). This should be immediately followed by an action to set the text on the title card.

"Script_Common_QueueHideTitleCard" queues an event to hide the title card after a fixed amount of time has elapsed.

The custom actions are defined as follows:
[Script_Tutorial_Intro_AddObjective] ActionType = "NeonAddObjective" Objective = "Objective_Tutorial_DoTheThing" [Script_Tutorial_Intro_TryCall] ActionType = "SendEvent" EventName = "TryCall" DispatchDelay = 2.0 Recipient = "SelfPE" [Script_Tutorial_Intro_SetTitleCard] ActionType = "SetConfigVar" VarContext = "HUD" VarName = "TitleCardText" ValuePE = "TitleCard_Tutorial_PE" [TitleCard_Tutorial_PE] PEType = "ConstantString" Value = "TitleCard_Tutorial" [Script_Tutorial_Intro_SetTitleCardSub] ActionType = "SetConfigVar" VarContext = "HUD" VarName = "TitleCardSubText" ValuePE = "TitleCardSub_Tutorial_PE" [TitleCardSub_Tutorial_PE] PEType = "ConstantString" Value = "TitleCardSub_Tutorial" [English] Objective_Tutorial_DoTheThing = "Do the thing to win the game." TitleCard_Tutorial = "IN A PLACE" TitleCardSub_Tutorial = "at a time"

Each action is defined by its type (the ActionType variable) and some properties that describe the action. For example, the NeonAddObjective action expects a property called Objective that names the objective in the string table.

The SendEvent action is a powerful action to communicate between entities. In this case, it is used to queue a phone call after two seconds, by sending the event "TryCall" to the script itself, with a DispatchDelay of 2.0.

The SendEvent and SetConfigVar actions both make use of PEs (short for property/parameter evaluators). PEs are pure functions that return a Boolean, number, string, vector, or reference. In most cases, they are simple, like the ConstantString PE type that simply returns a static string; but they can also be nested to build complex expressions.

The phone call is initiated by a second reaction in the script:
[Script_Tutorial_Intro_TryCallReaction] Rule = "Script_Tutorial_Intro_TryCallRule" NumActions = & @ Action @@& = "Script_Tutorial_Intro_Call" [Script_Tutorial_Intro_TryCallRule] Event = "TryCall" [Script_Tutorial_Intro_Call] ActionType = "NeonIncomingCall" Caller = "Speaker_Unknown" Convo = "Convo_Tutorial"

Once again, we have a reaction (rule + sequence of actions), a simple rule (handling the "TryCall" event sent above), and an action (a NeonIncomingCall with an unknown speaker).

Conversations are described in a later tutorial. For now, answering the phone will do nothing.

Note: This sort of scripting looks very verbose, and it is. Later, we will look at an optional offline compiler that converts from a simpler language (informally called Loom) into the config file format. This same script in Loom would be written as:
[Script_Tutorial_Intro_Reactions] lmReactions { Reaction( Rule( "OnSpawned" ) ) { "Script_Common_FlushWorldFilesAction" "Script_Common_ResetResults" "Script_Common_ClearObjectivesAction" NeonAddObjective( "Objective_Tutorial_DoTheThing" ) SendEvent( "TryCall", "SelfPE", DispatchDelay = 2.0 ) // Title card "Script_Common_ShowTitleCard" SetConfigVar( "HUD", "TitleCardText", ConstantString( "TitleCard_Tutorial" ) ) SetConfigVar( "HUD", "TitleCardSubText", ConstantString( "TitleCardSub_Tutorial" ) ) "Script_Common_QueueHideTitleCard" } Reaction( Rule( "TryCall" ) ) { NeonIncomingCall( "Speaker_Unknown", "Convo_Tutorial" ) } }
Tutorial: Objectives
Objectives tell the player what to do next. They are mainly used for display purposes; the state of an objective (incomplete, succeeded, or failed) typically has no bearing on gameplay.

As seen in the previous tutorial, objectives can be cleared and added with the NeonClearObjectives and NeonAddObjective actions:
[Tutorial_ClearObjectives] ActionType = "NeonClearObjectives" [Tutorial_AddObjective] ActionType = "NeonAddObjective" Objective = "Objective_Tutorial"

Objectives can be marked as completed through either success or failure:
[Tutorial_SucceedObjective] ActionType = "NeonCompleteObjective" Objective = "Objective_Tutorial" [Tutorial_FailObjective] ActionType = "NeonCompleteObjective" Objective = "Objective_Tutorial" Fail = true

Scripts can query the status of an objective with the NeonIsObjectiveComplete PE:
# Return true if the objective is completed (either succeeded or failed) [Tutorial_IsObjectiveComplete] PEType = "NeonCompleteObjective" Objective = "Objective_Tutorial" # Return true only if the objective is succeeded [Tutorial_IsObjectiveSucceeded] PEType = "NeonCompleteObjective" Objective = "Objective_Tutorial" RejectFail = true
Tutorial: Conversations
A conversation in NEON STRUCT is a sequence of dialogue that optionally concludes with the player making a dialogue choice. Depending on the player's choice—or if there is no choice to be made—a conversation may either continue to another conversation, or end and return control to normal gameplay.

Conversation Lines
A conversation's main body consists of any number of lines. Each line is defined by its speaker and a string.

[Convo_Tutorial_A] NumLines = & @ Line @@&Speaker = "Speaker_Player" @@^ = "Convo_Tutorial_A1" @@&Speaker = "Speaker_David" @@^ = "Convo_Tutorial_A2" [English] # Note: Speaker_Player is already defined as "Jillian Cleary". # Other speakers need names in the string table. Speaker_David = "David Pittman" Convo_Tutorial_A1 = "Hi, I'm Jill." Convo_Tutorial_A2 = "Hi Jill. I'm David."

Speaker Portraits
Story characters often have a portrait to accompany their dialogue. Portraits are defined like so:
[Speaker_David] Image = "Textures/david.tga" Location = "Right"
Location defines which side of the conversation box the portrait appears on. Conventionally, the player's portrait appears on the left, while other speakers appear on the right.

Continuing Conversations
A conversation without dialogue choices may automatically continue to another conversation after its final line has been read.
[Convo_Tutorial_A] NextConvo = "Convo_Tutorial_B" NumLines = & # ...snip...

Conversation Choices
A conversation may end with a dialogue choice for the player to make. Each choice may continue to different conversation, or end the dialogue completely.

[Convo_Tutorial_A] ChoiceSpeaker = "Speaker_Player" NumLines = & @ Line @@&Speaker = "Speaker_David" @@^ = "Convo_Tutorial_A1" NumChoices = & @ Choice @@& = "Convo_Tutorial_A_C1" @@^Convo = "Convo_Tutorial_B" @@& = "Convo_Tutorial_A_C2" [English] Speaker_David = "David Pittman" Convo_Tutorial_A1 = "Hi, what can I do for you?" Convo_Tutorial_A_C1 = "Can you teach me to write a conversation?" Convo_Tutorial_A_C2 = "Nothing right now, thanks."

In this example, the first choice will continue to a further conversation called "Convo_Tutorial_B", while the second choice will end the dialogue.

Note: The ChoiceSpeaker property allows the author to replace Jillian as the player character for the purpose of custom missions. If it is not defined, it defaults to "Speaker_Player".

Conditional Choices
In certain cases, it is useful to hide or disable a choice based on some condition. For example, if the player is presented with a menu of items to purchase from a shopkeeper, the items the player cannot afford may be disabled (grayed out) or hidden entirely.

[Convo_Tutorial_A] ChoiceSpeaker = "Speaker_Player" NumLines = & @ Line @@&Speaker = "Speaker_David" @@^ = "Convo_Tutorial_A1" NumChoices = & @ Choice @@& = "Convo_Tutorial_A_C1" @@^Convo = "Convo_Tutorial_B" @@^EnabledPE = "Convo_Tutorial_HasMoneyPE" @@& = "Convo_Tutorial_A_C2" [Convo_Tutorial_HasMoneyPE] PEType = "NeonHasMoney" Amount = 2 [English] Speaker_David = "David Pittman" Convo_Tutorial_A1 = "Excuse me, can you spare two dollars?" Convo_Tutorial_A_C1 = "Sure, here you are." Convo_Tutorial_A_C2 = "Sorry, no."

Valid conditional properties are:
Choice#HiddenPE Choice#ShownPE Choice#DisabledPE Choice#EnabledPE

Likewise, the next conversation in the sequence may be conditionally evaluated. NextConvoPE or Choice#ConvoPE may be used instead of NextConvo and Choice#Convo to specify a function that returns the name of the next conversation.

Conversation Actions
A conversation may execute an ordered sequence of actions. These are always executed as soon as the conversation begins; if they need to be deferred until some time later in the dialogue, the conversation should be split in two so the actions can be executed with the appropriate line of dialogue.

[Convo_Tutorial_B] NumActions = & @ Action @@& = "Convo_Tutorial_B_SubtractMoney" NumLines = & @ Line @@&Speaker = "Speaker_David" @@^ = "Convo_Tutorial_B1" [Convo_Tutorial_B_SubtractMoney] ActionType = "NeonRemoveMoney" Amount = 2 [English] Convo_Tutorial_B1 = "Thank you so much!"
Tutorial: Character Configurations
In NEON STRUCT, enemy AIs usually have a randomized appearance (gender, skin tone, hair style, hair color) with a fixed uniform color; while friendly characters often have a completely fixed appearance. This tutorial will describe the various ways to configure both kinds of characters, using randomness where desired.

Character configurations are distributed amongst a handful of different components:
  • The EldMesh component defines the character's body and various attached meshes (hair, weapons, helmets, etc.).
  • The NeonCharacterConfig component defines how to render the character's body mesh (including clothing colors and skin tone).
  • The EldFrobbable component gives the character a name (visible at interactive range).
  • The EldAIMotion component defines a pose (sitting in a chair, leaning against a wall, etc.).

Randomized Enemies
Custom enemy AIs should usually inherit from the BaseGuard or BaseRangedGuard entities (for melee or ranged combat enemies respectively). These classes provide a character with a random gender, hair style, and hair color, as well as the appropriate weapon in hand.

The custom enemy should implement a NeonCharacterConfig component to define their clothing color. This component should typically inherit from BaseAICharacterConfig_Random, which provides a random skin tone.

Finally, the custom enemy should implement an EldFrobbable component to give them the appropriate name. This component should inherit from BaseGuardFrobbable, which provides the usual frob actions (Pickpocket/Takedown) and other properties.

[AI_Tutorial] Extends = "BaseRangedGuard" NeonCharacterConfig = "AI_Tutorial_CharacterConfig" EldFrobbable = "AI_Tutorial_Frobbable" [AI_Tutorial_CharacterConfig] Extends = "BaseAICharacterConfig_Random" # Blue police uniform PrimaryColorH = 0.6 PrimaryColorS = 0.8 PrimaryColorV = 0.8 SecondaryColorH = 0.6 SecondaryColorS = 0.8 SecondaryColorV = 0.2 [AI_Tutorial_FrobbableFrobbable] Extends = "BaseGuardFrobbable" FriendlyName = "AI_Tutorial" [English] AI_Tutorial = "Tutorial Guard"

NeonCharacterConfig Component
A NeonCharacterConfig component defines four colors: skin, primary, secondary, and accent. The meaning of the latter three colors depends on the body mesh's texture map; but for guards and most friendly characters, it is as follows:
  • Primary: shirt color
  • Secondary: pants color
  • Accent: shoe color

These colors are defined in HSV space, and may be randomly selected from a given range. For example:
PrimaryColorH = 0.1666 PrimaryColorHRange = 0.333 PrimaryColorS = 1.0 PrimaryColorV = 1.0
This defines a shirt color in the range of yellow-green-cyan.

Non-Random Characters
Friendly characters (or NPCs) in NEON STRUCT typically have a fixed (non-random) appearance and a custom pose. They should inherit from the BaseNPC entity class.

An NPC should implement an EldMesh component, deriving from BaseAIMesh_Woman or BaseAIMesh_Man (or BaseAIMesh_Random if the character's gender should be randomized). This component may include an attachment set for the character's hair or other attached meshes.

NPCs should also implement a NeonCharacterConfig component just like enemy AIs. This may extend from BaseAICharacterConfig_Random (if the character's skin tone should be randomized), or it may extend from one of the following presets, or the skin tone may be customized to be any color.
  • BaseAICharacterConfig_White
  • BaseAICharacterConfig_Black
  • BaseAICharacterConfig_Indian
  • BaseAICharacterConfig_Asian

If a custom pose is desired, the NPC should implement an EldAIMotion component from the following list (with the noted spawner offsets to position them properly):
  • "BaseNPC_AIMotion_HandsBack" (Use OffsetY = 0.25 if behind a counter)
  • "BaseNPC_AIMotion_LeanWall" (Use OffsetY = -0.36)
  • "BaseNPC_AIMotion_SitBed" (Use OffsetY = 0.25; use OffsetZ = -0.305 if sitting on voxel instead)
  • "BaseNPC_AIMotion_LeanForward" (Use OffsetY = 0.25)
  • "BaseNPC_AIMotion_Sitting"
  • "BaseNPC_AIMotion_SitWall" (Use OffsetY = -0.25)

NPCs should implement an EldFrobbable component to give them a readable name, just like enemy AIs. This should inherit from BaseNPCFrobbable, which provides the Talk action.

Finally, NPCs should implement a PEMap component which provides a lookup to the conversation to start when the player speaks to them.

[NPC_Tutorial] Extends = "BaseNPC" EldMesh = "NPC_Tutorial_Mesh" NeonCharacterConfig = "NPC_Tutorial_CharacterConfig" EldAIMotion = "BaseNPC_AIMotion_HandsBack" EldFrobbable = "NPC_Tutorial_Frobbable" [NPC_Tutorial_Mesh] Extends = "BaseAIMesh_Woman" NumAttachmentSets = & @ AttachmentSet @@& = "AttachmentSet_RetroHair_Strawbie" NumAttachedMeshes = & @ AttachedMesh @@& = "Meshes/attachment-hudson.cms" @@^MaterialOverride = "Material_EntityStaticEmissive" @@^Bone = "Head" [NPC_Tutorial_CharacterConfig] Extends = "BaseAICharacterConfig_White" PrimaryColorH = 0.56 PrimaryColorS = 0.33 PrimaryColorV = 0.4 SecondaryColorH = 0.56 SecondaryColorS = 0.66 SecondaryColorV = 0.25 AccentColorV = 0.1 [NPC_Tutorial_Frobbable] Extends = "BaseNPCFrobbable" FriendlyName = "NPC_Tutorial" [NPC_Tutorial_PEMap] NumPEs = & @ PE @@&Key = "Convo" @@^Def = "NPC_Tutorial_ConvoPE" [NPC_Tutorial_ConvoPE] PEType = "ConstantString" Value = "Convo_Tutorial_A" [English] NPC_Tutorial = "Tutorial Character"

Note: The available hair attachment sets are:
  • AttachmentSet_Bun_Brunette
  • AttachmentSet_Bun_Blonde
  • AttachmentSet_Bun_Black
  • AttachmentSet_Bun_Gray
  • AttachmentSet_Bun_White
  • AttachmentSet_Bun_Strawbie
  • AttachmentSet_Ponytail_Brunette
  • AttachmentSet_Ponytail_Blonde
  • AttachmentSet_Ponytail_Black
  • AttachmentSet_Ponytail_Gray
  • AttachmentSet_Ponytail_White
  • AttachmentSet_Ponytail_Strawbie
  • AttachmentSet_RetroHair_Brunette
  • AttachmentSet_RetroHair_Blonde
  • AttachmentSet_RetroHair_Black
  • AttachmentSet_RetroHair_Gray
  • AttachmentSet_RetroHair_White
  • AttachmentSet_RetroHair_Strawbie
  • AttachmentSet_ShoulderHair_Brunette
  • AttachmentSet_ShoulderHair_Blonde
  • AttachmentSet_ShoulderHair_Black
  • AttachmentSet_ShoulderHair_Gray
  • AttachmentSet_ShoulderHair_White
  • AttachmentSet_ShoulderHair_Strawbie
  • AttachmentSet_FlatTop_Brunette
  • AttachmentSet_FlatTop_Blonde
  • AttachmentSet_FlatTop_Black
  • AttachmentSet_FlatTop_Gray
  • AttachmentSet_FlatTop_White
  • AttachmentSet_FlatTop_Strawbie
  • AttachmentSet_PartedHair_Brunette
  • AttachmentSet_PartedHair_Blonde
  • AttachmentSet_PartedHair_Black
  • AttachmentSet_PartedHair_Gray
  • AttachmentSet_PartedHair_White
  • AttachmentSet_PartedHair_Strawbie
  • AttachmentSet_MessyHair_Brunette
  • AttachmentSet_MessyHair_Blonde
  • AttachmentSet_MessyHair_Black
  • AttachmentSet_MessyHair_Gray
  • AttachmentSet_MessyHair_White
  • AttachmentSet_MessyHair_Strawbie
Tutorial: Level Aesthetics
Although all NEON STRUCT levels share a common set of voxel types for their geometry, a wide range of moods and tones can be achieved through use of lighting and post-processing.

In the Mission 101 example, we used some common defaults to get things working quickly. Now we can examine those properties in detail.

Your level's WorldDef context should include (among other things) these five references:
[WorldDef_Tutorial] # (snip) - Don't forget, world size and other properties go here! - (snip) SkyDef = "Sky_Tutorial" LightDef = "Light_Tutorial" FogDef = "Fog_Tutorial" BloomDef = "Bloom_Tutorial" PostDef = "Post_Tutorial"

Sky Definition
Skyboxes are defined by a sunlight position (or direction), a sun color (called the "hi color"), and an ambient color (called the "lo color").

[Sky_Tutorial] SunVectorYaw = 45.0 # Sun's position: degrees from north (Y+), counter-clockwise SunVectorPitch = -75.0 # Sun's position: degrees from ground level (XY-plane) SkyColorHiR = 1.0 SkyColorHiG = 1.5 SkyColorHiB = 2.0 SkyColorLoR = 0.02 SkyColorLoG = 0.02 SkyColorLoB = 0.05

Note: Colors may optionally be defined in HSV space instead (e.g., SkyColorLoH = 0.5 gives a cyan hued ambience), but the blend is always performed in RGB space.

Light Definition
Voxels and entities that are not in direct light are still lit by the ambient light in the scene. The "hi color" represents ambient light directed downward, while the "lo color" is light directed up. "AO color" is the hueless ambient occlusion color used to shade corners where voxels meet.

[Light_Tutorial] HiColorH = 0.9 HiColorS = 0.5 HiColorV = 0.15 LoColorH = 0.6 LoColorS = 0.9 LoColorV = 0.05 AOColorV = 0.02

Note: Ambient light must be defined in HSV space; RGB values are not accepted here. Hues are defined in the range [0,1] instead of the more common [0,360] range.

Note: Any lo color properties may be ignored, and the hi color properties will be reused.

Fog Definition
NEON STRUCT uses distance fog across the entire scene. The fog gradient is configurable by a 1D gradient texture with an alpha channel (in compressed or uncompressed 32-bit TGA format) so that fog color and opacity can vary nonlinearly.

[Fog_Tutorial] Near = 5.0 Far = 25.0 Texture = "Textures/fog-tutorial.tga" EmissiveScalar = 4.0

Note: See http://neonstruct.com/bin/tutorial/fog-tutorial.tga for an example of the gradient texture.

Note: EmissiveScalar is a hack that doesn't properly belong in the fog definition. It should generally be set at 4.0 and left alone.

Bloom Definition
Like fog, bloom in NEON STRUCT uses a gradient texture to define its shape and falloff. Bloom kernels are 1D gradient textures without alpha channels (in compressed or uncompressed 24-bit TGA format). The kernel is applied twice: first to blur the scene horizontally, then again to blur the scene vertically.

[Bloom_Tutorial] Kernel = "Textures/bloom-tutorial.tga" Radius = 0.02 AspectRatio = 1.0 ThresholdR = 1.0 ThresholdG = 1.0 ThresholdB = 1.0

Note: See http://neonstruct.com/bin/tutorial/bloom-tutorial.tga for an example of the gradient texture. In most cases, "Textures/Post/bloom-default_NODXT.tga" is a good default kernel for a neutral, circular bloom.

Note: Radius is the extent of the bloom relative to the screen height.

Note: AspectRatio is used to give the bloom an elongated shape. An aspect ratio > 1.0 creates a horizontal bloom; an aspect ratio < 1.0 creates a vertical bloom.

Note: ThresholdR/G/B defines how much light is subtracted from the scene before the blur step. Lower values can create a softer look, with even dimly lit surfaces blooming.

Post-process Definition
The final post-process step applies chromatic aberration, film grain, and color grading.

[Post_Tutorial] ColorGrading = "Textures/color-grading-tutorial.tga" Noise = "Textures/noise-tutorial.tga" NoiseScaleLo = 3.0 NoiseScaleHi = 4.0 Aberration = 0.001

Note: See http://neonstruct.com/bin/tutorial/noise-tutorial.tga and http://neonstruct.com/bin/tutorial/color-grading-tutorial.tga for examples of these textures.

Note: NoiseScaleLo/Hi define a random range of how many times the noise texture is tiled over the screen. Lower values produce larger, coarser grain.
Loom Compiler
Certain kinds of scripting—especially reactions and conversations—are extremely verbose, requiring many separate configuration contexts to implement simple, common behaviors.

Though optional, it is highly recommended to use the Loom scripting language instead, especially for larger projects. Loom is a higher level language which can be compiled into the config format as an offline step. ("Offline" in this context means it is a step performed outside the game. NEON STRUCT cannot load or compile Loom script files.)

The Loom compiler requires Python[www.python.org] to run. (It has been developed and tested on Python 2; compatibility with Python 3 is currently unknown.)

The compiler is available at http://neonstruct.com/bin/tutorial/loom.py. It is invoked as:
python loom.py script.loom script.config
This takes a Loom script file as input (script.loom) and emits a config file as output (script.config).

The Loom file format subsumes the config format, meaning everything that can be written in a config file is also valid in a Loom script. Most of a Loom script will look identical to a config file; where the additional features of Loom are required, special identifiers are used to open Loom language blocks. For example:
# This is a normal config context, in a Loom script file [Tutorial_Reactions] # The following tag opens a Loom reactions block. lmReactions { // Loom reactions code goes here Reaction( Rule( "OnFrobbed" ) ) { NeonCompleteObjective( "Objective_Tutorial" ) } } # We've closed the Loom block and now we are back in the config format

Loom Syntax
Loom blocks are opened with a special identifier (lmReactions, lmLines, etc.), and followed by any number of statements in curly braces.
SpecialIdentifier { Statements }

Loom statements take the form:
Identifier( Parameters ) { Statements }
Some statements may omit the parameters list or the nested statements block.

Loom parameters are comma-separated and may be either named or anonymous. For example:
SendEvent( "TryCall", "SelfPE", DispatchDelay = 2.0 )
"TryCall" and "SelfPE" are anonymous parameters. (Within the compiler, their names are inferred from their position in the parameter list and the type of statement they belong to.) DispatchDelay = 2.0 is a named parameter.

Loom comments are single-line comments starting with //. Take care not to mix config comments (starting with #) with Loom comments.
NeonAddObjective( "Objective_Tutorial" ) // Give the player their first objective

Switching to Loom
In most common cases, actions can be written as a single-line Loom statement. The action's name is the statement identifier, and its properties are the statement parameters. For example, this config definition:
[AddTutorialObjective] ActionType = "NeonAddObjective" Objective = "Objective_Tutorial"
becomes this piece of Loom script:
NeonAddObjective( "Objective_Tutorial" )

In this case, the parameter name Objective can be inferred, so the parameter is anonymous. If needed, the fully-qualified form would be:
NeonAddObjective( Objective = "Objective_Tutorial" )

PEs can be used as the right-hand side of parameter, and they can be nested to more easily build complex expressions. For example:
NeonAddMoney( Add( Mul( ConstantInt( 2 ), ConstantInt( 50 ) ), ConstantInt( 25 ) ) )
gives the player $125.

There is not currently a full list of action and PE types, but the first page of loom.py is a self-documenting list of supported types and their inferred anonymous parameters.

Loom Reactions
A reactions block is opened with the identifier lmReactions and should contain a sequence of Reaction statements. A Reaction should have one parameter—its Rule—and a sequence of associated actions in its statements block.

lmReactions { Reaction( Rule( "OnSpawned" ) ) { NeonAddObjective( "Objective_Tutorial" ) } Reaction( Rule( "OnFrobbed" ) ) { NeonCompleteObjective( "Objective_Tutorial" ) Destroy( "SelfPE" ) } }

A Rule's first parameter is its triggering event ("OnFrobbed", "OnSpawned", etc.). It may contain additional parameters defining further conditions:
Rule( "OnFrobbed", NOT( NeonIsObjectiveComplete( "Objective_Tutorial" ) ) )

Loom Conversations
Blocks of conversation lines, choices, and actions may be opened with the lmLines, lmChoices, and lmActions identifiers respectively. Here, the tutorial conversations have been simplified using Loom:
[Convo_Tutorial_A] ChoiceSpeaker = "Speaker_Player" lmLines { Line( "Speaker_David", "Convo_Tutorial_A1" ) } lmChoices { Choice( "Convo_Tutorial_A_C1", Convo = "Convo_Tutorial_B", EnabledPE = NeonHasMoney( 2 ) ) Choice( "Convo_Tutorial_A_C2" ) } [Convo_Tutorial_B] lmLines { Line( "Speaker_David", "Convo_Tutorial_B1" ) } lmActions { NeonRemoveMoney( 2 ) }

Note: Each Loom block will be compiled into config format and inserted inline without awareness of neighboring Loom blocks. It is important to consider what the output will look like; lmActions and lmChoices will typically open new contexts for their objects. So despite appearances in the Loom script file, any text that follows those blocks would not belong to the conversation context, but to some randomly named action or PE context. Therefore, lmLines should precede lmActions and lmChoices; and lmActions and lmChoices should generally not be mixed in the same conversation.
2 Comments
road-trip-girl 5 May, 2019 @ 3:20pm 
@ObamaBoss you can actually! it's quite helpfully outlined in the "Level Editor Interface: Basics" section of this guide. :)
Coach 5 May, 2019 @ 11:59am 
this is a scam , you cant edit levels in game