Downloads containing DialogueBoxTutorial.txt

Downloads
Name Author Game Mode Rating
TSF with JJ2+ Only: Characters Cranky Mutator N/A Download file
TSF with JJ2+ Only: DialogueBox Helper Script Cranky Mutator N/A Download file

File preview

DialogueBox.asc Tutorial
========================

What this is
------------

DialogueBox.asc is a small helper script for making JJ2 AngelScript
windows.

It can draw and handle common window things:

- a window frame
- a title
- an optional X close button
- an optional draggable title bar
- buttons
- tabs
- steppers like < Option >
- wrapped small text without ellipsis clipping
- optional UP / DOWN scroll buttons for text that is taller than its box
- reusable editable-text helpers for cursor movement and insertion
- keyboard focus
- mouse clicks
- Enter / Fire / Select to activate the focused control
- arrow keys to move the focus
- saved window positions for draggable windows

The script does not know what your mod is doing.
It only tells you what button, tab, or control was activated.
Your script decides what that action means.


Including the script
--------------------

Put this near the top of your script:

#include "DialogueBox.asc"

Then create one global window variable:

DbWindow myWindow;

You usually also need a bool to decide if the window is open:

bool myWindowOpen = false;


The basic idea
--------------

Using DialogueBox.asc usually has 3 parts:

1. Draw the window in onDrawScore.
2. Process mouse/keyboard input in onMain or onPlayerInput.
3. Handle the action ids returned by the window.

Each button has:

action - what kind of action happened
a      - extra number 1
b      - extra number 2
text   - text drawn on the button

The action, a, and b values are yours to choose.


Action ids
----------

DialogueBox.asc already reserves these action ids:

DB_ACTION_NONE  = 0
DB_ACTION_CLOSE = -1
DB_ACTION_TAB   = -2
DB_ACTION_SCROLL_UP   = -3
DB_ACTION_SCROLL_DOWN = -4

For your own controls, use positive numbers:

const int ACT_HELLO = 1;
const int ACT_ADD = 2;
const int ACT_SUBTRACT = 3;


Smallest possible window
------------------------

This draws a simple window with an X button.

#include "DialogueBox.asc"

bool myWindowOpen = true;
DbWindow myWindow;

void onDrawScore(jjPLAYER@ play, jjCANVAS@ canvas)
{
if(!myWindowOpen) return;

DbBegin(@myWindow, "myWindow", "My Window", 40, 40, 180, 80);
myWindow.closeButton = true;
myWindow.draggable = false;
myWindow.titleBar = true;

DbDrawFrame(canvas, @myWindow);
canvas.drawString(myWindow.x + 10, myWindow.y + 35, "Hello!",
STRING::SMALL, STRING::NORMAL);
}

void onMain()
{
if(!myWindowOpen) return;

DbResult result;
DbProcessMouse(@myWindow, @result);

if(result.closeRequested)
myWindowOpen = false;
}

Important:
DbBegin must be called before you draw controls or process the window.
It sets up the window id, title, position, size, and clears old controls.
If you set window.preferencesFile, set it before DbBegin.


Window ids
----------

This part is important:

DbBegin(@myWindow, "myWindow", "My Window", 40, 40, 180, 80);

"myWindow" is the window id.

Use a unique id for each window.

Good ids:

"npcQuestTracker"
"freezeTagMenu"
"myModSettings"

Bad ids:

"window"
"menu"
"box"

If two windows use the same id, saved positions can conflict.


Draggable windows
-----------------

To make a window draggable, turn on draggable and titleBar:

myWindow.draggable = true;
myWindow.titleBar = true;

Example:

myWindow.preferencesFile = "myModDialogueBox.asdat";
DbBegin(@myWindow, "myDraggableWindow", "Drag me", 40, 40, 180, 90);
myWindow.closeButton = true;
myWindow.draggable = true;
myWindow.titleBar = true;
DbDrawFrame(canvas, @myWindow);

The player can drag the title bar.
When the drag ends, DialogueBox.asc saves the position in the file named by
window.preferencesFile.

If you do not set preferencesFile, the default file is:

DialogueBoxPreferences.asdat

For a real mod, it is better to set your own file:

myWindow.preferencesFile = "myModDialogueBox.asdat";

The next time the same window id is used, the saved position is restored.


Drawing content inside the window
---------------------------------

You can draw normal JJ2 canvas things inside the window.

Use the window's x and y:

canvas.drawString(myWindow.x + 10, myWindow.y + 35, "Text here",
STRING::SMALL, STRING::NORMAL);

Or ask DialogueBox.asc for the content rectangle:

int x, y, w, h;
DbContentRect(@myWindow, x, y, w, h);
canvas.drawString(x, y, "Text inside content area", STRING::SMALL,
STRING::NORMAL);

DbContentRect gives you:

x - left side of the content area
y - top of the content area
w - content width
h - content height

For text that should stay inside a width, use DialogueBox.asc wrapping
instead of clipping:

y = DbDrawWrappedSmall(canvas, x, y, "This text wraps to more lines instead
of being cut short.", w, y + h);

Long words are split across lines if needed. DialogueBox.asc does not add
ellipsis.


Editable text helpers
---------------------

DialogueBox.asc has helpers for editable boxes. They are low-level on
purpose: your script owns the field value, cursor integer, and key
handling.

Common helpers:

DbClampTextCursor(text, cursor)
DbInsertTextAtCursor(text, cursor, insert)
DbBackspaceTextAtCursor(text, cursor)
DbMoveTextCursor(text, cursor, delta)
DbClampEditableScrollToCursor(text, width, height, scrollLine, cursor)
DbDrawEditableSmallCursor(canvas, x, y, text, width, maxY, cursor,
scrollLine)

Typical key order:

if(shiftEnter)
{
value = DbInsertTextAtCursor(value, cursor, "\n");
cursor++;
}
else if(enter)
{
closeOrAcceptTheField();
}

Check Shift+Enter before Enter if Enter normally exits the field. Left and
Right should call DbMoveTextCursor while editing instead of changing menu
focus.


Adding a button
---------------

First define an action id:

const int ACT_SAY_HELLO = 1;

Draw the button:

DbDrawButton(canvas, @myWindow, myWindow.x + 10, myWindow.y + 45, 100, 16,
ACT_SAY_HELLO, 0, 0, "Say hello");

Button text wraps instead of being shortened. If wrapped text needs more
than the height you passed, DialogueBox.asc expands that button's
click/focus rectangle downward.

Then process it:

DbResult result;
DbProcessMouse(@myWindow, @result);

if(result.action == ACT_SAY_HELLO)
jjAlert("Hello!");

Full example:

#include "DialogueBox.asc"

const int ACT_SAY_HELLO = 1;

bool myWindowOpen = true;
DbWindow myWindow;

void onDrawScore(jjPLAYER@ play, jjCANVAS@ canvas)
{
if(!myWindowOpen) return;

myWindow.preferencesFile = "helloDialogueBox.asdat";
DbBegin(@myWindow, "helloWindow", "Hello Window", 40, 40, 180, 95);
myWindow.closeButton = true;
myWindow.draggable = true;
myWindow.titleBar = true;

DbDrawFrame(canvas, @myWindow);
DbDrawButton(canvas, @myWindow, myWindow.x + 10, myWindow.y + 40, 100, 16,
ACT_SAY_HELLO, 0, 0, "Say hello");
DbDrawFocus(canvas, @myWindow);
}

void onMain()
{
if(!myWindowOpen) return;

DbResult result;
DbProcessMouse(@myWindow, @result);

if(result.closeRequested)
myWindowOpen = false;
else if(result.action == ACT_SAY_HELLO)
jjAlert("Hello!");
}


Keyboard support
----------------

Use DbProcessKeyboard in onPlayerInput.

Example:

void onPlayerInput(jjPLAYER@ play)
{
if(!myWindowOpen || !play.isLocal) return;

DbResult result;
DbProcessKeyboard(@myWindow, play, @result);
HandleMyWindowResult(result);
}

DbProcessKeyboard does these:

- Up / Left moves focus backward.
- Down / Right moves focus forward.
- Enter / Fire / Select activates the focused control.

You should usually draw the focus:

DbDrawFocus(canvas, @myWindow);


Handling mouse and keyboard in one function
-------------------------------------------

It is useful to make your own handler function:

void HandleMyWindowResult(DbResult@ result)
{
if(result.closeRequested)
myWindowOpen = false;
else if(result.action == ACT_SAY_HELLO)
jjAlert("Hello!");
}

Then call it from both mouse and keyboard processing:

void onMain()
{
if(!myWindowOpen) return;

DbResult result;
DbProcessMouse(@myWindow, @result);
HandleMyWindowResult(@result);
}

void onPlayerInput(jjPLAYER@ play)
{
if(!myWindowOpen || !play.isLocal) return;

DbResult result;
DbProcessKeyboard(@myWindow, play, @result);
HandleMyWindowResult(@result);
}


Important drawing/input order
-----------------------------

DialogueBox.asc needs controls to exist before it can click them.

That means:

1. Your draw function calls DbBegin.
2. Your draw function calls DbDrawFrame.
3. Your draw function calls DbDrawButton / DbDrawTab / DbDrawStepper.
4. Later, input functions can detect those controls.

In JJ2, onDrawScore usually happens often, so the control list stays fresh.


Tabs
----

Tabs are buttons that change window.activeTab.

Example action ids are not needed for tabs because DialogueBox.asc uses
DB_ACTION_TAB.

Drawing tabs:

DbDrawTab(canvas, @myWindow, myWindow.x + 10, myWindow.y + 30, 60, 0,
"Info", true);
DbDrawTab(canvas, @myWindow, myWindow.x + 80, myWindow.y + 30, 80, 1,
"Settings", true);

Draw different content depending on activeTab:

if(myWindow.activeTab == 0)
canvas.drawString(myWindow.x + 10, myWindow.y + 55, "Info tab",
STRING::SMALL, STRING::NORMAL);
else if(myWindow.activeTab == 1)
canvas.drawString(myWindow.x + 10, myWindow.y + 55, "Settings tab",
STRING::SMALL, STRING::NORMAL);

Handle tab changes:

DbResult result;
DbProcessMouse(@myWindow, @result);

if(result.tabChanged)
{
// result.tab is the new tab number
// myWindow.activeTab was already changed by DialogueBox.asc
}

If you store your tab in your own variable, copy it:

if(result.tabChanged)
myCurrentTab = result.tab;


Disabled tabs and buttons
-------------------------

Most drawing functions have an enabled argument.

Enabled button:

DbDrawButton(canvas, @myWindow, x, y, 100, 16, ACT_DO_THING, 0, 0, "Do
thing", true);

Disabled button:

DbDrawButton(canvas, @myWindow, x, y, 100, 16, ACT_DO_THING, 0, 0, "Do
thing", false);

Disabled controls:

- are drawn grey using JJ2 color codes
- cannot be clicked
- cannot be activated by keyboard focus


Steppers
--------

A stepper is a control like this:

< Easy >

Clicking the left arrow sends one value.
Clicking the right arrow sends another value.

Example:

const int ACT_DIFFICULTY = 10;
int difficulty = 0;
array<string> difficultyNames = {"Easy", "Normal", "Hard"};

Draw it:

DbDrawStepper(canvas, @myWindow, myWindow.x + 10, myWindow.y + 45, 80,
ACT_DIFFICULTY, 0, -1, 1, difficultyNames[difficulty]);

Handle it:

if(result.action == ACT_DIFFICULTY)
{
difficulty += result.b;
if(difficulty < 0) difficulty = int(difficultyNames.length) - 1;
if(difficulty >= int(difficultyNames.length)) difficulty = 0;
}

What the parameters mean:

ACT_DIFFICULTY - action id
0              - result.a
-1             - result.b for left arrow
1              - result.b for right arrow
difficultyNames[difficulty] - text in the middle


Using a, b, and action
----------------------

Controls can send extra numbers.

This is useful for lists.

Example: 5 buttons that select a player.

const int ACT_SELECT_PLAYER = 20;

for(int i = 0; i < 5; i++)
{
DbDrawButton(canvas, @myWindow, x, y + i * 18, 140, 16, ACT_SELECT_PLAYER,
i, 0, "Player " + i);
}

Handle it:

if(result.action == ACT_SELECT_PLAYER)
{
int playerIndex = result.a;
jjAlert("Selected player " + playerIndex);
}


Manual controls with DbAddControl
---------------------------------

Sometimes you draw custom text yourself, but still want it clickable.

Use DbAddControl:

canvas.drawString(x, y, "Custom row", STRING::SMALL, STRING::NORMAL);
DbAddControl(@myWindow, x, y, 120, 16, ACT_CUSTOM_ROW, rowIndex, 0, "Custom
row");

This registers the rectangle for mouse and keyboard activation.

Use this for complex tables where DialogueBox.asc should handle
focus/clicks but your script draws the content.


Focus highlight
---------------

To draw focus:

DbDrawFocus(canvas, @myWindow);

You can customize focus color:

myWindow.focusColor = 15;

By default, the focus highlight is drawn above the registered
button/control rectangle so it sits over JJ2 small text.
You can still change the vertical offset if you need custom graphics:

DbDrawFocus(canvas, @myWindow, DB_FOCUS_OFFSET_Y, 12);

The first number is y offset.
The second number is highlight height.


Scrollable text
---------------

Use DbDrawScrollableSmallText for help panels or messages that may be
taller than the box.
It wraps text first, clamps the scroll line, and can draw UP / DOWN buttons
when scrolling is possible.

const int ACT_HELP_UP = 20;
const int ACT_HELP_DOWN = 21;
int helpScroll = 0;

helpScroll = DbDrawScrollableSmallText(canvas, @myWindow, x, y, w, y + 120,
helpText, helpScroll, true, ACT_HELP_UP, ACT_HELP_DOWN);

Handle the buttons:

if(DbIsScrollResult(@result, ACT_HELP_UP, ACT_HELP_DOWN))
{
helpScroll = DbHandleScrollResult(@result, helpScroll, ACT_HELP_UP,
ACT_HELP_DOWN);
return;
}

To draw custom graphics or put scroll controls somewhere else, pass false:

helpScroll = DbDrawScrollableSmallText(canvas, @myWindow, x, y, w, y + 120,
helpText, helpScroll, false);


Changing colors
---------------

Before DbDrawFrame:

myWindow.backgroundColor = 32;
myWindow.titleColor = 72;
myWindow.focusColor = 15;

Then:

DbDrawFrame(canvas, @myWindow);

Colors are JJ2 palette indexes.


Title bar or no title bar
-------------------------

Windows-style title bar:

myWindow.titleBar = true;

Plain title:

myWindow.titleBar = false;

If you want dragging, use a title bar:

myWindow.titleBar = true;
myWindow.draggable = true;


Close button or no close button
-------------------------------

With X:

myWindow.closeButton = true;

Without X:

myWindow.closeButton = false;

If closeButton is true, DbDrawFrame adds the X button automatically.


Handling the X button
---------------------

When the player clicks X or activates it with keyboard:

result.closeRequested == true
result.action == DB_ACTION_CLOSE

Example:

if(result.closeRequested)
myWindowOpen = false;


Saving positions
----------------

DialogueBox.asc automatically saves draggable window positions.

The default file is:

DialogueBoxPreferences.asdat

But each mod should usually use its own file:

myWindow.preferencesFile = "npcBunniesDialogueBox.asdat";

or:

myWindow.preferencesFile = "freezeTagDialogueBox.asdat";

Each line stores:

windowId x y

Example:

npcQuestTracker 12 64

If a different mod uses a different preferences file, it will not touch
this file.

Make sure every window has a unique id.

Note:
If multiple windows in the same mod use the same preferences file, that is
fine.
Just give every window a different id.


Example: complete tabbed settings window
----------------------------------------

#include "DialogueBox.asc"

const int ACT_TOGGLE = 1;
const int ACT_SPEED = 2;

bool settingsOpen = true;
bool featureEnabled = false;
int speed = 1;
array<string> speedNames = {"Slow", "Normal", "Fast"};
DbWindow settingsWindow;

void onDrawScore(jjPLAYER@ play, jjCANVAS@ canvas)
{
if(!settingsOpen) return;

settingsWindow.preferencesFile = "settingsDialogueBox.asdat";
DbBegin(@settingsWindow, "settingsWindow", "Settings", 40, 40, 260, 140);
settingsWindow.closeButton = true;
settingsWindow.draggable = true;
settingsWindow.titleBar = true;
settingsWindow.activeTab = settingsWindow.activeTab;

DbDrawFrame(canvas, @settingsWindow);

int x, y, w, h;
DbContentRect(@settingsWindow, x, y, w, h);

DbDrawTab(canvas, @settingsWindow, x, y, 70, 0, "Main", true);
DbDrawTab(canvas, @settingsWindow, x + 80, y, 70, 1, "Speed", true);

y += 25;

if(settingsWindow.activeTab == 0)
{
DbDrawButton(canvas, @settingsWindow, x, y, 150, 16, ACT_TOGGLE, 0, 0,
featureEnabled ? "Feature: ON" : "Feature: OFF");
}
else if(settingsWindow.activeTab == 1)
{
canvas.drawString(x, y, "Speed:", STRING::SMALL, STRING::NORMAL);
DbDrawStepper(canvas, @settingsWindow, x + 70, y, 80, ACT_SPEED, 0, -1, 1,
speedNames[speed]);
}

DbDrawFocus(canvas, @settingsWindow);
}

void HandleSettingsResult(DbResult@ result)
{
if(result.closeRequested)
{
settingsOpen = false;
return;
}

if(result.action == ACT_TOGGLE)
{
featureEnabled = !featureEnabled;
}
else if(result.action == ACT_SPEED)
{
speed += result.b;
if(speed < 0) speed = int(speedNames.length) - 1;
if(speed >= int(speedNames.length)) speed = 0;
}
}

void onMain()
{
if(!settingsOpen) return;

DbResult result;
DbProcessMouse(@settingsWindow, @result);
HandleSettingsResult(@result);
}

void onPlayerInput(jjPLAYER@ play)
{
if(!settingsOpen || !play.isLocal) return;

DbResult result;
DbProcessKeyboard(@settingsWindow, play, @result);
HandleSettingsResult(@result);
}


Common mistakes
---------------

Mistake:
Calling DbProcessMouse before any controls were drawn.

Fix:
Make sure your window is drawn every frame while open.


Mistake:
Two windows use the same id.

Fix:
Use unique ids like "myModSettings" and "myModQuestWindow".


Mistake:
Forgetting to handle result.closeRequested.

Fix:
Always check this if closeButton is true.


Mistake:
Using negative action ids for your own actions.

Fix:
Use positive action ids. Negative ids are reserved by DialogueBox.asc.


Mistake:
Expecting DialogueBox.asc to change your mod's settings automatically.

Fix:
DialogueBox.asc only reports actions. Your script must change variables.


Quick reference
---------------

Classes:

DbWindow
DbControl
DbResult

Main functions:

DbBegin(window, id, title, x, y, w, h)
DbDrawFrame(canvas, window)
DbDrawButton(canvas, window, x, y, w, h, action, a, b, text)
DbDrawTab(canvas, window, x, y, w, tab, text)
DbDrawStepper(canvas, window, x, y, textW, action, a, prevB, nextB, text)
DbDrawFocus(canvas, window)
DbDrawWrappedSmall(canvas, x, y, text, maxPixels, maxY)
DbDrawScrollableSmallText(canvas, window, x, y, w, maxY, text, scrollLine,
scrollButtons, upAction, downAction)
DbHandleScrollResult(result, scrollLine, upAction, downAction)
DbIsScrollResult(result, upAction, downAction)
DbContentRect(window, x, y, w, h)
DbProcessMouse(window, result)
DbProcessKeyboard(window, player, result)
DbAddControl(window, x, y, w, h, action, a, b, text)

Useful result fields:

result.activated
result.closeRequested
result.tabChanged
result.positionChanged
result.action
result.a
result.b
result.tab

Useful window settings:

window.closeButton
window.preferencesFile
window.draggable
window.titleBar
window.activeTab
window.focusIndex
window.pad
window.titleHeight
window.backgroundColor
window.titleColor
window.focusColor