1. // map some server side building qualities
  2. // note: for expansion purposes, we make this 2d:
  3. //  first index is the building index, second is the building level
  4. // this isn't perfect by any means, but it does give you a direction to go in
  5. // for upgradeable buildings
  6.  
  7. // factory
  8. $Buildings::requiredBuildSupplies[0,0] = "Gold 250 Stone 45";
  9. $Buildings::requestId[0,0]             = "PlaceFactoryBuilding";
  10. $Buildings::ActionType[0,0]            = "Supply Wood 6 10";
  11.  
  12. // shop
  13. $Buildings::requiredBuildSupplies[1,0] = "Gold 300 Wood 75";
  14. $Buildings::requestId[1,0]             = "PlaceShopBuilding";
  15. $Buildings::ActionType[1,0]           = "Supply Gold 50 8";
  16.  
  17. // barracks
  18. $Buildings::requiredBuildSupplies[2,0] = "Gold 200 Wood 50 Stone 100";
  19. $Buildings::requestId[2,0]             = "PlaceBarracksBuilding";
  20. $Buildings::ActionType[2,0]           = "Hire warrior knightress archer"; // only the "hire" portion is implemented
  21.  
  22. // farm
  23. $Buildings::requiredBuildSupplies[3,0] = "Gold 150 Wood 75";
  24. $Buildings::requestId[3,0]             = "PlaceFarmBuilding";
  25. $Buildings::ActionType[3,0]           = "Supply Food 15 12";
  26.  
  27. // foundry
  28. $Buildings::requiredBuildSupplies[4,0] = "Gold 350 Stone 45 Wood 20";
  29. $Buildings::requestId[4,0]             = "PlaceFoundryBuilding";
  30. $Buildings::ActionType[4,0]           = "Supply Stone 10 12";
  31.  
  32. // town center
  33. $Buildings::requiredBuildSupplies[5,0] = "Gold 350 Stone 45 Wood 20";
  34. $Buildings::requestId[5,0]             = "PlaceTownCenterBuilding";
  35. $Buildings::ActionType[5,0]           = "Hire villager";
  36.  
  37. // ---- ORC (ENEMY) BUILDS --- //
  38. // factory
  39. $Buildings::requiredBuildSupplies[6,0] = "Gold 250 Stone 45";
  40. $Buildings::requestId[6,0]             = "PlaceFactoryBuilding";
  41. $Buildings::ActionType[6,0]            = "Supply Wood 10 10";
  42.  
  43. // shop
  44. $Buildings::requiredBuildSupplies[7,0] = "Gold 300 Wood 75";
  45. $Buildings::requestId[7,0]             = "PlaceShopBuilding";
  46. $Buildings::ActionType[7,0]           = "Supply Gold 50 8";
  47.  
  48. // barracks
  49. $Buildings::requiredBuildSupplies[8,0] = "Gold 200 Wood 50 Stone 100";
  50. $Buildings::requestId[8,0]             = "PlaceBarracksBuilding";
  51. $Buildings::ActionType[8,0]           = "Hire warrior knightress archer"; // only the "hire" portion is implemented
  52.  
  53. // farm
  54. $Buildings::requiredBuildSupplies[9,0] = "Gold 150 Wood 75";
  55. $Buildings::requestId[9,0]             = "PlaceFarmBuilding";
  56. $Buildings::ActionType[9,0]           = "Supply Food 5 12";
  57.  
  58. // foundry
  59. $Buildings::requiredBuildSupplies[10,0] = "Gold 350 Stone 45 Wood 20";
  60. $Buildings::requestId[10,0]             = "PlaceFoundryBuilding";
  61. $Buildings::ActionType[10,0]           = "Supply Stone 10 12";
  62.  
  63. // town center
  64. $Buildings::requiredBuildSupplies[11,0] = "Gold 350 Stone 45 Wood 20";
  65. $Buildings::requestId[11,0]             = "PlaceTownCenterBuilding";
  66. $Buildings::ActionType[11,0]           = "Hire villager";
  67.  
  68. // NOTE: as described in /client/scripts/buildings.cs, this is not a clean
  69. // implementation. All this information should be stored in a script array
  70. // and get methods made for each input/output combination, instead of
  71. // manually copying the information in each method
  72.  
  73. function getBuildingIndexFromDataBlockName( %datablockName )
  74. {
  75.   // simple indexer to map datablock names to an index for use in arrays
  76.   switch$(%datablockName)
  77.   {
  78.     case "TestBuildingBlock":
  79.       return 0;
  80.     case "factoryBlock":
  81.       return 0;
  82.     case "shopBlock":
  83.       return 1;
  84.     case "barracksBlock":
  85.       return 2;
  86.     case "farmBlock":
  87.       return 3;
  88.     case "foundryBlock":
  89.       return 4;
  90.     case "townCenterBlock":
  91.       return 5;
  92.    // ORC buildings
  93.     case "orcFactoryBlock":
  94.       return 6;
  95.     case "orcShopBlock":
  96.       return 7;
  97.     case "orcBarracksBlock":
  98.       return 8;
  99.     case "orcFarmBlock":
  100.       return 9;
  101.     case "orcFoundryBlock":
  102.       return 10;
  103.     case "orcTownCenterBlock":
  104.       return 11;      
  105.   }
  106. }
  107.  
  108. function getBuildingIndexFromUnitTypeName( %unitTypeName )  
  109. {
  110.   // simple indexer to map rts unit type names (only thing client knows about)
  111.   // to an index for use in arrays
  112.   switch$(%UnitTypeName)
  113.   {
  114.     case "testbuilding":
  115.       return 0;
  116.     case "factory":
  117.       return 0;
  118.     case "shop":
  119.       return 1;
  120.     case "barracks":
  121.       return 2;
  122.     case "farm":
  123.       return 3;
  124.     case "foundry":
  125.       return 4;
  126.     case "townCenter":
  127.       return 5;
  128.    // orcs
  129.     case "orcFactory":
  130.       return 6;
  131.     case "orcShop":
  132.       return 7;
  133.     case "orcBarracks":
  134.       return 8;
  135.     case "orcFarm":
  136.       return 9;
  137.     case "orcFoundry":
  138.       return 10;
  139.     case "orcTownCenter":
  140.       return 11;    
  141.   }
  142. }
  143.  
  144. function getDataBlockFromUnitTypeName( %unitTypeName )  
  145. {
  146.   // simple indexer to map rts unit type names (only thing client knows about)
  147.   // to an index for use in arrays
  148.   switch$(%datablockName)
  149.   {
  150.     case "testbuilding":
  151.       return "TestBuildingBlock";
  152.     case "factory":
  153.       return "factoryBlock";
  154.     case "shop":
  155.       return "shopBlock";
  156.     case "barracks":
  157.       return "barracksBlock";
  158.     case "farm":
  159.       return "farmBlock";
  160.     case "foundry":
  161.       return "foundryBlock";
  162.     case "townCenter":
  163.       return "townCenterBlock";
  164.     // ORCS
  165.     case "orcFactory":
  166.       return "orcFactoryBlock";
  167.     case "orcShop":
  168.       return "orcShopBlock";
  169.     case "orcBarracks":
  170.       return "orcBarracksBlock";
  171.     case "orcFarm":
  172.       return "orcFarmBlock";
  173.     case "orcFoundry":
  174.       return "orcFoundryBlock";
  175.     case "orcTownCenter":
  176.       return "orcTownCenterBlock";
  177.   }
  178. }
  179.  
  180. // NOTE: The above three functions MUST be refactored
  181.  
  182. datablock RTSUnitData(TestBuildingBlock : UnitBaseBlock)
  183. {
  184.    shapeFile = "~/data/shapes/animated/human/snow/human1.dts";
  185.    RTSUnitTypeName = "factory";
  186.    boundingBox = "10.0 10.0 3.0";
  187. };
  188.  
  189. datablock RTSUnitData(factoryBlock : UnitBaseBlock)
  190. {
  191.    shapeFile = "~/data/shapes/animated/human/snow/human1.dts";
  192.    RTSUnitTypeName = "factory";  
  193.    boundingBox = "10.0 10.0 3.0";
  194.    Collects = "Wood";
  195.    maxDamage = 800;
  196. };
  197.  
  198. datablock RTSUnitData(barracksBlock : UnitBaseBlock)
  199. {
  200.    shapeFile = "~/data/shapes/animated/human/snow/human4.dts";
  201.    RTSUnitTypeName = "barracks";
  202.    boundingBox = "10.0 10.0 3.0";
  203.    maxDamage = 900;
  204. };
  205.  
  206. datablock RTSUnitData(shopBlock : UnitBaseBlock)
  207. {
  208.    shapeFile = "~/data/shapes/animated/human/snow/human1.dts";  
  209.    RTSUnitTypeName = "shop";
  210.    boundingBox = "10.0 10.0 3.0";
  211.    Collects = "Gold";
  212.    maxDamage = 800;
  213. };
  214.  
  215. datablock RTSUnitData(farmBlock : UnitBaseBlock)
  216. {  
  217.    shapeFile = "~/data/shapes/animated/human/snow/human2.dts";
  218.    RTSUnitTypeName = "farm";
  219.    boundingBox = "10.0 10.0 3.0";
  220.    Collects = "Food";
  221.    maxDamage = 700;
  222. };
  223.  
  224. datablock RTSUnitData(foundryBlock : UnitBaseBlock)
  225. {  
  226.    shapeFile = "~/data/shapes/animated/human/snow/human3.dts";  
  227.    RTSUnitTypeName = "foundry";
  228.    boundingBox = "10.0 10.0 3.0";
  229.    Collects = "Stone";
  230.    maxDamage = 800;
  231. };
  232.  
  233. datablock RTSUnitData(townCenterBlock : UnitBaseBlock)
  234. {
  235.    shapeFile = "~/data/shapes/animated/human/snow/human5.dts";
  236.    RTSUnitTypeName = "townCenter";
  237.    boundingBox = "10.0 10.0 3.0";
  238.    maxDamage = 1200;
  239. };
  240. // ORC buildings
  241. datablock RTSUnitData(orcFactoryBlock : UnitBaseBlock)
  242. {
  243.    shapeFile = "~/data/shapes/animated/orc/snow/orc1.dts";
  244.    RTSUnitTypeName = "orcFactory";
  245.    boundingBox = "10.0 10.0 3.0";
  246.    Collects = "Wood";
  247.    maxDamage = 600;
  248. };
  249.  
  250. datablock RTSUnitData(orcBarracksBlock : UnitBaseBlock)
  251. {
  252.    shapeFile = "~/data/shapes/animated/orc/snow/orc4.dts";
  253.    RTSUnitTypeName = "orcBarracks";
  254.    boundingBox = "10.0 10.0 3.0";
  255.    maxDamage = 800;
  256. };
  257.  
  258. datablock RTSUnitData(orcShopBlock : UnitBaseBlock)
  259. {
  260.    shapeFile = "~/data/shapes/animated/orc/snow/orc1.dts";
  261.    RTSUnitTypeName = "orcShop";
  262.    boundingBox = "10.0 10.0 3.0";
  263.    Collects = "Gold";
  264.    maxDamage = 600;
  265. };
  266.  
  267. datablock RTSUnitData(orcFarmBlock : UnitBaseBlock)
  268. {  
  269.    shapeFile = "~/data/shapes/animated/orc/snow/orc2.dts";
  270.    RTSUnitTypeName = "orcFarm";
  271.    boundingBox = "10.0 10.0 3.0";
  272.    Collects = "Food";
  273.    maxDamage = 500;
  274. };
  275.  
  276. datablock RTSUnitData(orcFoundryBlock : UnitBaseBlock)
  277. {  
  278.    shapeFile = "~/data/shapes/animated/orc/snow/orc3.dts";
  279.    RTSUnitTypeName = "orcFoundry";
  280.    boundingBox = "10.0 10.0 3.0";
  281.    Collects = "Stone";
  282.    maxDamage = 800;
  283. };
  284.  
  285. datablock RTSUnitData(orcTownCenterBlock : UnitBaseBlock)
  286. {
  287.    shapeFile = "~/data/shapes/animated/orc/snow/orc5.dts";
  288.    RTSUnitTypeName = "orcTownCenter";
  289.    boundingBox = "10.0 10.0 3.0";
  290.    maxDamage = 1000;
  291. };
  292.  
  293. function RTSUnit::initBuildingActions(%this)
  294. {
  295.   // handler to hook in functionality of a building from it's base infoset
  296.   echo("RTSUnit::initBuildingActions");
  297.   %buildingIndex = getBuildingIndexFromDataBlockName(%this.getDataBlock().getName());
  298.   %actionList = $Buildings::ActionType[%buildingIndex,0]; // note hardcoded 0, building levels not implemented
  299.   %actionType = getWord(%actionList, 0); // grab the first word in the info string, it's what the building does
  300.   echo("RTSUnit::initBuildingActions--index is (" @ %buildingIndex @
  301.       ") actionList is (" @ %actionList @ ") Action type is (" @ %actionType @ ")");
  302.  
  303.   switch$(%actionType)  
  304.   {
  305.     case "Shop" : // not implemented
  306.       return;
  307.     case "Supply":
  308.       %suppliesString = getWords(%actionList, 1); // grab everything after the type
  309.       // parse it out
  310.       for (%supplyIndex = 0; 1 ; %supplyIndex++)
  311.       {
  312.        
  313.         %supplyToAdd = getWords(%suppliesString, %supplyIndex * 3,
  314.                                (%supplyIndex *3) + 1);
  315.         echo("inside action type, parsing type (" @ %actionType @ "), supplies to add (" @
  316.              %supplyToAdd @ ")");
  317.         if (%supplyToAdd $= "")
  318.           break; // no more supplies to parse
  319.         %supplyScheduleTime = getWord(%suppliesString, (%supplyIndex * 3) + 2);
  320.         repeatingAddSupplies(%this, %this, (%supplyScheduleTime * 1000), "LOCAL",
  321.         "ScheduledRepeatingSupplyAdd", %supplyToAdd, "true");
  322.       }
  323.     case "Train": // functionality is implemented via several client side
  324.                   // and server side commandToClient/commandToServer calls.
  325.                   // would be best to re-factor and implement here if possible.
  326.       return;
  327.   } // end switch
  328. }  
  329.  
  330. // JY -- function to place building w/o regard to costs - used when loading mission,etc
  331. function serverCmdPlantBuilding(%conn, %transform, %data)
  332. {
  333.    echo("serverCmdPlantBuilding--client(" @ %conn @ ") building at " SPC %transform );  
  334.    // JY - skip all the checks
  335.    
  336.    %b = new RTSBuilding()
  337.    {
  338.       datablock = %data;
  339.       scale = "1 1 1";
  340.    };
  341.    %b.setTransform( %transform );
  342.    
  343.    %b.client = %conn;
  344.    %b.setTeam(%conn.getTeam());
  345.    %b.setControllingConnection(%conn);
  346.    %conn.buildings.add(%b);
  347.    
  348. //   %b.playRootAnimation();
  349.    %b.setActionThread( "idle" ); // skip the build stage for these buildings
  350. //   %b.schedule(10000, "setActionThread", "idle"); // 10s delay for it to be built -
  351.    
  352.   echo("serverCmdPlaceBuilding()--building (" @ %b @ "):");
  353.   %b.initBuildingActions();
  354. }
  355. // -------------------------------------
  356. //     %b.spawnAI = %b.schedule(4000,"AiTrain",4,%conn); // random build unit ?
  357. function RTSUnit::AiTrain(%this,%index,%conn)
  358. {
  359.    echo("\c9 AiTrain -- enter ... " @ %this.getClassName @ " ," @ %this.getId() SPC "team=" @ %this.getTeam() );
  360.    %state = %this.getState();
  361.    
  362.    // cancel any pending spawns if building is destroyed
  363.    if ( (%state $= "Dead") || (! isObject(%this)) )
  364.       {
  365.          echo("\c9 AI building destroyed - canceling its build cycle");
  366.          cancel(%this.spawnAI);
  367.          return;
  368.       }  
  369.    
  370.    // location to build unit
  371.    %location = %this.getId().getTransform();
  372.    %startX = getWord(%location, 0);              
  373.    %startY = getWord(%location, 1);  
  374.    %location = %startX SPC %startY SPC "250" SPC "0 0" SPC %conn.getTeam();
  375.        
  376.    // build the unit
  377.    %conn.createPlayer(%location, %index);  
  378.    MapHud.createPingEvent(%startX SPC %startY, "1 0 0"); // red ... DEBUG (JY)
  379.    
  380.    // determine the next unit to spawn
  381.    %newUnit = getRandom(4,6);
  382.    
  383.    // and time to build it
  384.    %buildTime = 10000;
  385.    switch(%newUnit)
  386.    {
  387.       case 4:      
  388.         %buildTime = 4000; // 4s
  389.       case 5:
  390.         %buildTime = 8000; // 8s
  391.       case 6:
  392.         %buildTime = 9000; // 9s
  393.    }
  394.    %buildTime = (%buildTime * 2); // increase the delay a bit, so doesnt spawn so fast -
  395.    
  396.    // create next spawn            
  397.    %this.spawnAI = %this.schedule(%buildTime,"AiTrain",%newUnit,%conn);
  398.    echo("\c9 AiTrain (delay = " @ %buildTime @ "-- exit");
  399. }
  400. // JY -- function to place ENEMY AI buildings w/o regard to costs - used when loading mission,etc
  401. function serverCmdPlaceAiBuilding(%conn, %transform, %data)
  402. {
  403.   echo("serverCmdPlaceAiBuilding--client(" @ %conn @ ") building at " SPC %transform );  
  404.    // JY - skip all the checks
  405.    
  406.   %b = new RTSBuilding()
  407.    {
  408.       datablock = %data;
  409.       scale = "1 1 1";
  410.    };
  411.   %b.setTransform( %transform );
  412.    
  413.   %b.client = %conn;
  414.   %b.setTeam(%conn.getTeam());
  415.   %b.setControllingConnection(%conn);
  416.   %conn.buildings.add(%b);
  417.    
  418.   %b.setActionThread( "idle" ); // skip the build stage for these buildings
  419.    
  420.   echo("serverCmdPlaceAiBuilding()--building (" @ %b SPC %data @ "):");
  421.  //  %b.initBuildingActions();
  422.  // maybe add a .schedule here for AI
  423.  // JY - TO DO -- the schedule can check if bldg alive and spawn a unit if so -- or there could be a queue of sorts w/ array and counter
  424.  if (%data $= "orcBarracksBlock")
  425.  {
  426.     // "fake" the building of units
  427.     %b.spawnAI = %b.schedule(8000,"AiTrain",4,%conn); // 8s
  428.  }
  429. }
  430. // ----------------------------------------------------  
  431. function serverCmdPlaceBuilding(%conn, %store, %transform, %data, %zTweak)
  432. {
  433.   echo("serverCmdPlaceBuilding--client(" @ %conn @ ") building (" @  
  434.        %data @ ")\n transform (" @ %transform @ ") Tweak (" @ %zTweak @ ")");
  435.    //TODO: do some checks to verify that we can place a building here
  436.    
  437.    // first, check to see if we have the right supplies
  438.    if (%store $= "LOCAL")
  439.    {
  440.      %activeStore = %conn.resourceStore;
  441.    }
  442.    else
  443.    {
  444.      %activeStore = %store;
  445.    }
  446.    
  447.    %requiredSupplies = $Buildings::requiredBuildSupplies[(getBuildingIndexFromDataBlockName( %data )),0];
  448.    %requestId = $Buildings::requestId[(getBuildingIndexFromDataBlockName( %data )),0];
  449.    
  450.    %authString = resourceStore::requestSpendSupplies(%conn,
  451.                                                      %activeStore,
  452.                                                      %requestId,
  453.                                                      %requiredSupplies,
  454.                                                      "false");
  455.  
  456.    %successStatus = getWord(%authString,0);
  457.    if (%successStatus $= "DENY")
  458.    {
  459. //      echo("serverCmdPlaceBuilding--request by (" @ %conn @ ")"  SPC getWord(%authString, 1) SPC "DENIED");
  460.      messageClient(%conn, 'MsgPurchaseDenied', "", "Cannot place Building! missing:" SPC getWords(%authString, 2) );
  461.     return;
  462.   }
  463.   else
  464.   {
  465. //      echo("serverCmdPlaceBuilding--request by" SPC %client SPC getWord(%authString, 1) SPC "APPROVED");
  466.   }
  467.    %b = new RTSBuilding()
  468.    {
  469.       datablock = %data;
  470.       scale = "1 1 1";
  471.    };
  472.    %b.setTransform( %transform );
  473.    
  474.    %b.client = %conn;
  475.    %b.setTeam(%conn.getTeam());
  476.    %b.setControllingConnection(%conn);
  477.    %conn.buildings.add(%b);
  478.    
  479. //   %b.playRootAnimation();    
  480.    %b.setActionThread( "Create" );
  481.    %b.schedule(10000, "setActionThread", "idle"); // 10s delay for it to be built -
  482.    // JY -- PLAY BUILD SOUND here
  483.    serverPlay3D(buildSound,%transform);
  484.    
  485.    // Note: using schedule is a very basic example. In a real game, adding the supply
  486.    // to the store would probably be dependent on some form of game event, like
  487.    // a working returning a load of carried supplies to a player's building
  488.    // for example, how the villagers work. As you increase your building's capabilities,
  489.    // most (if not all) will not be supply type buildings.
  490.   echo("serverCmdPlaceBuilding()--building (" @ %b @ "):");
  491.   %b.initBuildingActions();
  492. }
  493.  
  494. function serverCmdQueueTrainUnit(%client, %store, %unitType)
  495. {
  496.  
  497.     echo("serverCmdQueueTrainUnit--request by (" @ %client @ ") unit type"  SPC %unitType);
  498.  
  499.  
  500.   // first, see if we have a good building selected
  501.   if (%client.selection.getCount() > 1)
  502.   {
  503.     error("serverCmdQueueTrainUnit--client (" @ %client @ ") had more than one selection in selection group!");
  504.     return;
  505.   }
  506.   %activeBuilding = %client.selection.getObject(0);
  507.    
  508.    // second, check to see if we have the right supplies
  509.    switch$(%unitType)
  510.    {
  511.      case "0" :
  512.        %requiredSupplies = "Gold 170 Food 225 Stone 100 Wood 5";
  513.        %requestId = "QueueTrainWarrior";
  514.        %trainDuration = 10000;
  515.      case "1" :
  516.        %requiredSupplies = "Gold 105 Food 200 Stone 120 Wood 5";
  517.        %requestId = "QueueTrainKnightress";
  518.        %trainDuration = 8600;
  519.      case "2" :
  520.        %requiredSupplies = "Gold 90 Food 110 Stone 50 Wood 130";
  521.        %requestId = "QueueTrainArcher";
  522.        %trainDuration = 7600;
  523.      case "3" :
  524.        %requiredSupplies = "Gold 45 Food 90 Stone 20";
  525.        %requestId = "QueueTrainVillager";
  526.        %trainDuration = 3100;
  527.    }
  528.      
  529.    if (%store $= "LOCAL")
  530.    {
  531.      %activeStore = %client.resourceStore;
  532.    }
  533.    else
  534.    {
  535.      %activeStore = %store;
  536.    }
  537.    %authString = resourceStore::requestSpendSupplies(%client,
  538.                                                      %activeStore,
  539.                                                      %requestId,
  540.                                                      %requiredSupplies,
  541.                                                      "false");
  542.  
  543.    %successStatus = getWord(%authString,0);
  544.    if (%successStatus $= "DENY")
  545.    {
  546. //      echo("serverCmdPlaceBuilding--request by (" @ %conn @ ")"  SPC getWord(%authString, 1) SPC "DENIED");
  547.      messageClient(%client, 'MsgPurchaseDenied', "", "Cannot train unit! missing:" SPC getWords(%authString, 2) );
  548.      commandToClient(%client, 'BuildUnitDenied', %client.getGhostId(%activeBuilding) );
  549.  
  550.     return;
  551.   }
  552.   else
  553.   {
  554. //      echo("serverCmdPlaceBuilding--request by" SPC %client SPC getWord(%authString, 1) SPC "APPROVED");
  555.   }
  556.  
  557.   %activeBuilding.TrainUnitEventId = schedule(%trainDuration,
  558.                                      %activeBuilding,
  559.                                      "trainUnitDurationComplete",
  560.                                        %client, %activeBuilding, %unitType);
  561.   echo("serverCmdQueueTrainUnit--queued up unit type" SPC %unitType SPC
  562.        "at builiding (" @ %activeBuilding @ ") for client (" @ %client @ "), duration" @
  563.        %trainDuration );
  564.   commandToClient(%client, 'InitBuildMenuStatusBar', %client.getGhostId(%activeBuilding),
  565.                    %trainDuration );
  566. }
  567.  
  568. function trainUnitDurationComplete(%client, %building, %unitType)
  569. {
  570. //  echo("trainUnitDurationComplete--request by (" @ %client @ ") unit type" SPC
  571. //     %unitType SPC "placing near building (" @ %building @ ")");
  572.  
  573.   // ok, unit is paid for, training time is complete, let's hand 'em over!
  574.   // figure out a spawn point here.
  575.   %spawnCenter = %building.getPosition();
  576.   %spawnOffset = "-8 -8 0";
  577.   %spawnPoint = VectorAdd( %spawnCenter, %spawnOffset);
  578.   %client.createPlayer( getWords(%spawnPoint,0,2), %unitType);
  579.   messageClient(%client, 'MsgUnitComplete', "", %spawnPoint);
  580.   commandToClient(%client, 'BuildUnitComplete', %client.getGhostId(%building) );
  581. }