#include "Card.hh" #include "../CommandFormats.hh" #include "Server.hh" using namespace std; namespace Episode3 { Card::Card( uint16_t card_id, uint16_t card_ref, uint16_t client_id, shared_ptr server) : w_server(server), w_player_state(server->get_player_state(client_id)), client_id(client_id), card_id(card_id), card_ref(card_ref), card_flags(0), loc(0, 0, Direction::RIGHT), facing_direction(Direction::INVALID_FF), action_chain(), action_metadata(), num_ally_fcs_destroyed_at_set_time(0), num_cards_destroyed_by_team_at_set_time(0), unknown_a9(1), last_attack_preliminary_damage(0), last_attack_final_damage(0), num_destroyed_ally_fcs(0), current_defense_power(0) {} void Card::init() { auto s = this->server(); auto ps = this->player_state(); this->clear_action_chain_and_metadata_and_most_flags(); this->team_id = ps->get_team_id(); this->def_entry = s->definition_for_card_id(this->card_id); if (!this->def_entry) { // The original implementation replaces the card ID and definition with 0009 // (Saber) if the SC is Hunters-side, and 0056 (Booma) if the SC is // Arkz-side. This could break things later on in the battle, and even if it // doesn't, it certainly isn't behavior that the player would expect, so we // prevent it instead. throw runtime_error("card definition is missing"); } this->sc_card_ref = ps->get_sc_card_ref(); this->sc_def_entry = s->definition_for_card_id(ps->get_sc_card_id()); this->sc_card_type = ps->get_sc_card_type(); if (this->sc_card_ref == this->card_ref) { if (s->options.is_nte()) { if (s->map_and_rules->rules.char_hp) { this->max_hp = s->map_and_rules->rules.char_hp; this->current_hp = s->map_and_rules->rules.char_hp; } else { this->max_hp = this->def_entry->def.hp.stat; this->current_hp = this->def_entry->def.hp.stat; } } else { int16_t rules_char_hp = s->map_and_rules->rules.char_hp; int16_t base_char_hp = (rules_char_hp == 0) ? 15 : rules_char_hp; int16_t hp = clamp(base_char_hp + this->def_entry->def.hp.stat, 1, 99); this->max_hp = hp; this->current_hp = hp; } } else { this->max_hp = this->def_entry->def.hp.stat; this->current_hp = this->def_entry->def.hp.stat; } this->ap = this->def_entry->def.ap.stat; this->tp = this->def_entry->def.tp.stat; this->num_ally_fcs_destroyed_at_set_time = s->team_num_ally_fcs_destroyed[this->team_id]; this->num_cards_destroyed_by_team_at_set_time = s->team_num_cards_destroyed[this->team_id]; this->action_chain.chain.card_ap = this->ap; this->action_chain.chain.card_tp = this->tp; this->loc.direction = ps->start_facing_direction; // Ep3 NTE always sends 6xB4x0A at construction time; final only does for // non-SC cards if (s->options.is_nte() || (this->sc_card_ref != this->card_ref)) { this->send_6xB4x4E_4C_4D_if_needed(); } } shared_ptr Card::server() { auto s = this->w_server.lock(); if (!s) { throw runtime_error("server is deleted"); } return s; } shared_ptr Card::server() const { auto s = this->w_server.lock(); if (!s) { throw runtime_error("server is deleted"); } return s; } shared_ptr Card::player_state() { auto s = this->w_player_state.lock(); if (!s) { throw runtime_error("server is deleted"); } return s; } shared_ptr Card::player_state() const { auto s = this->w_player_state.lock(); if (!s) { throw runtime_error("server is deleted"); } return s; } ssize_t Card::apply_abnormal_condition( const CardDefinition::Effect& eff, uint8_t def_effect_index, uint16_t target_card_ref, uint16_t sc_card_ref, int16_t value, int8_t dice_roll_value, int8_t random_percent) { auto s = this->server(); auto log = s->log_stack(string_printf("apply_abnormal_condition(%02hhX, @%04X, @%04X, %hd, %hhd, %hhd): ", def_effect_index, target_card_ref, sc_card_ref, value, dice_roll_value, random_percent)); bool is_nte = s->options.is_nte(); ssize_t existing_cond_index; for (size_t z = 0; z < this->action_chain.conditions.size(); z++) { const auto& cond = this->action_chain.conditions[z]; if (cond.type == eff.type) { existing_cond_index = z; if ((!is_nte && eff.type == ConditionType::MV_BONUS) || ((cond.card_definition_effect_index == def_effect_index) && (cond.card_ref == target_card_ref))) { break; } } else { existing_cond_index = -1; } } ssize_t cond_index = existing_cond_index; if (existing_cond_index < 0) { cond_index = existing_cond_index; for (size_t z = 0; z < this->action_chain.conditions.size(); z++) { if (this->action_chain.conditions[z].type == ConditionType::NONE) { cond_index = z; break; } } log.debug("existing_cond_index < 0 (new condition) => cond_index = %zd", cond_index); } else { log.debug("existing_cond_index = %zd (existing condition)", existing_cond_index); } if (cond_index < 0) { log.debug("no space for condition"); return -1; } int16_t existing_cond_value = 0; auto& cond = this->action_chain.conditions[cond_index]; if ((eff.type == ConditionType::MV_BONUS) && (cond.type == ConditionType::MV_BONUS)) { existing_cond_value = clamp(cond.value, -99, 99); log.debug("MV_BONUS combines => existing_cond_value = %hd", existing_cond_value); } s->card_special->apply_stat_deltas_to_card_from_condition_and_clear_cond(cond, this->shared_from_this()); cond.type = eff.type; cond.card_ref = target_card_ref; cond.condition_giver_card_ref = sc_card_ref; cond.card_definition_effect_index = def_effect_index; cond.order = 10; if (dice_roll_value < 0) { cond.dice_roll_value = this->player_state()->roll_dice_with_effects(1); } else { cond.dice_roll_value = dice_roll_value; } cond.flags = 0; cond.value = value + existing_cond_value; cond.value8 = value + existing_cond_value; cond.random_percent = random_percent; switch (eff.arg1.at(0)) { case 'a': { string s = eff.arg1.decode(); cond.a_arg_value = atoi(s.c_str() + 1); break; } case 'e': cond.remaining_turns = 99; break; case 'f': cond.remaining_turns = 100; break; case 'r': cond.remaining_turns = 102; break; case 't': { string s = eff.arg1.decode(); cond.remaining_turns = atoi(s.c_str() + 1); } } string cond_str = cond.str(s); log.debug("wrote condition %zd => %s", cond_index, cond_str.c_str()); if (!is_nte) { s->card_special->update_condition_orders(this->shared_from_this()); for (size_t z = 0; z < this->action_chain.conditions.size(); z++) { if (this->action_chain.conditions[z].type == ConditionType::NONE) { continue; } string cond_str = cond.str(s); log.debug("sorted conditions: [%zu] => %s", z, cond_str.c_str()); } } return cond_index; } void Card::apply_ap_and_tp_adjust_assists_to_attack( shared_ptr attacker_card, int16_t* inout_attacker_ap, int16_t* in_defense_power, int16_t* inout_attacker_tp) const { auto s = this->server(); bool is_nte = s->options.is_nte(); uint8_t client_id = attacker_card->get_client_id(); size_t num_assists = s->assist_server->compute_num_assist_effects_for_client(client_id); for (size_t z = 0; z < num_assists; z++) { switch (s->assist_server->get_active_assist_by_index(z)) { case AssistEffect::POWERLESS_RAIN: if (is_nte) { *inout_attacker_ap = max(*inout_attacker_ap - 2, 0); } break; case AssistEffect::BRAVE_WIND: if (is_nte) { *inout_attacker_ap = max(*inout_attacker_ap + 2, 0); } break; case AssistEffect::SILENT_COLOSSEUM: if (*inout_attacker_ap - *in_defense_power >= 7) { *inout_attacker_ap = 0; } break; case AssistEffect::INFLUENCE: if (is_nte) { *inout_attacker_ap += attacker_card->player_state()->count_set_cards_for_env_stats_nte(); } break; case AssistEffect::FIX: if (!is_nte && attacker_card && !attacker_card->def_entry->def.is_sc()) { *inout_attacker_ap = 2; } break; default: break; } } num_assists = s->assist_server->compute_num_assist_effects_for_client(this->client_id); for (size_t z = 0; z < num_assists; z++) { auto eff = s->assist_server->get_active_assist_by_index(z); if (eff == AssistEffect::AP_ABSORPTION) { if (is_nte) { if (attacker_card->action_chain.chain.attack_medium == AttackMedium::TECH) { *inout_attacker_ap = *inout_attacker_ap + 2; } else if (attacker_card->action_chain.chain.attack_medium == AttackMedium::PHYSICAL) { *inout_attacker_tp = *inout_attacker_ap; *inout_attacker_ap = 0; } } else if (attacker_card->action_chain.chain.attack_medium == AttackMedium::PHYSICAL) { *inout_attacker_ap = 0; } } } } bool Card::card_type_is_sc_or_creature() const { return (this->def_entry->def.type == CardType::HUNTERS_SC) || (this->def_entry->def.type == CardType::ARKZ_SC) || (this->def_entry->def.type == CardType::CREATURE); } bool Card::check_card_flag(uint32_t flags) const { return this->card_flags & flags; } void Card::commit_attack( int16_t damage, shared_ptr attacker_card, G_ApplyConditionEffect_Ep3_6xB4x06* cmd, size_t strike_number, int16_t* out_effective_damage) { auto s = this->server(); auto log = s->log_stack(string_printf("commit_attack(@%04hX #%04hX, @%04hX #%04hX => %hd (str%zu)): ", this->get_card_ref(), this->get_card_id(), attacker_card->get_card_ref(), attacker_card->get_card_id(), damage, strike_number)); bool is_nte = s->options.is_nte(); int16_t effective_damage = damage; s->card_special->adjust_attack_damage_due_to_conditions( this->shared_from_this(), &effective_damage, attacker_card->get_card_ref()); log.debug("adjusted damage = %hd", effective_damage); size_t num_assists = s->assist_server->compute_num_assist_effects_for_client(this->client_id); for (size_t z = 0; z < num_assists; z++) { auto eff = s->assist_server->get_active_assist_by_index(z); if ((eff == AssistEffect::RANSOM) && (attacker_card->action_chain.chain.attack_medium == AttackMedium::PHYSICAL)) { uint8_t team_id = this->player_state()->get_team_id(); int16_t exp_amount = clamp(s->team_exp[team_id], 0, effective_damage); s->team_exp[team_id] -= exp_amount; effective_damage -= exp_amount; if (!is_nte) { s->compute_team_dice_bonus(team_id); s->update_battle_state_flags_and_send_6xB4x03_if_needed(); if (cmd) { cmd->effect.ap += exp_amount; } } } } log.debug("after assists = %hd", effective_damage); if (this->action_metadata.check_flag(0x10)) { effective_damage = 0; log.debug("flag 0x10 => effective damage = %hd", effective_damage); } auto attacker_ps = attacker_card->player_state(); attacker_ps->stats.damage_given += effective_damage; this->player_state()->stats.damage_taken += effective_damage; log.debug("updated stats"); this->current_hp = clamp(this->current_hp - effective_damage, 0, this->max_hp); log.debug("hp set to %hd", this->current_hp); if ((effective_damage > 0) && (attacker_ps->stats.max_attack_damage < effective_damage)) { attacker_ps->stats.max_attack_damage = effective_damage; log.debug("attacker new max damage %hd", effective_damage); } this->last_attack_final_damage = effective_damage; log.debug("last attack final damage = %hd", effective_damage); if (effective_damage > 0) { this->card_flags = this->card_flags | 4; log.debug("set flag 4"); } if (this->current_hp < 1) { this->destroy_set_card(attacker_card); log.debug("card destroyed"); } G_ApplyConditionEffect_Ep3_6xB4x06 cmd_to_send; if (cmd) { cmd_to_send = *cmd; } cmd_to_send.effect.flags = (strike_number == 0) ? 0x11 : 0x01; cmd_to_send.effect.attacker_card_ref = attacker_card->card_ref; cmd_to_send.effect.target_card_ref = this->card_ref; cmd_to_send.effect.value = effective_damage; s->send(cmd_to_send); this->propagate_shared_hp_if_needed(); if (!is_nte && this->def_entry->def.is_sc()) { this->player_state()->stats.sc_damage_taken += effective_damage; } if (out_effective_damage) { *out_effective_damage = effective_damage; } } int16_t Card::compute_defense_power_for_attacker_card(shared_ptr attacker_card) { if (!attacker_card) { return 0; } this->action_metadata.defense_power = 0; this->action_metadata.defense_bonus = 0; auto s = this->server(); for (size_t z = 0; z < this->action_metadata.defense_card_ref_count; z++) { if (attacker_card->card_ref != this->action_metadata.original_attacker_card_refs[z]) { continue; } auto ce = s->definition_for_card_ref(this->action_metadata.defense_card_refs[z]); if (ce) { this->action_metadata.defense_power += ce->def.hp.stat; } } s->card_special->apply_action_conditions(3, attacker_card, this->shared_from_this(), 0x08, nullptr); s->card_special->apply_action_conditions(3, attacker_card, this->shared_from_this(), 0x10, nullptr); return this->action_metadata.defense_power + this->action_metadata.defense_bonus; } void Card::destroy_set_card(shared_ptr attacker_card) { auto s = this->server(); auto ps = this->player_state(); this->current_hp = 0; if (!(this->card_flags & 2)) { if (!s->ruler_server->card_ref_or_any_set_card_has_condition_46(this->card_ref)) { s->card_special->on_card_destroyed(attacker_card, this->shared_from_this()); this->card_flags = this->card_flags | 2; this->update_stats_on_destruction(); ps->stats.num_owned_cards_destroyed++; if (attacker_card && (attacker_card->team_id != this->team_id)) { attacker_card->player_state()->stats.num_opponent_cards_destroyed++; s->add_team_exp(this->team_id ^ 1, 3); } if ((this->sc_card_type == CardType::HUNTERS_SC) && (this->def_entry->def.type == CardType::ITEM)) { auto sc_card = ps->get_sc_card(); if (!(sc_card->card_flags & 2) && !sc_card->get_condition_value(ConditionType::ELUDE)) { int16_t hp = sc_card->get_current_hp(); sc_card->set_current_hp(hp - 1); sc_card->player_state()->stats.sc_damage_taken++; if (attacker_card && (attacker_card->team_id != this->team_id)) { G_ApplyConditionEffect_Ep3_6xB4x06 cmd; cmd.effect.flags = 0x41; cmd.effect.attacker_card_ref = attacker_card->card_ref; cmd.effect.target_card_ref = sc_card->card_ref; cmd.effect.value = 1; s->send(cmd); } if (sc_card->get_current_hp() < 1) { sc_card->destroy_set_card(attacker_card); } } } if ((s->map_and_rules->rules.hp_type == HPType::DEFEAT_TEAM) && (ps->get_sc_card().get() == this)) { for (size_t set_index = 0; set_index < 8; set_index++) { auto card = ps->get_set_card(set_index); if (card) { card->card_flags |= 2; } } } for (size_t client_id = 0; client_id < 4; client_id++) { if (!s->player_states[client_id]) { continue; } size_t num_assists = s->assist_server->compute_num_assist_effects_for_client(client_id); for (size_t z = 0; z < num_assists; z++) { auto eff = s->assist_server->get_active_assist_by_index(z); if (eff == AssistEffect::HOMESICK) { if (client_id == this->client_id) { ps->return_set_card_to_hand2(this->card_ref); } } else if (eff == AssistEffect::INHERITANCE) { uint8_t other_team_id = s->player_states[client_id]->get_team_id(); uint8_t this_team_id = ps->get_team_id(); if (this_team_id == other_team_id) { s->add_team_exp(team_id, this->max_hp); } } } } } else if (!this->w_destroyer_sc_card.lock() && attacker_card) { this->w_destroyer_sc_card = attacker_card->player_state()->get_sc_card(); } } } int32_t Card::error_code_for_move_to_location(const Location& loc) const { // TODO: NTE has different logic here, which appears to be similar enough to // the final logic that I didn't bother to reverse-engineer it completely. // Eventually, we should revisit this, but I suspect doing so would be a lot // of tedium for little to no benefit. if (this->player_state()->assist_flags & AssistFlag::IS_SKIPPING_TURN) { return -0x76; } if (this->card_flags & 2) { return -0x60; } if (!this->server()->ruler_server->card_ref_can_move( this->client_id, this->card_ref, 1)) { return -0x7B; } // Note: The original code passes non-null pointers here but ignores the // values written to them; we use nulls since the behavior should be the same. if (!this->server()->ruler_server->get_move_path_length_and_cost( this->client_id, this->card_ref, loc, nullptr, nullptr)) { return -0x79; } return 0; } void Card::execute_attack(shared_ptr attacker_card) { if (!attacker_card) { return; } auto s = this->server(); auto log = s->log_stack(string_printf("execute_attack(@%04X #%04X, @%04X #%04X): ", this->get_card_ref(), this->get_card_id(), attacker_card->get_card_ref(), attacker_card->get_card_id())); bool is_nte = s->options.is_nte(); this->card_flags &= 0xFFFFFFF3; int16_t attack_ap = this->action_metadata.attack_bonus; int16_t attack_tp = 0; int16_t defense_power = is_nte ? 0 : this->compute_defense_power_for_attacker_card(attacker_card); log.debug("ap=%hd, tp=%hd", attack_ap, attack_tp); if (!is_nte && (attack_ap == 0) && !this->action_metadata.check_flag(0x20)) { log.debug("ap == 0 and flag 0x20 not set"); return; } else { log.debug("ap != 0 or flag 0x20 set; continuing..."); } G_ApplyConditionEffect_Ep3_6xB4x06 cmd; cmd.effect.flags = 0x01; cmd.effect.attacker_card_ref = attacker_card->card_ref; cmd.effect.target_card_ref = this->card_ref; if (attacker_card->action_chain.chain.attack_medium == AttackMedium::UNKNOWN_03) { // Probably Resta for (size_t strike_num = 0; strike_num < attacker_card->action_chain.chain.strike_count; strike_num++) { this->current_hp = min( this->current_hp + attacker_card->action_chain.chain.effective_tp, this->max_hp); } this->propagate_shared_hp_if_needed(); cmd.effect.tp = attacker_card->action_chain.chain.effective_tp; cmd.effect.value = -cmd.effect.tp; s->send(cmd); } else { if (is_nte) { defense_power = this->compute_defense_power_for_attacker_card(attacker_card); log.debug("ap=%hd, tp=%hd, defense=%hd", attack_ap, attack_tp, defense_power); attacker_card->compute_action_chain_results(true, false); attack_ap = attacker_card->action_chain.chain.damage; if (this->action_chain.chain.attack_medium == AttackMedium::TECH) { attack_ap += this->action_chain.chain.tech_attack_bonus_nte; } else if (this->action_chain.chain.attack_medium == AttackMedium::PHYSICAL) { attack_ap += this->action_chain.chain.physical_attack_bonus_nte; } } s->card_special->compute_attack_ap(this->shared_from_this(), &attack_ap, attacker_card->get_card_ref()); log.debug("computed ap %hd", attack_ap); this->apply_ap_and_tp_adjust_assists_to_attack(attacker_card, &attack_ap, &defense_power, &attack_tp); log.debug("assist adjusts ap=%hd, defense=%hd", attack_ap, defense_power); int16_t raw_damage = attack_ap - defense_power; int16_t preliminary_damage = max(raw_damage, 0) - attack_tp; this->last_attack_preliminary_damage = preliminary_damage; log.debug("raw_damage=%hd, preliminary_damange=%hd", raw_damage, preliminary_damage); uint32_t unknown_a9 = 0; auto target = s->card_special->compute_replaced_target_based_on_conditions( this->get_card_ref(), 1, 0, attacker_card->get_card_ref(), 0xFFFF, 0, &unknown_a9, 0xFF, nullptr, 0xFFFF); if (!target) { target = this->shared_from_this(); log.debug("target is not replaced"); } else { log.debug("target replaced with @%04hX #%04hX", target->get_card_ref(), target->get_card_id()); } if (!is_nte) { if (unknown_a9 != 0) { preliminary_damage = 0; log.debug("a9 nonzero; preliminary_damage = 0"); } if (!(this->card_flags & 2) && (!attacker_card || !(attacker_card->card_flags & 2))) { s->card_special->check_for_defense_interference(attacker_card, this->shared_from_this(), &preliminary_damage); log.debug("checked for defense interference"); } } cmd.effect.current_hp = is_nte ? attack_ap : min(attack_ap, 99); cmd.effect.ap = is_nte ? defense_power : min(defense_power, 99); cmd.effect.tp = attack_tp; auto ps = this->player_state(); ps->stats.num_attacks_taken++; if (!(target->card_flags & 2)) { log.debug("flag 2 not set"); for (size_t strike_num = 0; strike_num < attacker_card->action_chain.chain.strike_count; strike_num++) { int16_t final_effective_damage = 0; target->commit_attack(preliminary_damage, attacker_card, &cmd, strike_num, &final_effective_damage); ps->stats.action_card_negated_damage += max(0, this->current_defense_power - final_effective_damage); } } else { log.debug("flag 2 set; committing zero-damage attack"); target->commit_attack(0, attacker_card, &cmd, 0, nullptr); } if (!is_nte && (this != target.get())) { log.debug("target was replaced; committing zero-damage attack on original card"); this->commit_attack(0, attacker_card, &cmd, 0, nullptr); } s->send_6xB4x39(); } } bool Card::get_condition_value( ConditionType cond_type, uint16_t card_ref, uint8_t def_effect_index, uint16_t value, uint16_t* out_value) const { return this->action_chain.get_condition_value(cond_type, card_ref, def_effect_index, value, out_value); } Condition* Card::find_condition(ConditionType cond_type) { for (size_t z = 0; z < this->action_chain.conditions.size(); z++) { auto& cond = this->action_chain.conditions[z]; if (cond.type == cond_type) { return &cond; } } return nullptr; } const Condition* Card::find_condition(ConditionType cond_type) const { return const_cast(this)->find_condition(cond_type); } shared_ptr Card::get_definition() const { return this->def_entry; } uint16_t Card::get_card_ref() const { return this->card_ref; } uint16_t Card::get_card_id() const { return this->get_definition()->def.card_id; } uint8_t Card::get_client_id() const { return this->client_id; } uint8_t Card::get_current_hp() const { return this->current_hp; } uint8_t Card::get_max_hp() const { return this->max_hp; } CardShortStatus Card::get_short_status() { CardShortStatus ret; if (this->def_entry->def.type == CardType::ITEM) { this->loc = this->player_state()->get_sc_card()->loc; } ret.card_ref = this->card_ref; ret.current_hp = this->current_hp; ret.max_hp = this->max_hp; ret.card_flags = this->card_flags; ret.loc = this->loc; return ret; } uint8_t Card::get_team_id() const { return this->team_id; } int32_t Card::move_to_location(const Location& loc) { auto s = this->server(); int32_t code = this->error_code_for_move_to_location(loc); if (code) { return code; } uint32_t path_cost; uint32_t path_length; if (!s->ruler_server->get_move_path_length_and_cost( this->client_id, this->card_ref, loc, &path_length, &path_cost)) { return -0x79; } this->player_state()->stats.total_move_distance += path_length; this->player_state()->subtract_atk_points(path_cost); this->loc = loc; this->card_flags = this->card_flags | 0x80; // On NTE, traps happen now, not after the Move phase if (s->options.is_nte() && this->def_entry->def.is_sc() && ((s->overlay_state.tiles[loc.y][loc.x] & 0xF0) == 0x40)) { for (size_t z = 0; z < 4; z++) { auto other_ps = s->player_states[z]; if (!other_ps) { continue; } auto other_sc = other_ps->get_sc_card(); if (!other_sc) { continue; } if ((abs(other_sc->loc.x - loc.x) < 2) && (abs(other_sc->loc.y - loc.y) < 2)) { uint8_t trap_type = s->overlay_state.tiles[loc.y][loc.x] & 0x0F; uint16_t trap_card_id = s->overlay_state.trap_card_ids_nte[trap_type]; if (other_ps->replace_assist_card_by_id(trap_card_id)) { G_Unknown_Ep3_6xB4x2C cmd; cmd.change_type = 1; cmd.client_id = other_ps->client_id; cmd.unknown_a2[0] = trap_card_id; s->send(cmd); } } } } for (size_t warp_type = 0; warp_type < 5; warp_type++) { for (size_t warp_end = 0; warp_end < 2; warp_end++) { if ((s->warp_positions[warp_type][warp_end][0] == this->loc.x) && (s->warp_positions[warp_type][warp_end][1] == this->loc.y)) { G_Unknown_Ep3_6xB4x2C cmd; cmd.loc.x = this->loc.x; cmd.loc.y = this->loc.y; this->loc.x = s->warp_positions[warp_type][warp_end ^ 1][0]; this->loc.y = s->warp_positions[warp_type][warp_end ^ 1][1]; cmd.change_type = 0; cmd.card_refs.clear(0xFFFF); cmd.card_refs[0] = this->card_ref; cmd.unknown_a2.clear(0xFFFFFFFF); s->send(cmd); return 0; } } } return 0; } void Card::propagate_shared_hp_if_needed() { if ((this->server()->map_and_rules->rules.hp_type == HPType::COMMON_HP) && ((this->def_entry->def.type == CardType::HUNTERS_SC) || (this->def_entry->def.type == CardType::ARKZ_SC))) { for (size_t other_client_id = 0; other_client_id < 4; other_client_id++) { auto other_ps = this->server()->player_states[other_client_id]; if ((other_client_id != this->client_id) && other_ps && (other_ps->get_team_id() == this->team_id)) { other_ps->get_sc_card()->set_current_hp(this->current_hp, false); } } } } void Card::send_6xB4x4E_4C_4D_if_needed(bool always_send) { auto ps = this->player_state(); ssize_t index = -1; if (this->card_ref == ps->get_sc_card_ref()) { index = 0; } else { for (size_t set_index = 0; set_index < 8; set_index++) { if (this->card_ref == ps->get_set_ref(set_index)) { index = set_index + 1; break; } } } if (index < 0) { return; } this->action_chain.chain.card_ap = this->ap; this->action_chain.chain.card_tp = this->tp; auto& chain = ps->set_card_action_chains->at(index); auto& metadata = ps->set_card_action_metadatas->at(index); auto s = this->server(); if (s->options.is_nte()) { chain = this->action_chain; metadata = this->action_metadata; G_UpdateActionChainAndMetadata_Ep3NTE_6xB4x0A cmd; cmd.client_id = this->client_id; cmd.index = index; cmd.chain = this->action_chain; cmd.metadata = this->action_metadata; s->send(cmd); } else { this->send_6xB4x4E_if_needed(always_send); if (always_send || (chain != this->action_chain)) { chain = this->action_chain; if (!s->get_should_copy_prev_states_to_current_states()) { G_UpdateActionChain_Ep3_6xB4x4C cmd; cmd.client_id = this->client_id; cmd.index = index; cmd.chain = this->action_chain.chain; s->send(cmd); } } if (always_send || (metadata != this->action_metadata)) { metadata = this->action_metadata; G_UpdateActionMetadata_Ep3_6xB4x4D cmd; cmd.client_id = this->client_id; cmd.index = index; cmd.metadata = this->action_metadata; s->send(cmd); } } } void Card::send_6xB4x4E_if_needed(bool always_send) { ssize_t chain_index = -1; if (this->card_ref == this->player_state()->get_sc_card_ref()) { chain_index = 0; } else { for (size_t set_index = 0; set_index < 8; set_index++) { if (this->card_ref == this->player_state()->get_set_ref(set_index)) { chain_index = set_index + 1; break; } } } if (chain_index >= 0) { auto& prev_conds = this->player_state()->set_card_action_chains->at(chain_index).conditions; const auto& curr_conds = this->action_chain.conditions; if ((prev_conds != curr_conds) || (always_send != 0)) { prev_conds = curr_conds; if (!this->server()->get_should_copy_prev_states_to_current_states()) { G_UpdateCardConditions_Ep3_6xB4x4E cmd; cmd.client_id = this->client_id; cmd.index = chain_index; cmd.conditions = this->action_chain.conditions; this->server()->send(cmd); } } } } void Card::set_current_and_max_hp(int16_t hp) { this->current_hp = hp; this->max_hp = hp; } void Card::set_current_hp( uint32_t new_hp, bool propagate_shared_hp, bool enforce_max_hp) { if (!enforce_max_hp) { new_hp = max(new_hp, 0); this->max_hp = max(this->max_hp, new_hp); } else { new_hp = clamp(new_hp, 0, this->max_hp); } this->current_hp = new_hp; this->player_state()->update_hand_and_equip_state_and_send_6xB4x02_if_needed(); if (propagate_shared_hp) { this->propagate_shared_hp_if_needed(); } } void Card::update_stats_on_destruction() { auto s = this->server(); this->player_state()->num_destroyed_fcs++; s->team_num_ally_fcs_destroyed[this->team_id]++; s->team_num_cards_destroyed[this->team_id]++; for (size_t client_id = 0; client_id < 4; client_id++) { auto other_ps = s->player_states[client_id]; if (other_ps && (other_ps->get_team_id() == this->team_id)) { auto card = other_ps->get_sc_card(); if (card) { card->num_destroyed_ally_fcs++; } for (size_t set_index = 0; set_index < 8; set_index++) { card = other_ps->get_set_card(set_index); if (card) { card->num_destroyed_ally_fcs++; } } } } } void Card::clear_action_chain_and_metadata_and_most_flags() { this->card_flags &= 0x8000FA7F; this->action_chain.clear_inner(); this->action_chain.chain.acting_card_ref = this->card_ref; this->action_metadata.clear(); this->action_metadata.card_ref = this->card_ref; } void Card::compute_action_chain_results(bool apply_action_conditions, bool ignore_this_card_ap_tp) { auto s = this->server(); auto log = s->log_stack(string_printf("compute_action_chain_results(@%04hX #%04hX): ", this->get_card_ref(), this->get_card_id())); bool is_nte = s->options.is_nte(); this->action_chain.compute_attack_medium(s); this->action_chain.chain.strike_count = 1; this->action_chain.chain.ap_effect_bonus = 0; this->action_chain.chain.tp_effect_bonus = 0; log.debug("(initial) medium=%s, strike_count=%hhu, ap_effect_bonus=%hhd, tp_effect_bonus=%hhd", name_for_enum(this->action_chain.chain.attack_medium), this->action_chain.chain.strike_count, this->action_chain.chain.ap_effect_bonus, this->action_chain.chain.tp_effect_bonus); int16_t effective_ap; int16_t effective_tp; StatSwapType stat_swap_type; if (is_nte) { effective_ap = this->ap; effective_tp = this->tp; stat_swap_type = StatSwapType::NONE; } else { stat_swap_type = s->card_special->compute_stat_swap_type(this->shared_from_this()); log.debug("stat_swap_type = %zu (0=none, 1=a/t, 2=a/h)", static_cast(stat_swap_type)); s->card_special->get_effective_ap_tp(stat_swap_type, &effective_ap, &effective_tp, this->get_current_hp(), this->ap, this->tp); log.debug("effective_ap = %hd, effective_tp = %hd", effective_ap, effective_tp); } // This option doesn't exist in NTE ignore_this_card_ap_tp &= !is_nte; for (size_t z = 0; (!ignore_this_card_ap_tp && (z < 8) && (z < this->action_chain.chain.attack_action_card_ref_count)); z++) { auto ce = s->definition_for_card_ref(this->action_chain.chain.attack_action_card_refs[z]); if (ce) { effective_ap += ce->def.ap.stat; effective_tp += ce->def.tp.stat; log.debug("(action card @%04hX) updated effective_ap = %hd, effective_tp = %hd", this->action_chain.chain.attack_action_card_refs[z].load(), effective_ap, effective_tp); } } // Add AP/TP from MAG items to SC's AP/TP auto ps = this->player_state(); if (this->def_entry->def.is_sc()) { for (size_t set_index = 0; set_index < 8; set_index++) { auto card = ps->get_set_card(set_index); if ((card && (card->def_entry->def.card_class() == CardClass::MAG_ITEM)) && !(card->card_flags & 2)) { int16_t card_ap, card_tp; s->card_special->get_effective_ap_tp( stat_swap_type, &card_ap, &card_tp, card->get_current_hp(), card->ap, card->tp); effective_ap += card_ap; effective_tp += card_tp; log.debug("(mag card set_index %zu @%04hX) updated effective_ap = %hd, effective_tp = %hd", set_index, card->get_card_ref(), effective_ap, effective_tp); } } } if ((this->def_entry->def.type == CardType::ITEM) && this->sc_def_entry) { auto sc_card = ps->get_sc_card(); sc_card->compute_action_chain_results(apply_action_conditions, true); effective_ap += sc_card->action_chain.chain.effective_ap + sc_card->action_chain.chain.ap_effect_bonus; effective_tp += sc_card->action_chain.chain.effective_tp + sc_card->action_chain.chain.tp_effect_bonus; log.debug("(item is attacking; adding SC stats) updated effective_ap = %hd, effective_tp = %hd", effective_ap, effective_tp); } if (!this->action_chain.check_flag(0x10)) { this->action_chain.chain.effective_ap = is_nte ? effective_ap : min(effective_ap, 99); log.debug("set chain effective_ap = %hd", this->action_chain.chain.effective_ap); } if (!this->action_chain.check_flag(0x20)) { this->action_chain.chain.effective_tp = is_nte ? effective_tp : min(effective_tp, 99); log.debug("set chain effective_tp = %hd", this->action_chain.chain.effective_tp); } if (apply_action_conditions) { auto this_sh = this->shared_from_this(); s->card_special->apply_action_conditions(3, this_sh, this_sh, 1, nullptr); log.debug("applied action conditions (1)"); } else { log.debug("skipped applying action conditions (1)"); } size_t num_assists = s->assist_server->compute_num_assist_effects_for_client(this->client_id); for (size_t z = 0; z < num_assists; z++) { switch (s->assist_server->get_active_assist_by_index(z)) { case AssistEffect::POWERLESS_RAIN: if (!is_nte && this->card_type_is_sc_or_creature() && (this->action_chain.chain.attack_medium == AttackMedium::PHYSICAL)) { this->action_chain.chain.ap_effect_bonus -= 2; } break; case AssistEffect::BRAVE_WIND: if (!is_nte && this->card_type_is_sc_or_creature() && (this->action_chain.chain.attack_medium == AttackMedium::PHYSICAL)) { this->action_chain.chain.ap_effect_bonus += 2; } break; case AssistEffect::INFLUENCE: if (!is_nte && this->card_type_is_sc_or_creature()) { int16_t count = ps->count_set_refs(); this->action_chain.chain.ap_effect_bonus += (count >> 1); } break; case AssistEffect::AP_ABSORPTION: if (!is_nte && (this->action_chain.chain.attack_medium == AttackMedium::TECH)) { this->action_chain.chain.tp_effect_bonus += 2; } break; case AssistEffect::FIX: if (is_nte && !this->def_entry->def.is_sc()) { this->action_chain.chain.ap_effect_bonus = 2 - this->action_chain.chain.card_ap; } break; case AssistEffect::TECH_FIELD: if (is_nte ? this->def_entry->def.is_sc() : this->card_type_is_sc_or_creature()) { this->action_chain.chain.tp_effect_bonus += 2; } break; case AssistEffect::FOREST_RAIN: if (this->def_entry->def.card_class() == CardClass::NATIVE_CREATURE) { this->action_chain.chain.ap_effect_bonus += 2; } break; case AssistEffect::CAVE_WIND: if (this->def_entry->def.card_class() == CardClass::A_BEAST_CREATURE) { this->action_chain.chain.ap_effect_bonus += 2; } break; case AssistEffect::MINE_BRIGHTNESS: if (this->def_entry->def.card_class() == CardClass::MACHINE_CREATURE) { this->action_chain.chain.ap_effect_bonus += 2; } break; case AssistEffect::RUIN_DARKNESS: if (this->def_entry->def.card_class() == CardClass::DARK_CREATURE) { this->action_chain.chain.ap_effect_bonus += 2; } break; case AssistEffect::SABER_DANCE: if (this->def_entry->def.card_class() == CardClass::SWORD_ITEM) { this->action_chain.chain.ap_effect_bonus += 2; } break; case AssistEffect::BULLET_STORM: if (this->def_entry->def.card_class() == CardClass::GUN_ITEM) { this->action_chain.chain.ap_effect_bonus += 2; } break; case AssistEffect::CANE_PALACE: if (this->def_entry->def.card_class() == CardClass::CANE_ITEM) { this->action_chain.chain.tp_effect_bonus += 2; } break; case AssistEffect::GIANT_GARDEN: if (!this->def_entry->def.is_sc() && (this->def_entry->def.self_cost > 3)) { this->action_chain.chain.ap_effect_bonus += 2; } break; case AssistEffect::MARCH_OF_THE_MEEK: if (!this->def_entry->def.is_sc() && (this->def_entry->def.self_cost <= 3)) { this->action_chain.chain.ap_effect_bonus += 2; } break; case AssistEffect::SUPPORT: if (this->def_entry->def.is_sc()) { size_t num_scs_in_range = 0; for (size_t client_id = 0; client_id < 4; client_id++) { auto other_ps = s->get_player_state(client_id); if (!other_ps || (client_id == this->client_id) || (other_ps->get_team_id() != this->team_id)) { continue; } auto other_sc_card = other_ps->get_sc_card(); if (other_sc_card && (abs(this->loc.x - other_sc_card->loc.x) < 2) && (abs(this->loc.y - other_sc_card->loc.y) < 2)) { num_scs_in_range++; } } if (num_scs_in_range > 0) { this->action_chain.chain.ap_effect_bonus += 3; } } break; case AssistEffect::VENGEANCE: if (!this->def_entry->def.is_sc()) { size_t denom = is_nte ? 2 : 3; this->action_chain.chain.ap_effect_bonus += (s->team_num_ally_fcs_destroyed[this->team_id] / denom); } break; default: break; } } int16_t damage = 0; if (this->action_chain.chain.attack_medium == AttackMedium::TECH) { damage = this->action_chain.chain.effective_tp + this->action_chain.chain.tp_effect_bonus; log.debug("(tech) damage = %hhd (eff) + %hhd (bonus) = %hd", this->action_chain.chain.effective_tp, this->action_chain.chain.tp_effect_bonus, damage); } else if (this->action_chain.chain.attack_medium == AttackMedium::PHYSICAL) { damage = this->action_chain.chain.effective_ap + this->action_chain.chain.ap_effect_bonus; log.debug("(physical) damage = %hhd (eff) + %hhd (bonus) = %hd", this->action_chain.chain.effective_ap, this->action_chain.chain.ap_effect_bonus, damage); } else { log.debug("(unknown attack medium) damage = 0"); } this->action_chain.chain.damage = is_nte ? (damage * this->action_chain.chain.damage_multiplier) : min(damage * this->action_chain.chain.damage_multiplier, 99); log.debug("overall chain damage = %hd (base) * %hhd (mult) = %hhd", damage, this->action_chain.chain.damage_multiplier, this->action_chain.chain.damage); if (apply_action_conditions) { auto this_sh = this->shared_from_this(); s->card_special->apply_action_conditions(0x03, this_sh, this_sh, 2, nullptr); log.debug("applied action conditions (2)"); if (!is_nte && this->action_chain.check_flag(0x100)) { this->action_chain.chain.damage = min(this->action_chain.chain.damage + 5, 99); log.debug("(has flag 0x100) chain damage = %hhd", this->action_chain.chain.damage); } } else { log.debug("skipped applying action conditions (2)"); } if (!is_nte) { num_assists = s->assist_server->compute_num_assist_effects_for_client(this->get_client_id()); for (size_t z = 0; z < num_assists; z++) { switch (s->assist_server->get_active_assist_by_index(z)) { case AssistEffect::AP_ABSORPTION: if (this->action_chain.chain.attack_medium == AttackMedium::PHYSICAL) { this->action_chain.chain.damage = 0; } break; case AssistEffect::SILENT_COLOSSEUM: if (this->action_chain.chain.damage >= 7) { this->action_chain.chain.damage = 0; } break; case AssistEffect::FIX: if (!this->def_entry->def.is_sc()) { this->action_chain.chain.damage = 2; } break; default: break; } } } } void Card::unknown_802380C0() { bool is_nte = this->server()->options.is_nte(); this->card_flags &= 0xFFFFF7FB; this->action_metadata.clear_flags(is_nte ? 0x10 : 0x30); this->action_chain.clear_flags(is_nte ? 0x40 : 0x140); this->unknown_80237F98(0); } void Card::unknown_80237F98(bool require_condition_20_or_21) { auto s = this->server(); bool should_send_updates = false; for (ssize_t z = 8; z >= 0; z--) { if (this->action_chain.conditions[z].type != ConditionType::NONE) { if (!require_condition_20_or_21 || s->card_special->condition_has_when_20_or_21(this->action_chain.conditions[z])) { ActionState as; auto& cond = this->action_chain.conditions[z]; if (!s->card_special->is_card_targeted_by_condition(cond, as, this->shared_from_this())) { s->card_special->apply_stat_deltas_to_card_from_condition_and_clear_cond(cond, this->shared_from_this()); should_send_updates = true; } else if (this->action_chain.conditions[z].remaining_turns == 0) { if (--this->action_chain.conditions[z].a_arg_value < 1) { s->card_special->apply_stat_deltas_to_card_from_condition_and_clear_cond(cond, this->shared_from_this()); should_send_updates = true; } } } } } this->compute_action_chain_results(1, false); if (!s->options.is_nte()) { this->unknown_80236554(nullptr, nullptr); } if (should_send_updates) { this->send_6xB4x4E_4C_4D_if_needed(); } } void Card::unknown_80237F88() { this->card_flags &= 0xFFFFF8FF; } void Card::draw_phase_before() { if (!this->server()->options.is_nte()) { this->facing_direction = Direction::INVALID_FF; } this->server()->card_special->draw_phase_before_for_card(this->shared_from_this()); } void Card::action_phase_before() { if (!this->server()->options.is_nte()) { this->clear_action_chain_and_metadata_and_most_flags(); } this->server()->card_special->action_phase_before_for_card(this->shared_from_this()); } void Card::move_phase_before() { this->server()->card_special->move_phase_before_for_card(this->shared_from_this()); } void Card::unknown_80236374(shared_ptr other_card, const ActionState* as) { auto log = this->server()->log_stack(string_printf("unknown_80236374(@%04hX #%04hX, @%04hX #%04hX): ", this->get_card_ref(), this->get_card_id(), other_card->get_card_ref(), other_card->get_card_id())); auto check_card = [&](shared_ptr card) -> void { if (card) { if (!card->unknown_80236554(other_card, as)) { card->action_metadata.clear_flags(0x20); } else { card->action_metadata.set_flags(0x20); } } }; for (size_t client_id = 0; client_id < 4; client_id++) { auto ps = this->server()->player_states[client_id]; if (ps) { if (this->server()->get_current_team_turn2() != ps->get_team_id()) { check_card(ps->get_sc_card()); for (size_t set_index = 0; set_index < 8; set_index++) { check_card(ps->get_set_card(set_index)); } } } } for (size_t client_id = 0; client_id < 4; client_id++) { auto ps = this->server()->player_states[client_id]; if (ps) { if (this->server()->get_current_team_turn2() == ps->get_team_id()) { check_card(ps->get_sc_card()); for (size_t set_index = 0; set_index < 8; set_index++) { check_card(ps->get_set_card(set_index)); } } } } } void Card::unknown_802379BC(uint16_t card_ref) { this->action_chain.chain.unknown_card_ref_a3 = (card_ref == 0xFFFF) ? this->card_ref : card_ref; } void Card::unknown_802379DC(const ActionState& pa) { this->action_metadata.add_defense_card_ref( pa.defense_card_ref, this->shared_from_this(), pa.original_attacker_card_ref); this->server()->card_special->unknown_8024A9D8(pa, 0xFFFF); for (size_t z = 0; z < this->action_metadata.target_card_ref_count; z++) { shared_ptr card = this->server()->card_for_set_card_ref(this->action_metadata.target_card_refs[z]); if (card) { card->action_chain.set_action_subphase_from_card(this->shared_from_this()); card->send_6xB4x4E_4C_4D_if_needed(); } } this->send_6xB4x4E_4C_4D_if_needed(); } void Card::unknown_80237A90(const ActionState& pa, uint16_t action_card_ref) { auto s = this->server(); this->facing_direction = pa.facing_direction; this->action_chain.add_attack_action_card_ref(action_card_ref, s); if (s->options.is_nte()) { if (s->ruler_server->count_targets_with_rampage_and_not_pierce_nte(pa)) { this->action_chain.set_flags(0x02); } if (s->ruler_server->count_targets_with_pierce_and_not_rampage_nte(pa)) { this->action_chain.set_flags(0x80); } } else { for (size_t z = 0; z < 4; z++) { if (s->ruler_server->count_rampage_targets_for_attack(pa, z) != 0) { this->action_chain.set_flags(0x200 << z); } if (s->ruler_server->attack_action_has_pierce_and_not_rampage(pa, z)) { this->action_chain.set_flags(0x2000 << z); } } if (s->ruler_server->any_attack_action_card_is_support_tech_or_support_pb(pa)) { this->action_chain.set_flags(0x20000); } } if (this->action_chain.chain.target_card_ref_count == 0) { for (size_t z = 0; (z < 4 * 9) && (pa.target_card_refs[z] != 0xFFFF); z++) { this->action_chain.add_target_card_ref(pa.target_card_refs[z]); shared_ptr sc_card = s->card_for_set_card_ref(pa.target_card_refs[z]); if (sc_card) { sc_card->action_metadata.add_target_card_ref(this->card_ref); sc_card->card_flags |= 8; sc_card->send_6xB4x4E_4C_4D_if_needed(); } } } if (this->action_chain.chain.attack_number & 0x80) { this->action_chain.chain.attack_number = s->num_pending_attacks_with_cards; s->attack_cards[s->num_pending_attacks_with_cards] = this->shared_from_this(); s->pending_attacks_with_cards[s->num_pending_attacks_with_cards] = pa; s->num_pending_attacks_with_cards++; } s->card_special->unknown_8024A9D8(pa, action_card_ref); this->send_6xB4x4E_4C_4D_if_needed(); } void Card::dice_phase_before() { auto s = this->server(); this->unknown_a9++; for (ssize_t z = 8; z >= 0; z--) { auto& cond = this->action_chain.conditions[z]; if (cond.type != ConditionType::NONE) { ActionState as; if ((this->card_flags & 2) || !s->card_special->is_card_targeted_by_condition(cond, as, this->shared_from_this())) { cond.remaining_turns = 1; } if (cond.remaining_turns < 99) { // Note: There is at least one case in the original implementation where // remaining_turns can go negative: Creinu's HP Assist. The condition is // applied with remaining_turns=0 to all affected cards (so it should be // immediately removed here). But since remaining_turns is unsigned in // our implementation, we have to check for underflow here. if (cond.remaining_turns > 0) { cond.remaining_turns--; } if (cond.remaining_turns < 1) { s->card_special->apply_stat_deltas_to_card_from_condition_and_clear_cond( cond, this->shared_from_this()); } } } } if (s->options.is_nte()) { this->clear_action_chain_and_metadata_and_most_flags(); } s->card_special->dice_phase_before_for_card(this->shared_from_this()); } bool Card::is_guard_item() const { return (this->def_entry->def.card_class() == CardClass::GUARD_ITEM); } bool Card::unknown_80236554(shared_ptr other_card, const ActionState* as) { auto s = this->server(); auto log = s->log_stack(other_card ? string_printf("unknown_80236554(@%04hX #%04hX, @%04hX #%04hX): ", this->get_card_ref(), this->get_card_id(), other_card->get_card_ref(), other_card->get_card_id()) : string_printf("unknown_80236554(@%04hX #%04hX, null): ", this->get_card_ref(), this->get_card_id())); if (as) { string as_str = as->str(s); log.debug("as = %s", as_str.c_str()); } else { log.debug("as = null"); } bool ret = false; int16_t attack_bonus = 0; if (other_card) { if (!as) { for (size_t z = 0; z < other_card->action_chain.chain.target_card_ref_count; z++) { if (other_card->action_chain.chain.target_card_refs[z] == this->get_card_ref()) { attack_bonus = other_card->action_chain.chain.damage; ret = true; log.debug("attack_bonus = %hd (matched other_card->action_chain.chain.target_card_refs)", attack_bonus); break; } } } else { for (size_t z = 0; (z < 4 * 9) && (as->target_card_refs[z] != 0xFFFF); z++) { if (as->target_card_refs[z] == this->get_card_ref()) { attack_bonus = other_card->action_chain.chain.damage; log.debug("attack_bonus = %hd (matched as->target_card_refs)", attack_bonus); ret = true; break; } } } } this->action_metadata.attack_bonus = max(attack_bonus, 0); log.debug("attack_bonus = %hhd", this->action_metadata.attack_bonus); this->last_attack_preliminary_damage = 0; this->last_attack_final_damage = 0; log.debug("last attack damage stats cleared"); if (other_card) { s->card_special->apply_action_conditions(0x03, other_card, this->shared_from_this(), 0x20, as); s->card_special->apply_action_conditions(0x17, other_card, this->shared_from_this(), 0x40, as); if (other_card->action_chain.check_flag(0x20000)) { this->action_metadata.attack_bonus = 0; return ret; } } if (this->card_flags & 2) { this->action_metadata.attack_bonus = 0; } return ret; } void Card::unknown_802362D8(shared_ptr other_card) { auto s = this->server(); for (size_t client_id = 0; client_id < 4; client_id++) { auto ps = s->player_states[client_id]; if (ps) { shared_ptr card = ps->get_sc_card(); if (card) { card->execute_attack(other_card); } for (size_t set_index = 0; set_index < 8; set_index++) { shared_ptr card = ps->get_set_card(set_index); if (card) { card->execute_attack(other_card); } } } } } void Card::apply_attack_result() { auto s = this->server(); auto ps = this->player_state(); bool is_nte = s->options.is_nte(); auto log = s->log_stack(string_printf("apply_attack_result(@%04hX #%04hX): ", this->get_card_ref(), this->get_card_id())); if (!this->action_chain.can_apply_attack()) { return; } if (is_nte) { if (this->action_chain.check_flag(0x02)) { auto first_target_card = s->card_for_set_card_ref(this->action_chain.chain.target_card_refs[0]); if (first_target_card) { auto first_target_ps = first_target_card->player_state(); if (first_target_ps->count_set_cards() == 0) { this->action_chain.clear_target_card_refs(); this->action_chain.chain.target_card_refs[0] = first_target_ps->get_sc_card_ref(); this->action_chain.chain.target_card_ref_count = 1; } } } ActionChainWithConds temp_chain = this->action_chain; temp_chain.chain.target_card_ref_count = 0; for (size_t z = 0; z < this->action_chain.chain.target_card_ref_count; z++) { auto target_card = s->card_for_set_card_ref(this->action_chain.chain.target_card_refs[z]); if (!target_card) { continue; } if (!(target_card->card_flags & 2)) { temp_chain.chain.target_card_refs[temp_chain.chain.target_card_ref_count] = target_card->get_card_ref(); temp_chain.chain.target_card_ref_count++; } else if ((target_card->get_definition()->def.type == CardType::ITEM) && !this->action_chain.check_flag(0x02)) { auto target_ps = target_card->player_state(); shared_ptr candidate_card; for (size_t set_index = 0; set_index < 8; set_index++) { auto set_card = target_ps->get_set_card(set_index); if (set_card && (set_card != target_card) && !(set_card->card_flags & 2) && set_card->is_guard_item()) { candidate_card = set_card; break; } } if (!candidate_card) { for (size_t set_index = 0; set_index < 8; set_index++) { auto set_card = target_ps->get_set_card(set_index); if (set_card && (set_card != target_card) && !(set_card->card_flags & 2)) { candidate_card = set_card; break; } } } if (candidate_card) { temp_chain.chain.target_card_refs[temp_chain.chain.target_card_ref_count] = candidate_card->get_card_ref(); temp_chain.chain.target_card_ref_count++; } else { auto target_sc = target_ps->get_sc_card(); if (!(target_sc->card_flags & 2)) { temp_chain.chain.target_card_refs[temp_chain.chain.target_card_ref_count] = target_sc->get_card_ref(); temp_chain.chain.target_card_ref_count++; } } } } this->action_chain.chain.target_card_ref_count = 0; for (size_t z = 0; z < temp_chain.chain.target_card_ref_count; z++) { this->action_chain.add_target_card_ref(temp_chain.chain.target_card_refs[z]); } if (!this->action_chain.check_flag(0x40)) { s->card_special->unknown_8024945C(this->shared_from_this(), nullptr); } for (size_t z = 0; z < this->action_chain.chain.target_card_ref_count; z++) { auto target_card = s->card_for_set_card_ref(this->action_chain.chain.target_card_refs[z]); if (target_card && !this->action_chain.check_flag(0x40)) { s->card_special->unknown_8024A6DC(this->shared_from_this(), target_card); } } this->compute_action_chain_results(true, false); if (!this->action_chain.check_flag(0x40)) { s->card_special->unknown_8024997C(this->shared_from_this()); } this->compute_action_chain_results(true, false); for (size_t z = 0; z < this->action_chain.chain.target_card_ref_count; z++) { auto target_card = s->card_for_set_card_ref(this->action_chain.chain.target_card_refs[z]); if (target_card) { target_card->execute_attack(this->shared_from_this()); } } } else { if (ps->stats.max_attack_combo_size < this->action_chain.chain.attack_action_card_ref_count) { ps->stats.max_attack_combo_size = this->action_chain.chain.attack_action_card_ref_count; } ActionState as; as.attacker_card_ref = this->get_card_ref(); as.target_card_refs = this->action_chain.chain.target_card_refs; s->replace_targets_due_to_destruction_or_conditions(&as); this->action_chain.chain.target_card_refs = as.target_card_refs; this->action_chain.chain.target_card_ref_count = 0; for (size_t z = 0; z < 4 * 9; z++) { if (this->action_chain.chain.target_card_refs[z] != 0xFFFF) { this->action_chain.chain.target_card_ref_count++; } else { break; } } for (size_t z = 0; z < this->action_chain.chain.target_card_ref_count; z++) { shared_ptr card = s->card_for_set_card_ref(this->action_chain.chain.target_card_refs[z]); if (card) { card->current_defense_power = card->action_metadata.attack_bonus; if (!this->action_chain.check_flag(0x40)) { s->card_special->unknown_8024A6DC(this->shared_from_this(), card); } } } this->compute_action_chain_results(1, 0); if (!this->action_chain.check_flag(0x40)) { s->card_special->unknown_8024997C(this->shared_from_this()); } if (!(this->card_flags & 2)) { this->compute_action_chain_results(1, 0); s->card_special->check_for_attack_interference(this->shared_from_this()); } this->compute_action_chain_results(1, 0); this->unknown_80236374(this->shared_from_this(), nullptr); this->unknown_802362D8(this->shared_from_this()); } if (!this->action_chain.check_flag(0x40)) { s->card_special->unknown_8024A394(this->shared_from_this()); } ps->stats.num_attacks_given++; this->action_chain.clear_flags(8); this->action_chain.set_flags(4); this->card_flags |= 0x200; this->action_chain.clear_target_card_refs(); if (!is_nte) { for (size_t client_id = 0; client_id < 4; client_id++) { auto ps = s->player_states[client_id]; if (ps) { ps->unknown_8023C110(); } } } this->send_6xB4x4E_4C_4D_if_needed(); } } // namespace Episode3