Browse Source

Implemented official ammo equip behavior (#3438)

* Fixes #1155.
* Added specific checks for weapons when equipping ammo.
* Ammo is removed if the required weapon is removed.
* Added battle configs to disable behavior.
Thanks to @Atemo, @exneval, and @benching!
Aleos 6 years ago
parent
commit
d3d8f3c5a7
5 changed files with 208 additions and 116 deletions
  1. 8 0
      conf/battle/battle.conf
  2. 2 0
      src/map/battle.cpp
  3. 2 0
      src/map/battle.hpp
  4. 6 2
      src/map/clif.hpp
  5. 190 114
      src/map/pc.cpp

+ 8 - 0
conf/battle/battle.conf

@@ -141,6 +141,14 @@ delay_battle_damage: yes
 //     skills should consume ammo when it's acquired via a card or plagiarize)
 arrow_decrement: 1
 
+// Should ammo be unequipped when unequipping a weapon?
+// Official behavior is "yes".
+ammo_unequip: yes
+
+// Should a suitable weapon be equipped when equipping ammo?
+// Official behavior is "yes".
+ammo_check_weapon: yes
+
 // Should the item script bonus 'Autospell' check for range/obstacles before casting?
 // Official behavior is "no", setting this to "yes" will make skills use their defined
 // range. For example, Sonic Blow requires a 2 cell distance before autocasting is allowed.

+ 2 - 0
src/map/battle.cpp

@@ -8141,6 +8141,8 @@ static const struct _battle_data {
 	{ "natural_heal_weight_rate",           &battle_config.natural_heal_weight_rate,        50,     0,      100             },
 	{ "natural_heal_weight_rate_renewal",   &battle_config.natural_heal_weight_rate_renewal,70,     0,      100             },
 	{ "arrow_decrement",                    &battle_config.arrow_decrement,                 1,      0,      2,              },
+	{ "ammo_unequip",                       &battle_config.ammo_unequip,                    1,      0,      1,              },
+	{ "ammo_check_weapon",                  &battle_config.ammo_check_weapon,               1,      0,      1,              },
 	{ "max_aspd",                           &battle_config.max_aspd,                        190,    100,    199,            },
 	{ "max_third_aspd",                     &battle_config.max_third_aspd,                  193,    100,    199,            },
 	{ "max_walk_speed",                     &battle_config.max_walk_speed,                  300,    100,    100*DEFAULT_WALK_SPEED, },

+ 2 - 0
src/map/battle.hpp

@@ -263,6 +263,8 @@ struct Battle_Config
 	int natural_heal_weight_rate;
 	int natural_heal_weight_rate_renewal;
 	int arrow_decrement;
+	int ammo_unequip;
+	int ammo_check_weapon;
 	int max_aspd;
 	int max_walk_speed;	//Maximum walking speed after buffs [Skotlex]
 	int max_hp_lv99;

+ 6 - 2
src/map/clif.hpp

@@ -491,25 +491,29 @@ enum clif_messages : uint16_t {
 	SKILL_CANT_USE_AREA = 0x536,
 	ITEM_CANT_USE_AREA = 0x537,
 	VIEW_EQUIP_FAIL = 0x54d,
+	ITEM_NEED_MADOGEAR = 0x59b,
+	ITEM_NEED_CART = 0x5ef,
 	RUNE_CANT_CREATE = 0x61b,
 	ITEM_CANT_COMBINE = 0x623,
 	INVENTORY_SPACE_FULL = 0x625,
 	ITEM_PRODUCE_SUCCESS = 0x627,
 	ITEM_PRODUCE_FAIL = 0x628,
 	ITEM_UNIDENTIFIED = 0x62d,
+	ITEM_NEED_BOW = 0x69b,
 	ITEM_REUSE_LIMIT = 0x746,
 	WORK_IN_PROGRESS = 0x783,
 	NEED_REINS_OF_MOUNT = 0x78c,
 	PARTY_MASTER_CHANGE_SAME_MAP = 0x82e, ///< "It is only possible to change the party leader while on the same map."
 	MERGE_ITEM_NOT_AVAILABLE = 0x887,
-	GUILD_MASTER_WOE = 0xb93, /// <"Currently in WoE hours, unable to delegate Guild leader"
-	GUILD_MASTER_DELAY = 0xb94, /// <"You have to wait for one day before delegating a new Guild leader"
+	ITEM_BULLET_EQUIP_FAIL = 0x9bd,
 	SKILL_NEED_GATLING = 0x9fa,
 	SKILL_NEED_SHOTGUN = 0x9fb,
 	SKILL_NEED_RIFLE = 0x9fc,
 	SKILL_NEED_REVOLVER = 0x9fd,
 	SKILL_NEED_HOLY_BULLET = 0x9fe,
 	SKILL_NEED_GRENADE = 0xa01,
+	GUILD_MASTER_WOE = 0xb93, /// <"Currently in WoE hours, unable to delegate Guild leader"
+	GUILD_MASTER_DELAY = 0xb94, /// <"You have to wait for one day before delegating a new Guild leader"
 	MSG_ATTENDANCE_DISABLED = 0xd92,
 };
 

+ 190 - 114
src/map/pc.cpp

@@ -1122,6 +1122,51 @@ uint8 pc_isequip(struct map_session_data *sd,int n)
 	if (!battle_config.allow_equip_restricted_item && itemdb_isNoEquip(item, sd->bl.m))
 		return ITEM_EQUIP_ACK_FAIL;
 
+	if (item->equip&EQP_AMMO) {
+		switch (item->look) {
+			case AMMO_ARROW:
+				if (battle_config.ammo_check_weapon && sd->status.weapon != W_BOW && sd->status.weapon != W_MUSICAL && sd->status.weapon != W_WHIP) {
+					clif_msg(sd, ITEM_NEED_BOW);
+					return ITEM_EQUIP_ACK_FAIL;
+				}
+				break;
+			case AMMO_THROWABLE_DAGGER:
+				if (!pc_checkskill(sd, AS_VENOMKNIFE))
+					return ITEM_EQUIP_ACK_FAIL;
+				break;
+			case AMMO_BULLET:
+			case AMMO_SHELL:
+				if (battle_config.ammo_check_weapon && sd->status.weapon != W_REVOLVER && sd->status.weapon != W_RIFLE && sd->status.weapon != W_GATLING && sd->status.weapon != W_SHOTGUN
+#ifdef RENEWAL
+					&& sd->status.weapon != W_GRENADE
+#endif
+					) {
+					clif_msg(sd, ITEM_BULLET_EQUIP_FAIL);
+					return ITEM_EQUIP_ACK_FAIL;
+				}
+				break;
+#ifndef RENEWAL
+			case AMMO_GRENADE:
+				if (battle_config.ammo_check_weapon && sd->status.weapon != W_GRENADE) {
+					clif_msg(sd, ITEM_BULLET_EQUIP_FAIL);
+					return ITEM_EQUIP_ACK_FAIL;
+				}
+				break;
+#endif
+			case AMMO_CANNONBALL:
+				if (!pc_ismadogear(sd) && (sd->status.class_ == JOB_MECHANIC_T || sd->status.class_ == JOB_MECHANIC)) {
+					clif_msg(sd, ITEM_NEED_MADOGEAR); // Item can only be used when Mado Gear is mounted.
+					return ITEM_EQUIP_ACK_FAIL;
+				}
+				if (sd->state.active && !pc_iscarton(sd) && //Check if sc data is already loaded
+					(sd->status.class_ == JOB_GENETIC_T || sd->status.class_ == JOB_GENETIC)) {
+					clif_msg(sd, ITEM_NEED_CART); // Only available when cart is mounted.
+					return ITEM_EQUIP_ACK_FAIL;
+				}
+				break;
+		}
+	}
+
 	if (sd->sc.count) {
 		if(item->equip & EQP_ARMS && item->type == IT_WEAPON && sd->sc.data[SC_STRIPWEAPON]) // Also works with left-hand weapons [DracoRPG]
 			return ITEM_EQUIP_ACK_FAIL;
@@ -9875,38 +9920,6 @@ bool pc_equipitem(struct map_session_data *sd,short n,int req_pos)
 		return false;
 	}
 
-	if ((sd->class_&MAPID_BASEMASK) == MAPID_GUNSLINGER) {
-		/** Failing condition:
-		 * 1. Always failed to equip ammo if no weapon equipped yet
-		 * 2. Grenade only can be equipped if weapon is Grenade Launcher
-		 * 3. Bullet cannot be equipped if the weapon is Grenade Launcher
-		 * (4. The rest is relying on item job/class restriction).
-		 **/
-		if (id->type == IT_AMMO) {
-			int w_idx = sd->equip_index[EQI_HAND_R];
-			enum weapon_type w_type = (w_idx != -1) ? (enum weapon_type)sd->inventory_data[w_idx]->look : W_FIST;
-			if (w_idx == -1 ||
-				(id->look == A_GRENADE && w_type != W_GRENADE) ||
-				(id->look != A_GRENADE && w_type == W_GRENADE))
-			{
-				clif_equipitemack(sd, 0, 0, ITEM_EQUIP_ACK_FAIL);
-				return false;
-			}
-		}
-		else if (id->type == IT_WEAPON && id->look >= W_REVOLVER && id->look <= W_GRENADE) {
-			int a_idx = sd->equip_index[EQI_AMMO];
-			if (a_idx != -1) {
-				enum ammo_type a_type = (enum ammo_type)sd->inventory_data[a_idx]->look;
-				if ((a_type == A_GRENADE && id->look != W_GRENADE) ||
-					(a_type != A_GRENADE && id->look == W_GRENADE))
-				{
-					clif_equipitemack(sd, 0, 0, ITEM_EQUIP_ACK_FAIL);
-					return false;
-				}
-			}
-		}
-	}
-
 	if (id->flag.bindOnEquip && !sd->inventory.u.items_inventory[n].bound) {
 		sd->inventory.u.items_inventory[n].bound = (char)battle_config.default_bind_on_equip;
 		clif_notify_bindOnEquip(sd,n);
@@ -9947,7 +9960,7 @@ bool pc_equipitem(struct map_session_data *sd,short n,int req_pos)
 	for(i=0;i<EQI_MAX;i++) {
 		if(pos & equip_bitmask[i]) {
 			if(sd->equip_index[i] >= 0) //Slot taken, remove item from there.
-				pc_unequipitem(sd,sd->equip_index[i],2);
+				pc_unequipitem(sd,sd->equip_index[i],2 | 4);
 
 			sd->equip_index[i] = n;
 		}
@@ -9990,6 +10003,34 @@ bool pc_equipitem(struct map_session_data *sd,short n,int req_pos)
 	if(pos & EQP_SHOES)
 		clif_changelook(&sd->bl,LOOK_SHOES,0);
 
+	if (battle_config.ammo_unequip && (pos&EQP_ARMS) && id->type == IT_WEAPON) {
+		short idx = sd->equip_index[EQI_AMMO];
+
+		if (idx >= 0) {
+			switch (sd->inventory_data[idx]->look) {
+				case AMMO_ARROW:
+					if (id->look != W_BOW && id->look != W_MUSICAL && id->look != W_WHIP)
+						pc_unequipitem(sd, idx, 2 | 4);
+					break;
+				case AMMO_BULLET:
+				case AMMO_SHELL:
+					if (id->look != W_REVOLVER && id->look != W_RIFLE && id->look != W_GATLING && id->look != W_SHOTGUN
+#ifdef RENEWAL
+						&& id->look != W_GRENADE
+#endif
+						)
+						pc_unequipitem(sd, idx, 2 | 4);
+					break;
+#ifndef RENEWAL
+				case AMMO_GRENADE:
+					if (id->look != W_GRENADE)
+						pc_unequipitem(sd, idx, 2 | 4);
+					break;
+#endif
+			}
+		}
+	}
+
 	pc_set_costume_view(sd);
 
 	pc_checkallowskill(sd); //Check if status changes should be halted.
@@ -10040,25 +10081,101 @@ bool pc_equipitem(struct map_session_data *sd,short n,int req_pos)
 	return true;
 }
 
-/*==========================================
- * Called when attemting to unequip an item from player
- * type:
- * 0 - only unequip
- * 1 - calculate status after unequipping
- * 2 - force unequip
- * return: false - fail; true - success
- *------------------------------------------*/
-bool pc_unequipitem(struct map_session_data *sd, int n, int flag) {
+/**
+ * Recalculate player status on unequip
+ * @param sd: Player data
+ * @param n: Item inventory index
+ * @param flag: Whether to recalculate a player's status or not
+ * @return True on success or false on failure
+ */
+static void pc_unequipitem_sub(struct map_session_data *sd, int n, int flag) {
 	int i, iflag;
 	bool status_calc = false;
 
+	if (sd->state.autobonus&sd->inventory.u.items_inventory[n].equip)
+		sd->state.autobonus &= ~sd->inventory.u.items_inventory[n].equip; //Check for activated autobonus [Inkfish]
+
+	sd->inventory.u.items_inventory[n].equip = 0;
+	pc_checkallowskill(sd);
+	iflag = sd->npc_item_flag;
+
+	/* check for combos (MUST be before status_calc_pc) */
+	if (sd->inventory_data[n]) {
+		if (sd->inventory_data[n]->combos_count) {
+			if (pc_removecombo(sd, sd->inventory_data[n]))
+				status_calc = true;
+		}
+		if (itemdb_isspecial(sd->inventory.u.items_inventory[n].card[0]))
+			; //No cards
+		else {
+			for (i = 0; i < MAX_SLOTS; i++) {
+				struct item_data *data;
+
+				if (!sd->inventory.u.items_inventory[n].card[i])
+					continue;
+				if ((data = itemdb_exists(sd->inventory.u.items_inventory[n].card[i])) != NULL) {
+					if (data->combos_count) {
+						if (pc_removecombo(sd, data))
+							status_calc = true;
+					}
+				}
+			}
+		}
+	}
+
+	if (status_calc)
+		status_calc_pc(sd, SCO_NONE);
+
+	if (sd->sc.data[SC_SIGNUMCRUCIS] && !battle_check_undead(sd->battle_status.race, sd->battle_status.def_ele))
+		status_change_end(&sd->bl, SC_SIGNUMCRUCIS, INVALID_TIMER);
+
+	//OnUnEquip script [Skotlex]
+	if (sd->inventory_data[n]) {
+		if (sd->inventory_data[n]->unequip_script)
+			run_script(sd->inventory_data[n]->unequip_script, 0, sd->bl.id, fake_nd->bl.id);
+		if (itemdb_isspecial(sd->inventory.u.items_inventory[n].card[0]))
+			; //No cards
+		else {
+			for (i = 0; i < MAX_SLOTS; i++) {
+				struct item_data *data;
+				if (!sd->inventory.u.items_inventory[n].card[i])
+					continue;
+
+				if ((data = itemdb_exists(sd->inventory.u.items_inventory[n].card[i])) != NULL) {
+					if (data->unequip_script)
+						run_script(data->unequip_script, 0, sd->bl.id, fake_nd->bl.id);
+				}
+
+			}
+		}
+	}
+
+	if (flag & 1)
+		status_calc_pc(sd, SCO_FORCE);
+	sd->npc_item_flag = iflag;
+}
+
+/**
+ * Called when attempting to unequip an item from a player
+ * @param sd: Player data
+ * @param n: Item inventory index
+ * @param flag: Type of unequip
+ *  0 - only unequip
+ *  1 - calculate status after unequipping
+ *  2 - force unequip
+ *  4 - unequip by switching equipment
+ * @return True on success or false on failure
+ */
+bool pc_unequipitem(struct map_session_data *sd, int n, int flag) {
+	int i, pos;
+
 	nullpo_retr(false,sd);
 
 	if (n < 0 || n >= MAX_INVENTORY) {
 		clif_unequipitemack(sd,0,0,0);
 		return false;
 	}
-	if (!sd->inventory.u.items_inventory[n].equip) {
+	if (!(pos = sd->inventory.u.items_inventory[n].equip)) {
 		clif_unequipitemack(sd,n,0,0);
 		return false; //Nothing to unequip
 	}
@@ -10076,18 +10193,14 @@ bool pc_unequipitem(struct map_session_data *sd, int n, int flag) {
 	}
 
 	if (battle_config.battle_log)
-		ShowInfo("unequip %d %x:%x\n",n,pc_equippoint(sd,n),sd->inventory.u.items_inventory[n].equip);
+		ShowInfo("unequip %d %x:%x\n",n,pc_equippoint(sd,n),pos);
 
-	if (!sd->inventory.u.items_inventory[n].equip) { //Nothing to unequip
-		clif_unequipitemack(sd, n, 0, 0);
-		return false;
-	}
 	for(i = 0; i < EQI_MAX; i++) {
-		if (sd->inventory.u.items_inventory[n].equip & equip_bitmask[i])
+		if (pos & equip_bitmask[i])
 			sd->equip_index[i] = -1;
 	}
 
-	if(sd->inventory.u.items_inventory[n].equip & EQP_HAND_R) {
+	if(pos & EQP_HAND_R) {
 		sd->weapontype1 = 0;
 		sd->status.weapon = sd->weapontype2;
 		pc_calcweapontype(sd);
@@ -10095,7 +10208,7 @@ bool pc_unequipitem(struct map_session_data *sd, int n, int flag) {
 		if( !battle_config.dancing_weaponswitch_fix )
 			status_change_end(&sd->bl, SC_DANCING, INVALID_TIMER); // Unequipping => stop dancing.
 	}
-	if(sd->inventory.u.items_inventory[n].equip & EQP_HAND_L) {
+	if(pos & EQP_HAND_L) {
 		if (sd->status.shield && battle_getcurrentskill(&sd->bl) == LG_SHIELDSPELL)
 			unit_skillcastcancel(&sd->bl, 0); // Cancel Shield Spell if player swaps shields.
 
@@ -10104,15 +10217,36 @@ bool pc_unequipitem(struct map_session_data *sd, int n, int flag) {
 		clif_changelook(&sd->bl,LOOK_SHIELD,sd->status.shield);
 	}
 
-	if(sd->inventory.u.items_inventory[n].equip & EQP_SHOES)
+	if(pos & EQP_SHOES)
 		clif_changelook(&sd->bl,LOOK_SHOES,0);
 
-	clif_unequipitemack(sd,n,sd->inventory.u.items_inventory[n].equip,1);
+	clif_unequipitemack(sd,n,pos,1);
 	pc_set_costume_view(sd);
 
 	status_change_end(&sd->bl,SC_HEAT_BARREL,INVALID_TIMER);
 	// On weapon change (right and left hand)
-	if ((sd->inventory.u.items_inventory[n].equip & EQP_ARMS) && sd->inventory_data[n]->type == IT_WEAPON) {
+	if ((pos & EQP_ARMS) && sd->inventory_data[n]->type == IT_WEAPON) {
+		if (battle_config.ammo_unequip && !(flag & 4)) {
+			switch (sd->inventory_data[n]->look) {
+				case W_BOW:
+				case W_MUSICAL:
+				case W_WHIP:
+				case W_REVOLVER:
+				case W_RIFLE:
+				case W_GATLING:
+				case W_SHOTGUN:
+				case W_GRENADE: {
+					short idx = sd->equip_index[EQI_AMMO];
+
+					if (idx >= 0) {
+						sd->equip_index[EQI_AMMO] = -1;
+						clif_unequipitemack(sd, idx, sd->inventory.u.items_inventory[idx].equip, 1);
+						pc_unequipitem_sub(sd, idx, 0);
+					}
+				}
+				break;
+			}
+		}
 		if (!sd->sc.data[SC_SEVENWIND] || sd->sc.data[SC_ASPERSIO]) //Check for seven wind (but not level seven!)
 			skill_enchant_elemental_end(&sd->bl, SC_NONE);
 		status_change_end(&sd->bl, SC_FEARBREEZE, INVALID_TIMER);
@@ -10120,7 +10254,7 @@ bool pc_unequipitem(struct map_session_data *sd, int n, int flag) {
 	}
 
 	// On armor change
-	if (sd->inventory.u.items_inventory[n].equip & EQP_ARMOR) {
+	if (pos & EQP_ARMOR) {
 		if (sd->sc.data[SC_HOVERING] && sd->inventory_data[n]->nameid == ITEMID_HOVERING_BOOSTER)
 			status_change_end(&sd->bl, SC_HOVERING, INVALID_TIMER);
 		//status_change_end(&sd->bl, SC_BENEDICTIO, INVALID_TIMER); // No longer is removed? Need confirmation
@@ -10131,65 +10265,7 @@ bool pc_unequipitem(struct map_session_data *sd, int n, int flag) {
 	if (sd->inventory_data[n]->type == IT_AMMO && (sd->inventory_data[n]->nameid != ITEMID_SILVER_BULLET || sd->inventory_data[n]->nameid != ITEMID_PURIFICATION_BULLET || sd->inventory_data[n]->nameid != ITEMID_SILVER_BULLET_))
 		status_change_end(&sd->bl, SC_P_ALTER, INVALID_TIMER);
 
-	if (sd->state.autobonus&sd->inventory.u.items_inventory[n].equip)
-		sd->state.autobonus &= ~sd->inventory.u.items_inventory[n].equip; //Check for activated autobonus [Inkfish]
-
-	sd->inventory.u.items_inventory[n].equip = 0;
-	iflag = sd->npc_item_flag;
-
-	/* check for combos (MUST be before status_calc_pc) */
-	if ( sd->inventory_data[n] ) {
-		if( sd->inventory_data[n]->combos_count ) {
-			if( pc_removecombo(sd,sd->inventory_data[n]) )
-				status_calc = true;
-		}
-		if(itemdb_isspecial(sd->inventory.u.items_inventory[n].card[0]))
-			; //No cards
-		else {
-			for( i = 0; i < MAX_SLOTS; i++ ) {
-				struct item_data *data;
-
-				if (!sd->inventory.u.items_inventory[n].card[i])
-					continue;
-				if ( ( data = itemdb_exists(sd->inventory.u.items_inventory[n].card[i]) ) != NULL ) {
-					if( data->combos_count ) {
-						if( pc_removecombo(sd,data) )
-							status_calc = true;
-					}
-				}
-			}
-		}
-	}
-
-	if(flag&1 || status_calc) {
-		pc_checkallowskill(sd);
-		status_calc_pc(sd,SCO_NONE);
-	}
-
-	if(sd->sc.data[SC_SIGNUMCRUCIS] && !battle_check_undead(sd->battle_status.race,sd->battle_status.def_ele))
-		status_change_end(&sd->bl, SC_SIGNUMCRUCIS, INVALID_TIMER);
-
-	//OnUnEquip script [Skotlex]
-	if (sd->inventory_data[n]) {
-		if (sd->inventory_data[n]->unequip_script)
-			run_script(sd->inventory_data[n]->unequip_script,0,sd->bl.id,fake_nd->bl.id);
-		if(itemdb_isspecial(sd->inventory.u.items_inventory[n].card[0]))
-			; //No cards
-		else {
-			for( i = 0; i < MAX_SLOTS; i++ ) {
-				struct item_data *data;
-				if (!sd->inventory.u.items_inventory[n].card[i])
-					continue;
-
-				if ( ( data = itemdb_exists(sd->inventory.u.items_inventory[n].card[i]) ) != NULL ) {
-					if( data->unequip_script )
-						run_script(data->unequip_script,0,sd->bl.id,fake_nd->bl.id);
-				}
-
-			}
-		}
-	}
-	sd->npc_item_flag = iflag;
+	pc_unequipitem_sub(sd, n, flag);
 
 	return true;
 }