It is currently 16 Apr 2024, 17:04
   
Text Size

Card Development - talk about cards code here

Discuss Upcoming Releases, Coding New Cards, Etc.
PLEASE DO NOT REPORT BUGS HERE!

Moderators: BAgate, drool66, Aswan jaguar, gmzombie, stassy, CCGHQ Admins

DONE - Elvish Healer target opponent issue

Postby Aswan jaguar » 12 Nov 2020, 15:35

I have made Elvish Healer but not using the usual Manalink method to target the damage_card but went for the prevention shield way. The issue happens only if you target opponent. The prevention shield effect doesn't go to opponent's field like it does with other cards like Hold at Bay (shows as not owned by that player) doesn't have the point of damage on it, doesn't prevent damage and produces a rukh token at eot.

See this post for reason for and against prevention shield:
viewtopic.php?f=86&t=16985&p=194613&hilit=prevention+shield#p194626
I don't know if it is possible but if we could make prevention shield to work both ways like the regeneration shield does it would be the best solution. Although I fear that Korath didn't implement the function to use both ways because it is not possible. For me if it comes to choose one way although I like the manalink way, it produces more bugs than the shield one does, so I go for shield.
Code: Select all
int card_elvish_healer(int player, int card, event_t event){
   /* CARD_ID_ELVISH_HEALER   1912
   Elvish Healer   |2|W
   Creature - Elf Cleric 1/2
   |T: Prevent the next 1 damage that would be dealt to any target this turn. If it's a |Sgreen creature, prevent the next 2 damage instead.*/

   if( ! IS_GAA_EVENT(event) ){
      return 0;
   }

   target_definition_t td;
   default_target_definition(player, card, &td, TYPE_CREATURE|TARGET_TYPE_PLANESWALKER);
   td.preferred_controller = player;
   td.zone = TARGET_ZONE_CREATURE_OR_PLAYER;

   if (event == EVENT_CAN_ACTIVATE){
     int gaa_pre = generic_activated_ability(player, card, event, GAA_CAN_TARGET | GAA_UNTAPPED, MANACOST0, 0, &td, NULL);
     return ! gaa_pre ? 0 : (land_can_be_played & LCBP_DAMAGE_PREVENTION) ? 99 : 1; // so that it can work as a prevention shield and at manalink time.
   }

    if( event == EVENT_ACTIVATE ){
       card_instance_t* instance = get_card_instance(player, card);
       instance->number_of_targets = 0;
      generic_activated_ability(player, card, event, GAA_CAN_TARGET | GAA_LITERAL_PROMPT | GAA_UNTAPPED, MANACOST0, 0,
                          &td, "Select any target to prevent next 1 damage or 2 for a green creature.");
   }

   if( event == EVENT_RESOLVE_ACTIVATION ){
      if( valid_target(&td) ){
          card_instance_t* instance = get_card_instance(player, card);
         int targ_p = instance->targets[0].player;
         int targ_c = instance->targets[0].card;
         if( instance->targets[0].card >= 0 && (get_color(targ_p, targ_c) & get_sleighted_color_test(targ_p, targ_c, COLOR_TEST_GREEN)) ){
            prevent_the_next_n_damage(player, card, targ_p, targ_c, 2, 0, 0, 0);
         }
         else{
               prevent_the_next_n_damage(player, card, targ_p, targ_c, 1, 0, 0, 0);
         }
      }
   }

   return 0;
}
Last edited by Aswan jaguar on 21 Nov 2020, 16:34, edited 1 time in total.
Reason: tag
---
Trying to squash some bugs and playtesting.
User avatar
Aswan jaguar
Super Tester Elite
 
Posts: 8078
Joined: 13 May 2010, 12:17
Has thanked: 730 times
Been thanked: 458 times

Re: Card Development - talk about cards code here

Postby drool66 » 12 Nov 2020, 23:27

You just need to get the parent for effect resolution:
Code: Select all
   if( event == EVENT_RESOLVE_ACTIVATION ){
      if( validate_target(player, card, &td, 0) ){
          card_instance_t* instance = get_card_instance(player, card);
        card_instance_t* parent = get_card_instance(instance->parent_controller, instance->parent_card);
         int targ_p = parent->targets[0].player;
         int targ_c = parent->targets[0].card;
         if( targ_c >= 0 && (get_color(targ_p, targ_c) & get_sleighted_color_test(instance->parent_controller, instance->parent_card, COLOR_TEST_GREEN)) ){
            prevent_the_next_n_damage(instance->parent_controller, instance->parent_card, targ_p, targ_c, 2, 0, 0, 0);
         }
         else{
               prevent_the_next_n_damage(instance->parent_controller, instance->parent_card, targ_p, targ_c, 1, 0, 0, 0);
         }
      }
   }
And then it's get_sleighted_color_test(instance->parent_controller, instance->parent_card, COLOR_TEST_GREEN), not targ_p, targ_c

For finishing touches you could also use get_sleighted_color_text() in EVENT_ACTIVATE
Code: Select all
get_sleighted_color_text(player, card, "Select any target to prevent next 1 damage or 2 for a %s creature.", COLOR_GREEN)
and I think your EVENT_CAN_ACTIVATE could be simplified by just adding GAA_DAMAGE_PREVENTION

One thing I've been meaning to bring up is valid_target() vs. validate_target() in EVENT_RESOLVE_ACTIVATION/_SPELL. I think valid_target() is just "are there any valid targets?", whereas validate_target() is "is the thing I've targeted a valid target?" So if the target becomes invalid between activation/cast & resolution but there is another valid target available, valid_target() will return true when you want it to return false, whereas validate_target() will not. Is that right? I've been changing these in the cards I've worked on, but not on every one I see since it's just too many.
User avatar
drool66
Programmer
 
Posts: 1163
Joined: 25 Nov 2010, 22:38
Has thanked: 186 times
Been thanked: 267 times

Re: Card Development - talk about cards code here

Postby Korath » 13 Nov 2020, 04:41

drool66 wrote:I think valid_target() is just "are there any valid targets?", whereas validate_target() is "is the thing I've targeted a valid target?"
Read the source, Luke:
Code: Select all
int valid_target(target_definition_t *td ){
    return validate_target(td->player, td->card, td, 0);
}
"Are there any valid targets?" is target_available(int player, int card, target_definition_t *td).
User avatar
Korath
DEVELOPER
 
Posts: 3707
Joined: 02 Jun 2013, 05:57
Has thanked: 496 times
Been thanked: 1106 times

[DONE]Hipparion prompt & vs must block

Postby Aswan jaguar » 21 Nov 2020, 17:29

With Hipparion the issue I have is that it prompts for mana once before choosing blockers ( EVENT_BLOCK_LEGALITY right? )and then re-prompts after I choose which attacker it should block. If I cancel the first prompt and pay the second it still works.
Code: Select all
int card_hipparion(int player, int card, event_t event){//in progress
   /*Hipparion   |1|W   0x000000
    * Creature - Horse 1/3
    * ~ can't block creatures with power 3 or greater unless you pay |1. */

    if (event == EVENT_BLOCK_LEGALITY && current_phase == PHASE_DECLARE_BLOCKERS && affect_me(player, card) && !is_humiliated(player, card)
       && get_power(attacking_card_controller, attacking_card) >= 3)
   {
            if(!has_mana(player, COLOR_COLORLESS, 1)){
              event_result = 1;
         }
           else if( charge_mana_while_resolving(player, card, 0, player, COLOR_COLORLESS, 1) ){
                  if(spell_fizzled !=1){
                      event_result = 0;
                     return 0;
                  }
          }
   }

   return 0;
}
---
Trying to squash some bugs and playtesting.
User avatar
Aswan jaguar
Super Tester Elite
 
Posts: 8078
Joined: 13 May 2010, 12:17
Has thanked: 730 times
Been thanked: 458 times

Re: Card Development - talk about cards code here

Postby drool66 » 21 Nov 2020, 19:04

Looking at engine.c > human_assign_blockers(), the next thing I would try would be to add "instance->state & STATE_BLOCKING" to your event conditions. EVENT_BLOCK_LEGALITY is first checked at 0x434E70 ("any_can_block") - you don't want Hipparion to trigger here. I don't see it checked in 0x4350E0 (or 0x434F30, which it calls), so maybe it's done during TRIGGER_BLOCKER_CHOSEN (which would probably call 0x434E70 again). If that's true, this fix should work, since STATE_BLOCKING is added between the two checks in human_assign_blockers().

EDIT: Found it. Event is dispatched in 0x434E70 and in is_legal_block_impl(), the latter of which is called from 0x434F30 ie. is_legal_block(). This is unfortunate because nothing meaningful is set between the two events. I guess you could use an internal variable like:
Code: Select all
int card_hipparion(int player, int card, event_t event){//in progress
   /*Hipparion   |1|W   0x000000
    * Creature - Horse 1/3
    * ~ can't block creatures with power 3 or greater unless you pay |1. */

    if (event == EVENT_BLOCK_LEGALITY && current_phase == PHASE_DECLARE_BLOCKERS && affect_me(player, card) && !is_humiliated(player, card) && get_power(attacking_card_controller, attacking_card) >= 3)
   {
      if( get_card_instance(player, card)->info_slot == 66){
            if(!has_mana(player, COLOR_COLORLESS, 1)){
              event_result = 1;
            }
           else if( !charge_mana_while_resolving(player, card, 0, player, COLOR_COLORLESS, 1) ){
                  if(spell_fizzled !=1){
                      event_result = 0;
                     return 0;
                  }
          }
        get_card_instance(player, card)->info_slot = 0;
      }
      else{
         get_card_instance(player, card)->info_slot = 66;
      }
        return 0;
   }

   return 0;
}
(just checked this & it works) It's not great, but the persistent storage is only relevant for a very short time, so not nearly as bad as some other uses of info_slot. Not 100% sure if anything could break the chain and keep info_slot set to 66 without resetting it.
User avatar
drool66
Programmer
 
Posts: 1163
Joined: 25 Nov 2010, 22:38
Has thanked: 186 times
Been thanked: 267 times

Re: Card Development - talk about cards code here

Postby Aswan jaguar » 22 Nov 2020, 12:37

I have also fixed that you could cancel to pay mana and then be able to make the illegal block with Hipparion.
There is also an annoying little "bug" that you are asked to pay mana even if you don't have available mana (which is not the case with the previous bugged to pay twice version). If you have an idea how to fix that without messing everything else (which is what I did trying to fix this) I will wait for that tip otherwise the card works rules wise and I will commit it, thanks.
---
Trying to squash some bugs and playtesting.
User avatar
Aswan jaguar
Super Tester Elite
 
Posts: 8078
Joined: 13 May 2010, 12:17
Has thanked: 730 times
Been thanked: 458 times

Re: Card Development - talk about cards code here

Postby drool66 » 23 Nov 2020, 03:50

The version I posted above doesn't reliably clear info_slot - I just banged it out before bed. But even if it is managed properly, the dispatch from any_can_block() doesn't happen reliably due to something involving dword_4EF520[].
I have come up with a "clean-ish" solution that works 100% perfectly as far as I can tell. It involves adding a variable to check where you are in the declare attackers cycle. Looks like this:

engine.c
Code: Select all
int hack_second_dispatch_event_block_legality = 0;
void human_assign_blockers(int player)
{
[skip down to...]
  while (EXE_FN(int, 0x434E70, int)(1-player))   // any_can_block() <--first check of EVENT_BLOCK_LEGALITY; variable still 0
[...]
     if (!forbid_attack
        && select_target(player, -1000, &td_block_which_attacker, text_lines[1], &blocked))
      {
//Begin additions
        hack_second_dispatch_event_block_legality = 1; <--set to 1 before second check happens in try_block() - this is where we want to check the event
//End additions
        // 0x4350e0 clears blocking, checks legality, and then, if it was illegal, restores the previous values
        if (EXE_FN(int, 0x4350E0, int, int, int, int)(blocker.player, blocker.card, blocked.player, blocked.card))   // try_block()
         {
           int band = get_card_instance(blocked.player, blocked.card)->blocking;
           if (band == 255)
            band = blocked.card;

           card_instance_t* instance = get_card_instance(blocker.player, blocker.card);
           instance->blocking = band;
           instance->state |= STATE_BLOCKING;
           // Begin removals
           // if (band_before_being_set_to_blocked.card != 255)   // Suppresse the block sound when blocking a band
           // End removals
           play_sound_effect(WAV_BLOCK2);

           EXE_FN(void, 0x472260, void)();   // TENTATIVE_reassess_all_cards()

           if (event_flags & EA_SELECT_BLOCK)
            dispatch_trigger2(current_turn, TRIGGER_BLOCKER_CHOSEN, EXE_STR(0x790074)/*PROMPT_BLOCKERSELECTION[0]*/, 0, blocker.player, blocker.card);
         }
        else // if (ai_is_speculating != 1)   // Redundant, already checked above
         {
           load_text(0, "PROMPT_CHOOSEBLOCKERS");
           set_centerwindow_txt(text_lines[2]);
           EXE_STDCALL_FN(void, 0x4D5D32, int)(2000);   // Sleep(2000)
           set_centerwindow_txt("");
         }
//Begin additions
         hack_second_dispatch_event_block_legality = 0; <-- clear before end of loop, ie. before we go back to the dispatch in any_can_block()
//End additions
      }
   {
You then just declare the variable again in ice_age.c (preferably right before Hipparion), add "&& hack_second_dispatch_event_block_legality" to your conditions, include the fix you describe above, and viola, your card works 100% AFAIK.
This isn't an incredibly hackneyed fix, but it does feel weird to add a variable to a function called most turns of most games to serve exactly one card. On the other hand, Ice Age cards are weird and we did just add a new event just for Balduvian Shaman, so eh...
As a side note, TRIGGER_PAY_TO_BLOCK won't work because it's dispatched before blockers are chosen, and this tax depends on the chosen blocker.
User avatar
drool66
Programmer
 
Posts: 1163
Joined: 25 Nov 2010, 22:38
Has thanked: 186 times
Been thanked: 267 times

Re: Card Development - talk about cards code here

Postby Aswan jaguar » 23 Nov 2020, 15:39

Yes, it works better with this variable. I think also Awesome Presence needs the same or similar approach but I am not touching it, as it is more complicated than Hipparion and I don't want to ](*,) .
Current card's code (I don't include the needed changes in engine.c that you have already posted) & (still prompts even if you don't have available mana but at least now works fine ).
Code: Select all
int hack_second_dispatch_event_block_legality;
int card_hipparion(int player, int card, event_t event){//in progress
   /*Hipparion   |1|W
   * Creature - Horse 1/3
   * ~ can't block creatures with power 3 or greater unless you pay |1. */

   if (event == EVENT_BLOCK_LEGALITY && current_phase == PHASE_DECLARE_BLOCKERS && affect_me(player, card) && !is_humiliated(player, card)
      && get_power(attacking_card_controller, attacking_card) >= 3 && hack_second_dispatch_event_block_legality)
   {
      if(!has_mana(player, COLOR_COLORLESS, 1)){
         event_result = 1;
      }
      else if( !charge_mana_while_resolving(player, card, 0, player, COLOR_COLORLESS, 1) ){
             if(spell_fizzled !=1){
               event_result = 0;
               return 0;
             }
             event_result = 1;
      }
   }

    return 0;
}
EDIT: Unfortunately I just tested Hipparion against cards with "must block if able" and with power bigger than two and although initially finds the block illegal you get prompt again and then it doesn't find it as illegal block anymore. I tested with Avalanche Tusker (after fixing it to target creatures) but any other will do. I tested also with Blaze of Glory and there things were worse as I didn't get prompt at all and Hipparion was forced to block the three illegal blockers without paying mana.
---
Trying to squash some bugs and playtesting.
User avatar
Aswan jaguar
Super Tester Elite
 
Posts: 8078
Joined: 13 May 2010, 12:17
Has thanked: 730 times
Been thanked: 458 times

Re: Card Development - talk about cards code here

Postby drool66 » 24 Nov 2020, 18:41

Oof, that's real Gordian Knot. So SP_KEYWORD_MUST_BLOCK forwards to block_if_able() on EVENT_DECLARE_BLOCKERS, then to select_blocker(), whose target definition includes can_block_target(), calling can_block_me(), which returns is_legal_block() / is_legal_block_impl(), which returns the event_result (well, actually its binary opposite) of EVENT_BLOCK_LEGALITY. Issue is I can't tell exactly when EVENT_DECLARE_BLOCKERS is dispatched. FWIW, here is my current state of hippparion & human_assign_blockers() - most of it is a mess & will probably end up being unnecessary, but I'm trying to isolate this call to EVENT_BLOCK_LEGALITY:
Hipparion:
Code: Select all
int hack_first_call_to_event_block_legality;
int hack_second_call_to_event_block_legality;
int hack_cancel_blockers;
int card_hipparion(int player, int card, event_t event){//in progress
   /*Hipparion   |1|W   0x000000
    * Creature - Horse 1/3
    * ~ can't block creatures with power 3 or greater unless you pay |1. */

   if ((event == EVENT_BLOCK_LEGALITY || event == EVENT_DECLARE_BLOCKERS) && current_phase == PHASE_DECLARE_BLOCKERS && affect_me(player, card) && !is_humiliated(player, card) && get_power(attacking_card_controller, attacking_card) >= 3 ){
      ASSERT(!(get_card_instance(player, card)->info_slot == 0xFF));
      if( hack_cancel_blockers ){
         get_card_instance(player, card)->info_slot |= 0xF;
         get_card_instance(player, card)->targets[16].card &= ~SP_KEYWORD_MUST_BLOCK;
      }
      if( hack_second_call_to_event_block_legality && !(get_card_instance(player, card)->info_slot & 0xFF)){
         if(!has_mana(player, COLOR_COLORLESS, 1)){
            event_result |= 1;
         }
         else if( !charge_mana_while_resolving(player, card, 0, player, COLOR_COLORLESS, 1) ){
            if(spell_fizzled !=1){
               get_card_instance(player, card)->info_slot |= 0xF0;//paid
               get_card_instance(player, card)->info_slot &= ~0xF;//canceled
               event_result = 0;
               return 0;
            }
            get_card_instance(player, card)->info_slot |= 0xF;//canceled
            event_result |= 1;
         }
      }
      else if(!hack_first_call_to_event_block_legality && !(get_card_instance(player, card)->info_slot & 0xFF) ){//outside of normal blocking loop & hasn't been prompted
         if(!has_mana(player, COLOR_COLORLESS, 1)){
            event_result |= 1;
         }
         else if( !charge_mana_while_resolving(player, card, 0, player, COLOR_COLORLESS, 1) ){
            if(spell_fizzled !=1){
               get_card_instance(player, card)->info_slot |= 0xF0;//paid
               get_card_instance(player, card)->info_slot &= ~0xF;//canceled
               event_result = 0;
               return 0;
            }
            get_card_instance(player, card)->info_slot |= 0xF;//canceled
            event_result |= 1;
         }
      }
      else if( hack_first_call_to_event_block_legality && !(get_card_instance(player, card)->info_slot & 0xF0) && !has_mana(player, COLOR_COLORLESS, 1))
         event_result |= 1;
      else if(!hack_first_call_to_event_block_legality && get_card_instance(player, card)->info_slot & 0xF0 && !(get_card_instance(player, card)->info_slot & 0xF))
         event_result = 0;
      else if(!hack_first_call_to_event_block_legality && get_card_instance(player, card)->info_slot & 0xF && !(get_card_instance(player, card)->info_slot & 0xF0))
         event_result |= 1;
   }

   if( event == EVENT_PHASE_CHANGED && (current_phase >= PHASE_MAIN2 || current_phase <= PHASE_MAIN1))
      get_card_instance(player, card)->info_slot = 0;

   return 0;
}
human_assign_blockers()
Code: Select all
int hack_first_call_to_event_block_legality = 0;
int hack_second_call_to_event_block_legality = 0;
int hack_cancel_blockers = 0;
void human_assign_blockers(int player)
{
  // 0x434960

  if (ai_is_speculating == 1)
   return;

  // Begin additions
  int who_chooses;
  if (event_flags & EF_ATTACKER_CHOOSES_BLOCKERS)
   {
     who_chooses = player;
     if (current_turn == AI)   // Nothing blocks
      return;
   }
  else
   who_chooses = 1-player;
  // End additions

  EXE_FN(void, 0x472260, void)();   // TENTATIVE_reassess_all_cards()

  // Begin removals
#if 0
  // This seems to be the remains of primitive AI handling; the values it computes aren't used.
  int indices[16], powers[16], highest_power = 0, pos = 0, c;
  for (c = 0; active_cards_count[player] > c; ++c)
   {
     card_instance_t* instance = get_card_instance(player, c);
     if (instance->internal_card_id != -1 && (instance->state & STATE_ATTACKING))
      {
        int pow = get_abilities(player, c, EVENT_POWER, -1);
        indices[pos] = c;
        powers[pos] = pow;
        ++pos;
        if (highest_power < pow)
         highest_power = pow;
      }
   }
#endif
  // End removals

  target_definition_t td_choose_blocker;
  base_target_definition(1-player, 0, &td_choose_blocker, TARGET_TYPE_NONCREATURE_CAN_BLOCK | TYPE_CREATURE);
  td_choose_blocker.who_chooses = who_chooses;
  td_choose_blocker.allowed_controller = 1-player;
  td_choose_blocker.preferred_controller = 1-player;
  td_choose_blocker.zone = TARGET_ZONE_0x2000 | TARGET_ZONE_IN_PLAY;
  td_choose_blocker.special = TARGET_SPECIAL_ALLOW_MULTIBLOCKER;
  td_choose_blocker.illegal_state = TARGET_STATE_BLOCKING | TARGET_STATE_TAPPED;
  td_choose_blocker.allow_cancel = 2;   // I suspect this is what makes the "Done" button.

  target_definition_t td_block_which_attacker;
  base_target_definition(player, 0, &td_block_which_attacker, TYPE_CREATURE);
  td_block_which_attacker.who_chooses = who_chooses;
  td_block_which_attacker.allowed_controller = player;
  td_block_which_attacker.preferred_controller = player;
  td_block_which_attacker.required_state = TARGET_STATE_ATTACKING;

  target_t blocker, blocked;

  hack_first_call_to_event_block_legality = 1;
  while (EXE_FN(int, 0x434E70, int)(1-player))   // any_can_block()
   {
     hack_first_call_to_event_block_legality = 0;
     load_text(0, "PROMPT_CHOOSEBLOCKERS");
     if (!select_target(1-player, -1000, &td_choose_blocker, text_lines[0], &blocker))
      return;

     forbid_attack = 0;
     if (event_flags & EA_PAID_BLOCK)
      {
        push_affected_card_stack();

        trigger_cause_controller = blocker.player;
        trigger_cause = blocker.card;
        dispatch_trigger(1-player, TRIGGER_PAY_TO_BLOCK, EXE_STR(0x790248)/*PROMPT_TURNSEQUENCE[0]*/, 1);
        pop_affected_card_stack();
      }

     if (!forbid_attack
        && select_target(player, -1000, &td_block_which_attacker, text_lines[1], &blocked))
      {
        hack_second_call_to_event_block_legality = 1;
        // 0x4350e0 clears blocking, checks legality, and then, if it was illegal, restores the previous values
        if (EXE_FN(int, 0x4350E0, int, int, int, int)(blocker.player, blocker.card, blocked.player, blocked.card))   // try_block()
         {
           hack_second_call_to_event_block_legality = 0;
           int band = get_card_instance(blocked.player, blocked.card)->blocking;
           if (band == 255)
            band = blocked.card;

           card_instance_t* instance = get_card_instance(blocker.player, blocker.card);
           instance->blocking = band;
           instance->state |= STATE_BLOCKING;
           // Begin removals
           // if (band_before_being_set_to_blocked.card != 255)   // Suppresse the block sound when blocking a band
           // End removals
           play_sound_effect(WAV_BLOCK2);

           EXE_FN(void, 0x472260, void)();   // TENTATIVE_reassess_all_cards()

           if (event_flags & EA_SELECT_BLOCK)
            dispatch_trigger2(current_turn, TRIGGER_BLOCKER_CHOSEN, EXE_STR(0x790074)/*PROMPT_BLOCKERSELECTION[0]*/, 0, blocker.player, blocker.card);
         }
        else // if (ai_is_speculating != 1)   // Redundant, already checked above
         {
           hack_cancel_blockers = 1;
           load_text(0, "PROMPT_CHOOSEBLOCKERS");
           set_centerwindow_txt(text_lines[2]);
           EXE_STDCALL_FN(void, 0x4D5D32, int)(2000);   // Sleep(2000)
           set_centerwindow_txt("");
         }
      }
     hack_first_call_to_event_block_legality = 1;
   }
  hack_first_call_to_event_block_legality = 0;
  hack_cancel_blockers = 0;
}
User avatar
drool66
Programmer
 
Posts: 1163
Joined: 25 Nov 2010, 22:38
Has thanked: 186 times
Been thanked: 267 times

Infinite Hourglass bad AI reactivation behaviour

Postby Aswan jaguar » 27 Nov 2020, 17:18

Infinite Hourglass is working but has a bad AI issue which is described in my comments above card's code. Is there any other method to stop AI from re-activating cards while on stack? Especially while we force AI to activate the card because it is the upkeep phase like in this case?

Code: Select all
static int infinite_hourglass_legacy(int player, int card, event_t event ){

   if( event == EVENT_STATIC_EFFECTS && affect_me(player, card) ){

      card_instance_t *instance = get_card_instance(player, card);
      if( instance->targets[0].player > -1 ){
         if( get_id(instance->targets[0].player, instance->targets[0].card) != CARD_ID_INFINITE_HOURGLASS ){
            kill_card(player, card, KILL_EXILE);
         }
         if( ! in_play(instance->targets[0].player, instance->targets[0].card) ){
            kill_card(player, card, KILL_EXILE);
         }
      }

   }

   if( get_card_instance(player, card)->targets[0].player != -1 ){

      card_instance_t *instance = get_card_instance(player, card);

      if (!IS_GAA_EVENT(event)){
         return 0;
      }

      int p = instance->targets[0].player;
      int c = instance->targets[0].card;

      int rval = granted_generic_activated_ability(p, c, player, card, event, GAA_ONLY_ON_UPKEEP, MANACOST_X(3), 0, NULL, NULL);

      if( event == EVENT_CAN_ACTIVATE ){

         if (current_phase != PHASE_UPKEEP
           || count_counters(p, c, COUNTER_TIME) <= 0)
           //|| !has_mana_for_activated_ability(p, c, MANACOST_X(3)))
           // || get_card_instance(p, c)->info_slot & 1)
         return 0;

          if( rval && IS_AI(player) && count_counters(p, c, COUNTER_TIME) > 0 ){
            if( count_subtype(p, TYPE_CREATURE, -1)+1 < count_subtype(1-player, TYPE_CREATURE, -1) ){
               EXE_DWORD(0x736808) |= 3;
            }
         }
      }

      /*if (event == EVENT_ACTIVATE)
          get_card_instance(p, c)->info_slot |= 1; */

      if (event == EVENT_RESOLVE_ACTIVATION){
         remove_counter(p, c, COUNTER_TIME);
      }

      /*if( event == EVENT_STATIC_EFFECTS  ){
           if (stack_size == 0){ //prohibit activation on stack.
             get_card_instance(p, c)->info_slot = 0;
         }
      }*/
       return rval;
   }

   return 0;
}

// I tried to make AI not activating it when there is already an activation card on stack as it will waste all it's mana - several activations even when there is
//   only 1 counter to remove - (it is the code I have disabled both in card and it's legacy) it works for parent card itself but I never managed to do it for the legacy card at all.
// For parent card unfortunately when this code is inserted it makes legacy card being activated only sometimes and I can't find out why this happens.
int card_infinite_hourglass(int player, int card, event_t event){
   /* Infinite Hourglass   |4
    * Artifact
    * At the beginning of your upkeep, put a time counter on ~.
    * All creatures get +1/+0 for each time counter on ~.
    * |3: Remove a time counter from ~. Any player may activate this ability but only during any upkeep step. */

   upkeep_trigger_ability(player, card, event, player);

   if( event == EVENT_UPKEEP_TRIGGER_ABILITY )
      add_counter(player, card, COUNTER_TIME);

   int amount = count_counters(player, card, COUNTER_TIME);

    boost_creature_type(player, card, event, -1, amount, 0, 0, BCT_INCLUDE_SELF);

   if( event == EVENT_STATIC_EFFECTS && affect_me(player, card) ){
      int found[2] = {-1, -1};
      CYCLE_ALL_CARDS(p, c,    {
                           if( in_play(p, c) && is_what(p, c, TYPE_EFFECT) ){
                              card_instance_t *inst = get_card_instance(p, c);
                              if( inst->info_slot == (int)infinite_hourglass_legacy ){
                                 if( inst->targets[0].player == player && inst->targets[0].card == card ){
                                    found[p] = c;
                                 }
                              }
                           }
                        };
      );
      if( found[player] > -1 ){
         kill_card(player, found[player], KILL_EXILE);
      }
      if( found[1-player] == -1 ){
         int fake = add_card_to_hand(1-player, get_card_instance(player, card)->internal_card_id);
         int legacy = create_legacy_activate(1-player, fake, &infinite_hourglass_legacy);
         card_instance_t *inst = get_card_instance(1-player, legacy);
         inst->targets[0].player = player;
         inst->targets[0].card = card;
         inst->number_of_targets = 1;
         obliterate_card(1-player, fake);
      }
   }

   /*if (event == EVENT_STATIC_EFFECTS && stack_size == 0)
       get_card_instance(player, card)->info_slot = 0;*/

   if (!IS_GAA_EVENT(event)){
      return 0;
   }
   int rval = generic_activated_ability(player, card, event, GAA_ONLY_ON_UPKEEP, MANACOST_X(3), 0, NULL, NULL);
   if( event == EVENT_CAN_ACTIVATE){

      if (current_phase != PHASE_UPKEEP
           || count_counters(player, card, COUNTER_TIME) <= 0)
           //|| !has_mana_for_activated_ability(player, card, MANACOST_X(3))
           // || get_card_instance(player, card)->info_slot & 1
         return 0;

      if( rval &&  IS_AI(player) && count_counters(player, card, COUNTER_TIME) > 0 ){
          if( count_subtype(player, TYPE_CREATURE, -1)+1 < count_subtype(1-player, TYPE_CREATURE, -1) ){
               EXE_DWORD(0x736808) |= 3;// Force AI to activate. (Same as allowing it to.)
         }
      }
   }

   /* if (event == EVENT_ACTIVATE)
       get_card_instance(player, card)->info_slot |= 1; */

   if (event == EVENT_RESOLVE_ACTIVATION){
      remove_counter(player, card, COUNTER_TIME);
   }

   return rval;
}
---
Trying to squash some bugs and playtesting.
User avatar
Aswan jaguar
Super Tester Elite
 
Posts: 8078
Joined: 13 May 2010, 12:17
Has thanked: 730 times
Been thanked: 458 times

Re: Card Development - talk about cards code here

Postby drool66 » 27 Nov 2020, 19:29

Funny that this came up now. I was just looking at the Shandalar implementation of Sagas and Korath uses a function to check if the triggering saga has an activation on the stack (Sagas aren't sacrificed due to having more counters than their final chapter if they have an activation on the stack); I was thinking that we need a function like that - shouldn't be too difficult to do at all.
User avatar
drool66
Programmer
 
Posts: 1163
Joined: 25 Nov 2010, 22:38
Has thanked: 186 times
Been thanked: 267 times

[DONE]Krovikan Sorcerer 2nd ability tracks all drawn cards t

Postby Aswan jaguar » 05 Dec 2020, 14:20

Krovikan Sorcerer I have an issue with 2nd ability to limit tracked cards only to the ones drawn with it's ability and not to all cards drawn this turn which my current code does.
Code: Select all
// in progress keeps track of all cards drawn this turn instead of the ones drawn with it's 2nd ability to discard one of them.
int card_krovikan_sorcerer(int player, int card, event_t event){
   /* Krovikan Sorcerer   |2|U
    * Creature - Human Wizard 1/1
    * |T, Discard a non|Sblack card: Draw a card.
    * |T, Discard a |Sblack card: Draw two cards, then discard one of them. */

   if( ! IS_GAA_EVENT(event) ){
      return 0;
   }

    if( event == EVENT_DRAW_PHASE ){
       if(current_turn == player){
          get_card_instance(player, card)->info_slot = 3;
       }
    }

   card_instance_t* instance = get_card_instance(player, card);

   enum{
        CHOICE_DISCARD_NON_BLACK = 1,
        CHOICE_DISCARD_BLACK
   };

   if( event == EVENT_CAN_ACTIVATE ){
       return generic_activated_ability(player, card, event, GAA_UNTAPPED | GAA_DISCARD, MANACOST0, 0, NULL, NULL);
   }

   if( event == EVENT_ACTIVATE ){

      test_definition_t this_test;
      new_default_test_definition(&this_test, TYPE_ANY, get_sleighted_color_text(player, card, "Select a non %s ", COLOR_BLACK));
      this_test.color = get_sleighted_color_test(player, card, COLOR_TEST_BLACK);
      this_test.color_flag = DOESNT_MATCH;
      this_test.zone = TARGET_ZONE_HAND;

      test_definition_t this_test2;
      new_default_test_definition(&this_test2, TYPE_ANY, get_sleighted_color_text(player, card, "Select a %s ", COLOR_BLACK));
      this_test2.color = get_sleighted_color_test(player, card, COLOR_TEST_BLACK);
      this_test2.color_flag = MATCH;
      this_test2.zone = TARGET_ZONE_HAND;

      int dis_non_black = check_battlefield_for_special_card(player, card, player, 0, &this_test);
      int dis_black = check_battlefield_for_special_card(player, card, player, 0, &this_test2);
      int choice = DIALOG(player, card, event, DLG_RANDOM,
                     "Discard a non-black card", dis_non_black, 5,
                     "Discard a black card", dis_black, 10
                );

      if( ! choice ){
         spell_fizzled = 1;
         return 0;
      }
      instance->info_slot = choice;

      if( choice == CHOICE_DISCARD_NON_BLACK){
         if( generic_activated_ability(player, card, event, GAA_UNTAPPED, MANACOST0, 0, NULL, NULL) ){
            int result = new_select_a_card(player, player, TUTOR_FROM_HAND, 0, AI_MIN_VALUE, -1, &this_test);
            if( result != -1){
               discard_card(player, result);
            }
            else{
               spell_fizzled = 1;
            }
         }
      }
      if( choice == CHOICE_DISCARD_BLACK){
         if( generic_activated_ability(player, card, event, GAA_UNTAPPED, MANACOST0, 0, NULL, NULL) ){
            int result2 = new_select_a_card(player, player, TUTOR_FROM_HAND, 0, AI_MIN_VALUE, -1, &this_test2);
            if( result2 != -1){
               discard_card(player, result2);
            }
            else{
                 spell_fizzled = 1;
             }
         }
      }
   }

   if( event == EVENT_RESOLVE_ACTIVATION ){
      if( instance->info_slot == CHOICE_DISCARD_NON_BLACK){
         draw_cards(player, 1);
      }
      if( instance->info_slot == CHOICE_DISCARD_BLACK){
         draw_cards(player, 2);

         int dtt[2][100] = {{0}};
         int dtt_count = 0;
         int i;
         for(i=0; i<active_cards_count[player]; i++){
            if( in_hand(player, i) && check_state(player, i, STATE_DRAWN_THIS_TURN) ){
               dtt[0][dtt_count] = get_card_instance(player, i)->internal_card_id;
               dtt[1][dtt_count] = i;
               dtt_count++;
            }
         }
         test_definition_t this_test;
         new_default_test_definition(&this_test, TYPE_ANY, "Select a card to discard.");

         while( dtt_count > -1 ){
               int selected = select_card_from_zone(player, player, dtt[0], dtt_count, 0, AI_MIN_VALUE, -1, &this_test);
               if( selected != -1 ){
                  discard_card(player, dtt[1][selected]);
               }
               dtt_count--;
               break;
         }
      }
   }

   return 0;
}
Last edited by Aswan jaguar on 16 Dec 2020, 16:40, edited 1 time in total.
Reason: done
---
Trying to squash some bugs and playtesting.
User avatar
Aswan jaguar
Super Tester Elite
 
Posts: 8078
Joined: 13 May 2010, 12:17
Has thanked: 730 times
Been thanked: 458 times

Re: Card Development - talk about cards code here

Postby drool66 » 10 Dec 2020, 23:57

Two ways I can think of that might work - the first would be to use TRIGGER_CARD_DRAWN. Take a look at gatecrash.c > coerced_confession() - it creates a legacy that uses XTRIGGER_MILLED to find the cards milled. You could do the same with TRIGGER_CARD_DRAWN, except without the legacy so you can load the cards to your array within the card's function - it would conceptually look like this:
Code: Select all
int array[2][128];    //that will cover six Thought Reflections, or you could do [500] to match the size of Deck[]
if( trigger_condition == TRIGGER_CARD_DRAWN && affect_me(player, card) && reason_for_trigger_controller == player && instance->info_slot == 66 ){
    int i;
    for(i=0;i<128 && i != -1;i++){
        if( i == -1 ){
            array[0][i] = get_card_instance(trigger_cause_controller, trigger_cause)->internal_card_id;
            array[1][i] = trigger_cause;
            break;    //should also be covered by the i != -1 part
        }
    }
    cant_be_responded_to = 1    //this isn't an actual trigger
}

then...

if( instance->info_slot == CHOICE_DISCARD_BLACK){
    memset(array, -1, sizeof array);
    int old_info_slot = intance->info_slot;
    intance->info_slot = 66;
    draw_cards(player, 2);
    intance->info_slot = old_info_slot;
...and now the cards drawn are loaded into array[][]
The other, much better way to do it would be to import EVENT_DRAW to Manalink; I don't know if that can be done properly without taking draw_cards_exe() out of exe
User avatar
drool66
Programmer
 
Posts: 1163
Joined: 25 Nov 2010, 22:38
Has thanked: 186 times
Been thanked: 267 times

Re: Card Development - talk about cards code here

Postby Aswan jaguar » 11 Dec 2020, 16:23

I tried few times with TRIGGER_CARD_DRAWN method that you suggested but I can't find how to use it properly so I didn't even manage to make it to load select_card_from_zone().
---
Trying to squash some bugs and playtesting.
User avatar
Aswan jaguar
Super Tester Elite
 
Posts: 8078
Joined: 13 May 2010, 12:17
Has thanked: 730 times
Been thanked: 458 times

Re: Card Development - talk about cards code here

Postby drool66 » 14 Dec 2020, 03:15

Ok, I got it to work for Liliana, Untouched by Death. The semantics are a little different but I'll take a crack at it once I'm done with M19 if you don't mind.
User avatar
drool66
Programmer
 
Posts: 1163
Joined: 25 Nov 2010, 22:38
Has thanked: 186 times
Been thanked: 267 times

PreviousNext

Return to Development

Who is online

Users browsing this forum: No registered users and 18 guests


Who is online

In total there are 18 users online :: 0 registered, 0 hidden and 18 guests (based on users active over the past 10 minutes)
Most users ever online was 4143 on 23 Jan 2024, 08:21

Users browsing this forum: No registered users and 18 guests

Login Form