First order of business is fixing BMInteractObject(), which selects (picks up and drags) placeable objects. It appears to work well, except when clicking over both a map object and a HUD object. In that case, it never lets go of the HUD object. This is caused by a small bug.
Actually, the only reason it works at all is due to that bug. The code is divided into two sections: a drop and a pickup. In the drop code at the top, the switch statement checks for “CrossHairMovie” to see if the HUD has an object selected. That is incorrect, as the code checks the name of the OBJECT not the name of the CLASS. That causes the drop section to always fail for the HUD, which is actually a good thing. The code then falls to the pickup section, which calls the HUD’s InteractObject() again if a map object was not selected.
If we fix the reference in the drop section, another problem appears: the cursors never lets go of any HUD object. The HUD’s InteractObject() is self-contained with its own drop and pickup code, including a special clause to ensure it doesn’t pick up the same thing it dropped. If we try to drop and pick up in two separate calls, it always picks up what it dropped.
The easy answer was to move the call to the HUD’s InteractObject() to the top and always do it first. That’s less than ideal however, since HUD objects overlay map objects. We had to move a HUD object out of the way in order to click on a map object. So, the HUD’s InteractObject() call needs to be at the end. But what if we clicked on a map object? We passed a parameter to InteractObject() to only drop what’s selected and not select anything new.
Then it worked! And created a new problem. If we moved a map object under a HUD object, or a HUD object directly over a map object, and clicked to let go it would hot-swap between the two. We couldn’t let go unless there was nothing else to select.
The final solution is to simply do one or the other. Swapping was an interesting bit of code, but simply not feasible during gameplay. Here’s the final BMInteractObject():
BattleMapPlayerInput.uc
exec function BMInteractObject()BattleMapGfxHud.uc
{
local BattleMapTorch To;
local Trigger Tr;
local int i, j, k;
if (WorldInfo.NetMode == NM_Standalone || WorldInfo.NetMode == NM_ListenServer)
{
// stop doing whatever the currently selected object is doing
if (ObjectSelected != none)
{
switch(ParseObjectName(ObjectSelected.Name))
{
case "Torch":
To = BattleMapTorch(ObjectSelected);
To.StopTorch(MouseOrigin);
break;
case "GfxHud":
BattleMapHUD(myHUD).CrossHairMovie.CallInteractObject(false);
break;
}
ObjectSelected = none;
}
else
{
//start doing something with a newly selected object
switch(ParseObjectName(ObjectUnderMouse))
{
case "Torch":
foreach DynamicActors(class'BattleMapTorch', To)
if (To.Name == ObjectUnderMouse)
{
ObjectSelected = To;
To.MoveTorch();
}
break;
case "Trigger":
// activate each Kismet event attached to this trigger
foreach DynamicActors(class'Trigger', Tr)
if (Tr.Name == ObjectUnderMouse)
for (i=0; i<Tr.GeneratedEvents.Length; i++)
for (j=0; j<Tr.GeneratedEvents[i].OutputLinks.Length; j++)
for (k=0; k<Tr.GeneratedEvents[i].OutputLinks[j].Links.Length; k++)
Tr.GeneratedEvents[i].OutputLinks[j].Links[k].LinkedOp.ForceActivateInput(Tr.GeneratedEvents[i].OutputLinks[j].Links[k].InputLinkIdx);
break;
}
//if no map object found, check for a HUD object
if (ObjectSelected == none)
if(BattleMapHUD(myHUD).CrossHairMovie.CallInteractObject(true) > 0)
ObjectSelected = BattleMapHUD(myHUD).CrossHairMovie;
}
}
}
Add a parameter to CallInteractObject:
function int CallInteractObject(bool Param1)ActionScript
In InteractObject(), update the definition to include the new parameter:
function InteractObject(Param1:Boolean)And preface the loop that checks for objects under the mouse with the passed parameter:
//find new object to interact withFinally, update the call in DeleteObject() to include the parameter:
if (Param1)
for (var MousedObject in _root)
InteractObject(true);
Now, let’s take what we’ve done with Zones and create map tokens for our monsters and players. Even though we place miniatures on the table during gameplay, it is still helpful to represent creatures within the battlemap for several reasons:
- The DM doesn’t have to move around the table to reposition minis. He can move a token with a click then ask a player (or demand, depending on the DM) to move the miniature.
- The DM can easily see the position of creatures without moving around for a better view. I can’t tell you how many times I’ve declared an invalid opportunity attack by an NPC, sparking a bewildered debate among the players. Or missed an opportunity attack on players, all due to my skewed view from the end of the table.
- The minis are lit by whatever texture they happen to be standing on. A white token would “spotlight” the minis, despite standing on black rock, yellow stone or green goo.
Flash
Create a new token symbol within Flash. I simply made a 256x256 white circle with a black border. Don’t forget to check “Export for ActionScript”:
Next, add a block of Dynamic Text with a “P1” placeholder. Give it a variable name of “TokenText”:
You will also need to Embed the font (button next to Style). I chose only Uppercase and Numerals.
ActionScript
Add new global variables to the top:
Add a “Token” case to the Spawn() function:var DefaultZoneScale:Number = 70;var DefaultTokenScale:Number = 50;var Tokens:Number = 0;
case "Token":Add the new SpawnToken() function below. It’s a separate function like SpawnZone() just to be consistent, though it didn’t need to be. SpawnZone() had additional parameters because the SwapObject() function replaced a zone by spawning a new zone with the same properties as the old one. When we swap a token, we’ll simply be changing the text.
SpawnToken();
break;
For now, we just assign a counter to the token text. When we implement a pop-up UI within the HUD for manipulating objects, we'll pass in typed text. For now, just note the P# or M# next to the creature in your combat tracker.
We’re also ensuring that the cursor is always on top. Since a token will never need to remember what layer it was on (like swapping a zone) we just grab the highest available depth. That, of course, places it over the cursor so we need to bump that as well.
function SpawnToken()Now, update the InteractObject() function so it will grab tokens as well. We want it to work exactly the same as selecting a Zone, so we simply need to update the case statement:
{
Tokens += 1;
var t1 = _root.attachMovie("TokenSymbol", "Tokn" + Tokens, _root.getNextHighestDepth());
t1._x = _root._xmouse;
t1._y = _root._ymouse;
t1._xscale = DefaultTokenScale;
t1._yscale = DefaultTokenScale;
t1.TokenText = "M" + Tokens;
CursorInst.swapDepths(_root.getNextHighestDepth());
}
case "Zone":The same is true for the ModifyObject() function. Modifying a token will manipulate its scale just like a zone. However, we want to implement a new feature while we’re at it. If we size a token to a 1x1 square on our battlemap, it is highly likely that additional tokens will need to be that size as well. We already used the DefaultTokenScale global variable when spawning a new token, so we need to set that value here when modifying an object:
case "Tokn":
//find zones/tokens, but not the one we had clicked on
case "Zone":The SwapObject() function will simply update the text on the token, alternating between “P#” (PC) and “M#” (monster):
_root[ObjectSelected]._xscale *= (1 + (0.1 * Param1));
_root[ObjectSelected]._yscale = _root[ObjectSelected]._xscale;
DefaultZoneScale = _root[ObjectSelected]._xscale;
break;
case "Tokn":
_root[ObjectSelected]._xscale *= (1 + (0.1 * Param1));
_root[ObjectSelected]._yscale = _root[ObjectSelected]._xscale;
DefaultTokenScale = _root[ObjectSelected]._xscale;
break;
case "Tokn":Finally, let’s implement the same default scaling for zones. We've already done most of the work along the way. In SpawnZone(), change the ZoneScale variable to:
switch(_root[ObjectSelected].TokenText.substr(0, 1))
{
case "P":
_root[ObjectSelected].TokenText = "M" + _root[ObjectSelected].TokenText.substr(1);
break;
case "M":
_root[ObjectSelected].TokenText = "P" + _root[ObjectSelected].TokenText.substr(1);
break;
}
break;
var ZoneScale = (Param5 != undefined) ? Param5 : DefaultZoneScale;BattleMapPlayerInput.uc
Add a new case to BMSpawn():
case "Token":DefaultInput.ini
BattleMapHUD(myHUD).CrossHairMovie.CallSpawn("Token");
Add a new key binding:
.Bindings=(Name="N",Command="BMSpawn Token")
Let’s enhance our blood decal a bit. We’ll borrow the same movement functionality as our torch and apply it to the blood decal.
BattleMapBlood.uc
Insert the following block into the BattleMapBlood class between the declaration and PostBeginPlay(). This code is nearly identical to the Torch code (we changed the names and removed the VerticalOffset that the torch light required):
var repnotify Vector BloodLoc,BloodStopLoc;BattleMapPlayerInput.uc
replication
{
if (bNetDirty)
BloodLoc,BloodStopLoc;
}
// called on clients when BloodStopLoc gets replicated
simulated event ReplicatedEvent( name VarName )
{
if (VarName == nameof(BloodLoc) )
SetLocation( BloodLoc );
else if (VarName == nameof(BloodStopLoc) )
StopBlood( BloodStopLoc );
else
super.ReplicatedEvent( VarName );
}
// this function should be run on both clients and server
simulated function MoveBlood( )
{
bCollideWorld = false;
SetCollision(false, false);
GotoState( 'BloodMoving' );
}
simulated function StopBlood( Vector NewBloodStopLoc )
{
bCollideWorld = true;
SetCollision(true, true);
GotoState('BloodStopped');
BloodStopLoc = NewBloodStopLoc;
SetLocation(NewBloodStopLoc);
}
simulated state BloodMoving
{
simulated event Tick( float DeltaTime )
{
// move the blood...
BloodLoc = BattleMapPlayerController(Owner).MouseOrigin;
SetLocation(BloodLoc);
}
}
simulated state BloodStopped
{
simulated event Tick( float DeltaTime )
{
// stop the blood...
}
}
In BMInteractObject(), add a new variable:
local BattleMapBlood Bl;Add a blood case to the “drop” switch statement:
case "Blood":And a blood case to the “select” switch statement:
Bl = BattleMapBlood(ObjectSelected);
Bl.StopBlood(MouseOrigin);
break;
case "Blood":Next, we’ll borrow the same sizing functionality as our zones and tokens. Add the following new variable to the top:
foreach DynamicActors(class'BattleMapBlood', Bl)
if (Bl.Name == ObjectUnderMouse)
{
ObjectSelected = Bl;
Bl.MoveBlood();
}
break;
var float DefaultBloodSize;In BMModifyObject(), add a new local variable:
local BattleMapBlood B;And a blood case to the switch statement. We’ll manipulate the decal’s size:
case "BattleMapBlood":In BMSpawn (), add a new local variable:
B = BattleMapBlood(ObjectSelected);
B.Decal.Width = B.Decal.Width * (1 + (0.1 * Modifier));
B.Decal.Height = B.Decal.Height * (1 + (0.1 * Modifier));
DefaultBloodSize = B.Decal.Width;
local BattleMapBlood Bl;Next, update the “Blood” case in the switch statement to resize after spawning. The call to SetLocation() is simply a dirty way to force a replication:
case "Blood":Finally, set the DefaultBloodSize in DefaultProperties:
Bl = Spawn(class'BattleMapBlood',Outer,,MouseOrigin );
Bl.Decal.Width = DefaultBloodSize;
Bl.Decal.Height = DefaultBloodSize;
Bl.SetLocation(MouseOrigin);
break;
DefaultBloodSize = 200fNow our HUD interactivity is fixed, we have new creature tokens and we can manipulate our blood markers:
BattleMap mode source files