Browse Source

* Fixed bugreport:9034 (Hercules aff9c14)
* 'bonus_script' improvement:
-- Added flag values (partial merge from sc_config branch):
-- * 16 : Removeable by Banishing Buster.
-- * 32 : Removable by Refresh.
-- * 128 : Removable by Luxanima.
-- * 256 : Remove when Madogear is activated or deactivated.
-- * 512 : Remove when receive damage.
-- * 1024: Script is permanent, cannot be cleared by bonus_script_clear
-- Added 'bonus_script_clear' command to remove active bonus_script from player (works like sc_end) Thank Napster to mention this. (bugreport:9042)

Signed-off-by: Cydh Ramdh <house.bad@gmail.com>

Cydh Ramdh 11 năm trước cách đây
mục cha
commit
0fca530278
11 tập tin đã thay đổi với 185 bổ sung59 xóa
  1. 12 0
      db/const.txt
  2. 21 5
      doc/script_commands.txt
  3. 1 1
      src/common/mmo.h
  4. 4 5
      src/map/chrif.c
  5. 4 3
      src/map/mob.c
  6. 48 19
      src/map/pc.c
  7. 14 11
      src/map/pc.h
  8. 42 3
      src/map/script.c
  9. 7 4
      src/map/skill.c
  10. 14 1
      src/map/status.c
  11. 18 7
      src/map/status.h

+ 12 - 0
db/const.txt

@@ -4620,5 +4620,17 @@ Bound_Guild	2
 Bound_Party	3
 Bound_Char	4
 
+// bonus_script
+BSF_REM_ON_DEAD	0x001
+BSF_REM_ON_DISPELL	0x002
+BSF_REM_ON_CLEARANCE	0x004
+BSF_REM_ON_LOGOUT	0x008
+BSF_REM_ON_BANISHING_BUSTER	0x010
+BSF_REM_ON_REFRESH	0x020
+BSF_REM_ON_LUXANIMA	0x040
+BSF_REM_ON_MADOGEAR	0x080
+BSF_REM_ON_DAMAGED	0x100
+BSF_PERMANENT	0x200
+
 false	0
 true	1

+ 21 - 5
doc/script_commands.txt

@@ -5343,11 +5343,17 @@ logs out.
 Note that the maximum number of 'bonus_script' commands that can run simultaneously
 for a player is 10 (MAX_PC_BONUS_SCRIPT in 'src/map/pc.h').
 
-Flags: (Default is 0)
-	&1: Remove when dead.
-	&2: Removable by Dispell.
-	&4: Removable by Clearance.
-	&8: Remove when player logs out.
+Flags (bitmask):
+	1   : Remove when dead.
+	2   : Removable by Dispell.
+	4   : Removable by Clearance.
+	8   : Remove when player logs out.
+	16  : Removeable by Banishing Buster.
+	32  : Removable by Refresh.
+	128 : Removable by Luxanima.
+	256 : Remove when Madogear is activated or deactivated.
+	512 : Remove when receive damage.
+	1024: Script is permanent, cannot be cleared by bonus_script_clear
 
 Types:
 	This will be used to decide negative or positive buff for 'debuff_on_logout'.
@@ -5363,6 +5369,16 @@ Example:
 
 ---------------------------------------
 
+*bonus_script_clear {<flag>,{<char_id>}};
+
+Removes atatched bonus_script from player. If no 'char_id' given, it will removes
+from the invoker.
+
+If 'flag' is 1, means will clears all scripts even it's Permanent effect. By default,
+it just removes non-permanent script.
+
+---------------------------------------
+
 *skill <skill id>,<level>{,<flag>};
 *skill "<skill name>",<level>{,<flag>};
 *addtoskill <skill id>,<level>{,<flag>};

+ 1 - 1
src/common/mmo.h

@@ -73,7 +73,7 @@
 #define MAX_GUILDLEVEL 50 ///Max Guild level
 #define MAX_GUARDIANS 8	///Local max per castle. If this value is increased, need to add more fields on MySQL `guild_castle` table [Skotlex]
 #define MAX_QUEST_OBJECTIVES 3 ///Max quest objectives for a quest
-#define MAX_PC_BONUS_SCRIPT 20
+#define MAX_PC_BONUS_SCRIPT 20 ///Max bonus script
 
 // for produce
 #define MIN_ATTRIBUTE 0

+ 4 - 5
src/map/chrif.c

@@ -1819,11 +1819,11 @@ int chrif_save_bsdata(struct map_session_data *sd) {
 	WFIFOW(char_fd,0) = 0x2b2e;
 	WFIFOL(char_fd,4) = sd->status.char_id;
 	
-	i = BONUS_FLAG_REM_ON_LOGOUT; //Remove bonus with this flag
+	i = BSF_REM_ON_LOGOUT; //Remove bonus with this flag
 	if (battle_config.debuff_on_logout&1) //Remove negative buffs
-		i |= BONUS_FLAG_REM_DEBUFF;
+		i |= BSF_REM_DEBUFF;
 	if (battle_config.debuff_on_logout&2) //Remove positive buffs
-		i |= BONUS_FLAG_REM_BUFF;
+		i |= BSF_REM_BUFF;
 	
 	//Clear data that won't be stored
 	pc_bonus_script_clear(sd,i);
@@ -1843,8 +1843,7 @@ int chrif_save_bsdata(struct map_session_data *sd) {
 		bs.icon = sd->bonus_script[i].icon;
 
 		memcpy(WFIFOP(char_fd,10+count*sizeof(struct bonus_script_data)),&bs,sizeof(struct bonus_script_data));
-		delete_timer(sd->bonus_script[i].tid,pc_bonus_script_timer);
-		pc_bonus_script_remove(sd,i);
+		pc_bonus_script_remove(&sd->bonus_script[i]);
 		count++;
 	}
 

+ 4 - 3
src/map/mob.c

@@ -1643,9 +1643,10 @@ static bool mob_ai_sub_hard(struct mob_data *md, unsigned int tick)
 		else { // Attack once and find a new random target
 			int search_size = (view_range < md->status.rhw.range) ? view_range : md->status.rhw.range;
 			unit_attack(&md->bl, tbl->id, 0);
-			tbl = battle_getenemy(&md->bl, DEFAULT_ENEMY_TYPE(md), search_size);
-			md->target_id = tbl->id;
-			md->min_chase = md->db->range3;
+			if (tbl = battle_getenemy(&md->bl, DEFAULT_ENEMY_TYPE(md), search_size)) {
+				md->target_id = tbl->id;
+				md->min_chase = md->db->range3;
+			}
 		}
 		return true;
 	}

+ 48 - 19
src/map/pc.c

@@ -1159,6 +1159,8 @@ bool pc_authok(struct map_session_data *sd, int login_id2, time_t expiration_tim
 		sd->autobonus2[i].active = INVALID_TIMER;
 	for(i = 0; i < ARRAYLENGTH(sd->autobonus3); i++)
 		sd->autobonus3[i].active = INVALID_TIMER;
+	for(i = 0; i < ARRAYLENGTH(sd->bonus_script); i++)
+		sd->bonus_script[i].tid = INVALID_TIMER;
 
 	if (battle_config.item_auto_get)
 		sd->state.autoloot = 10000;
@@ -7313,9 +7315,9 @@ int pc_dead(struct map_session_data *sd,struct block_list *src)
 		item_tmp.card[3]=GetWord(sd->status.char_id,1);
 		map_addflooritem(&item_tmp,1,sd->bl.m,sd->bl.x,sd->bl.y,0,0,0,0);
 	}
-	
+
 	//Remove bonus_script when dead
-	pc_bonus_script_clear(sd,BONUS_FLAG_REM_ON_DEAD);
+	pc_bonus_script_clear(sd,BSF_REM_ON_DEAD);
 
 	// changed penalty options, added death by player if pk_mode [Valaris]
 	if(battle_config.death_penalty_type
@@ -8253,6 +8255,7 @@ void pc_setoption(struct map_session_data *sd,int type)
 			status_change_end(&sd->bl,SC_CARTBOOST,INVALID_TIMER);
 			status_change_end(&sd->bl,SC_MELTDOWN,INVALID_TIMER);
 			status_change_end(&sd->bl,SC_MAXOVERTHRUST,INVALID_TIMER);
+			pc_bonus_script_clear(sd,BSF_REM_ON_MADOGEAR);
 		} else if( !(type&OPTION_MADOGEAR) && p_type&OPTION_MADOGEAR ) {
 			status_calc_pc(sd,SCO_NONE);
 			status_change_end(&sd->bl,SC_SHAPESHIFT,INVALID_TIMER);
@@ -8260,6 +8263,7 @@ void pc_setoption(struct map_session_data *sd,int type)
 			status_change_end(&sd->bl,SC_ACCELERATION,INVALID_TIMER);
 			status_change_end(&sd->bl,SC_OVERHEAT_LIMITPOINT,INVALID_TIMER);
 			status_change_end(&sd->bl,SC_OVERHEAT,INVALID_TIMER);
+			pc_bonus_script_clear(sd,BSF_REM_ON_MADOGEAR);
 		}
 	}
 
@@ -10872,28 +10876,30 @@ int pc_bonus_script_timer(int tid, unsigned int tick, int id, intptr_t data) {
 		return 0;
 	}
 
-	pc_bonus_script_remove(sd,i);
+	pc_bonus_script_remove(&sd->bonus_script[i]);
 	status_calc_pc(sd,SCO_NONE);
 	return 0;
 }
 
 /** [Cydh]
-* Remove bonus_script data from sd (not deleting timer)
+* Remove bonus_script data from player
 * @param sd: Target player
 * @param i: Bonus script index
 **/
-void pc_bonus_script_remove(struct map_session_data *sd, uint8 i) {
-	if (!sd || i >= MAX_PC_BONUS_SCRIPT)
+void pc_bonus_script_remove(struct s_bonus_script *bscript) {
+	if (!bscript)
 		return;
 
-	script_free_code(sd->bonus_script[i].script);
-	memset(&sd->bonus_script[i].script,0,sizeof(sd->bonus_script[i].script));
-	sd->bonus_script[i].script_str[0] = '\0';
-	sd->bonus_script[i].tick = 0;
-	sd->bonus_script[i].tid = 0;
-	sd->bonus_script[i].flag = 0;
-	clif_status_change(&sd->bl,sd->bonus_script[i].icon,0,0,0,0,0);
-	sd->bonus_script[i].icon = SI_BLANK;
+	if (bscript->script)
+		script_free_code(bscript->script);
+	bscript->script = NULL;
+	memset(bscript->script_str, '\0', sizeof(bscript->script_str));
+	bscript->tick = 0;
+	bscript->flag = 0;
+	bscript->icon = SI_BLANK;
+	if (bscript->tid != INVALID_TIMER)
+		delete_timer(bscript->tid,pc_bonus_script_timer);
+	bscript->tid = INVALID_TIMER;
 }
 
 /** [Cydh]
@@ -10910,19 +10916,42 @@ void pc_bonus_script_clear(struct map_session_data *sd, uint16 flag) {
 		if (&sd->bonus_script[i] && sd->bonus_script[i].script &&
 			(sd->bonus_script[i].flag&flag || //Remove bonus script based on e_bonus_script_flags
 			(sd->bonus_script[i].type && (
-				(flag&BONUS_FLAG_REM_BUFF && sd->bonus_script[i].type == 1) || //Remove bonus script based on buff type
-				(flag&BONUS_FLAG_REM_DEBUFF && sd->bonus_script[i].type == 2)) //Remove bonus script based on debuff type
+				(flag&BSF_REM_BUFF && sd->bonus_script[i].type == 1) || //Remove bonus script based on buff type
+				(flag&BSF_REM_DEBUFF && sd->bonus_script[i].type == 2)) //Remove bonus script based on debuff type
 			))) 
 		{
-			delete_timer(sd->bonus_script[i].tid,pc_bonus_script_timer);
-			pc_bonus_script_remove(sd,i);
+			clif_status_change(&sd->bl,sd->bonus_script[i].icon,0,0,0,0,0);
+			pc_bonus_script_remove(&sd->bonus_script[i]);
 			count++;
 		}
 	}
-	if (count && !(flag&BONUS_FLAG_REM_ON_LOGOUT)) //Don't need to do this if log out
+	if (count && !(flag&BSF_REM_ON_LOGOUT)) //Don't need to do this if log out
 		status_calc_pc(sd,SCO_NONE);
 }
 
+/**
+* Clear all bonus script from player
+* @param sd
+* @param permanent If true, will removes permanent bonus script.
+* @author [Cydh]
+*/
+void pc_bonus_script_clear_all(struct map_session_data *sd, bool permanent) {
+	uint8 i, count = 0;
+	if (!sd)
+		return;
+
+	for (i = 0; i < MAX_PC_BONUS_SCRIPT; i++) {
+		if (!&sd->bonus_script[i] && !sd->bonus_script[i].script)
+			continue;
+		if (!permanent && sd->bonus_script[i].flag&BSF_PERMANENT)
+			continue;
+		clif_status_change(&sd->bl,sd->bonus_script[i].icon,0,0,0,0,0);
+		pc_bonus_script_remove(&sd->bonus_script[i]);
+		count++;
+	}
+	status_calc_pc(sd,SCO_NONE);
+}
+
 /** [Cydh]
  * Gives/removes SC_BASILICA when player steps in/out the cell with 'cell_basilica'
  * @param sd: Target player

+ 14 - 11
src/map/pc.h

@@ -155,6 +155,17 @@ struct s_pc_itemgrouphealrate {
 	short rate; /// Rate
 };
 
+///Timed bonus 'bonus_script' struct [Cydh]
+struct s_bonus_script {
+	struct script_code *script;
+	char script_str[MAX_BONUS_SCRIPT_LENGTH]; //Used for comparing and storing on table
+	uint32 tick;
+	uint8 flag;
+	char type; //0 - Ignore; 1 - Buff; 2 - Debuff
+	int16 icon;
+	int tid;
+};
+
 struct map_session_data {
 	struct block_list bl;
 	struct unit_data ud;
@@ -589,16 +600,7 @@ struct map_session_data {
 	struct vip_info vip;
 	bool disableshowrate; //State to disable clif_display_pinfo(). [Cydh]
 #endif
-	///Timed bonus 'bonus_script' struct [Cydh]
-	struct s_script {
-		struct script_code *script;
-		char script_str[MAX_BONUS_SCRIPT_LENGTH]; //Used for comparing and storing on table
-		uint32 tick;
-		uint8 flag;
-		char type; //0 - Ignore; 1 - Buff; 2 - Debuff
-		int16 icon;
-		int tid;
-	} bonus_script[MAX_PC_BONUS_SCRIPT];
+	struct s_bonus_script bonus_script[MAX_PC_BONUS_SCRIPT]; ///Bonus Script [Cydh]
 	
 	struct s_pc_itemgrouphealrate **itemgrouphealrate; /// List of Item Group Heal rate bonus
 	uint8 itemgrouphealrate_count; /// Number of rate bonuses
@@ -1094,8 +1096,9 @@ void pc_crimson_marker_clear(struct map_session_data *sd);
 void pc_show_version(struct map_session_data *sd);
 
 int pc_bonus_script_timer(int tid, unsigned int tick, int id, intptr_t data);
-void pc_bonus_script_remove(struct map_session_data *sd, uint8 i);
+void pc_bonus_script_remove(struct s_bonus_script *bscript);
 void pc_bonus_script_clear(struct map_session_data *sd, uint16 flag);
+void pc_bonus_script_clear_all(struct map_session_data *sd, bool permanent);
 
 void pc_cell_basilica(struct map_session_data *sd);
 

+ 42 - 3
src/map/script.c

@@ -2309,6 +2309,12 @@ void script_hardcoded_constants(void) {
 	/* status option compounds */
 	script_set_constant("Option_Dragon",OPTION_DRAGON,false);
 	script_set_constant("Option_Costume",OPTION_COSTUME,false);
+
+	/* bonus_script commands */
+	script_set_constant("BSF_REM_BUFF",BSF_REM_BUFF,false);
+	script_set_constant("BSF_REM_DEBUFF",BSF_REM_DEBUFF,false);
+	script_set_constant("BSF_ALL",BSF_ALL,false);
+	script_set_constant("BSF_CLEARALL",BSF_CLEARALL,false);
 }
 
 /*==========================================
@@ -18678,13 +18684,15 @@ BUILDIN_FUNC(montransform) {
 	return SCRIPT_CMD_SUCCESS;
 }
 
-/** [Cydh]
+/**
+ * Attach script to player for certain duration
  * bonus_script "<script code>",<duration>{,<flag>{,<type>{,<status_icon>{,<char_id>}}}};
  * @param "script code"
  * @param duration
  * @param flag
  * @param icon
  * @param char_id
+* @author [Cydh]
  **/
 BUILDIN_FUNC(bonus_script) {
 	uint8 i, flag = 0;
@@ -18748,8 +18756,36 @@ BUILDIN_FUNC(bonus_script) {
 	return SCRIPT_CMD_SUCCESS;
 }
 
+/**
+* Removes all bonus script from player
+* bonus_script_clear {<flag>,{<char_id>}};
+* @param flag 0 - Except permanent bonus, 1 - With permanent bonus
+* @param char_id Clear script from this character
+* @author [Cydh]
+*/
+BUILDIN_FUNC(bonus_script_clear) {
+	TBL_PC* sd;
+	bool flag = 0;
+
+	if (script_hasdata(st,2))
+		flag = script_getnum(st,2);
+
+	if (script_hasdata(st,3))
+		sd = map_charid2sd(script_getnum(st,3));
+	else
+		sd = script_rid2sd(st);
+
+	if (sd == NULL)
+		return SCRIPT_CMD_FAILURE;
+
+	pc_bonus_script_clear_all(sd,flag); /// Don't remove permanent script
+	return SCRIPT_CMD_SUCCESS;
+}
+
 /** Allows player to use atcommand while talking with NPC
-* @author [Cydh], [Kichi] */
+* enable_command;
+* @author [Cydh], [Kichi]
+*/
 BUILDIN_FUNC(enable_command) {
 	TBL_PC* sd = script_rid2sd(st);
 
@@ -18760,7 +18796,9 @@ BUILDIN_FUNC(enable_command) {
 }
 
 /** Prevents player to use atcommand while talking with NPC
-* @author [Cydh], [Kichi] */
+* disable_command;
+* @author [Cydh], [Kichi]
+*/
 BUILDIN_FUNC(disable_command) {
 	TBL_PC* sd = script_rid2sd(st);
 
@@ -19419,6 +19457,7 @@ struct script_function buildin_func[] = {
 	BUILDIN_DEF(vip_status,"i?"),
 	BUILDIN_DEF(vip_time,"i?"),
 	BUILDIN_DEF(bonus_script,"si????"),
+	BUILDIN_DEF(bonus_script_clear,"??"),
 	BUILDIN_DEF(getgroupitem,"i"),
 	BUILDIN_DEF(enable_command,""),
 	BUILDIN_DEF(disable_command,""),

+ 7 - 4
src/map/skill.c

@@ -1646,6 +1646,9 @@ int skill_additional_effect (struct block_list* src, struct block_list *bl, uint
 				status_change_end(bl, (sc_type)i, INVALID_TIMER);
 				n--;
 			}
+			//Remove bonus_script by Banishing Buster
+			if (dstsd)
+				pc_bonus_script_clear(dstsd,BSF_REM_ON_BANISHING_BUSTER);
 		}
 		break;
 	case RL_S_STORM:
@@ -7144,9 +7147,9 @@ int skill_castend_nodamage_id (struct block_list *src, struct block_list *bl, ui
 			if(status_isimmune(bl))
 				break;
 
-			//Remove bonus_script when dispelled
+			//Remove bonus_script by Dispell
 			if (dstsd)
-				pc_bonus_script_clear(dstsd,BONUS_FLAG_REM_ON_DISPELL);
+				pc_bonus_script_clear(dstsd,BSF_REM_ON_DISPELL);
 
 			if(!tsc || !tsc->count)
 				break;
@@ -8643,9 +8646,9 @@ int skill_castend_nodamage_id (struct block_list *src, struct block_list *bl, ui
 			if(status_isimmune(bl))
 				break;
 
-			//Remove bonus_script when cleared
+			//Remove bonus_script by Clearance
 			if (dstsd)
-				pc_bonus_script_clear(dstsd,BONUS_FLAG_REM_ON_CLEARANCE);
+				pc_bonus_script_clear(dstsd,BSF_REM_ON_CLEARANCE);
 
 			if(!tsc || !tsc->count)
 				break;

+ 14 - 1
src/map/status.c

@@ -1432,6 +1432,9 @@ int status_damage(struct block_list *src,struct block_list *target,int64 dhp, in
 			if(sc->data[SC_KAGEMUSYA] && --(sc->data[SC_KAGEMUSYA]->val3) <= 0)
 				status_change_end(target, SC_KAGEMUSYA, INVALID_TIMER);
 		}
+
+		if (target->type == BL_PC)
+			pc_bonus_script_clear(BL_CAST(BL_PC,target),BSF_REM_ON_DAMAGED);
 		unit_skillcastcancel(target, 2);
 	}
 
@@ -3134,7 +3137,7 @@ int status_calc_pc_(struct map_session_data* sd, enum e_status_calc_opt opt)
 	for (i = 0; i < MAX_PC_BONUS_SCRIPT; i++) { //Process script Bonus [Cydh]
 		if (!(&sd->bonus_script[i]) || !sd->bonus_script[i].script)
 			continue;
-		if (!sd->bonus_script[i].tid) //Just add timer only for new attached script
+		if (sd->bonus_script[i].tid == INVALID_TIMER) //Just add timer only for new attached script
 			sd->bonus_script[i].tid = add_timer(sd->bonus_script[i].tick,pc_bonus_script_timer,sd->bl.id,i);
 		run_script(sd->bonus_script[i].script,0,sd->bl.id,0);
 	}
@@ -12273,6 +12276,16 @@ void status_change_clear_buffs (struct block_list* bl, int type)
 		status_change_end(bl, (sc_type)i, INVALID_TIMER);
 	}
 
+	//Removes bonus_script
+	if (bl->type == BL_PC) {
+		i = 0;
+		if (type&1) i |= BSF_REM_BUFF;
+		if (type&2) i |= BSF_REM_DEBUFF;
+		if (type&4) i |= BSF_REM_ON_REFRESH;
+		if (type&8) i |= BSF_REM_ON_LUXANIMA;
+		pc_bonus_script_clear(BL_CAST(BL_PC,bl),i);
+	}
+
 	// Cleaning all extras vars
 	sc->comet_x = 0;
 	sc->comet_y = 0;

+ 18 - 7
src/map/status.h

@@ -1732,14 +1732,25 @@ enum e_status_calc_opt {
 	SCO_FORCE = 0x2, /* Only relevant to BL_PC types, ensures call bypasses the queue caused by delayed damage */
 };
 
-///Enum for bonus_script's flag
+///Enum for bonus_script's flag [Cydh]
 enum e_bonus_script_flags {
-	BONUS_FLAG_REM_ON_DEAD		= 0x01,	//Remove bonus when dead
-	BONUS_FLAG_REM_ON_DISPELL	= 0x02,	//Removable by Dispell
-	BONUS_FLAG_REM_ON_CLEARANCE	= 0x04,	//Removable by Clearance
-	BONUS_FLAG_REM_ON_LOGOUT	= 0x08,	//Remove bonus when player logged out
-	BONUS_FLAG_REM_BUFF			= 0x10,	//Remove bonus when player logged out
-	BONUS_FLAG_REM_DEBUFF		= 0x20,	//Remove bonus when player logged out
+	BSF_REM_ON_DEAD				= 0x001, ///Removed when dead
+	BSF_REM_ON_DISPELL			= 0x002, ///Removed by Dispell
+	BSF_REM_ON_CLEARANCE		= 0x004, ///Removed by Clearance
+	BSF_REM_ON_LOGOUT			= 0x008, ///Removed when player logged out
+	BSF_REM_ON_BANISHING_BUSTER	= 0x010, ///Removed by Banishing Buster
+	BSF_REM_ON_REFRESH			= 0x020, ///Removed by Refresh
+	BSF_REM_ON_LUXANIMA			= 0x040, ///Removed by Luxanima
+	BSF_REM_ON_MADOGEAR			= 0x080, ///Removed when Madogear is activated or deactivated
+	BSF_REM_ON_DAMAGED			= 0x100, ///Removed when receive damage
+	BSF_PERMANENT				= 0x200, ///Cannot be removed by sc_end SC_ALL
+
+	// These flags better in the last of everything
+	BSF_REM_BUFF	= 0x1000,	///Remove positive buff
+	BSF_REM_DEBUFF	= 0x2000,	///Remove negative buff
+
+	BSF_ALL = 0x0FFF|BSF_REM_BUFF|BSF_REM_DEBUFF,
+	BSF_CLEARALL = BSF_ALL&~BSF_PERMANENT,
 };
 
 ///Enum for status_get_hpbonus and status_get_spbonus