Menu

Show posts

This section allows you to view all posts made by this member. Note that you can only see posts made in areas you currently have access to.

Show posts Menu

Messages - Khris

#481
@cat Finally made a module thread :)

If the 1:2 thing turns out to be an issue for more users I will look into addressing it.
#482
In a basic BASS / Sierra game, start a non-blocking walk toward something when interacting with it that can be canceled by the player at any time.

Download: GotThere.scm

The module provides a single function:

bool WalkFace(int x, int y, CharacterDirection dir)

Use it like this:
Code: ags
function hSomeHotspot_Interact() {
  if (WalkFace(123, 45, eDirectionLeft)) {
    // what happens when the player reaches those coordinates goes in here
  }
}
#483
That code needs to go inside the repeatedly_execute function, you cannot paste it directly into a script.
It also requires the existence of a GUI called gLocationName that has a label as its first control (i.e. the ID is 0)
#484
You can automate this almost completely:

1. create a custom bool property (like "walkbehind_fade") and set it to true for all relevant objects

2. call this to your global rep_exec:

Code: ags
void HandleFadingObjects() {
  for (int i = 0; i < Room.ObjectCount; i++) {
    if (!object[i].GetProperty("walkbehind_fade")) continue; // skip other objects
    int dy = object[i].Y - player.y;
    if (dy < 0) continue; // player is in front of object
    int hw = Game.SpriteWidth[object[i].Graphic] / 2;
    int dx = object[i].X - player.x;
    if (dx < -hw || dx > hw) continue; // player not behind horizontally
    if (dx < 0) dx = -dx;
    object[i].Transparency = (hw - dx) * 100 / hw;
  }
}

Not tested!
#485
Exactly, for example you have this
Code: ags
// header
AudioChannel* channel[5];

Change that to
Code: ags
// header
import AudioChannel* channel[5];

// script
AudioChannel* channel[5]; export channel; // exporting requires just the name

Same for all other variables.


On that note, the global script header says:
Code: ags
// Main header script - this will be included into every script in
// the game (local and global). Do not place functions here; rather,
// place import definitions and #define names here to be used by all
// scripts.
Maybe we could change that message to explicitly state not to declare variables here either?
#486
You said you don't really understand what SetTimer does, so excuse me for mentioning IsTimerExpired :P

Anyway, glad we could be of help :)
#487
AGS calls the function when a key is pressed, and passes along the keycode. Thus you can use this function to react to keypresses by checking k (or whatever else you called the variable in the declaration) against various keycodes to handle specific keys.

The bucket has three positions, so I'm using an int to store the position and make the key handling code alter the int between 0, 1 and 2.

The ++ and -- operators are shorter versions of adding / subtracting one, yes.

The last line calculates the new position (not movement) by multiplying the 0/1/2 int by 70 and adding 500.

Not sure why you're asking how the bucket goes to the left; if it's in the center at x = 570, pressing the left arrow key will set the position to 0, then set the bucket's x coordinate to 500 + 0 * 70 i.e. 500.

To be fair, the function should look like this:
Code: ags
function on_key_press(eKeyCode k) {
  if (k == eKeyLeftArrow && pos > 0) {
    ClaimEvent();
    pos--;
    oBucket.X = 500 + pos * 70;
  }
  if (k == eKeyRightArrow && pos < 2) {
    ClaimEvent();
    pos++;
    oBucket.X = 500 + pos * 70;
  }
  // handle other keys
}
#488
SetTimer() on its own doesn't really do anything, you also need to call IsTimerExpired() in a repeatedly_execute function.

If you want to chain multiple non-blocking animations, and have them run periodically, you might want to use a different approach.
One way is to simply count frames yourself:

Code: ags
int frames = 0;

bool AfterEvery(int after, int every) {
  int gs = GetGameSpeed();
  return (frames - gs * after) % (gs * every) == 0;
}

  // inside room_RepExec
  frames++;
  if (frames == GetGameSpeed() * 10000) frames = 0;

  if (AfterEvery(0, 5)) {
    // do something after 5, 10, 15, etc. seconds
  }
  if (AfterEvery(2, 5)) {
    // do something after 7, 12, 17, etc. seconds
  }

Untested!
#489
It should work if you use timers, like this:

Code: ags
  // inside some function
  cNpc1.Animate(...);
  SetTimer(1, GetGameSpeed() * 5); // five seconds from now

  // inside room_RepExec()
  if (IsTimerExpired(1)) cNpc2.Animate(...);

Is this not working for you? If so, can you show your code and tell us exactly what's happening instead?
#490
Right, that makes sense.
You can also draw the messages on a dynamic sprite using DrawingSurface.DrawStringWrapped(), then crop the sprite to a certain height and put it on a non-clickable button.
#491
Why not use on_key_press instead?
You can just add the function to the room script and AGS will call it before the global one.

Code: ags
int pos = 1; // 0, 1 or 2

function on_key_press(eKeyCode k) {
  if (k == eKeyLeftArrow && pos > 0) pos--;
  if (k == eKeyRightArrow && pos < 2) pos++;
  oBucket.X = 500 + pos * 70;
}
#492
You can use a string array to store text.
Code: ags
// above functions that need it
String npc_text[3]; // show last three lines of dialogue

  // inside a function where line is said
  storage += "[" + npc_text[0];
  npc_text[0] = npc_text[1];
  npc_text[1] = npc_text[2];
  npc_text[2] = line;

  lblNpc.Text = npc_text[0] + "[" + npc_text[1] + "[" + npc_text[2];
#493
Side note: unless there's a third state, you can use a bool variable to store the state of a light.
Code: ags
bool lampLit = false;

  // turn on
  lampLit = true;

  // check
  if (lampLit) ...

I've also noticed that your TurnOn function doesn't seem to use the light parameter. Since you're passing in the overlay that gets toggled but then simply ignore it, that's probably where the issue arises.

Tbh I'd change the approach entirely. I'd use a function that redraws the entire room back to front once a bool changes and simply call that when a switch occurs.
#494
Quote from: Custerly on Wed 13/12/2023 02:35:24why does it require the first parameter (dgOpt1) when I've already stated [if (keycode == eKey1) dgOpt1_OnClick...]? That first parameter doesn't seem to actually serve any purpose.

The control/button is passed along because you can use a single function to handle multiple events to avoid duplicate code.
If I were to code this I'd call the function "dgOpt_OnClick", then paste this name into each dialog button's event table. This way, all button clicks cause the same function to run, and I can now use the "control" parameter to find out which button was clicked:

Code: ags
  if (control == dgOpt1) ...
  else if (control == dgOpt2) ...
  ...

Since controls are numbered, I can even skip that step and use the ID directly:
Code: ags
  int choice = control.ID;


Regarding the height of the label:
AGS has a function called GetTextHeight.

Assuming the labels have IDs 0, 2, 4, ... and the respective buttons have IDs 1, 3, 5, ...:

Code: ags
  int y = 10;
  for (int i = 0; i < 5; i++) {
    Label* l = gDialog.Controls[i * 2].AsLabel;
    Button * b = gDialog.Controls[i * 2 + 1].AsButton;
    int h = GetTextHeight(l.Text, l.Font, l.Width);
    // resize and position label and button
    l.Height = h;
    l.Y = y;
    b.Height = h;
    b.Y = y;
    y += h + 5; // next button moves down accordingly, plus 5 pixels 
  }
#495
Quote from: Dave Gilbert on Tue 12/12/2023 13:36:38Thanks Khris! One question. It looks like your function plays the animation and THEN counts the frames/ms length. Is there a reason why you played the animation beforehand?

This basically happened on accident, I did the easy thing first (calling the actual Animate command) and later noticed I can leave it up there because it doesn't make a difference :)
The approach changes if you have a blocking animation; in that case you have to move the Animate call outside the counting function anyway.
#496
With the game being parser-based one advantage is that you don't need to be able to click the key, you just have to see it in the room. Which means you don't really need an object or a character, you can simply draw it on an overlay for instance (and those can be created dynamically).

We can also change custom properties at runtime, so each inventory item could get a room_number int property which is changed to 0 once picked up.

Now all you need is a function that updates the room by redrawing / removing the overlays representing dropped items and call it in room_Load and whenever something is dropped or picked up.
#497
I tested this with a basic animation and it seems to work:
Code: ags
// header
import int AnimateFrames(this Character*, int loop, int delay, RepeatStyle repeatStyle = eOnce, BlockingStyle blockingStyle = eBlock, Direction direction = eForwards, int frame = 0, int volume = 100);

// main
int AnimateFrames(this Character*, int loop, int delay, RepeatStyle repeatStyle, BlockingStyle blockingStyle, Direction direction, int frame, int volume) { 
  this.Animate(loop, delay, repeatStyle, blockingStyle, direction, frame, volume);
  int frames = 0;
  // TODO: handle eBackwards
  for (int i = frame; i < Game.GetFrameCountForLoop(this.View, loop); i++) {
    ViewFrame* vf = Game.GetViewFrame(this.View, loop, i);
    frames += delay + vf.Speed + 1;
  }
  return frames;
}

Use like:
Code: ags
  player.LockView(VIEW2);
  int frames = player.AnimateFrames(0, 10, eOnce, eNoBlock, eForwards);

If you need milliseconds, use
Code: ags
  int ms = (frames * 1000) / GetGameSpeed();


(Note: this only makes sense with eOnce and eNoBlock so one could skip these but I simply copied all Animate parameters)
#498
True, but performance isn't really an issue with parser commands.
I avoided else ifs because afaik there's a hard limit on how many you can have, and in the context of parser commands this could quickly become an issue.
OTOH it's probably preferable anyway to start by using "rol" to branch into various other functions.
#499
I'm guessing you're using a text box for the parser input? In this case you should have an ??_OnActivate(...) function in the Global Script (your post implies that processing commands works in general so I assume you got this far).

AGS re-runs this function whenever you hit enter while the textbox is visible, so inside this function is where all the parser command processing goes.

Like this:
Code: ags
function textBoxParser_OnActivate(GUIControl* control) {
  Parser.ParseText(control.Text); // tell AGS to test understood commands against the current textbox content

  if (Parser.Said("quit")) QuitGame(1);
  
  if (Parser.Said("look")) CallRoomScript(1);

  if (Parser.Said("take anyword")) {
    // ... process take command
  }

  if (Parser.Said("drop key")) {
    player.LoseInventory(iKey);
  }
}

Note that I don't use "else if" here because these commands are all different so the parser will never match multiple commands at the same time.

Also note that using the LoseInventory() function is not related to the parser in any way, and you haven't told us which error you got. I assume the issue is what most beginner's trip over: not placing code like that inside a function.
AGS is event based, which means that all commands go inside the various event handler functions.
Putting a command like player.LoseInventory(iKey) directly inside a script wouldn't make any sense because AGS wouldn't know when to run it.

I can see how the if (Parser.Said("drop key")) {...} part might sound like something you can tell AGS to do at the very start of the game, and it will act accordingly as soon as the player has typed the "drop key" command, however that's not how the parser commands work. It's not a setting, it's a test that runs live at the time the command runs. Which means you have to re-do the Parser.Said("drop key") test every time the player has typed something.

(side note: the parser commands allow the use of special keywords like "rol" "anyword", optional words and synonyms. You don't have to use them, you could also simply directly compare whatever the player typed against a bunch of known commands.)



As for dropping the key (which is, again, completely unrelated to your using a parser): inventory items and room objects don't have any relation to each other whatsoever. The fact that both represent the same physical object is not known to AGS (and there's no built-in way to set up that connection).
So your question is based on a wrong assumption. The actual question is: how do I implement a system that allows me to drop items in any room? Since room objects cannot leave their room, you would have to add a key object to every room in theory.
You also cannot create room objects on the fly, so this question is in fact much more complex than it might seem unfortunately.

This post is already way too long so I'll leave it at that for now.
#500
AGS4 will support this iirc but current versions don't.

You'll have to implement this in a different way, for instance using structs. I imagine the dialog functions will all be relatively similar, which means you should be able to do this by making the referenced dialog data dynamic, as opposed to the function itself.

You can for instance use an array of structs, then store the array index in a variable, as opposed to the function name.

Minimal code example:

Code: ags
// header
struct Conversation {
  String options[10];
  bool active[10];
  int option_count;
  import bool AddOption(String text);
  import void Run();
};

// main

Conversation convo[5];

// game_start
  convo[0].AddOption("Hello");

// elsewhere
  convo[current_convo].Run();
SMF spam blocked by CleanTalk