An overview of how fleets are implemented.
The following section is nuts-and-bolts; the meta details on how they're implemented comes later.
We add two objects, an object array fleets[MAX_FLEETS] and an object wdmgrid.
Because POTC does not support two-dimensional arrays, I have implemented the boxes as an attribute tree array under wdmgrid to allow for the two-dimensionality.
(MAX_FLEETS = 32)
There is the structure:
wdmgrid.boxes
then wdmgrid.boxes.0.0 through WDMGRID_NUM-1 in each (currently 40, so
wdmgrid.boxes.39.39)
Note that POTC uses Z as the depth axis, not Y (Y is height), so I use X and Z for the coordinates when dealing with boxes and fleet position (as did the worldmap in dealing with island positions).
Each box has the following fixed properties:
(bX is the box number in the x axis and bZ is the box number in the z axis.)
wdmgrid.boxes.(bX).(bZ).x = bX;
wdmgrid.boxes.(bX).(bZ).z = bZ;
wdmgrid.boxes.(bX).(bZ).index = bX+","+bZ;
For example, box 13,15 is:
wdmgrid.boxes.13.15.x = 13;
wdmgrid.boxes.13.15.z = 15;
wdmgrid.boxes.13.15.index = "13,15";
The init code that create all those boxes is done without loops (all 263kb of it) because there is such a speed hit for using loops--in fact all those 1600 boxes are created in only a second or two at most, vs. probably 60 seconds to create the old 400-box array.
There are a number of box handling functions:
<!--c1--><div class='codetop'>CODE</div><div class='codemain'><!--ec1-->//gets box from wdmap coords
aref wdmGetBoxFromCoords(float x, float z);
//gets box index from coords
string wdmGetBoxIndexFromCoords(float x, float z);
// gets wdmap coords from box index--the center of the box.
void wdmGetCoordsFromBoxIndex(string boxid, ref x, ref z);
//gets box by box coords
aref wdmGetBox(int x, int z);
//gets box by index
aref wdmGetBoxFromIndex(string idx);
//are the boxes the same?
bool wdmCompareBoxes(aref box1, aref box2);<!--c2--></div><!--ec2-->
Fleets[] is a traditional object array, with each instance having curfleet.index and curfleet.id. If the fleet is not used, the id is "blank" (that is to say, curfleet.id = "blank"; not curfleet.id = ""
We also track FLEETS_QUANTITY, so we don't have to page through the entire array on updates.
Because any fleet may be removed at any time I have implemented some clearing/cleaning/sorting functions. However the tricky part is that a fleet may be cleared _while updating the fleets array_, and we update the fleets array in passes. Therefore we need a way to clear it, but not replace it with a later fleet until after that fleet has been updated.
There are the basic three obj-array handling functions: ref wdmGetFleet(int idx); int wdmGetFleetIndex(string id); ref wdmFleetFromID(string id);
Then there are the other functions:
<!--c1--><div class='codetop'>CODE</div><div class='codemain'><!--ec1-->// will clear slot idx in Fleets[], replacing it with a non-blank fleet now if sortnow, else queuing that for later.
void wdmClearFleetSlot(int idx, bool sortnow);
// takes blank fleet slot and fills with the last nonblank fleet. Returns false if all later fleets are now blank.
bool wdmFillBlankSlot(ref curfleet);
// if we've queued blank fleets for later filling, do that now
void wdmCleanFleets();
//sorts fleets to remove blank slots -- should hardly ever be run because we refill whenever we delete.
void wdmSortFleets();<!--c2--></div><!--ec2-->
---------------------------------
Then there are the fleet creation and update functions, and here I'll talk about how they're implemented in the non-nuts-and-bolts sense.
Fleet creation.
---------
Fleets have the following attributes:
curfleet.id - if a randfleet, a generated ID; else what you make it
curfleet.nation - nation of the fleet
curfleet.origin - the name of the origin of the fleet, either mapedgeXZ or town id.
---mapedge01 is up, 10 is right, 00 is down, and 11 is left. (This is arbitrary)
curfleet.origin.x - the x coordinate of the origin (random if on a vertical mapedge)
curfleet.origin.z - the z coordinate of the origin (random if on a horizontal mapedge)
curfleet.type - "trade" or "war" or "pirate"
curfleet.RealEncounterType - the index of the encounter template.
---We use the existing Encounter functionality, although somewhat edited--we find an encounter via FindMerchantEncounter() or FindWarEncounter(), check if that encounter allowed by the nation, and if so generate the number of ships it will have.
curfleet.strength - this is where the existing encounter functionality is modded. If strength != 1.0, we decrease proportionally the number of each type of ship recorded by the encounter finder.
curfleet.speed - given as a ratio of player speed. For now always 0.8, but it should be dependent on the ships assigned (or if we only assign specific ships later, a speed for the encounter template).
curfleet.task - the orders for the fleet. Currently allowed are:
<!--c1--><div class='codetop'>CODE</div><div class='codemain'><!--ec1-->FLEET_TASK_RAID 0
FLEET_TASK_CAPTURE 1
FLEET_TASK_PATROL 2
FLEET_TASK_TRADE 3
FLEET_TASK_LEAVE 4
FLEET_TASK_WAIT 5<!--c2--></div><!--ec2-->
---NOTE: WAIT is never assigned as a task during creation, only on update.
---The orders given are determined by some chances (also defined), i.e. chance for a war fleet to go to an (enemy) town (task 0 through 2), if not chance to patrol an empty box (or patrol its home), if not it will leave via a mapedge.
---if merchant, either trade in a friendly town (TRADE) or leave via a mapedge.
---note that if no enemy towns found for war/pirate fleet (and fail patrol test), or no friendly ones for a trade fleet, mapedge will also thus be chosen by default.
---if leaving via a mapedge, task is LEAVE and the mapedge is not the origin (if the origin was a mapedge). Non-edge coord is random.
curfleet.dest - name of the destination (town ID, box index, or mapedge)
curfleet.dest.x - x coord of the destination
curfleet.dest.z - z coord of the destination
then:
curfleet.pos.box - index of the box in which the fleet currently is
curfleet.pos.x - current x coord of fleet (if not updated yet, curfleet.origin.x)
curfleet.pos.z - current z coord of fleet (if not updated yet, curfleet.origin.z)
curfleet.pos.ay - the heading of the fleet in radians. Not assigned until fleet updated.
The fleet is added to box (curfleet.pos.box), as box.fleets.(curfleet.id)
If the fleet is in a box where there is another fleet, or a town, that box is placed in the update queue.
----
Fleet updates.
------------
(update of a fleet is aborted if the player is currently in sea mode and the fleet is in the player's box--that is to say, in sea mode with the player)
1. The fleet's position vs. its destination is checked. If not there yet:
---the distance is checked. If the distance is less than the fleet's speed (as modified by the number of fleet updates per day), the fleet is placed at destination. Otherwise the heading is found, and then on a random chance (1/4 chance for now) the heading is modified by +/-45 degrees (to account for tacking, nav problems, etc.)
----Then the fleet is moved (modified speed) units along that heading. Its box is updated, and if the box changed the fleet's entry in the old box is removed.
------if for any reason the fleet is outside the map, it is cleared.
2. If the fleet is at its destination
---The fleet's task is checked.
----If the task is LEAVE, the fleet is immediately removed.
----If the task is not LEAVE and not WAIT (see below):
------the fleet is set to task WAIT. It is then assigned a post-wait task and destination. For now the post-wait task is always LEAVE and the post-wait destination is always the fleet's origin. If the fleet was given task RAID or CAPTURE, on a frnd() the fleet may be immediately cleared. Otherwise, the fleet is assigned a number of days to wait (well, not days; a number of fleet-updates, and each fleet is usually updated only once per day).
----If the task is WAIT, the current waitdays is checked. If not yet the desired waitdays, the current waitdays attribute is incremented and that's it; otherwise the waittask and wait dest are assigned, and on next update the fleet starts moving again.
3. If the fleet is not cleared (i.e. it is either not at dest, or its task is now WAIT):
----if the fleet's box changed due to fleet movement, the fleet's entry from the old box is cleared, and the fleet is added to the new box. If there is another fleet already there, or there is a town, the box is added to the update queue.
------
Box updates.
-------------
If there are no boxes queued for updating, we don't touch them (see playerbox below)
Otherwise:
----If the player is in that box and we are at sea or on the worldmap, we skip the box
------otherwise we just see what's in the box and do nothing. :]
-Now, if we're on the worldmap we separately check the pchar's box.
---We then output what fleet(s) (minus the player's) and what town(s) are in the box with the player.
Lastly we delete the update queue from wdmgrid.
-------
Update mechanism.
----------------
For now, the updates are handled by a recursing series of postevent() calls on the worldmap, and by straight wdmUpdateFleets() calls if we are adding time via an add time function (addtime() or adddatatocurrent() or something).
The recursing postevents are done to minimize the stutter on the worldmap, or at least to spread it out so everything is slower (rather than normal-stop-normal-stop it's more like 1/2speed-1/2speed-fullspeed-1/2speed-1/4speed and so forth).
The wdmap updates are triggered by adding a check to wdmEvent_UpdateDate() in worldmap_events, NOT the normal wdmNextDayUpdate() that usually gets used.
-----
Sea mode integration.
--------------------
I have either deleted or rewritten the old fantom/encounter code, both in worldmap_reload.c and in sea.c
First, a little bit of background. The function that handles sea mode loading is SeaLogin(), and it gets passed something called a login object. That object is filled with the player group's position, any quest groups, any encounters, and the name of the island (if it exists). In POTC normally only on reloading from worldmap to sea are encounters added to that object (via wdmReloadToSea() ).
I have taken the encounter/fleet code out of that function and placed it in its own function { void wdmFillLoginObjectWithFleets(ref loginobj, float baseX, float baseZ) } at the bottom of worldmap_reload.c
That function is run by SeaLogin() itself, and fills the login object with any fleets in the current box. Note that this means that _any_ time you reload to sea, fleets will be there.
The function loops through all those fleets, assigning group names ("Fleet_" + the fleet's ID), group pos/angle, and task (for now, just MOVE along current heading for everybody).
Because of the aforementioned size limits, I have just now added a section that checks the extents of the positions (i.e. max - min for both X and Y), and if > a define, we do a sqrt of position and multiply by the sqrt of the define so as to bring the fleets in closer.
Now, in Sea.c when we start loading all those fleets we filled the login object with, we do a check to see if there are any ships/characters already in the fleet.
---If not, we do the traditional fantom creation stuff.
---Then, when we next _leave_ sea mode, we check all groups. If there are any survivors in each fleet/group, we create a tree of curfleet.ships.characters.(fantomID), and copy the entire character object in to that attribute-tree. We also create curfleet.ships.commander and set it to the ID of the fantom with the best ship class.
-So if we _do_ have that tree in the fleet object on loading to sea, we create the appropriate number of fantoms, but then copy the old data back over. We do create a new ID (I didn't do that before and I'm hoping that's why it was (occasionally) crashing on me), and then add the fantom to its group and to sea.
------------------------
So. That's about it.