Question

I need to call an event handler to tell a user to select a TCard or TLabel and then return this value as a parameter.

I have two units GAME , SS_SPELL

This is how the code in SS_SPEll works.

// TSPELL
// ======
// The chronology is:
// 1.  At appropriate points in the game (such as before turn, after cast etc aka trigger points)
//     the game calls the SpellMeister's RunSpells method.
// 2.  RunSpells checks the database for spells matching the card that
//     initiated the spell action, and the trigger point at which it did so.
//     For each one that it finds it will create an appropriate object, which
//     could be a TSpell descendent or a TSpellAdjuster descendent.
//     For each TSpell it finds it fires off an onFindSpell event.
//     See below for details of how TSpellAdjusters are handled.
// 3.  The handler for the onFindSpell events can (should) call the spells'
//     AimAt method for each potential target. A potential target is a card
//     or a player.
// 4.  A spell's AimAt method checks if the potential target is a legitimate
//     target for that spell and if so it calls its ApplySpellTo method to
//     actually do the dirty deed.

So what i need is once the RunSpells gets the db info it will check if needs2ndtarget := 1 , if so then i know i need a second target for this spell.

Here is TSpellBase it is the class TSpell is created from. In an attampt to create this event i have added FOnSeek2ndTarget in the privite section and FNeed2Target in the Protected and the public property OnSeek2ndTarget. You will also see the TTargetEvt, currently its setup to recive TCARD but i need it to recive TCard or TLAbel no idea how to do this.

 TTargetEvt = procedure (Card : TCard) of Object;  


 TSpellBase = class
  private
    FOnManaChange: TManaEvt;
    FOnSeek2ndTarget: TTargetEvt;

  protected
    FCardType            : TCardType;
    FOriginator          : TCard;
    FNeed2Target         : integer;

    function  LegitimateTarget    (Candidate : TObject) : boolean; virtual;
  public
    constructor Create; virtual;

    property CardType    : TCardType     read FCardType     write FCardType;    // ctLava, ctNature, ctWizard, etc etc
    property Originator  : TCard         read FOriginator   write FOriginator;

    property Need2Target : integer       read FNeed2Target  write FNeed2Target;
    property OnManaChange  : TManaEvt  read FOnManaChange  write FOnManaChange;
    property OnSeek2ndTarget : TTargetEvt read FonSeek2ndTarget write FOnSeek2ndTarget;
  end;

Now in TSpell , i dont think i need anything chnaged here but its needed for the spellmiester.runspell procedure

TSpell = class(TSpellBase)
  private
  protected
    FCategory    : TCategory;
    FLifeToAdd   : Byte;
    FMaxRandom   : Byte;
    FReplaceDmg  : Byte;
    FReplacement : string;
    FStatTarget  : Byte;
    FTrigger     : TTrigger;
    procedure ApplySpellTo(Target : TObject); virtual; abstract; // Apply the spell to the target
  public
    procedure AimAt(Candidate: TObject); virtual;

    property Category    : TCategory     read FCategory     write FCategory;
    property LifeToAdd   : Byte          read FLifeToAdd    write FLifeToAdd;
    property MaxRandom   : Byte          read FMaxRandom    write FMaxRandom;
    property ReplaceDmg  : Byte          read FReplaceDmg   write FReplaceDmg;
    property Replacement : string        read FReplacement  write FReplacement;
    property StatTarget  : Byte          read FStatTarget   write FStatTarget;
    property Trigger     : TTrigger      read FTrigger      write FTrigger;
  end;

Here i added the FOnSeek2ndTarget in private section. and the property So now when a spell is casted it will get to here and now calls runsspells.

 TSpellMeister = class
  private
    FonFindSpell   : TRcvSpell;
    FOnManaChange  : TManaEvt;
    FOnSeek2ndTarget  : TTargetEvt;
//    FonNewAdjuster : TRcvSpell;
  protected
    FAdjusters : TAdjusters;
    FQuery : TADOQuery;

  public
    constructor Create(DBCon: TADOConnection);
    destructor Destroy; override;

    function IfNull( const Value, Default : OleVariant ) : OleVariant;
    procedure Adjust(Attacker : TCard; Victim : TObject; var TheDamage : integer); overload;
    procedure Adjust(Attacker : TCard;                   var TheCost   : integer); overload;
    procedure RunSpells(Card : TCard; Trigger : TTrigger);

    property onFindSpell   : TRcvSpell read FonFindSpell   write FonFindSpell;
    property OnManaChange  : TManaEvt  read FOnManaChange  write FOnManaChange;
    property OnSeek2ndTarget: TTargetEvt read FOnSeek2ndTarget write FOnSeek2ndTarget;
//    property onNewAdjuster : TRcvSpell read FonNewAdjuster write FonNewAdjuster;
  end;

This is where the issue is, i added foundspell.Need2ndTarget this gets data from database if it is a 1 then it needs the user to select another target for the spell. Currently i added

if FoundSpell.Need2Target = 1 then FOnSeek2ndTarget(Card);

but i am sure that is not correct...

 //**************************************************************************
procedure TSpellMeister.RunSpells(Card: TCard; Trigger: TTrigger);
//**************************************************************************
var
  OneSpell : TSpellBase;
  FoundSpell : TSpell;  // Just so only have to cast once
begin
  assert(assigned(FonFindSpell),'TSpellMeister.RunSpells : No onFindSpell event handler!');
  // Search the database
  FQuery.Active := FALSE;
  FQuery.Parameters.ParamByName(SQL_PARAM_SPELL_ORIGINATOR).Value := Card.CName;
  FQuery.Parameters.ParamByName(SQL_PARAM_SPELL_TRIGGER   ).Value := Trigger;
  FQuery.Active := TRUE;

  // Iterate through the spell records. For each one, create a category-specific
  // TSpell descendant and fire off an onFindSpell event.

  if FQuery.RecordCount > 0 then
  begin
    FQuery.Recordset.MoveFirst;
    while not FQuery.Recordset.EOF do
    begin
      case TCategory(FQuery.Recordset.Fields[DB_FLD_CATEGORY].Value) of
        caAboveLife               : OneSpell := TSpellAboveLife.Create;
        caDamage                  : OneSpell := TSpellDamage.Create;
        caDamagePlus              : OneSpell := TSpellDamagePlus.Create;
        caDamagePlusPercent       : OneSpell := TSpellDamagePlusPercent.Create;
        caDamagePercentIncrease   : OneSpell := TSpellDamagePercentIncrease.Create;
        caDamagePercentDecrease   : OneSpell := TSpellDamagePercentDecrease.Create;
        caDamageSpells            : OneSpell := TSpellDamageSpells.Create;
        caDestroy                 : OneSpell := TSpellDestroy.Create;
        .....
        else                        raise ERangeError.CreateFmt(ERROR_INVALID_DB_NUMBER,[DB_FLD_CATEGORY,FQuery.Recordset.Fields[DB_FLD_CATEGORY].Value]);
      end;
      try
        if assigned(OneSpell) then
        begin
          OneSpell.CardType     := TCardType  (IfNull( FQuery.Recordset.Fields[ DB_FLD_CARD_TYPE   ].Value,0) );
          OneSpell.Originator   := Card;
          OneSpell.OnManaChange := Self.OnManaChange;
          OneSpell.OnSeek2ndTarget := self.OnSeek2ndTarget;
          assert(OneSpell.Originator.COwner is TPlayer,'TSpellMeister.RunSpells : OneSpell.Originator.COwner not a player: ' + OneSpell.Originator.COwner.ClassName);
            try
              FoundSpell := TSpell(OneSpell);
              FoundSpell.Originator  := Card;
              FoundSpell.Trigger     := Trigger;
              FoundSpell.CardType    := TCardType  ( FQuery.Recordset.Fields[ DB_FLD_CARD_TYPE   ].Value );
              FoundSpell.Category    := TCategory  ( FQuery.Recordset.Fields[ DB_FLD_CATEGORY    ].Value );
              FoundSpell.LifeToAdd   :=             IfNull( FQuery.Recordset.Fields[ DB_FLD_LIFE_TO_ADD ].Value,0);
              FoundSpell.MaxRandom   :=             IfNull( FQuery.Recordset.Fields[ DB_FLD_MAX_RANDOM  ].Value,0);
              FoundSpell.PerCent     :=             IfNull( FQuery.Recordset.Fields[ DB_FLD_PER_CENT    ].Value,0);
              FoundSpell.Plus        :=             IfNull( FQuery.Recordset.Fields[ DB_FLD_PLUS        ].Value,0);
              FoundSpell.ReplaceDmg  :=             IfNull( FQuery.Recordset.Fields[ DB_FLD_REPLACE_DMG ].Value,0);
              FoundSpell.Replacement :=             IfNull( FQuery.Recordset.Fields[ DB_FLD_REPLACEMENT ].Value,0);
              FoundSpell.StatTarget  :=             IfNull( FQuery.Recordset.Fields[ DB_FLD_STAT_TARGET ].Value,0);
              FoundSpell.Target      := TTargetType( IfNull(FQuery.Recordset.Fields[ DB_FLD_TARGET      ].Value,0) );
              FoundSpell.Need2Target :=             IfNull( FQuery.Recordset.Fields[ DB_FLD_NEED2TARGET ].Value,0);
              assert(FoundSpell.Originator.COwner is TPlayer,'TSpellMeister.RunSpells : FoundSpell.Originator.COwner not a player: ' + OneSpell.Originator.COwner.ClassName);
              if FoundSpell.Need2Target = 1 then
                 FOnSeek2ndTarget(Card);
              FonFindSpell(FoundSpell);
            finally
              FreeAndNil(OneSpell);
            end;
            end;

      except                    // I think this is OK but is there a possible bug if
        FreeAndNil(OneSpell);   //  spell adjuster added to list then destroyed?
      end;                      // List item would then be invalid.

      FQuery.Recordset.MoveNext;
    end;
  end;
end;

So all that is the ss_spells unit, now the game unit which uses ss_spells unit in the forum.create i have

 FSpellMeister.OnSeek2ndTarget := self.Handle2ndTarget; 

with no idea what to put in Handle2ndTarget currently its just

//****************************************************************************
procedure TFGame.Handle2ndTarget(Card : TCard);
begin
  showmessage('Select a target HANDLE2ndTARGET');
end;

just to see if i could at least get here..

So with this my question if you cant make it out, How do i set a var in ss_Spells to an TObject (tcard or tlabel) when the foundspell.Need2ndTarget := 1 using the event FOnSeek2ndTarget();

Was it helpful?

Solution

Simply change the signature of your TTargetEvt type, eg:

TTargetEvt = procedure (Card : TCard; var Target: TObject) of Object;  

Then update RunSpells() accordingly:

var
  Target: TObject;
...
if FoundSpell.Need2Target = 1 then
begin
  Target := nil;
  if Assigned(FOnSeek2ndTarget) then FOnSeek2ndTarget(Card, Target);
  // use Target as needed...
end;
...

Then update your handler accordingly:

procedure TFGame.Handle2ndTarget(Card : TCard; var Target: TObject);
begin
  Target := ...;
end;

OTHER TIPS

I need to know how to use an event to return a var parameter.

Define your event like this:

type
  TMyEvent = procedure(var ReturnValue: Integer) of object;

And then you add the event property in the usual way:

....
private
  FOnMyEvent: TMyEvent;
....
published
  property OnMyEvent: TMyEvent read FOnMyEvent write FOnMyEvent;
....

The nuance comes in how you surface the event. Generally, if you are writing a component that offers events, you must cater for the eventuality that there will be no handler attached to the event. And if the event is meant to return a value, how can you have no hander and also a returned value? The trick is to assign the parameter to the default, before you surface the event. For example:

procedure TMyComponent.DoMyEvent(out ReturnValue: Integer);
begin
  Result := DefaultValueForMyEventHandler;// you supply something meaningful here
  if Assigned(FOnMyEvent) then
    FOnMyEvent(Result);
end;

So, if the consumer of the component has not supplied a handler for the event, then the method still yields a reasonable value.

If you read this and think that it makes no sense for FOnMyEvent to be nil, then your design is wrong. If you want to force the consumer to supply behaviour, and not be allowed to rely on a default, then an event is the wrong mechanism. In that case ask the consumer to supply the behaviour via a parameter, perhaps enforced by the signature of the component's constructor. Or maybe some other way.

I've just given you a basic example un-related to your code. I've tried to get across the concepts. Hopefully you can adapt this to your specific needs.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top