#include "Card.hh" #include "Server.hh" #include "../CommandFormats.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() { this->clear_action_chain_and_metadata_and_most_flags(); this->team_id = this->player_state()->get_team_id(); this->def_entry = this->server()->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 = this->player_state()->get_sc_card_ref(); this->sc_def_entry = this->server()->definition_for_card_id( this->player_state()->get_sc_card_id()); this->sc_card_type = this->player_state()->get_sc_card_type(); this->max_hp = this->def_entry->def.hp.stat; this->current_hp = this->def_entry->def.hp.stat; if (this->sc_card_ref == this->card_ref) { int16_t rules_char_hp = this->server()->base()->map_and_rules1->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; } this->ap = this->def_entry->def.ap.stat; this->tp = this->def_entry->def.tp.stat; this->num_ally_fcs_destroyed_at_set_time = this->server()->team_num_ally_fcs_destroyed[this->team_id]; this->num_cards_destroyed_by_team_at_set_time = this->server()->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 = this->player_state()->start_facing_direction; if (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) { 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 (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; } } } if (cond_index < 0) { 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); } this->server()->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[0]) { case 'a': cond.a_arg_value = atoi(&eff.arg1[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': cond.remaining_turns = atoi(&eff.arg1[1]); } this->server()->card_special->update_condition_orders(this->shared_from_this()); return cond_index; } void Card::apply_ap_adjust_assists_to_attack( shared_ptr attacker_card, int16_t* inout_attacker_ap, int16_t* in_defense_power) const { uint8_t client_id = attacker_card->get_client_id(); size_t num_assists = this->server()->assist_server->compute_num_assist_effects_for_client(client_id); for (size_t z = 0; z < num_assists; z++) { auto eff = this->server()->assist_server->get_active_assist_by_index(z); if ((eff == AssistEffect::FIX) && attacker_card && (attacker_card->def_entry->def.type != CardType::HUNTERS_SC) && (attacker_card->def_entry->def.type != CardType::ARKZ_SC)) { *inout_attacker_ap = 2; } else if ((eff == AssistEffect::SILENT_COLOSSEUM) && (*inout_attacker_ap - *in_defense_power >= 7)) { *inout_attacker_ap = 0; } } num_assists = this->server()->assist_server->compute_num_assist_effects_for_client(this->client_id); for (size_t z = 0; z < num_assists; z++) { auto eff = this->server()->assist_server->get_active_assist_by_index(z); if ((eff == AssistEffect::AP_ABSORPTION) && (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_GC_Ep3_6xB4x06* cmd, size_t strike_number, int16_t* out_effective_damage) { int16_t effective_damage = damage; this->server()->card_special->adjust_attack_damage_due_to_conditions( this->shared_from_this(), &effective_damage, attacker_card->get_card_ref()); size_t num_assists = this->server()->assist_server->compute_num_assist_effects_for_client(this->client_id); for (size_t z = 0; z < num_assists; z++) { auto eff = this->server()->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(this->server()->team_exp[team_id], 0, effective_damage); this->server()->team_exp[team_id] -= exp_amount; effective_damage -= exp_amount; this->server()->compute_team_dice_boost(team_id); this->server()->update_battle_state_flags_and_send_6xB4x03_if_needed(); if (cmd) { cmd->effect.ap += exp_amount; } } } if (this->action_metadata.check_flag(0x10)) { effective_damage = 0; } auto attacker_ps = attacker_card->player_state(); attacker_ps->stats.damage_given += effective_damage; this->player_state()->stats.damage_taken += effective_damage; this->current_hp = clamp( this->current_hp - effective_damage, 0, this->max_hp); if ((effective_damage > 0) && (attacker_ps->stats.max_attack_damage < effective_damage)) { attacker_ps->stats.max_attack_damage = effective_damage; } this->last_attack_final_damage = effective_damage; if (effective_damage > 0) { this->card_flags = this->card_flags | 4; } if (this->current_hp < 1) { this->destroy_set_card(attacker_card); } G_ApplyConditionEffect_GC_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; this->server()->send(cmd_to_send); this->propagate_shared_hp_if_needed(); if ((this->def_entry->def.type == CardType::HUNTERS_SC) || (this->def_entry->def.type == CardType::ARKZ_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; 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 = this->server()->definition_for_card_ref( this->action_metadata.defense_card_refs[z]); if (ce) { this->action_metadata.defense_power += ce->def.hp.stat; } } this->server()->card_special->apply_action_conditions( 3, attacker_card, this->shared_from_this(), 0x08, nullptr); this->server()->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) { this->current_hp = 0; if (!(this->card_flags & 2)) { if (!this->server()->ruler_server->card_ref_or_any_set_card_has_condition_46(this->card_ref)) { this->server()->card_special->on_card_destroyed( attacker_card, this->shared_from_this()); this->card_flags = this->card_flags | 2; this->update_stats_on_destruction(); this->player_state()->stats.num_owned_cards_destroyed++; if (attacker_card && (attacker_card->team_id != this->team_id)) { attacker_card->player_state()->stats.num_opponent_cards_destroyed++; this->server()->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 = this->player_state()->get_sc_card(); if (!(sc_card->card_flags & 2) && !sc_card->get_attack_condition_value(ConditionType::ELUDE, 0xFFFF, 0xFF, 0xFFFF, nullptr)) { 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_GC_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; this->server()->send(cmd); } if (sc_card->get_current_hp() < 1) { sc_card->destroy_set_card(attacker_card); } } } if ((this->server()->base()->map_and_rules1->rules.hp_type == HPType::DEFEAT_TEAM) && (this->player_state()->get_sc_card().get() == this)) { for (size_t set_index = 0; set_index < 8; set_index++) { auto card = this->player_state()->get_set_card(set_index); if (card) { card->card_flags |= 2; } } } for (size_t client_id = 0; client_id < 4; client_id++) { if (!this->server()->player_states[client_id]) { continue; } size_t num_assists = this->server()->assist_server->compute_num_assist_effects_for_client(client_id); for (size_t z = 0; z < num_assists; z++) { auto eff = this->server()->assist_server->get_active_assist_by_index(z); if (eff == AssistEffect::HOMESICK) { if (client_id == this->client_id) { this->player_state()->return_set_card_to_hand2(this->card_ref); } } else if (eff == AssistEffect::INHERITANCE) { uint8_t other_team_id = this->server()->player_states[client_id]->get_team_id(); uint8_t this_team_id = this->player_state()->get_team_id(); if (this_team_id == other_team_id) { this->server()->add_team_exp(team_id, this->max_hp); } } } } } else if (this->w_destroyer_sc_card.expired() && 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 { if (this->player_state()->assist_flags & 0x80) { 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; } this->card_flags = this->card_flags & 0xFFFFFFF3; int16_t attack_ap = this->action_metadata.attack_bonus; int16_t attack_tp = 0; int16_t defense_power = this->compute_defense_power_for_attacker_card(attacker_card); if ((attack_ap == 0) && !this->action_metadata.check_flag(0x20)) { return; } G_ApplyConditionEffect_GC_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) { 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; this->server()->send(cmd); } else { uint16_t attacker_card_ref = attacker_card->get_card_ref(); this->server()->card_special->compute_attack_ap( this->shared_from_this(), &attack_ap, attacker_card_ref); this->apply_ap_adjust_assists_to_attack(attacker_card, &attack_ap, &defense_power); int16_t raw_damage = attack_ap - defense_power; // Note: The original code uses attack_tp here, even though it is always // zero at this point int16_t preliminary_damage = max(raw_damage, 0) - attack_tp; this->last_attack_preliminary_damage = preliminary_damage; uint16_t targeted_card_ref = this->get_card_ref(); uint32_t unknown_a9 = 0; auto target = this->server()->card_special->compute_replaced_target_based_on_conditions( targeted_card_ref, 1, 0, attacker_card_ref, 0xFFFF, 0, &unknown_a9, 0xFF, 0, 0xFFFF); if (!target) { target = this->shared_from_this(); } if (unknown_a9 != 0) { preliminary_damage = 0; } if (!(this->card_flags & 2) && (!attacker_card || !(attacker_card->card_flags & 2))) { this->server()->card_special->check_for_defense_interference( attacker_card, this->shared_from_this(), &preliminary_damage); } cmd.effect.current_hp = min(attack_ap, 99); cmd.effect.ap = min(defense_power, 99); cmd.effect.tp = attack_tp; this->player_state()->stats.num_attacks_taken++; if (!(target->card_flags & 2)) { 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); // TODO: Is this the right interpretation? The original code does some // fancy bitwise magic that I didn't bother to decipher, because this // interpretation makes sense based on how the game works. this->player_state()->stats.action_card_negated_damage += max( 0, this->current_defense_power - final_effective_damage); } } else { target->commit_attack(0, attacker_card, &cmd, 0, nullptr); } if (this != target.get()) { this->commit_attack(0, attacker_card, &cmd, 0, nullptr); } this->server()->send_6xB4x39(); } } bool Card::get_attack_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); } shared_ptr Card::get_definition() const { return this->def_entry; } uint16_t Card::get_card_ref() const { return this->card_ref; } 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) { int32_t code = this->error_code_for_move_to_location(loc); if (code) { return code; } uint32_t path_cost; uint32_t path_length; if (!this->server()->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; for (size_t warp_type = 0; warp_type < 5; warp_type++) { for (size_t warp_end = 0; warp_end < 2; warp_end++) { if ((this->server()->warp_positions[warp_type][warp_end][0] == this->loc.x) && (this->server()->warp_positions[warp_type][warp_end][1] == this->loc.y)) { G_Unknown_GC_Ep3_6xB4x2C cmd; cmd.loc.x = this->loc.x; cmd.loc.y = this->loc.y; this->loc.x = this->server()->warp_positions[warp_type][warp_end ^ 1][0]; this->loc.y = this->server()->warp_positions[warp_type][warp_end ^ 1][1]; cmd.change_type = 0; cmd.card_refs[0] = this->card_ref; this->server()->send(cmd); return 0; } } } return 0; } void Card::propagate_shared_hp_if_needed() { if ((this->server()->base()->map_and_rules1->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) { ssize_t index = -1; if (this->card_ref == this->player_state()->get_sc_card_ref()) { 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)) { 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; this->send_6xB4x4E_if_needed(always_send); auto& chain = this->player_state()->set_card_action_chains->at(index); if (always_send || (chain != this->action_chain)) { chain = this->action_chain; if (!this->server()->get_should_copy_prev_states_to_current_states()) { G_UpdateActionChain_GC_Ep3_6xB4x4C cmd; cmd.client_id = this->client_id; cmd.index = index; cmd.chain = this->action_chain.chain; this->server()->send(cmd); } } auto& metadata = this->player_state()->set_card_action_metadatas->at(index); if (always_send || (metadata != this->action_metadata)) { metadata = this->action_metadata; G_UpdateActionMetadata_GC_Ep3_6xB4x4D cmd; cmd.client_id = this->client_id; cmd.index = index; cmd.metadata = this->action_metadata; this->server()->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_GC_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() { this->player_state()->num_destroyed_fcs++; this->server()->team_num_ally_fcs_destroyed[this->team_id]++; this->server()->team_num_cards_destroyed[this->team_id]++; for (size_t client_id = 0; client_id < 4; client_id++) { auto other_ps = this->server()->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) { this->action_chain.compute_attack_medium(this->server()); this->action_chain.chain.strike_count = 1; this->action_chain.chain.ap_effect_bonus = 0; this->action_chain.chain.tp_effect_bonus = 0; int16_t card_ap; int16_t card_tp; auto stat_swap_type = this->server()->card_special->compute_stat_swap_type(this->shared_from_this()); this->server()->card_special->get_effective_ap_tp( stat_swap_type, &card_ap, &card_tp, this->get_current_hp(), this->ap, this->tp); int16_t effective_tp = card_tp; int16_t effective_ap = card_ap; 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 = this->server()->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; } } // Add AP/TP from MAG items to SC's AP/TP if (this->def_entry->def.is_sc()) { for (size_t set_index = 0; set_index < 8; set_index++) { auto card = this->player_state()->get_set_card(set_index); if ((card && (card->def_entry->def.card_class() == CardClass::MAG_ITEM)) && !(card->card_flags & 2)) { this->server()->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; } } } if ((this->def_entry->def.type == CardType::ITEM) && this->sc_def_entry) { auto sc_card = this->player_state()->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; } if (!this->action_chain.check_flag(0x10)) { this->action_chain.chain.effective_ap = min(effective_ap, 99); } if (!this->action_chain.check_flag(0x20)) { this->action_chain.chain.effective_tp = min(effective_tp, 99); } if (apply_action_conditions) { this->server()->card_special->apply_action_conditions( 3, this->shared_from_this(), this->shared_from_this(), 1, nullptr); } size_t num_assists = this->server()->assist_server->compute_num_assist_effects_for_client(this->client_id); for (size_t z = 0; z < num_assists; z++) { switch (this->server()->assist_server->get_active_assist_by_index(z)) { case AssistEffect::POWERLESS_RAIN: if (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 (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 (this->card_type_is_sc_or_creature()) { int16_t count = this->player_state()->count_set_refs(); this->action_chain.chain.ap_effect_bonus += (count >> 1); } break; case AssistEffect::AP_ABSORPTION: if (this->action_chain.chain.attack_medium == AttackMedium::TECH) { this->action_chain.chain.tp_effect_bonus += 2; } break; case AssistEffect::TECH_FIELD: if (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 = this->server()->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()) { this->action_chain.chain.ap_effect_bonus += (this->server()->team_num_ally_fcs_destroyed[this->team_id] / 3); } 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; } else if (this->action_chain.chain.attack_medium == AttackMedium::PHYSICAL) { damage = this->action_chain.chain.effective_ap + this->action_chain.chain.ap_effect_bonus; } this->action_chain.chain.damage = min( damage * this->action_chain.chain.damage_multiplier, 99); if (apply_action_conditions) { this->server()->card_special->apply_action_conditions( 3, this->shared_from_this(), this->shared_from_this(), 2, nullptr); if (this->action_chain.check_flag(0x100)) { this->action_chain.chain.damage = min(this->action_chain.chain.damage + 5, 99); } } num_assists = this->server()->assist_server->compute_num_assist_effects_for_client(this->get_client_id()); for (size_t z = 0; z < num_assists; z++) { switch (this->server()->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() { this->card_flags &= 0xFFFFF7FB; this->action_metadata.clear_flags(0x30); this->action_chain.clear_flags(0x140); this->unknown_80237F98(0); } void Card::unknown_80237F98(bool require_condition_20_or_21) { 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 || this->server()->card_special->condition_has_when_20_or_21( this->action_chain.conditions[z])) { ActionState as; auto& cond = this->action_chain.conditions[z]; if (!this->server()->card_special->is_card_targeted_by_condition( cond, as, this->shared_from_this())) { this->server()->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) { this->server()->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); 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::unknown_80235AA0() { this->facing_direction = Direction::INVALID_FF; this->server()->card_special->unknown_80249060(this->shared_from_this()); } void Card::unknown_80235AD4() { this->clear_action_chain_and_metadata_and_most_flags(); this->server()->card_special->unknown_80249254(this->shared_from_this()); } void Card::unknown_80235B10() { this->server()->card_special->unknown_80244BE4(this->shared_from_this()); } void Card::unknown_80236374(shared_ptr other_card, const ActionState* as) { 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); 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::unknown_8023813C() { 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) || !this->server()->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) { this->server()->card_special->apply_stat_deltas_to_card_from_condition_and_clear_cond( cond, this->shared_from_this()); } } } } this->server()->card_special->unknown_80244CA8(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) { 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; 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; ret = true; break; } } } } this->action_metadata.attack_bonus = max(attack_bonus, 0); this->last_attack_preliminary_damage = 0; this->last_attack_final_damage = 0; if (other_card) { this->server()->card_special->apply_action_conditions( 3, other_card, this->shared_from_this(), 0x20, as); this->server()->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)) { return ret; } this->action_metadata.attack_bonus = 0; return ret; } void Card::unknown_802362D8(shared_ptr other_card) { for (size_t client_id = 0; client_id < 4; client_id++) { auto ps = this->server()->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::unknown_80237734() { if (!this->action_chain.unknown_8024DEC4()) { return; } if (this->player_state()->stats.max_attack_combo_size < this->action_chain.chain.attack_action_card_ref_count) { this->player_state()->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; this->server()->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 = this->server()->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)) { this->server()->card_special->unknown_8024A6DC(this->shared_from_this(), card); } } } this->compute_action_chain_results(1, 0); if (!this->action_chain.check_flag(0x40)) { this->server()->card_special->unknown_8024997C(this->shared_from_this()); } if (!(this->card_flags & 2)) { this->compute_action_chain_results(1, 0); this->server()->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)) { this->server()->card_special->unknown_8024A394(this->shared_from_this()); } this->player_state()->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(); for (size_t client_id = 0; client_id < 4; client_id++) { auto ps = this->server()->player_states[client_id]; if (ps) { ps->unknown_8023C110(); } } this->send_6xB4x4E_4C_4D_if_needed(); } } // namespace Episode3