Page 5 of 26

Re: Dynamic Script Usage

PostPosted: 14 Apr 2013, 18:08
by Ben
I guess that my question got lost:
One more thing: My tower-limiting script doesn't actually work because players can still place as many tower plans as they want before the towers are actually built. However ,there is no state that detects how many houseplans are laid out, so I can't figure out a way around this...
Is there a way to fix this?

Re: Dynamic Script Usage

PostPosted: 14 Apr 2013, 19:29
by Siegfried
It would be cool to support classes, maybe one day ;) Right now we haven't seen any suggestions that really need classes... but as people's ideas get more and more complicated maybe we will.
Mhm ... I will write you an email ;)

Re: Dynamic Script Usage

PostPosted: 15 Apr 2013, 04:58
by Krom
I guess that my question got lost:
One more thing: My tower-limiting script doesn't actually work because players can still place as many tower plans as they want before the towers are actually built. However ,there is no state that detects how many houseplans are laid out, so I can't figure out a way around this...
Is there a way to fix this?
Added to our todo list. Hopefully it will be included in next RC version.

Re: Dynamic Script Usage

PostPosted: 15 Apr 2013, 16:10
by Tef
Another one: ObjectInArea. Whether it is a unit or building, I'd like to be able to have a state that checks if something is in a certain spot.
(By Ben from the ideas topic)

I was also interested in this and made a script that checks the presence of any (of your own) military units in a defined square. Currently, this script is only usable for one player (single player) but could easily be extended. Just plop this function on top of your script
  Code:
function MilitaryUnitZoneTrigger(left: Integer; top: Integer; right: Integer; bottom: Integer): Boolean; var // define local constants ACTIVE_PLAYER,LOW_UNIT_NR,HIGH_UNIT_NR: Integer; // define local variables MilitaryUnitsAlive, MilitaryUnitsCounted, UnitsCounter, MilitaryType: Integer; begin // initialize local constants ACTIVE_PLAYER := 0; LOW_UNIT_NR := 14; // from militia up to... HIGH_UNIT_NR := 27; // ...vagabond //initialize local variables MilitaryUnitsAlive := 0; MilitaryUnitsCounted := 0; // calculate the number of living soldiers for human player for MilitaryType := LOW_UNIT_NR to HIGH_UNIT_NR do begin MilitaryUnitsAlive := MilitaryUnitsAlive + States.StatUnitTypeCount(ACTIVE_PLAYER,MilitaryType); end; for UnitsCounter := 0 to 999 do begin //escape loop if the number of evaluated units equals all units if MilitaryUnitsCounted = MilitaryUnitsAlive then break; // only add as counted unit if it is military and from human player // otherwise, go to next iteration if (States.UnitOwner(UnitsCounter) = ACTIVE_PLAYER) AND (States.UnitType(UnitsCounter) >= LOW_UNIT_NR) AND (States.UnitType(UnitsCounter) <= HIGH_UNIT_NR) then MilitaryUnitsCounted := MilitaryUnitsCounted + 1 else continue; // if unit is human and military, perform this check: // if unit is in zone (left, top, right, bottom) then return TRUE if (States.UnitPositionX(UnitsCounter) >= left) AND (States.UnitPositionX(UnitsCounter) <= right) AND (States.UnitPositionY(UnitsCounter) >= top) AND (States.UnitPositionY(UnitsCounter) <= bottom) then Result := true; end; end;
So far the function that checks for soldiers. To actually use this function, let's say once every second for soldiers you could use the script below. It also makes something happen once if a military unit has been found. Note that the zone coordinates as left-top-right-bottom are 10-15-20-25:
  Code:
var MilitaryUnitHasEnteredZone: boolean; procedure OnTick; begin if (States.GameTime mod 10 = 0) AND (MilitaryUnitHasEnteredZone = false) then begin if MilitaryUnitZoneTrigger(10,15,20,25) then begin action 1 action 2 action n MilitaryUnitHasEnteredZone := true; end; end; end;
I'm sure this script can be made better / more efficient. I welcome any improvements!

Re: Dynamic Script Usage

PostPosted: 15 Apr 2013, 16:47
by Lewin
@Tef: It's great that you wrote that script, we're happy to see people writing and sharing useful procedures/functions. However there are some issues with it. It's bad practice to rely on something like:
for UnitsCounter := 0 to 999 do begin
It's true that unit/house IDs are assigned sequentially, but it's quite likely that there will be more than 999 in big games, since IDs are never reused so every new unit or house that is created takes up another ID. Sure you could increase it to 9999 or 99999 but that will start to become noticeable performance wise.

We realise that there is currently no correct way to gain access to all of the players units/houses/groups, which is something that you may want to do in certain scripts such as yours. Because of this we are planned to add functions which give you an array of IDs of every unit which is alive. It could have a parameter for player (-1 for any player) and another which is a set of what unit types you are interested in (which is good because we need to introduce people about the beauty of sets in Pascal somehow :P). We'd also make versions of these for houses/groups. So that part of your script could change to something like this:
  Code:
var AllTheUnits: array of Integer; ... MilitaryUnits := States.GetUnits(ACTIVE_PLAYER, [14..27]); for I := 0 to Length(MilitaryUnits)-1 do begin if (States.UnitPositionX(MilitaryUnits[I]) >= left) AND ...
Note that this is just the plan and it might change, but we certainly want to provide an alternative to iterating from 0 to 999 and hoping the game doesn't last long enough to exceed 999 units/houses created.

I also have some other feedback which might help you:
You can replace "(MilitaryUnitHasEnteredZone = false)" with "not MilitaryUnitHasEnteredZone". If you're more comfortable with the first version there's nothing wrong with that, I just wanted to let you know that both options exist.

Constants: Use "const" instead of "var", like this:
  Code:
function MilitaryUnitZoneTrigger(left: Integer; top: Integer; right: Integer; bottom: Integer): Boolean; const // define local constants ACTIVE_PLAYER = 0; LOW_UNIT_NR = 14; HIGH_UNIT_NR = 27; // define local variables var MilitaryUnitsAlive, MilitaryUnitsCounted, UnitsCounter, MilitaryType: Integer; begin
There's also another way you could implement your script Run States.UnitAt() on every tile within the defined area, using for loops like this:
  Code:
for X:=Left to Right do for Y:=Top to Bottom do begin U := States.UnitAt(X,Y); ... end;
This will work quite well for small areas, that's how I did the king of the hill style maps. States.UnitAt is actually quite efficient because we have a reverse lookup to tell us which unit is standing on a certain tile (so it doesn't need to check whether every single unit in the game to see if it's standing there) On large areas the performance will be pretty bad because the number of tiles to be checked goes up exponentially (e.g. on a 100 by 100 area there's 100,000 tiles to be checked, not good).

I hope this helps, and thanks for posting your script here to help others :)

Re: Dynamic Script Usage

PostPosted: 15 Apr 2013, 17:06
by Tef
@ Lewin: Actually, I cannot get const to work within a function, because something like this...
  Code:
function Something (somevar: integer) : boolean; const SOME_CONSTANT = 0; var SomeVariable: Integer begin end;
...gives me an error in Kam Remake at the line in which const is placed. The error: 'BEGIN expected'. Const only seems to work for global constants, i.e. if used outside of functions? But thanks for the feedback and nice to hear that there will come something to avoid those nasty long iterations!

Re: Dynamic Script Usage

PostPosted: 17 Apr 2013, 01:03
by The Dark Lord
Hello, I'm having problems again. I'm trying to display a countdown to show players when the next attack will be.

I'm using these variables:
  Code:
TimeNextAttack: Integer; ShowTime: Boolean;
These are the settings OnStart:
  Code:
TimeNextAttack := 2400 ShowTime:= True
And this the procedure to make it work:
  Code:
procedure ShowTimeUntilNextAttack; var I, Minutes, Seconds: Integer; begin if (ShowTime = True) then begin ShowTime := False; Minutes := TimeNextAttack div 600; Seconds := (TimeNextAttack div 10) mod 60; for I := 0 to 3 do Actions.SetOverlayText(I, States.TextFormatted(20, [Minutes, Seconds])); end; end;
After OnTick, I placed this:
  Code:
ShowTimeUntilNextAttack;
And at the same time when the first attack is launched:
  Code:
TimeNextAttack := 4200 ShowTime:= True ShowTimeUntilNextAttack;
As you can see I, ehm, made grateful use of Annie's Hill's script. :P Anyway, The text does appear on my screen ("The next attack will arrive in 04:00"), but it doesn't count down, it just stays at 04:00. After four minutes, it changes to 07:00 as supposed to but it doesn't count down either (not that I expected it to). My guess would be that my condition is faulty and it somehow loops the script, thereby showing 04:00 or 07:00 the whole time? Or is something missing?

Then two more things to complicate things:
- I'd like to have the countdown in white, just like normal text; until the last minute, where '01:00' and onwards should be red. How can I do this? Would something like
  Code:
if (Time = 600) then begin Actions.SetOverlayText(I, States.TextFormatted(21, [Minutes, Seconds])); end;
work?
- And a larger problem: This countdown interferes with my previous 'Player has defeated the first attack wave' messages. Is it even possible to have both working correctly?

Re: Dynamic Script Usage

PostPosted: 17 Apr 2013, 01:47
by Lewin
@tef: Sorry about that, it must be a limitation of the script compiler. In Delphi/Lazarus you can have local constants, that's why I suggested it.

@TDL: The reason why it doesn't count down is because you set TimeNextAttack := 4200, then every tick you display TimeNextAttack to the player. Where is the countdown code? You need to decrease TimeNextAttack every tick otherwise it will always stay at 4200. Here's the code I would use:
  Code:
procedure ShowTimeUntilNextAttack; var I, Minutes, Seconds, MessageID: Integer; begin //Convert to minutes and seconds Minutes := TimeNextAttack div 600; Seconds := (TimeNextAttack div 10) mod 60; //Decide on the colour if TimeNextAttack < 600 then MessageID := 21 //Red for the last 60 seconds else MessageID := 20; //Otherwise white //Show to the players for I := 0 to 3 do Actions.SetOverlayText(I, States.TextFormatted(MessageID, [Minutes, Seconds])); end;
And in OnTick put this:
  Code:
if TimeNextAttack > 0 then begin ShowTimeUntilNextAttack; TimeNextAttack := TimeNextAttack - 1; end;
Then you don't need ShowTime at all. When you want to start the timer you set:
TimeNextAttack := 2400
And it will countdown, stopping when it reaches 0.

Now to the bigger problem, showing multiple things on the overlay text. I'd do something like this:
1. Change ShowTimeUntilNextAttack to be a function which returns AnsiString:
  Code:
function ShowTimeUntilNextAttack: AnsiString; var I, Minutes, Seconds, MessageID: Integer; begin //Convert to minutes and seconds Minutes := TimeNextAttack div 600; Seconds := (TimeNextAttack div 10) mod 60; //Decide on the colour if TimeNextAttack < 600 then MessageID := 21 //Red for the last 60 seconds else MessageID := 20; //Otherwise white //Return the message Result := States.TextFormatted(MessageID, [Minutes, Seconds]); end;
2. Your message for showing when someone has defeated a wave should also be a function returning AnsiString, something like this:
  Code:
var PopupShowTime, PopupPlayer: Integer; procedure GetOverlayPopup: AnsiString; begin //If we haven't reached the time to hide the popup yet, show it if PopupHideTime > States.GameTime then Result := States.TextFormatted(29, [States.PlayerName(PopupPlayer)])) else Result := ''; //No popup message end;
To start a popup message use this:
  Code:
PopupHideTime := States.GameTime+DELAY; //Time when the popup will get hidden (now + delay) PopupPlayer := 2; //Will show player 2's name in the popup
3. To combine these things, put something like this in OnTick:
  Code:
procedure OnTick; var OverlayMessage: AnsiString; begin OverlayMessage := ''; if TimeNextAttack > 0 then begin OverlayMessage := OverlayMessage + ShowTimeUntilNextAttack; TimeNextAttack := TimeNextAttack - 1; end; OverlayMessage := OverlayMessage + '|' + GetOverlayPopup; for I:=0 to 3 do Actions.SetOverlayText(I, OverlayMessage); end;
Does that all make sense? I hope you can understand my code. Basically it generates each message separately (countdown + popup) then combines them in OnTick and displays them to the player. Let me know if you have any questions.
Cheers,
Lewin.

Re: Dynamic Script Usage

PostPosted: 17 Apr 2013, 12:32
by The Dark Lord
I understand most of it; what I don't really understand is this part:
  Code:
procedure OnTick; begin OverlayMessage := OverlayMessage + ShowTimeUntilNextAttack; TimeNextAttack := TimeNextAttack - 1; end;
Meanwhile I have 2 new questions:

1. I have no idea how to make text red. I thought it wouldn't be really hard but that Delphi page about Format doesn't really explain it.
2. I was thinking it would actually be better to have a recruit in the tower and provide 5 stones at the start, so you lose a bit less quickly. However, once the recruit gets hungry he will be useless. Can I make his condition 'infinite', so he never gets hungry? :P
Edit: never mind, I found out about UnitHungerSet. :)
Edit 2: my script didn't work (the game froze and crashed) but I fixed it already. :) Everything now works as it should.

Re: Dynamic Script Usage

PostPosted: 17 Apr 2013, 15:13
by Lewin
I understand most of it; what I don't really understand is this part:
  Code:
procedure OnTick; begin OverlayMessage := OverlayMessage + ShowTimeUntilNextAttack; TimeNextAttack := TimeNextAttack - 1; end;
OverlayMessage is a local string (text) variable. "+" is used here to join two strings together. The first string is whatever we currently have in OverlayMessage (blank), and the second is the result of the function ShowTimeUntilNextAttack. So basically we get the result of that function and place it on the end of our variable OverlayMessage.

The second line decreases TimeNextAttack by 1. So if it was equal to 100, it will be equal to 99 after that line. This means the countdown timer actually counts down (decreases every tick)
1. I have no idea how to make text red. I thought it wouldn't be really hard but that Delphi page about Format doesn't really explain it.
We have our own markup for text colour:
[$0000FF]This text is red![] This text is not.
So put [$0000FF] around the bit of text that needs to be red. You can even do it in the code, something like this:
'[$0000FF]'+States.Text(1)+'[]'
Once again "+" is used to join strings.

Re: Dynamic Script Usage

PostPosted: 17 Apr 2013, 15:39
by The Dark Lord
Ah, now I understand it. :D
And the colouring works beautifully, thanks. I'm getting more and more excited about this. :P

Re: Dynamic Script Usage

PostPosted: 18 Apr 2013, 16:11
by Tef
Suppose I have this:

FancyArray: Array [1..10] of integer;

I know how to fill FancyArray one by one, or with a for loop. I also know how to access values from it. But I'd like to know what I can do more with arrays in dynamic scripts for Kam Remake. Could any expert here tell me how to do the following, if possible? (preferably with a built-in funcion!)
1 - is there a built-in function to get the lowest, or highest value out of FancyArray?
2 - is there a built-in function to sort FancyArray, ascending or descending?
3 - can I read a value at place n and then move all values higher than n to n-1. Example: read FancyArray[5] and move FancyArray[6] up to FancyArray[10] to position FancyArray[5] up to FancyArray[9]
4 - can I dynamically increase the length of FancyArray? Example: let FancyArray become an Array [1..20].
5 - can I dynamically create FancyArray2 (so not declaring beforehand under a 'var' section)
6 - any other useful things I could do with FancyArray that you know of?

I'm asking because documentation on the Internet isn't really helpful for me regarding array manipulation. Thanks in advance!

Re: Dynamic Script Usage

PostPosted: 18 Apr 2013, 16:20
by Krom
Suppose I have this:

FancyArray: Array [1..10] of integer;

I know how to fill FancyArray one by one, or with a for loop. I also know how to access values from it. But I'd like to know what I can do more with arrays in dynamic scripts for Kam Remake. Could any expert here tell me how to do the following, if possible? (preferably with a built-in funcion!)
1 - is there a built-in function to get the lowest, or highest value out of FancyArray?
2 - is there a built-in function to sort FancyArray, ascending or descending?
3 - can I read a value at place n and then move all values higher than n to n-1. Example: read FancyArray[5] and move FancyArray[6] up to FancyArray[10] to position FancyArray[5] up to FancyArray[9]
4 - can I dynamically increase the length of FancyArray? Example: let FancyArray become an Array [1..20].
5 - can I dynamically create FancyArray2 (so not declaring beforehand under a 'var' section)
6 - any other useful things I could do with FancyArray that you know of?

I'm asking because documentation on the Internet isn't really helpful for me regarding array manipulation. Thanks in advance!

Hi there,
I'll reply from Delphi standpoint. PascalScript may be a little more limiting:

1. Low(Arr) and High(Arr). Length(Arr) returns length
2. no, only manually
3. no, only manually
4. SetLength(Arr, new_length) or SetLength(Arr, Length(Arr) + 32) will grow or shrink the array; new elements are set to 0
5. yes, but it will have to start from 0, where arrays declared in VAR can start from any number. SetLength(Arr, new_length);
6. e.g. you can declare in VAR array [boolean] of Integer; arrays can start from any number. array [8..16] of string; maybe something else I forgot

Re: Dynamic Script Usage

PostPosted: 18 Apr 2013, 16:32
by Lewin
The functions Krom suggested Low(Arr) and High(Arr) get the first and last elements of the array (so with FancyArray they will give 1 and 10). I think you might have meant the maximum and minimum values, that's something you have to write yourself.

Something you might not be aware of: Pascal has two types of arrays, dynamic and static:
Static array:
- Declaration: FancyArray: array[1..10] of Integer;
- Changing size is not possible
- Slightly more efficient for the compiler

Dynamic:
- Declaration: FancyArray: array of Integer;
- Change size with SetLength(FancyArray, NewLength);
- First element is always 0, last is Length(FancyArray)-1

Currently there are some issues with using arrays in global variables, only static arrays are supported. I've recently improved the saving/loading of global variables so it now supports dynamic arrays and some other cool stuff like multidimensional arrays, sets, enums and records.

Do you know about multidimensional arrays? FancyArray: array[1..10] of array[1..10] of Integer. This gives you a 2 dimensional array (a 10x10 grid or matrix). You can access an element with FancyArray[1][2]. These also cannot be used in global variables at the moment, but I've fixed them. You can also have dynamic multidimensional arrays.

Re: Dynamic Script Usage

PostPosted: 19 Apr 2013, 00:59
by Ben
I'm having another problem:

I'm trying to set the hunger for a soldier using the UnitHungerSet command. However, I'm having trouble getting the command to work. I think it is because I am not getting the correct ID for the soldier. I tried using a variable to set the soldier with, and using it later as the ID, but this isn't working.
  Code:
var Spawner: Integer; procedure OnMissionStart; begin Spawner := Actions.GiveGroup(0, 22, 15, 45, 4, 1, 1); end; procedure OnTick; begin if (States.GameTime mod 30 = 0) then Actions.UnitHungerSet(Spawner, 60); end;