This part will look at the traits in VASSAL. A few of them will be partly implemented.
Note: Remember that the goal of this prototype is to implement the module Verdun: A generation Lost. In this module only a handful of traits are needed.
Note: The approach to traits in this engine will be different from VASSAL. In VASSAL there are a fixed number of traits to choose from. In this engine the module developer should be free to make new traits, given the possibilities offered by the C API (as defined in luau.cpp under extern "C"
).
Note: It has been some years since I made my VASSAL module for Verdun: A generation Lost. Therefore I am a bit rusty on VASSAL traits. Some of them I donât know. Be patient with that. The main issue below is to give some pointers on how traits are implemented on this engine.
I downloaded the latest version 3.7.14. My first impression was how well the documentation has become. Neat, tidy, with a lot of images and examples. I see other changes too. VASSAL seems more polished and better.
There are 45 traits starting from Action Button and going down to Trigger action. In discussing them remember what I said above, that it is not my ambition to implement them or understand all of them now.
Action Button - Buttons on counters. Buttons that can be activated under certain conditions. A simple way of activation a right-click menu item.
This made me address the ability to make GUI elements with script. GUI elements are limited to what the C API can offer. So I made two new API calls: create_button
and the corresponding delete_button
.
create_button
and delete_button
are not traits in themselves, but they can be used in traits.
The call in the script looks like this:
create_button("button 1", "text", "buttonHandler", 100, 100, 70, 30)
-
âbutton 1â - unique id
-
âtextâ - button text
-
âbuttonHandlerâ - the name of the Luau function to execute when button clicked
-
100, 100 - x, y on screen
-
70, 30 - width, height of button
delete_button("button 1")
-
âbutton 1â - unique id
Additional parameters may be added that define color, background, border, etc.
The function âbuttonHandlerâ can look like this
function buttonHandler(id)
delete_button(id)
create_button(id, id, "buttonHandler", 200+math.random(100), 100+math.random(100), 70, 30)
end
In frame.cpp the C++ side of the C API call looks like this:
void CentralFrame::createButton(const char *id, const char *text, const char *handler, int x, int y, int w, int h)
{
CentralFrame::Button *btn;
btn = new CentralFrame::Button(text, buttonParent);
QObject::connect(btn, &QPushButton::clicked, [=]() { Luau::handlers(id, handler); });
btn->setVisible(true);
btn->move(x, y);
btn->resize(w, h);
buttons[id] = btn;
}
On my GitHub wiki there is an image of what the buttons look like. Run the code, push the buttons and see what happens
Area Of Effect - To draw filled polygons around counters to signify that the counter has effects on its surroundings.
We start with the definition of a new trait in traits.luau.
AreaOfEffect = {}
function AreaOfEffect:new (radius, color, opacity)
local o = {}
setmetatable(o, self)
self.__index = self
o.radius = radius
o.color = color
o.opacity = opacity
o.apply = true
return o
end
o.radius is a number, in this case just a multiplier of the counter size. o.raduis can also be a table with vertices for a custom polygon. On a grid-map o.radius is the hex radius of the polygon. Readers may recall that I did make a grid-map by calculating the snap-to points, see here. It is possible to calculate the vertices of a polygon with a given hex grid radius with the same kind of calculation.
You add this trait to a counter in repository.luau like this:
Repository:addTrait("Markers/GAS", "AreaOfEffect", AreaOfEffect:new(0.5, "red", 0.2))
In the paintEvent
handler for the map (frame.cpp) you paint area of effects below the counters.
for (auto const& [point, stack] : stacks)
for ( auto obj = stack.begin(); obj != stack.end(); ++obj )
{
if ((obj->second->table).find("AreaOfEffect") != (obj->second->table).end())
{
Counter::Table trait = std::get<Counter::Table>(obj->second->table["AreaOfEffect"]);
if (std::get<bool>(trait["apply"]))
{
int x = obj->second->state.x + obj->second->margin;
int y = obj->second->state.y + obj->second->margin;
float radius = (float)std::get<double>(trait["radius"]);
const QPoint points[4] = {
QPoint(x - (int)(radius*obj->second->width), y - (int)(radius*obj->second->height)),
QPoint(x - (int)(radius*obj->second->width), y + (int)((1 + radius)*obj->second->height)),
QPoint(x + (int)((1 + radius)*obj->second->width), y + (int)((1 + radius)*obj->second->height)),
QPoint(x + (int)((1 + radius)*obj->second->width), y - (int)(radius*obj->second->height))
};
string str = std::get<std::string>(trait["color"]);
QColor color(QString::fromStdString(str));
painter.setBrush(color);
float opacity = (float)std::get<double>(trait["opacity"]);
painter.setOpacity(opacity);
painter.drawConvexPolygon(points, 4);
// reset
painter.setOpacity(1.0);
// draw only once per stack
break;
}
}
}
The marker GAS ATTACK now will have drawn a red transparent area around it, one-and-a-half size the counter, when placed on map, see illustration on GitHub wiki.
Attachment - It seems that this trait is about reading/writing values for different game pieces at the same time (sorry if I am mistaken). This kind of functionality is implicit when you have script.
Counter[id][key]
You can set the value like this:
Counter[id][key] = new_value
You are of course free to put whatever condition you want on reading and writing. It is easy to loop
through all counters to find those that need updating under a certain condition.
Note: VASSAL defines Key Commands to name functionality. In a script this is just the name of functions.
Note: it is possible for both the user and the C++ part to trigger functions under certain conditions. I have already implemented one such C++ trigger when a counter has moved and needs the Moved marker. The user can also trigger this function by right-clicking.
Again sorry if I have not comprehended all of the functionality of the Attachment trait. Still I am pretty sure that a trait like this can be implemented (or is an inherent feature) of script.
Basic Name - As I see it, this trait is only necessary because the tool you build VASSAL modules with has as default Basic Piece. It canât be added. I understand why you canât add it, because a game piece without an image would be meaningless. Also, a game piece without a coordinate on the map would be meaningless. The root of every trait in this engine is defined like this (in counter.luau):
function Repository:new (images)
local o = {}
setmetatable(o, self)
Repository.__index = self
o.Image = Image:new(images, 1)
o.x = 0
o.y = 0
return o
end
Now, Image is a trait (see traits.luau). Likewise x,y (and any additional coordinate that define the position of the counter in 3D space, like level, floor, inside/outside and so on) can be looked upon as a trait, the Location trait. You donât really need to define Image and Location in the root. They can be defined elsewhere, under the condition that the C++ part can find them. âFinding themâ means that this call gives the x coordinate of the game piece (in counter.cpp):
this->state.x = (int)std::get<double>((*state)["x"]);
âstateâ is the C++ representation of the Lua table associated with the game piece.
Border Outline - Something similar to this has already been implemented, but not as a trait and only for square counters. This is the border drawn when a counter is selected, see counter.cpp line 623. Non-square counters need their own implementation. But note: all counters are images that are always square, they only have transparent areas.
Calculated Property - This is a property that becomes superfluous in a script. You have the whole expressive power of the Lua language in addition to the functions of the C API. You have access the values, fields, traits and so on.
Can Pivot - This is like the Rotate trait but not around the center of the counter. I note rotation can be interactive, difficult to implement.
Can Rotate - This trait has been implemented. CCW (counter clock-wise) is easy to add, just rotate with a negative angle. In the right-click menu you now see both Rotate cw and Rotate ccw. Options like random rotation has not been implemented. The Rotate trait now has two menu entries with two different actions, see Traits.luau.
Clone - The ability to duplicate a piece with a right-click command is not difficult to implement. Again, letâs start with the trait definition.
Clone = {}
function Clone:new ()
local o = {}
setmetatable(o, self)
self.__index = self
o.menuname = "Clone"
o.menuclick = "actionClone"
return o
end
actionClone
is
local function actionClone (window, id)
if window == 'Map' then
if Counter[id].Clone ~= nil then
toId = next_id()
zorder = top_zorder()
cloneCounter(id, toId, zorder)
end
end
end
Cloning a counter means programmatically adding a copy of the counter. The cloned counter must
have a new id and zorder ( just the next higher integer; all new counters are always on top of all others). cloneCounter
looks like this
function cloneCounter(fromId, toId, zorder)
Counter[toId] = Counter:clone(fromId, zorder)
create_class_copy(Counter, toId)
end
We set the table of the cloned counter to be the table of what we clone and add a new zorder.
function Counter:clone (id, zorder)
local o = Counter:duplicate(Counter[id])
setmetatable(o, Counter)
Counter.__index = Counter
o.zorder = zorder
return o
end
Then we just call the usual create_class_copy
with the new id (toId). We add this new Clone trait to a counter like this:
Repository:addTrait("German/12R-51R", "Clone", Clone:new())
If you want all counters to be able to be cloned, you just add o.Clone = Clone:new() to Base (for example). Note it was not necessary to do anything with the C++ part to implement this cloning.
Comment - Obviously not applicable to this engine.
Delete - Implemented.
Deselect - In my opinion itâs more useful to toggle the selection status (like Mark When Moved).
We start off as usual with the trait definition.
Select = {}
function Select:new ()
local o = {}
setmetatable(o, self)
self.__index = self
o.menuname = "Select"
o.menuclick = "actionSelect"
return o
end
The action is:
local function actionSelect (window, id)
if window == 'Map' then
if Counter[id].Select ~= nil then
Counter:select(id)
end
end
end
function Counter:select (id)
if Counter[id].Select ~= nil then
toggle_select_status(id)
end
end
The C API toggle_select_status
is:
static int toggle_select_status(lua_State *L)
{
const char *id = lua_tostring(L, -1);
lua_pop(L, 1);
Counter::toggleSelect(id);
return 0;
}
void Counter::toggleSelect(const char *name)
{
Counter *counter = findObj(name);
if (counter != nullptr)
{
counter->selected = !counter->selected;
counter->setImage();
}
}
The C API is far from finished. New calls will be added, not least for the âGUI Libraryâ, the means the module developer has for painting and rendering GUI elements on the map board.
Does Not Stack - This trait has a number of additional options that control selection, dragging and more. I will only implement a simple way of disallowing a counter to be stacked.
The point is to not allow a counter to be dropped where another counter is. Dropping is done in frame.cpp. You can not drop a counter on a Does Not Stack and you can not drop a Does Not Stack on a counter.
The trait definition is very simple.
DoesNotStack = {
}
You add this trait to a counter like this.
Repository:addTrait("French/VII-40", "DoesNotStack", DoesNotStack)
In CentralFrame::dropEvent
(frame.cpp)
bool ok = Counter::snaptoDefaultGrid (obj->second, x, y);
if (!ok) // does not stack
continue;
else
.
.
.
In counter.cpp
bool Counter::snaptoDefaultGrid (Counter *counter, int &x, int &y)
{
// find the first (if any) counter close enough to snap to
// returns true if a snap found and no doesnotstack
int maxDistance = 8;
for ( auto obj = counters.begin(); obj != counters.end(); ++obj )
{
if (counter->name != obj->second->name)
{
if (abs(x - obj->second->state.x) < maxDistance &&
abs(y - obj->second->state.y) < maxDistance)
{
if (counter->doesNotStack)
return false;
if (obj->second->doesNotStack)
return false;
x = obj->second->state.x;
y = obj->second->state.y;
return true;
}
}
}
return true;
}
Note: The default grid just reflects that you must be a certain distance away from any other counter for it to drop without stacking. When the map has an actual grid, you are only allowed to drop on grid points.
Note: I implemented a hex grid before (see 04. Grid ¡ RhettTR/Alben Wiki ¡ GitHub). Verdun: A generation Lost has no grid, only the default grid, the minimum distance between counters.
Hmmm. If there is a hook before drop
if (!Luau::beforeDrag(child->owner->name.c_str()))
return;
why not implement a hook after drop
if (!Luau::afterDrag(obj->second->name.c_str(), x, y))
continue;
else
.
.
.
If Luau::afterDrag
returns false (for whatever reason) then the drop does not take place. What we need then is to implement a Lua version of Counter::snaptoDefaultGrid
. And here it is (in module.luau):
function afterDrag(id, x, y)
local counter
if Counter[id] ~= nil then
counter = Counter[id]
else
counter = Repository[id]
end
local maxDistance = 8;
for k, v in pairs(Counter) do
if type(k) == "string" and k ~= '__index' and type(v) ~= 'function' then
if k ~= id then
if math.abs(v["x"] - x) < maxDistance and math.abs(v["y"] - y) < maxDistance then
if counter["DoesNotStack"] ~= nil then
return false, x , y
end
if v["DoesNotStack"] ~= nil then
return false, x , y
end
x = v["x"]
y = v["y"]
return true, x , y
end
end
end
end
return true, x , y
end
This is the complete equivalent to Counter::snaptoDefaultGrid
written in Lua. Isnât that cool !!
By the way, I discovered a terrible bug in the previous version of Counter::snaptoDefaultGrid
. Can you see what it is? Also note the return true, x , y. Lua can return more than one value which is very handy.
Dynamic Property - This trait I believe is superfluous in this engine.
Global Hotkey, Global Key Command - A few global hot keys and commands have been implemented in main.cpp. They can not be set by the user. This is CTRL-R (reload script), CTRL-Z (undo) and CTRL-Y (redo). The ESC button closes pop-up windows showing stack. As I understand it, besides allowing the user to activate certain functions, key commands were also used to identify functions internally in VASSAL.
It is obvious that certain functionality should have key commands for ease of use. I do not think it is a good idea to let the module developer be free to override any key command. I argue that many key commands should have the same functionality regardless of module (and for that matter regardless of application). CTRL-Z/Y is used in many applications, likewise CTRL-C/V (copy/paste). CTRL-D should always be Delete and CTRL-M should always be toggle move status. There should also be fixed key commands for rotate cw/ccw and so on.
I need to get back to this.
Invisible - Setting visibility is an important trait. The Visibility trait has (for the time being) two fields.
Visibility = {}
function Visibility:new (opacity)
local o = {}
setmetatable(o, self)
self.__index = self
o.opacity = opacity
o.apply = true
return o
end
It is the usual apply flag and a field for opacity (1.0 = totally opaque). Visibility needs to be part of the rendering of the counter. Therefore it must be included in the state (state.h)
struct State // the subset of fields needed to render the counter/card
{
int x;
int y;
bool moved; // only true if trait
int degrees; // only not zero if trait
std::string image; // name of current (flipped) image
int zorder; // position in a stack
Table *overlays; // masks & labels if any
float opacity; // opacity 0.0 to 1.0 (full)
};
In counter.cpp
// visibility
if (state.opacity < 1.0)
{
auto itr = this->table.find("Visibility");
if (itr != this->table.end())
if (std::get<bool>(std::get<Table>((this->table)["Visibility"])["apply"]))
{
QImage temp(image.size(), QImage::Format_ARGB32_Premultiplied);
temp.fill(Qt::transparent);
QPainter *paint = new QPainter(&temp);
paint->setOpacity(this->state.opacity);
paint->drawImage(0, 0, image);
delete paint;
image = temp;
}
}
A counter has a given visibility like this:
Repository:addTrait("German/38-94", "Visibility", Visibility:new(0.3))
Note that the apply flag can be turned on and off to show visibility. Also note that hidden units are not rendered at all for the side that can not see them. Setting opacity to 0.0 is not enough.
Layer - Implemented, not as a trait but inherent in the definition of a counter as the table of alternative images.
Mark When Moved - Implemented as the trait MarkMoved.
Marker - Just a key/value field, implicit in the script (like o.Side = ""
).
Mask - Implemented as trait Mask.
Mat, Mat Cargo - I regard mats as a third type of playing piece besides counters and cards. They may be in their own window. For later.
Menu Separator - To make a large right-click menu easier to read. For later.
Move Fixed Distance - This is about programmatically moving a counter. Note I do not implement this as a trait now, only show how moving a counter is done with script.
I created a counter on board (in module.luau).
copyCounter("German/38-94", "1002", -3, 300, 100)
The script that moves this counter from 300,200 to 37,27 is:
local ok = beforeDrag("1002")
if ok then
local x,y
ok, x, y = afterDrag("1002", 37, 27)
if ok then
updatePos("1002", x, y)
Counter:generateGUI()
end
end
Note the calls to beforeDrag and afterDrag to make sure the move is legal.
function updatePos(id, x, y)
Counter[id]["x"] = x
Counter[id]["y"] = y
end
One additional thing I forgot (not related to Move Fixed Distance) is to update the Luau table with a new position after drag. This is important because when you clone a counter you duplicate the Luau table associated with the counter, and if this table is not updated the counter will have a wrong position. In frame.cpp:
Luau::updatePos(obj->second->name.c_str(), obj->second->state.x, obj->second->state.y);
Movement Trail - A complicated trait witch obviously belongs in the C++ part. For later.
Multi-Location Command - I suppose this in another trait that becomes unnecessary when you script.
Non-Rectangular - This is default. A mask is always set when you render a counter, whether rotated or not. I am not sure if being able to select a counter on its transparent parts has any meaning?
Place Marker - A handy trait for placing often placed status markers. For later.
Play Sound - Later
Property Sheet - Again I have a suspicion that this trait is not needed when you script. It seems superfluous. Have to look at it more.
Prototype - This trait is only needed when you canât script. In Lua any trait can be inherited by any other trait, see traits.luau.
Replace With Other - Isnât this just about being able to flip a counter?
Report Action - This trait is important. Mandatory reporting will be sent to the chat window (and also maybe to a log file). In addition it must be a possibility to customize reporting for custom actions.
Restrict Commands - A little of this has been done. It is for example totally meaningless to be able to delete a counter in the repository. In counter.cpp
if (strcmp(window, "Repository") == 0 &&
(e.entryname == "Delete" || e.entryname == "Moved"))
action->setEnabled(false);
Even if the functionality is there I see no âgraying outâ of the menu entries in Qt. This is an issue that needs more attention later.
Restricted Access - This is pretty much about can-not-move-if-not-your-side functionality. I did implement this in beforeDrag
.
Return To Deck - An important right-click action concerning cards. Later.
Send To Location - This trait is not unlike Move Fixed Distance (?).
Set Global Property, Set Piece Property - This is done easily with script.
Spreadsheet - Wow, VASSAL has become quite sophisticated. You open a small window which allows you to change the values of fields. For later.
Sub-Menu - Important for large right-click menus. For later.
Text Label - Implemented as the Label trait.
Translatable Message - All engine text in local language, a huge sub-topic.
Trigger Action - Likely easy to implement in a script.
Ok, that was my going through the VASSAL traits. Sorry if it was rather rudimentary (or even wrong). The topic will of course come up again. For now I focus on going on with the design of Verdun: A generation Lost.