diff --git a/Database Updates/UpdateDatabase_to_4-0-2.sql b/Database Updates/UpdateDatabase_to_4-0-2.sql
new file mode 100644
index 00000000..9d094a91
--- /dev/null
+++ b/Database Updates/UpdateDatabase_to_4-0-2.sql
@@ -0,0 +1,672 @@
+SET NAMES utf8mb4;
+SET FOREIGN_KEY_CHECKS = 0;
+
+-- =====================================================
+-- SECTION 1: Pet System Emulator Settings
+-- =====================================================
+
+INSERT INTO `emulator_settings` (`key`, `value`) VALUES
+-- Core pet limits
+('hotel.pets.max.room', '15'),
+('hotel.pets.max.inventory', '25'),
+('hotel.pets.name.length.min', '1'),
+('hotel.pets.name.length.max', '15'),
+('hotel.daily.respect.pets', '3'),
+
+-- Command cooldown and spam prevention
+('pet.command.cooldown_ms', '2000'),
+('pet.command.max_same_spam', '3'),
+('pet.command.spam_reset_ms', '10000'),
+('pet.command.min_energy', '15'),
+('pet.command.min_happiness', '10'),
+('pet.command.base_obey_chance', '70'),
+
+-- Pet behavior settings
+('pet.behavior.autonomous_action_delay', '5000'),
+('pet.behavior.idle_wander_min_ms', '10000'),
+('pet.behavior.idle_wander_max_ms', '30000'),
+
+-- Pet stats decay/recovery rates (per cycle)
+('pet.stats.hunger_decay', '1'),
+('pet.stats.thirst_decay', '1'),
+('pet.stats.energy_decay', '1'),
+('pet.stats.happiness_decay', '1'),
+('pet.stats.energy_recovery', '5'),
+('pet.stats.happiness_recovery', '1'),
+
+-- Pet thresholds (below this = needs attention)
+('pet.threshold.hungry', '50'),
+('pet.threshold.thirsty', '50'),
+('pet.threshold.tired', '30'),
+('pet.threshold.sad', '30'),
+
+-- Pet breeding
+('pet.breeding.timeout_seconds', '120')
+ON DUPLICATE KEY UPDATE `value` = VALUES(`value`);
+
+-- =====================================================
+-- SECTION 2: Pet Actions (Pet Type Definitions)
+-- =====================================================
+
+DROP TABLE IF EXISTS `pet_actions`;
+CREATE TABLE `pet_actions` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `pet_type` int(11) NOT NULL,
+ `pet_name` varchar(32) NOT NULL DEFAULT '',
+ `offspring_type` int(11) NOT NULL DEFAULT -1,
+ `happy_actions` varchar(100) NOT NULL DEFAULT 'sml',
+ `tired_actions` varchar(100) NOT NULL DEFAULT 'trd',
+ `random_actions` varchar(100) NOT NULL DEFAULT 'lov',
+ `can_swim` enum('0','1') NOT NULL DEFAULT '0',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `pet_type` (`pet_type`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+INSERT INTO `pet_actions` (`pet_type`, `pet_name`, `offspring_type`, `happy_actions`, `tired_actions`, `random_actions`, `can_swim`) VALUES
+(0, 'Dog', 29, 'sml,wav,joy', 'trd,yng', 'lov,snf', '0'),
+(1, 'Cat', 28, 'sml,pur', 'trd,yng', 'lov,lck', '0'),
+(2, 'Crocodile', -1, 'sml', 'trd', 'lov,snp', '1'),
+(3, 'Terrier', 25, 'sml,wav,joy', 'trd,yng', 'lov,snf', '0'),
+(4, 'Bear', 24, 'sml,grw', 'trd,yng', 'lov', '0'),
+(5, 'Pig', 30, 'sml,oink', 'trd,yng', 'lov,rol', '0'),
+(6, 'Lion', -1, 'sml,ror', 'trd,yng', 'lov', '0'),
+(7, 'Rhino', -1, 'sml', 'trd,yng', 'lov', '0'),
+(8, 'Tarantula', -1, 'sml', 'trd', 'lov,crw', '0'),
+(9, 'Turtle', -1, 'sml', 'trd', 'lov', '1'),
+(10, 'Chick', -1, 'sml,chp', 'trd', 'lov,pck', '0'),
+(11, 'Frog', -1, 'sml,crk', 'trd', 'lov,jmp', '1'),
+(12, 'Dragon', -1, 'sml,flm', 'trd,smk', 'lov,fly', '0'),
+(13, 'Monster', -1, 'sml', 'trd', 'lov', '0'),
+(14, 'Monkey', -1, 'sml,ook', 'trd,yng', 'lov,swg', '0'),
+(15, 'Horse', -1, 'sml,nei', 'trd,yng', 'lov', '0'),
+(16, 'Monsterplant', -1, 'sml', 'trd', 'lov', '0'),
+(17, 'Bunny', -1, 'sml,hop', 'trd,yng', 'lov', '0'),
+(18, 'Evil Bunny', -1, 'sml', 'trd', 'lov', '0'),
+(19, 'Bored Bunny', -1, 'sml', 'trd', 'lov', '0'),
+(20, 'Cute Bunny', -1, 'sml,hop', 'trd', 'lov', '0'),
+(21, 'Wise Pigeon', -1, 'sml,coo', 'trd', 'lov,pck', '0'),
+(22, 'Evil Pigeon', -1, 'sml', 'trd', 'lov,pck', '0'),
+(23, 'Evil Monkey', -1, 'sml', 'trd', 'lov,swg', '0'),
+(24, 'Baby Bear', -1, 'sml', 'trd,yng', 'lov', '0'),
+(25, 'Baby Terrier', -1, 'sml', 'trd,yng', 'lov', '0'),
+(26, 'Gnome', -1, 'sml,grn', 'trd', 'lov', '0'),
+(27, 'Leprechaun', -1, 'sml,grn', 'trd', 'lov,jig', '0'),
+(28, 'Baby Cat', -1, 'sml', 'trd,yng', 'lov', '0'),
+(29, 'Baby Dog', -1, 'sml', 'trd,yng', 'lov', '0'),
+(30, 'Baby Pig', -1, 'sml', 'trd,yng', 'lov', '0'),
+(31, 'Haloompa', -1, 'sml', 'trd', 'lov', '0'),
+(32, 'Fools Pet', -1, 'sml', 'trd', 'lov', '0'),
+(33, 'Pterodactyl', -1, 'sml,sqk', 'trd', 'lov,fly', '0'),
+(34, 'Velociraptor', -1, 'sml,hss', 'trd', 'lov,clw', '0'),
+(35, 'Cow', -1, 'sml,moo', 'trd,yng', 'lov,chw', '0');
+
+-- =====================================================
+-- SECTION 3: Pet Commands Data (English Command Names)
+-- =====================================================
+-- Command IDs mapped to PetManager.petActions:
+-- 0=Free, 1=Sit, 2=Down, 3=Here, 4=Beg, 5=PlayDead, 6=Stay, 7=Follow
+-- 8=Stand, 9=Jump, 10=Speak, 11=Play, 12=Silent, 13=Nest, 14=Drink
+-- 15=FollowLeft, 16=FollowRight, 17=PlayFootball, 18=Teleport, 19=Bounce
+-- 20=Flatten, 21=Dance, 22=Spin, 23=Switch, 24=MoveForward
+-- 25=TurnLeft, 26=TurnRight, 27=Relax, 28=Croak, 29=Dip, 30=Wave
+-- 31=Mambo, 32=HighJump, 33=ChickenDance, 34=TripleJump
+-- 35=Wings, 36=BreatheFire, 37=Hang, 38=Torch, 40=Swing, 41=Roll
+-- 42=RingOfFire, 43=Eat, 44=WagTail, 45=Count, 46=Breed
+
+DROP TABLE IF EXISTS `pet_commands_data`;
+CREATE TABLE `pet_commands_data` (
+ `command_id` int(11) NOT NULL,
+ `text` varchar(25) NOT NULL,
+ `required_level` int(11) NOT NULL DEFAULT 1,
+ `reward_xp` int(11) NOT NULL DEFAULT 5,
+ `cost_happiness` int(11) NOT NULL DEFAULT 0,
+ `cost_energy` int(11) NOT NULL DEFAULT 0,
+ PRIMARY KEY (`command_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+INSERT INTO `pet_commands_data` (`command_id`, `text`, `required_level`, `reward_xp`, `cost_happiness`, `cost_energy`) VALUES
+(0, 'free', 1, 5, 0, 0),
+(1, 'sit', 1, 5, 2, 2),
+(2, 'down', 2, 10, 3, 3),
+(3, 'come here', 2, 10, 2, 5),
+(4, 'beg', 2, 10, 3, 4),
+(5, 'play dead', 3, 15, 4, 5),
+(6, 'stay', 4, 10, 2, 3),
+(7, 'follow', 5, 15, 3, 8),
+(8, 'stand', 6, 15, 2, 3),
+(9, 'jump', 6, 15, 4, 8),
+(10, 'speak', 7, 10, 3, 3),
+(11, 'play', 8, 5, 5, 10),
+(12, 'silent', 8, 5, 2, 1),
+(13, 'nest', 5, 5, 0, 0),
+(14, 'drink', 1, 5, 0, 0),
+(15, 'follow left', 15, 15, 4, 10),
+(16, 'follow right', 15, 15, 4, 10),
+(17, 'play football', 10, 5, 5, 12),
+(18, 'teleport', 9, 5, 3, 5),
+(19, 'bounce', 9, 5, 5, 10),
+(20, 'flatten', 11, 5, 3, 4),
+(21, 'dance', 12, 10, 6, 12),
+(22, 'spin', 10, 5, 4, 8),
+(23, 'switch', 12, 5, 3, 3),
+(24, 'move forward', 17, 5, 2, 2),
+(25, 'turn left', 18, 5, 2, 2),
+(26, 'turn right', 18, 5, 2, 2),
+(27, 'relax', 13, 5, 0, 0),
+(28, 'croak', 14, 5, 3, 3),
+(29, 'dip', 14, 5, 5, 10),
+(30, 'wave', 5, 5, 2, 3),
+(31, 'mambo', 18, 5, 6, 12),
+(32, 'high jump', 18, 5, 5, 12),
+(33, 'chicken dance', 7, 5, 5, 10),
+(34, 'triple jump', 9, 5, 6, 15),
+(35, 'spread wings', 8, 5, 4, 6),
+(36, 'breathe fire', 10, 5, 5, 8),
+(37, 'hang', 12, 5, 4, 6),
+(38, 'torch', 6, 5, 3, 5),
+(40, 'swing', 13, 5, 4, 8),
+(41, 'roll', 10, 5, 5, 10),
+(42, 'ring of fire', 20, 10, 8, 15),
+(43, 'eat', 1, 5, 0, 0),
+(44, 'wag tail', 4, 5, 3, 4),
+(45, 'count', 6, 5, 4, 5),
+(46, 'breed', 1, 5, 10, 20);
+
+-- =====================================================
+-- SECTION 4: Pet Commands (Pet Type -> Command Mapping)
+-- =====================================================
+
+DROP TABLE IF EXISTS `pet_commands`;
+CREATE TABLE `pet_commands` (
+ `pet_id` int(11) NOT NULL,
+ `command_id` int(11) NOT NULL,
+ PRIMARY KEY (`pet_id`, `command_id`),
+ KEY `pet_id` (`pet_id`),
+ KEY `command_id` (`command_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+-- Dog (0) - Full standard pet commands
+INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES
+(0, 0), (0, 1), (0, 2), (0, 3), (0, 4), (0, 5), (0, 6), (0, 7), (0, 8), (0, 9),
+(0, 10), (0, 11), (0, 12), (0, 13), (0, 14), (0, 15), (0, 16), (0, 17), (0, 24),
+(0, 25), (0, 26), (0, 43), (0, 44), (0, 46);
+
+-- Cat (1) - Full standard pet commands + breed
+INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES
+(1, 0), (1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8), (1, 9),
+(1, 10), (1, 11), (1, 12), (1, 13), (1, 14), (1, 15), (1, 16), (1, 17), (1, 24),
+(1, 25), (1, 26), (1, 43), (1, 46);
+
+-- Crocodile (2) - Standard commands (can swim)
+INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES
+(2, 0), (2, 1), (2, 2), (2, 3), (2, 4), (2, 5), (2, 6), (2, 7), (2, 8), (2, 9),
+(2, 10), (2, 11), (2, 12), (2, 13), (2, 14), (2, 15), (2, 16), (2, 17), (2, 24),
+(2, 25), (2, 26), (2, 29), (2, 43);
+
+-- Terrier (3) - Standard commands + breed
+INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES
+(3, 0), (3, 1), (3, 2), (3, 3), (3, 4), (3, 5), (3, 6), (3, 7), (3, 8), (3, 9),
+(3, 10), (3, 11), (3, 12), (3, 13), (3, 14), (3, 15), (3, 16), (3, 17), (3, 24),
+(3, 25), (3, 26), (3, 43), (3, 46);
+
+-- Bear (4) - Standard commands + breed
+INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES
+(4, 0), (4, 1), (4, 2), (4, 3), (4, 4), (4, 5), (4, 6), (4, 7), (4, 8), (4, 9),
+(4, 10), (4, 11), (4, 12), (4, 13), (4, 14), (4, 15), (4, 16), (4, 17), (4, 24),
+(4, 25), (4, 26), (4, 43), (4, 46);
+
+-- Pig (5) - Standard commands + breed
+INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES
+(5, 0), (5, 1), (5, 2), (5, 3), (5, 4), (5, 5), (5, 6), (5, 7), (5, 8), (5, 9),
+(5, 10), (5, 11), (5, 12), (5, 13), (5, 14), (5, 15), (5, 16), (5, 17), (5, 24),
+(5, 25), (5, 26), (5, 43), (5, 46);
+
+-- Lion (6) - Standard commands
+INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES
+(6, 0), (6, 1), (6, 2), (6, 3), (6, 4), (6, 5), (6, 6), (6, 7), (6, 8), (6, 9),
+(6, 10), (6, 11), (6, 12), (6, 13), (6, 14), (6, 15), (6, 16), (6, 17), (6, 24),
+(6, 25), (6, 26), (6, 43);
+
+-- Rhino (7) - Standard commands
+INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES
+(7, 0), (7, 1), (7, 2), (7, 3), (7, 4), (7, 5), (7, 6), (7, 7), (7, 8), (7, 9),
+(7, 10), (7, 11), (7, 12), (7, 13), (7, 14), (7, 15), (7, 16), (7, 17), (7, 24),
+(7, 25), (7, 26), (7, 43);
+
+-- Tarantula (8) - Spider commands (bounce, flatten, spin, etc.)
+INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES
+(8, 0), (8, 2), (8, 3), (8, 5), (8, 6), (8, 7), (8, 9), (8, 10), (8, 11), (8, 13),
+(8, 14), (8, 15), (8, 16), (8, 17), (8, 19), (8, 20), (8, 21), (8, 22), (8, 23),
+(8, 24), (8, 25), (8, 26), (8, 43);
+
+-- Turtle (9) - Aquatic commands
+INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES
+(9, 0), (9, 1), (9, 2), (9, 3), (9, 6), (9, 7), (9, 8), (9, 10), (9, 11), (9, 13),
+(9, 14), (9, 15), (9, 16), (9, 24), (9, 25), (9, 26), (9, 29), (9, 41), (9, 43);
+
+-- Chick (10) - Bird commands
+INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES
+(10, 0), (10, 2), (10, 3), (10, 6), (10, 7), (10, 11), (10, 13), (10, 15), (10, 16),
+(10, 17), (10, 33);
+
+-- Frog (11) - Amphibian commands (croak, dip, wave, mambo)
+INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES
+(11, 0), (11, 1), (11, 2), (11, 3), (11, 4), (11, 5), (11, 6), (11, 7), (11, 9),
+(11, 13), (11, 14), (11, 15), (11, 16), (11, 17), (11, 27), (11, 28), (11, 29),
+(11, 30), (11, 31), (11, 43);
+
+-- Dragon (12) - Dragon special commands (fire, hang, swing, ring of fire)
+INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES
+(12, 0), (12, 2), (12, 3), (12, 5), (12, 6), (12, 7), (12, 8), (12, 9), (12, 10),
+(12, 11), (12, 12), (12, 13), (12, 14), (12, 15), (12, 16), (12, 22), (12, 35),
+(12, 36), (12, 37), (12, 38), (12, 40), (12, 41), (12, 42), (12, 43);
+
+-- Monster (13) - Basic commands
+INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES
+(13, 0), (13, 2), (13, 3), (13, 6), (13, 7), (13, 13);
+
+-- Monkey (14) - Monkey commands (wave, hang, swing)
+INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES
+(14, 0), (14, 1), (14, 2), (14, 3), (14, 4), (14, 5), (14, 6), (14, 7), (14, 9),
+(14, 13), (14, 14), (14, 15), (14, 16), (14, 17), (14, 27), (14, 29), (14, 30),
+(14, 31), (14, 37), (14, 40), (14, 43);
+
+-- Horse (15) - Rideable pet commands + wag tail, count
+INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES
+(15, 0), (15, 2), (15, 3), (15, 6), (15, 7), (15, 10), (15, 11), (15, 12), (15, 13),
+(15, 14), (15, 15), (15, 16), (15, 24), (15, 25), (15, 26), (15, 43), (15, 44), (15, 45);
+
+-- Monsterplant (16) - Minimal commands
+INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES
+(16, 0), (16, 14), (16, 43);
+
+-- Bunnies (17-20) - Bunny commands
+INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES
+(17, 0), (17, 2), (17, 3), (17, 6), (17, 7), (17, 11), (17, 13), (17, 15), (17, 16), (17, 17),
+(18, 0), (18, 2), (18, 3), (18, 6), (18, 7), (18, 11), (18, 13), (18, 15), (18, 16), (18, 17),
+(19, 0), (19, 2), (19, 3), (19, 6), (19, 7), (19, 11), (19, 13), (19, 15), (19, 16), (19, 17),
+(20, 0), (20, 2), (20, 3), (20, 6), (20, 7), (20, 11), (20, 13), (20, 15), (20, 16), (20, 17);
+
+-- Pigeons (21-22)
+INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES
+(21, 0), (21, 2), (21, 3), (21, 6), (21, 7), (21, 11), (21, 13), (21, 15), (21, 16), (21, 17),
+(22, 0), (22, 2), (22, 3), (22, 6), (22, 7), (22, 11), (22, 13), (22, 15), (22, 16), (22, 17);
+
+-- Evil Monkey (23) - Monkey commands
+INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES
+(23, 0), (23, 1), (23, 2), (23, 3), (23, 4), (23, 5), (23, 6), (23, 7), (23, 9),
+(23, 13), (23, 14), (23, 15), (23, 16), (23, 17), (23, 25), (23, 26), (23, 27),
+(23, 29), (23, 30), (23, 31), (23, 37), (23, 40), (23, 43);
+
+-- Baby Bear (24)
+INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES
+(24, 0), (24, 1), (24, 2), (24, 3), (24, 4), (24, 6), (24, 7), (24, 8), (24, 10),
+(24, 11), (24, 12), (24, 13), (24, 14), (24, 15), (24, 16), (24, 17), (24, 24),
+(24, 25), (24, 26), (24, 43);
+
+-- Baby Terrier (25)
+INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES
+(25, 0), (25, 1), (25, 2), (25, 3), (25, 4), (25, 6), (25, 7), (25, 8), (25, 10),
+(25, 11), (25, 12), (25, 13), (25, 14), (25, 15), (25, 16), (25, 17), (25, 24),
+(25, 25), (25, 26), (25, 43);
+
+-- Gnome (26)
+INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES
+(26, 0), (26, 1), (26, 2), (26, 3), (26, 4), (26, 6), (26, 7), (26, 8), (26, 13),
+(26, 14), (26, 15), (26, 16), (26, 17), (26, 25), (26, 26), (26, 43);
+
+-- Leprechaun (27)
+INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES
+(27, 0), (27, 1), (27, 2), (27, 3), (27, 4), (27, 6), (27, 7), (27, 8), (27, 13),
+(27, 14), (27, 15), (27, 16), (27, 17), (27, 25), (27, 26), (27, 43);
+
+-- Baby Cat (28)
+INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES
+(28, 0), (28, 1), (28, 2), (28, 3), (28, 4), (28, 6), (28, 7), (28, 8), (28, 10),
+(28, 11), (28, 12), (28, 13), (28, 14), (28, 15), (28, 16), (28, 17), (28, 24),
+(28, 25), (28, 26), (28, 43);
+
+-- Baby Dog (29)
+INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES
+(29, 0), (29, 1), (29, 2), (29, 3), (29, 4), (29, 6), (29, 7), (29, 8), (29, 10),
+(29, 11), (29, 12), (29, 13), (29, 14), (29, 15), (29, 16), (29, 17), (29, 24),
+(29, 25), (29, 26), (29, 43);
+
+-- Baby Pig (30)
+INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES
+(30, 0), (30, 1), (30, 2), (30, 3), (30, 4), (30, 6), (30, 7), (30, 8), (30, 10),
+(30, 11), (30, 12), (30, 13), (30, 14), (30, 15), (30, 16), (30, 17), (30, 24),
+(30, 25), (30, 26), (30, 43);
+
+-- Haloompa (31)
+INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES
+(31, 0), (31, 1), (31, 2), (31, 3), (31, 4), (31, 6), (31, 7), (31, 8), (31, 13),
+(31, 14), (31, 15), (31, 16), (31, 17), (31, 25), (31, 26), (31, 43);
+
+-- Fools Pet (32) - Full dance/trick set
+INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES
+(32, 0), (32, 1), (32, 2), (32, 3), (32, 4), (32, 5), (32, 6), (32, 7), (32, 8),
+(32, 9), (32, 13), (32, 14), (32, 15), (32, 16), (32, 17), (32, 21), (32, 25),
+(32, 26), (32, 43);
+
+-- Pterodactyl (33)
+INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES
+(33, 0), (33, 2), (33, 3), (33, 4), (33, 6), (33, 7), (33, 11), (33, 13), (33, 14),
+(33, 15), (33, 16), (33, 25), (33, 26), (33, 43);
+
+-- Velociraptor (34)
+INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES
+(34, 0), (34, 1), (34, 2), (34, 3), (34, 6), (34, 7), (34, 8), (34, 10), (34, 12),
+(34, 13), (34, 14), (34, 15), (34, 16), (34, 17), (34, 21), (34, 26), (34, 43);
+
+-- Cow (35)
+INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES
+(35, 0), (35, 2), (35, 3), (35, 4), (35, 6), (35, 7), (35, 13), (35, 14), (35, 15),
+(35, 16), (35, 17), (35, 25), (35, 26), (35, 30), (35, 43);
+
+-- =====================================================
+-- SECTION 5: Pet Vocals (Pet Speech Messages)
+-- =====================================================
+-- pet_id = -1 means general vocals for all pets
+-- pet_id >= 0 means specific to that pet type
+
+CREATE TABLE IF NOT EXISTS `pet_vocals` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `pet_id` int(11) NOT NULL DEFAULT -1,
+ `type` varchar(20) NOT NULL,
+ `message` varchar(255) NOT NULL,
+ PRIMARY KEY (`id`),
+ KEY `pet_id` (`pet_id`),
+ KEY `type` (`type`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+-- Clear existing vocals
+DELETE FROM `pet_vocals`;
+
+-- =====================================================
+-- GENERAL VOCALS (pet_id = -1, used by all pets)
+-- =====================================================
+
+-- GREET_OWNER - When owner enters room
+INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES
+(-1, 'GREET_OWNER', '*perks up excitedly*'),
+(-1, 'GREET_OWNER', 'You''re back!'),
+(-1, 'GREET_OWNER', '*bounces with joy*'),
+(-1, 'GREET_OWNER', 'I missed you!'),
+(-1, 'GREET_OWNER', '*runs in circles happily*'),
+(-1, 'GREET_OWNER', 'Yay! My favorite person!'),
+(-1, 'GREET_OWNER', '*jumps up and down*'),
+(-1, 'GREET_OWNER', 'Finally! You''re here!'),
+(-1, 'GREET_OWNER', '*tail wagging intensifies*');
+
+-- LEVEL_UP - When pet gains a level
+INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES
+(-1, 'LEVEL_UP', '*jumps with joy!*'),
+(-1, 'LEVEL_UP', 'I leveled up!'),
+(-1, 'LEVEL_UP', 'I feel stronger!'),
+(-1, 'LEVEL_UP', 'Woohoo! New level!'),
+(-1, 'LEVEL_UP', '*celebrates*'),
+(-1, 'LEVEL_UP', 'I''m getting better!'),
+(-1, 'LEVEL_UP', 'Level up! Yeah!');
+
+-- MUTED - When told to be silent
+INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES
+(-1, 'MUTED', '*stays quiet*'),
+(-1, 'MUTED', '...'),
+(-1, 'MUTED', '*zips lips*'),
+(-1, 'MUTED', '*nods silently*');
+
+-- UNKNOWN_COMMAND - When pet doesn't understand
+INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES
+(-1, 'UNKNOWN_COMMAND', '*tilts head confused*'),
+(-1, 'UNKNOWN_COMMAND', 'Huh?'),
+(-1, 'UNKNOWN_COMMAND', 'I don''t understand...'),
+(-1, 'UNKNOWN_COMMAND', '*looks puzzled*'),
+(-1, 'UNKNOWN_COMMAND', 'What do you mean?'),
+(-1, 'UNKNOWN_COMMAND', '*scratches head*');
+
+-- DISOBEY - When pet refuses command
+INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES
+(-1, 'DISOBEY', '*ignores command*'),
+(-1, 'DISOBEY', 'Maybe later...'),
+(-1, 'DISOBEY', 'I don''t feel like it'),
+(-1, 'DISOBEY', '*pretends not to hear*'),
+(-1, 'DISOBEY', 'Nah...'),
+(-1, 'DISOBEY', '*turns away*'),
+(-1, 'DISOBEY', 'Not right now'),
+(-1, 'DISOBEY', '*yawns dismissively*'),
+(-1, 'DISOBEY', 'Too tired for that'),
+(-1, 'DISOBEY', 'Ask me again later'),
+(-1, 'DISOBEY', '*looks the other way*'),
+(-1, 'DISOBEY', 'I''d rather not'),
+(-1, 'DISOBEY', '*shakes head*'),
+(-1, 'DISOBEY', 'No thanks'),
+(-1, 'DISOBEY', '*walks away slowly*'),
+(-1, 'DISOBEY', 'Can''t be bothered'),
+(-1, 'DISOBEY', '*pretends to be asleep*'),
+(-1, 'DISOBEY', 'You can''t make me!'),
+(-1, 'DISOBEY', '*stubbornly sits down*'),
+(-1, 'DISOBEY', 'I refuse!');
+
+-- DRINKING - When drinking water
+INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES
+(-1, 'DRINKING', '*laps up water*'),
+(-1, 'DRINKING', 'Refreshing!'),
+(-1, 'DRINKING', '*gulp gulp*'),
+(-1, 'DRINKING', 'Ah, that''s good!'),
+(-1, 'DRINKING', '*slurp*');
+
+-- EATING - When eating food
+INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES
+(-1, 'EATING', '*munches happily*'),
+(-1, 'EATING', 'Yum!'),
+(-1, 'EATING', 'Delicious!'),
+(-1, 'EATING', '*nom nom nom*'),
+(-1, 'EATING', 'This is tasty!'),
+(-1, 'EATING', '*chomps*'),
+(-1, 'EATING', 'More please!');
+
+-- PLAYFUL - When in playful mood
+INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES
+(-1, 'PLAYFUL', '*bounces excitedly*'),
+(-1, 'PLAYFUL', 'Let''s play!'),
+(-1, 'PLAYFUL', '*runs around happily*'),
+(-1, 'PLAYFUL', 'Play with me!'),
+(-1, 'PLAYFUL', '*jumps around*'),
+(-1, 'PLAYFUL', 'I wanna play!'),
+(-1, 'PLAYFUL', '*brings a toy*'),
+(-1, 'PLAYFUL', 'Wheee!'),
+(-1, 'PLAYFUL', '*zooms around the room*'),
+(-1, 'PLAYFUL', 'Catch me if you can!');
+
+-- SLEEPING - When sleeping
+INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES
+(-1, 'SLEEPING', '*snores softly*'),
+(-1, 'SLEEPING', 'Zzz...'),
+(-1, 'SLEEPING', '*mumbles in sleep*'),
+(-1, 'SLEEPING', 'ZzZzZz...'),
+(-1, 'SLEEPING', '*dreams peacefully*'),
+(-1, 'SLEEPING', '*twitches while dreaming*'),
+(-1, 'SLEEPING', '*snoozes*'),
+(-1, 'SLEEPING', '*breathes slowly*');
+
+-- TIRED - When tired/low energy
+INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES
+(-1, 'TIRED', '*yawns*'),
+(-1, 'TIRED', 'So sleepy...'),
+(-1, 'TIRED', '*eyes drooping*'),
+(-1, 'TIRED', 'I need rest...');
+
+-- THIRSTY - When thirsty
+INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES
+(-1, 'THIRSTY', '*pants*'),
+(-1, 'THIRSTY', 'Water please!'),
+(-1, 'THIRSTY', '*looks at water bowl*'),
+(-1, 'THIRSTY', 'So thirsty...'),
+(-1, 'THIRSTY', '*dry tongue*'),
+(-1, 'THIRSTY', 'Need a drink!'),
+(-1, 'THIRSTY', '*licks lips*'),
+(-1, 'THIRSTY', 'I''m parched!');
+
+-- HUNGRY - When hungry
+INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES
+(-1, 'HUNGRY', '*stomach growls*'),
+(-1, 'HUNGRY', 'I need food!'),
+(-1, 'HUNGRY', '*looks at food bowl*'),
+(-1, 'HUNGRY', 'Feed me!'),
+(-1, 'HUNGRY', 'So hungry...'),
+(-1, 'HUNGRY', '*tummy rumbles*'),
+(-1, 'HUNGRY', 'Food please!'),
+(-1, 'HUNGRY', '*drools at thought of food*'),
+(-1, 'HUNGRY', 'Is it dinner time?'),
+(-1, 'HUNGRY', '*sniffs around for food*'),
+(-1, 'HUNGRY', 'I could eat a horse!'),
+(-1, 'HUNGRY', '*begs for food*'),
+(-1, 'HUNGRY', 'Starving over here!');
+
+-- GENERIC_NEUTRAL - Random idle chat
+INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES
+(-1, 'GENERIC_NEUTRAL', '*looks around*'),
+(-1, 'GENERIC_NEUTRAL', '*sniffs the air*'),
+(-1, 'GENERIC_NEUTRAL', '*stretches*'),
+(-1, 'GENERIC_NEUTRAL', '*scratches ear*'),
+(-1, 'GENERIC_NEUTRAL', '*observes surroundings*'),
+(-1, 'GENERIC_NEUTRAL', '*sits quietly*'),
+(-1, 'GENERIC_NEUTRAL', '*watches curiously*');
+
+-- GENERIC_SAD - When sad/low happiness
+INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES
+(-1, 'GENERIC_SAD', '*whimpers*'),
+(-1, 'GENERIC_SAD', '*looks sad*'),
+(-1, 'GENERIC_SAD', '*sighs*'),
+(-1, 'GENERIC_SAD', '*droops head*'),
+(-1, 'GENERIC_SAD', 'I''m lonely...'),
+(-1, 'GENERIC_SAD', '*mopes around*'),
+(-1, 'GENERIC_SAD', '*looks dejected*'),
+(-1, 'GENERIC_SAD', 'Nobody loves me...'),
+(-1, 'GENERIC_SAD', '*sulks in corner*');
+
+-- GENERIC_HAPPY - When happy
+INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES
+(-1, 'GENERIC_HAPPY', '*wags tail happily*'),
+(-1, 'GENERIC_HAPPY', '*jumps with joy*'),
+(-1, 'GENERIC_HAPPY', ':)'),
+(-1, 'GENERIC_HAPPY', 'Life is good!'),
+(-1, 'GENERIC_HAPPY', '*prances around*'),
+(-1, 'GENERIC_HAPPY', '*does a happy dance*'),
+(-1, 'GENERIC_HAPPY', 'I''m so happy!'),
+(-1, 'GENERIC_HAPPY', '*beams with joy*'),
+(-1, 'GENERIC_HAPPY', 'What a great day!'),
+(-1, 'GENERIC_HAPPY', '*grins*'),
+(-1, 'GENERIC_HAPPY', '*radiates happiness*'),
+(-1, 'GENERIC_HAPPY', 'Yippee!'),
+(-1, 'GENERIC_HAPPY', '*spins around happily*'),
+(-1, 'GENERIC_HAPPY', 'This is the best!');
+
+-- =====================================================
+-- PET-SPECIFIC VOCALS
+-- =====================================================
+
+-- Dog (0) specific vocals
+INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES
+(0, 'GENERIC_HAPPY', 'Woof woof!'),
+(0, 'GENERIC_HAPPY', '*wags tail furiously*'),
+(0, 'GREET_OWNER', '*barks excitedly*'),
+(0, 'GREET_OWNER', 'Woof! You''re home!'),
+(0, 'PLAYFUL', '*drops ball at your feet*'),
+(0, 'PLAYFUL', 'Throw the ball!'),
+(0, 'HUNGRY', '*stares at food bowl*'),
+(0, 'DISOBEY', '*chases own tail instead*');
+
+-- Cat (1) specific vocals
+INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES
+(1, 'GENERIC_HAPPY', '*purrs*'),
+(1, 'GENERIC_HAPPY', 'Meow!'),
+(1, 'GENERIC_NEUTRAL', '*grooms self*'),
+(1, 'GREET_OWNER', '*rubs against leg*'),
+(1, 'DISOBEY', '*looks away disdainfully*'),
+(1, 'DISOBEY', '*yawns dismissively*'),
+(1, 'PLAYFUL', '*pounces on shadow*'),
+(1, 'SLEEPING', '*purrs while sleeping*');
+
+-- Dragon (12) specific vocals
+INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES
+(12, 'GENERIC_HAPPY', '*breathes small flames happily*'),
+(12, 'GENERIC_HAPPY', '*roars softly*'),
+(12, 'GENERIC_SAD', '*smoke puffs from nostrils*'),
+(12, 'DISOBEY', '*snorts flames*'),
+(12, 'DISOBEY', 'I am a DRAGON, not a servant!'),
+(12, 'HUNGRY', '*eyes the nearest villager*'),
+(12, 'HUNGRY', 'I require sustenance!'),
+(12, 'THIRSTY', '*smoke rises as throat dries*'),
+(12, 'PLAYFUL', '*chases own tail, breathing fire*'),
+(12, 'GREET_OWNER', '*bows majestic head*'),
+(12, 'SLEEPING', '*snores, causing small fires*'),
+(12, 'LEVEL_UP', '*ROARS triumphantly!*');
+
+-- Horse (15) specific vocals
+INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES
+(15, 'GENERIC_HAPPY', '*neighs happily*'),
+(15, 'GENERIC_NEUTRAL', '*swishes tail*'),
+(15, 'GREET_OWNER', '*whinnies in greeting*'),
+(15, 'DISOBEY', 'Nay. (Geddit?)'),
+(15, 'HUNGRY', '*looks at hay expectantly*'),
+(15, 'PLAYFUL', 'Let''s go for a ride!'),
+(15, 'TIRED', '*stamps hoof wearily*');
+
+-- Tarantula (8) specific vocals
+INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES
+(8, 'GREET_OWNER', 'You look more edible every time!'),
+(8, 'DISOBEY', '*hisses*'),
+(8, 'DISOBEY', 'I do not obey mammals'),
+(8, 'HUNGRY', 'Bring me fresh meat!'),
+(8, 'PLAYFUL', '*dances on eight legs*'),
+(8, 'GENERIC_HAPPY', '*clicks mandibles happily*');
+
+-- Frog (11) specific vocals
+INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES
+(11, 'GENERIC_HAPPY', 'Ribbit!'),
+(11, 'GENERIC_NEUTRAL', '*croaks*'),
+(11, 'GREET_OWNER', '*hops excitedly*'),
+(11, 'PLAYFUL', '*catches fly with tongue*'),
+(11, 'THIRSTY', '*seeks water*');
+
+-- Cow (35) specific vocals
+INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES
+(35, 'GENERIC_HAPPY', 'Moooo!'),
+(35, 'GREET_OWNER', 'Greetings. Did you bring kale?'),
+(35, 'EATING', '*chews grass thoughtfully*'),
+(35, 'DISOBEY', 'I''d rather meditate'),
+(35, 'LEVEL_UP', '*DING* I''m on the up!');
+
+-- Lion (6) specific vocals
+INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES
+(6, 'GENERIC_HAPPY', '*roars majestically*'),
+(6, 'GREET_OWNER', '*nods regally*'),
+(6, 'DISOBEY', 'I am the king!'),
+(6, 'HUNGRY', '*eyes prey*'),
+(6, 'PLAYFUL', '*pounces playfully*');
+
+-- Bear (4) specific vocals
+INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES
+(4, 'GENERIC_HAPPY', '*growls contentedly*'),
+(4, 'HUNGRY', '*sniffs for honey*'),
+(4, 'SLEEPING', '*hibernates*'),
+(4, 'GREET_OWNER', '*bear hug incoming*');
+
+-- Monkey (14) specific vocals
+INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES
+(14, 'GENERIC_HAPPY', 'Ook ook!'),
+(14, 'PLAYFUL', '*swings from furniture*'),
+(14, 'GREET_OWNER', '*does a backflip*'),
+(14, 'DISOBEY', '*throws something*'),
+(14, 'HUNGRY', '*looks for bananas*');
+
+-- Bunny (17) specific vocals
+INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES
+(17, 'GENERIC_HAPPY', '*hops happily*'),
+(17, 'GREET_OWNER', '*twitches nose excitedly*'),
+(17, 'PLAYFUL', '*binkies around*'),
+(17, 'HUNGRY', '*nibbles on carrot*');
+
+SET FOREIGN_KEY_CHECKS = 1;
\ No newline at end of file
diff --git a/Emulator/pom.xml b/Emulator/pom.xml
index 62e8f0ee..0f362d0a 100644
--- a/Emulator/pom.xml
+++ b/Emulator/pom.xml
@@ -6,7 +6,7 @@
com.eu.habbo
Habbo
- 4.0.1
+ 4.0.2
UTF-8
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/ItemManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/ItemManager.java
index 12baf283..c3e038e7 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/ItemManager.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/ItemManager.java
@@ -3,7 +3,10 @@ package com.eu.habbo.habbohotel.items;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.items.interactions.*;
import com.eu.habbo.habbohotel.items.interactions.games.InteractionGameTimer;
-import com.eu.habbo.habbohotel.items.interactions.games.battlebanzai.*;
+import com.eu.habbo.habbohotel.items.interactions.games.battlebanzai.InteractionBattleBanzaiPuck;
+import com.eu.habbo.habbohotel.items.interactions.games.battlebanzai.InteractionBattleBanzaiSphere;
+import com.eu.habbo.habbohotel.items.interactions.games.battlebanzai.InteractionBattleBanzaiTeleporter;
+import com.eu.habbo.habbohotel.items.interactions.games.battlebanzai.InteractionBattleBanzaiTile;
import com.eu.habbo.habbohotel.items.interactions.games.battlebanzai.gates.InteractionBattleBanzaiGateBlue;
import com.eu.habbo.habbohotel.items.interactions.games.battlebanzai.gates.InteractionBattleBanzaiGateGreen;
import com.eu.habbo.habbohotel.items.interactions.games.battlebanzai.gates.InteractionBattleBanzaiGateRed;
@@ -48,10 +51,10 @@ import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredBlob;
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraOrEval;
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraRandom;
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraUnseen;
-import com.eu.habbo.habbohotel.wired.highscores.WiredHighscoreManager;
import com.eu.habbo.habbohotel.items.interactions.wired.triggers.*;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboItem;
+import com.eu.habbo.habbohotel.wired.highscores.WiredHighscoreManager;
import com.eu.habbo.messages.outgoing.inventory.AddHabboItemComposer;
import com.eu.habbo.plugin.events.emulator.EmulatorLoadItemsManagerEvent;
import com.eu.habbo.threading.runnables.QueryDeleteHabboItem;
@@ -134,6 +137,8 @@ public class ItemManager {
this.interactionsList.add(new ItemInteraction("pet_drink", InteractionPetDrink.class));
this.interactionsList.add(new ItemInteraction("pet_food", InteractionPetFood.class));
this.interactionsList.add(new ItemInteraction("pet_toy", InteractionPetToy.class));
+ this.interactionsList.add(new ItemInteraction("pet_tree", InteractionPetTree.class));
+ this.interactionsList.add(new ItemInteraction("pet_trampoline", InteractionPetTrampoline.class));
this.interactionsList.add(new ItemInteraction("breeding_nest", InteractionPetBreedingNest.class));
this.interactionsList.add(new ItemInteraction("obstacle", InteractionObstacle.class));
this.interactionsList.add(new ItemInteraction("monsterplant_seed", InteractionMonsterPlantSeed.class));
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/pets/InteractionPetBreedingNest.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/pets/InteractionPetBreedingNest.java
index ee598d8d..c9dd8beb 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/pets/InteractionPetBreedingNest.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/pets/InteractionPetBreedingNest.java
@@ -121,9 +121,9 @@ public class InteractionPetBreedingNest extends HabboItem {
if (this.petOne != null) {
habbo.getClient().sendResponse(new PetPackageNameValidationComposer(this.getId(), PetPackageNameValidationComposer.CLOSE_WIDGET, ""));
}
- if (this.petTwo.getUserId() != habbo.getHabboInfo().getId()) {
- Habbo owner = this.petTwo.getRoom().getHabbo(this.petTwo.getUserId());
- if (owner != null) {
+ if (this.petTwo != null && this.petTwo.getUserId() != habbo.getHabboInfo().getId()) {
+ Habbo owner = this.petTwo.getRoom() != null ? this.petTwo.getRoom().getHabbo(this.petTwo.getUserId()) : null;
+ if (owner != null && owner.getClient() != null) {
owner.getClient().sendResponse(new PetPackageNameValidationComposer(this.getId(), PetPackageNameValidationComposer.CLOSE_WIDGET, ""));
}
}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/pets/InteractionPetDrink.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/pets/InteractionPetDrink.java
index 178e73e5..5f088ba1 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/pets/InteractionPetDrink.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/pets/InteractionPetDrink.java
@@ -2,22 +2,27 @@ package com.eu.habbo.habbohotel.items.interactions.pets;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.achievements.AchievementManager;
+import com.eu.habbo.habbohotel.gameclients.GameClient;
import com.eu.habbo.habbohotel.items.Item;
import com.eu.habbo.habbohotel.items.interactions.InteractionDefault;
import com.eu.habbo.habbohotel.pets.Pet;
-import com.eu.habbo.habbohotel.pets.PetTasks;
+import com.eu.habbo.habbohotel.pets.PetVocalsType;
import com.eu.habbo.habbohotel.pets.RideablePet;
-import com.eu.habbo.habbohotel.rooms.Room;
-import com.eu.habbo.habbohotel.rooms.RoomUnit;
-import com.eu.habbo.habbohotel.rooms.RoomUnitStatus;
-import com.eu.habbo.habbohotel.rooms.RoomUserRotation;
-import com.eu.habbo.messages.outgoing.rooms.users.RoomUserStatusComposer;
+import com.eu.habbo.habbohotel.rooms.*;
+import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.threading.runnables.PetClearPosture;
+import com.eu.habbo.threading.runnables.RoomUnitWalkToLocation;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.sql.ResultSet;
import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.List;
public class InteractionPetDrink extends InteractionDefault {
+ private static final Logger LOGGER = LoggerFactory.getLogger(InteractionPetDrink.class);
+
public InteractionPetDrink(ResultSet set, Item baseItem) throws SQLException {
super(set, baseItem);
}
@@ -26,32 +31,81 @@ public class InteractionPetDrink extends InteractionDefault {
super(id, userId, item, extradata, limitedStack, limitedSells);
}
+ @Override
+ public boolean canToggle(Habbo habbo, Room room) {
+ return RoomLayout.tilesAdjecent(room.getLayout().getTile(this.getX(), this.getY()), habbo.getRoomUnit().getCurrentLocation());
+ }
+
+ @Override
+ public void onClick(GameClient client, Room room, Object[] objects) throws Exception {
+ if (client == null)
+ return;
+
+ if (!this.canToggle(client.getHabbo(), room)) {
+ RoomTile closestTile = null;
+ for (RoomTile tile : room.getLayout().getTilesAround(room.getLayout().getTile(this.getX(), this.getY()))) {
+ if (tile.isWalkable() && (closestTile == null || closestTile.distance(client.getHabbo().getRoomUnit().getCurrentLocation()) > tile.distance(client.getHabbo().getRoomUnit().getCurrentLocation()))) {
+ closestTile = tile;
+ }
+ }
+
+ if (closestTile != null && !closestTile.equals(client.getHabbo().getRoomUnit().getCurrentLocation())) {
+ List onSuccess = new ArrayList<>();
+ onSuccess.add(() -> {
+ this.change(room, this.getBaseItem().getStateCount() - 1);
+ });
+
+ client.getHabbo().getRoomUnit().setGoalLocation(closestTile);
+ Emulator.getThreading().run(new RoomUnitWalkToLocation(client.getHabbo().getRoomUnit(), closestTile, room, onSuccess, new ArrayList<>()));
+ }
+ }
+ }
+
@Override
public void onWalkOn(RoomUnit roomUnit, Room room, Object[] objects) throws Exception {
super.onWalkOn(roomUnit, room, objects);
+ if (this.getExtradata() == null || this.getExtradata().isEmpty())
+ this.setExtradata("0");
+
+ // Check if there's water left (state 0 = full, higher = less water)
+ int currentState = 0;
+ try {
+ currentState = Integer.parseInt(this.getExtradata());
+ } catch (NumberFormatException e) {
+ currentState = 0;
+ }
+
+ // If water bowl is empty (state >= max states), don't allow drinking
+ if (currentState >= this.getBaseItem().getStateCount() - 1) {
+ return;
+ }
+
Pet pet = room.getPet(roomUnit);
- if (pet != null) {
- // Don't let ridden pets drink
- if (pet instanceof RideablePet && ((RideablePet) pet).getRider() != null)
- return;
+ if (pet != null && !(pet instanceof RideablePet && ((RideablePet) pet).getRider() != null)
+ && pet.getPetData().haveDrinkItem(this) && pet.levelThirst >= 35) {
+ pet.clearPosture();
+ pet.getRoomUnit().setGoalLocation(room.getLayout().getTile(this.getX(), this.getY()));
+ pet.getRoomUnit().setRotation(RoomUserRotation.values()[this.getRotation()]);
+ pet.getRoomUnit().clearStatus();
+ pet.getRoomUnit().setStatus(RoomUnitStatus.EAT, pet.getRoomUnit().getCurrentLocation().getStackHeight() + "");
+ pet.packetUpdate = true;
+
+ // Say drinking vocal
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.DRINKING));
- if (pet.getPetData().haveDrinkItem(this)) {
- if (pet.levelThirst >= 35) {
- pet.setTask(PetTasks.EAT);
- pet.getRoomUnit().setGoalLocation(room.getLayout().getTile(this.getX(), this.getY()));
- pet.getRoomUnit().setRotation(RoomUserRotation.values()[this.getRotation()]);
- pet.getRoomUnit().clearStatus();
- pet.getRoomUnit().removeStatus(RoomUnitStatus.MOVE);
- pet.getRoomUnit().setStatus(RoomUnitStatus.EAT, "0");
- pet.addThirst(-75);
- room.sendComposer(new RoomUserStatusComposer(roomUnit).compose());
- Emulator.getThreading().run(new PetClearPosture(pet, RoomUnitStatus.EAT, null, true), 500);
+ // Faster drinking - 500ms instead of 1000ms
+ Emulator.getThreading().run(() -> {
+ pet.addThirst(-75);
+ // Increase state to show less water (+1, not -1)
+ this.change(room, 1);
+ pet.getRoomUnit().clearStatus();
+ Emulator.getThreading().run(new PetClearPosture(pet, RoomUnitStatus.EAT, null, true), 0);
+ pet.packetUpdate = true;
+ }, 500);
- AchievementManager.progressAchievement(Emulator.getGameEnvironment().getHabboManager().getHabbo(pet.getUserId()), Emulator.getGameEnvironment().getAchievementManager().getAchievement("PetFeeding"), 75);
- }
- }
+ AchievementManager.progressAchievement(Emulator.getGameEnvironment().getHabboManager().getHabbo(pet.getUserId()), Emulator.getGameEnvironment().getAchievementManager().getAchievement("PetFeeding"), 75);
}
}
@@ -59,4 +113,31 @@ public class InteractionPetDrink extends InteractionDefault {
public boolean allowWiredResetState() {
return false;
}
+
+ private void change(Room room, int amount) {
+ int state = 0;
+
+ if (this.getExtradata() == null || this.getExtradata().isEmpty()) {
+ this.setExtradata("0");
+ }
+
+ try {
+ state = Integer.parseInt(this.getExtradata());
+ } catch (Exception e) {
+ LOGGER.error("Caught exception", e);
+ }
+
+ state += amount;
+ if (state > this.getBaseItem().getStateCount() - 1) {
+ state = this.getBaseItem().getStateCount() - 1;
+ }
+
+ if (state < 0) {
+ state = 0;
+ }
+
+ this.setExtradata(state + "");
+ this.needsUpdate(true);
+ room.updateItemState(this);
+ }
}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/pets/InteractionPetFood.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/pets/InteractionPetFood.java
index aec5233f..4fa374f3 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/pets/InteractionPetFood.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/pets/InteractionPetFood.java
@@ -32,6 +32,19 @@ public class InteractionPetFood extends InteractionDefault {
if (this.getExtradata().length() == 0)
this.setExtradata("0");
+ // Check if there's food left (state < stateCount means food remaining)
+ int currentState = 0;
+ try {
+ currentState = Integer.parseInt(this.getExtradata());
+ } catch (NumberFormatException e) {
+ currentState = 0;
+ }
+
+ // If food is empty (state >= max states), don't allow eating
+ if (currentState >= this.getBaseItem().getStateCount()) {
+ return;
+ }
+
Pet pet = room.getPet(roomUnit);
if (pet != null) {
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/pets/InteractionPetToy.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/pets/InteractionPetToy.java
index ef0a829f..d02e23df 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/pets/InteractionPetToy.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/pets/InteractionPetToy.java
@@ -1,15 +1,13 @@
package com.eu.habbo.habbohotel.items.interactions.pets;
import com.eu.habbo.Emulator;
+import com.eu.habbo.habbohotel.gameclients.GameClient;
import com.eu.habbo.habbohotel.items.Item;
import com.eu.habbo.habbohotel.items.interactions.InteractionDefault;
import com.eu.habbo.habbohotel.pets.Pet;
import com.eu.habbo.habbohotel.pets.PetTasks;
-import com.eu.habbo.habbohotel.pets.RideablePet;
-import com.eu.habbo.habbohotel.rooms.Room;
-import com.eu.habbo.habbohotel.rooms.RoomUnit;
-import com.eu.habbo.habbohotel.rooms.RoomUnitStatus;
-import com.eu.habbo.habbohotel.rooms.RoomUserRotation;
+import com.eu.habbo.habbohotel.pets.PetVocalsType;
+import com.eu.habbo.habbohotel.rooms.*;
import com.eu.habbo.habbohotel.users.HabboItem;
import com.eu.habbo.threading.runnables.PetClearPosture;
@@ -27,17 +25,41 @@ public class InteractionPetToy extends InteractionDefault {
this.setExtradata("0");
}
+ @Override
+ public void onClick(GameClient client, Room room, Object[] objects) {
+ // Toys are not clickable by users
+ }
+
+ @Override
+ public void onMove(Room room, RoomTile oldLocation, RoomTile newLocation) {
+ this.setExtradata("0");
+ room.updateItem(this);
+
+ for (Pet pet : room.getPetsAt(oldLocation)) {
+ pet.getRoomUnit().clearStatus();
+ pet.packetUpdate = true;
+ }
+ }
+
+ @Override
+ public void onPickUp(Room room) {
+ this.setExtradata("0");
+
+ for (RoomTile tile : this.getOccupyingTiles(room.getLayout())) {
+ for (Pet pet : room.getPetsAt(tile)) {
+ pet.getRoomUnit().clearStatus();
+ pet.packetUpdate = true;
+ }
+ }
+ }
+
@Override
public void onWalkOn(RoomUnit roomUnit, Room room, Object[] objects) throws Exception {
super.onWalkOn(roomUnit, room, objects);
Pet pet = room.getPet(roomUnit);
- if (pet != null) {
- // Don't let ridden pets play with toys
- if (pet instanceof RideablePet && ((RideablePet) pet).getRider() != null)
- return;
-
+ if (pet != null && pet.getPetData().haveToyItem(this.getBaseItem())) {
if (pet.getEnergy() <= 35) {
return;
}
@@ -46,15 +68,20 @@ public class InteractionPetToy extends InteractionDefault {
pet.getRoomUnit().setGoalLocation(room.getLayout().getTile(this.getX(), this.getY()));
pet.getRoomUnit().setRotation(RoomUserRotation.values()[this.getRotation()]);
pet.getRoomUnit().clearStatus();
- pet.getRoomUnit().removeStatus(RoomUnitStatus.MOVE);
- pet.getRoomUnit().setStatus(RoomUnitStatus.PLAY, "0");
+ pet.getRoomUnit().setStatus(RoomUnitStatus.PLAY, pet.getRoomUnit().getCurrentLocation().getStackHeight() + "");
pet.packetUpdate = true;
+
+ // Say playful vocal
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.PLAYFUL));
+
HabboItem item = this;
Emulator.getThreading().run(() -> {
pet.addHappiness(25);
item.setExtradata("0");
room.updateItem(item);
+ pet.getRoomUnit().clearStatus();
new PetClearPosture(pet, RoomUnitStatus.PLAY, null, true).run();
+ pet.packetUpdate = true;
}, 2500 + (Emulator.getRandom().nextInt(20) * 500));
this.setExtradata("1");
room.updateItemState(this);
@@ -69,10 +96,18 @@ public class InteractionPetToy extends InteractionDefault {
if (pet != null) {
this.setExtradata("0");
- room.updateItemState(this);
+ room.updateItem(this);
+ pet.getRoomUnit().clearStatus();
+ pet.packetUpdate = true;
}
}
+ @Override
+ public boolean canWalkOn(RoomUnit roomUnit, Room room, Object[] objects) {
+ Pet pet = room.getPet(roomUnit);
+ return roomUnit.getRoomUnitType() == RoomUnitType.PET && pet != null && pet.getPetData().haveToyItem(this.getBaseItem());
+ }
+
@Override
public boolean allowWiredResetState() {
return false;
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/pets/InteractionPetTrampoline.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/pets/InteractionPetTrampoline.java
new file mode 100644
index 00000000..bae8b0a5
--- /dev/null
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/pets/InteractionPetTrampoline.java
@@ -0,0 +1,106 @@
+package com.eu.habbo.habbohotel.items.interactions.pets;
+
+import com.eu.habbo.Emulator;
+import com.eu.habbo.habbohotel.gameclients.GameClient;
+import com.eu.habbo.habbohotel.items.Item;
+import com.eu.habbo.habbohotel.items.interactions.InteractionDefault;
+import com.eu.habbo.habbohotel.pets.Pet;
+import com.eu.habbo.habbohotel.pets.PetTasks;
+import com.eu.habbo.habbohotel.rooms.*;
+import com.eu.habbo.threading.runnables.PetClearPosture;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+public class InteractionPetTrampoline extends InteractionDefault {
+ public InteractionPetTrampoline(ResultSet set, Item baseItem) throws SQLException {
+ super(set, baseItem);
+ this.setExtradata("0");
+ }
+
+ public InteractionPetTrampoline(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) {
+ super(id, userId, item, extradata, limitedStack, limitedSells);
+ this.setExtradata("0");
+ }
+
+ @Override
+ public void onClick(GameClient client, Room room, Object[] objects) {
+ // Trampolines are not clickable by users
+ }
+
+ @Override
+ public void onMove(Room room, RoomTile oldLocation, RoomTile newLocation) {
+ this.setExtradata("0");
+ room.updateItem(this);
+
+ for (Pet pet : room.getPetsAt(oldLocation)) {
+ pet.getRoomUnit().removeStatus(RoomUnitStatus.JUMP);
+ pet.packetUpdate = true;
+ }
+ }
+
+ @Override
+ public void onPickUp(Room room) {
+ this.setExtradata("0");
+
+ for (Pet pet : room.getPetsAt(room.getLayout().getTile(this.getX(), this.getY()))) {
+ pet.getRoomUnit().removeStatus(RoomUnitStatus.JUMP);
+ pet.packetUpdate = true;
+ }
+ }
+
+ @Override
+ public void onWalkOn(RoomUnit roomUnit, Room room, Object[] objects) throws Exception {
+ super.onWalkOn(roomUnit, room, objects);
+
+ Pet pet = room.getPet(roomUnit);
+
+ if (pet != null && pet.getPetData().haveToyItem(this.getBaseItem()) && this.getOccupyingTiles(room.getLayout()).contains(pet.getRoomUnit().getGoal())) {
+ if (pet.getEnergy() <= 35) {
+ return;
+ }
+
+ pet.clearPosture();
+ pet.setTask(PetTasks.JUMP);
+ pet.getRoomUnit().setStatus(RoomUnitStatus.JUMP, "");
+ pet.packetUpdate = true;
+
+ Emulator.getThreading().run(() -> {
+ new PetClearPosture(pet, RoomUnitStatus.JUMP, null, false);
+ pet.getRoomUnit().setGoalLocation(room.getRandomWalkableTile());
+ this.setExtradata("0");
+ room.updateItemState(this);
+ }, 4000);
+
+ pet.addHappiness(25);
+
+ this.setExtradata("1");
+ room.updateItemState(this);
+ }
+ }
+
+ @Override
+ public void onWalkOff(RoomUnit roomUnit, Room room, Object[] objects) throws Exception {
+ super.onWalkOff(roomUnit, room, objects);
+
+ Pet pet = room.getPet(roomUnit);
+
+ if (pet != null) {
+ this.setExtradata("0");
+ room.updateItem(this);
+ pet.getRoomUnit().removeStatus(RoomUnitStatus.JUMP);
+ pet.packetUpdate = true;
+ }
+ }
+
+ @Override
+ public boolean canWalkOn(RoomUnit roomUnit, Room room, Object[] objects) {
+ Pet pet = room.getPet(roomUnit);
+ return roomUnit.getRoomUnitType() == RoomUnitType.PET && pet != null && pet.getPetData().haveToyItem(this.getBaseItem());
+ }
+
+ @Override
+ public boolean allowWiredResetState() {
+ return false;
+ }
+}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/pets/InteractionPetTree.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/pets/InteractionPetTree.java
new file mode 100644
index 00000000..e65e1e4f
--- /dev/null
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/pets/InteractionPetTree.java
@@ -0,0 +1,126 @@
+package com.eu.habbo.habbohotel.items.interactions.pets;
+
+import com.eu.habbo.habbohotel.gameclients.GameClient;
+import com.eu.habbo.habbohotel.items.Item;
+import com.eu.habbo.habbohotel.items.interactions.InteractionDefault;
+import com.eu.habbo.habbohotel.pets.Pet;
+import com.eu.habbo.habbohotel.pets.PetTasks;
+import com.eu.habbo.habbohotel.pets.PetVocalsType;
+import com.eu.habbo.habbohotel.rooms.*;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+/**
+ * Interaction for pet trees (dragon tree, monkey tree, etc.)
+ * Pets can hang from these and perform special actions like Ring of Fire.
+ */
+public class InteractionPetTree extends InteractionDefault {
+ public InteractionPetTree(ResultSet set, Item baseItem) throws SQLException {
+ super(set, baseItem);
+ this.setExtradata("0");
+ }
+
+ public InteractionPetTree(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) {
+ super(id, userId, item, extradata, limitedStack, limitedSells);
+ this.setExtradata("0");
+ }
+
+ @Override
+ public void onClick(GameClient client, Room room, Object[] objects) {
+ // Trees are not clickable by users
+ }
+
+ @Override
+ public void onMove(Room room, RoomTile oldLocation, RoomTile newLocation) {
+ this.setExtradata("0");
+ room.updateItem(this);
+
+ for (Pet pet : room.getPetsAt(oldLocation)) {
+ pet.getRoomUnit().removeStatus(RoomUnitStatus.HANG);
+ pet.getRoomUnit().removeStatus(RoomUnitStatus.SWING);
+ pet.getRoomUnit().removeStatus(RoomUnitStatus.FLAME);
+ pet.packetUpdate = true;
+ }
+ }
+
+ @Override
+ public void onPickUp(Room room) {
+ this.setExtradata("0");
+
+ for (Pet pet : room.getPetsAt(room.getLayout().getTile(this.getX(), this.getY()))) {
+ pet.getRoomUnit().removeStatus(RoomUnitStatus.HANG);
+ pet.getRoomUnit().removeStatus(RoomUnitStatus.SWING);
+ pet.getRoomUnit().removeStatus(RoomUnitStatus.FLAME);
+ pet.packetUpdate = true;
+ }
+ }
+
+ @Override
+ public void onWalkOn(RoomUnit roomUnit, Room room, Object[] objects) throws Exception {
+ super.onWalkOn(roomUnit, room, objects);
+
+ Pet pet = room.getPet(roomUnit);
+
+ // Only dragons (type 12) can use the tree
+ if (pet != null && pet.getPetData().getType() == 12 && this.getOccupyingTiles(room.getLayout()).contains(pet.getRoomUnit().getGoal())) {
+ if (pet.getEnergy() <= 35) {
+ return;
+ }
+
+ RoomUnitStatus task = RoomUnitStatus.HANG;
+ switch (pet.getTask()) {
+ case RING_OF_FIRE:
+ task = RoomUnitStatus.RINGOFFIRE;
+ break;
+ case SWING:
+ task = RoomUnitStatus.SWING;
+ break;
+ default:
+ // Default to HANG for all other tasks
+ break;
+ }
+
+ // Pet arrived at tree - set hang status
+ pet.setTask(PetTasks.FREE);
+ pet.getRoomUnit().setGoalLocation(room.getLayout().getTile(this.getX(), this.getY()));
+ pet.getRoomUnit().setRotation(RoomUserRotation.values()[this.getRotation()]);
+ pet.getRoomUnit().clearStatus();
+ pet.getRoomUnit().setStatus(task, "");
+ pet.packetUpdate = true;
+
+ // Say playful vocal
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.PLAYFUL));
+
+ this.setExtradata("1");
+ room.updateItemState(this);
+ }
+ }
+
+ @Override
+ public void onWalkOff(RoomUnit roomUnit, Room room, Object[] objects) throws Exception {
+ super.onWalkOff(roomUnit, room, objects);
+
+ Pet pet = room.getPet(roomUnit);
+
+ if (pet != null) {
+ this.setExtradata("0");
+ room.updateItem(this);
+ pet.getRoomUnit().removeStatus(RoomUnitStatus.HANG);
+ pet.getRoomUnit().removeStatus(RoomUnitStatus.SWING);
+ pet.getRoomUnit().removeStatus(RoomUnitStatus.FLAME);
+ pet.packetUpdate = true;
+ }
+ }
+
+ @Override
+ public boolean canWalkOn(RoomUnit roomUnit, Room room, Object[] objects) {
+ Pet pet = room.getPet(roomUnit);
+ return roomUnit.getRoomUnitType() == RoomUnitType.PET && pet != null;
+ }
+
+ @Override
+ public boolean allowWiredResetState() {
+ return false;
+ }
+}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/Pet.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/Pet.java
index 6e4659de..c28e39f0 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/Pet.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/Pet.java
@@ -2,6 +2,9 @@ package com.eu.habbo.habbohotel.pets;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.achievements.AchievementManager;
+import com.eu.habbo.habbohotel.items.Item;
+import com.eu.habbo.habbohotel.items.interactions.pets.InteractionPetToy;
+import com.eu.habbo.habbohotel.items.interactions.pets.InteractionPetTree;
import com.eu.habbo.habbohotel.rooms.*;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboItem;
@@ -14,6 +17,7 @@ import com.eu.habbo.messages.outgoing.rooms.users.RoomUserRemoveComposer;
import com.eu.habbo.messages.outgoing.rooms.users.RoomUserTalkComposer;
import com.eu.habbo.plugin.events.pets.PetTalkEvent;
import gnu.trove.map.hash.THashMap;
+import gnu.trove.set.hash.THashSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -52,6 +56,16 @@ public class Pet implements ISerialize, Runnable {
private int stayStartedAt = 0;
private int idleCommandTicks = 0;
private int freeCommandTicks = -1;
+
+ // Command cooldown tracking to prevent spam
+ private int lastCommandId = -1;
+ private long lastCommandTime = 0;
+ private int sameCommandCount = 0;
+
+ // New managers for improved pet behavior
+ private PetStatsManager statsManager;
+ private PetBehaviorManager behaviorManager;
+
private PetTasks task = PetTasks.FREE;
@@ -78,6 +92,10 @@ public class Pet implements ISerialize, Runnable {
this.levelThirst = set.getInt("thirst");
this.levelHunger = set.getInt("hunger");
this.level = PetManager.getLevel(this.experience);
+
+ // Initialize managers
+ this.statsManager = new PetStatsManager(this);
+ this.behaviorManager = new PetBehaviorManager(this);
}
public Pet(int type, int race, String color, String name, int userId) {
@@ -101,6 +119,10 @@ public class Pet implements ISerialize, Runnable {
this.levelHunger = 0;
this.created = Emulator.getIntUnixTimestamp();
this.level = 1;
+
+ // Initialize managers
+ this.statsManager = new PetStatsManager(this);
+ this.behaviorManager = new PetBehaviorManager(this);
}
@@ -216,10 +238,15 @@ public class Pet implements ISerialize, Runnable {
}
public void cycle() {
+ // Guard clause for null room or roomUnit
+ if (this.room == null || this.roomUnit == null) {
+ return;
+ }
+
this.idleCommandTicks++;
int time = Emulator.getIntUnixTimestamp();
- if (this.roomUnit != null && this.task != PetTasks.RIDE) {
+ if (this.task != PetTasks.RIDE) {
if (time - this.gestureTickTimeout > 5 && this.roomUnit.hasStatus(RoomUnitStatus.GESTURE)) {
this.roomUnit.removeStatus(RoomUnitStatus.GESTURE);
this.packetUpdate = true;
@@ -254,17 +281,28 @@ public class Pet implements ISerialize, Runnable {
if (this.levelThirst > 0)
this.levelThirst--;
- this.addEnergy(5);
+ // Check if we're about to reach max energy before adding
+ int maxEnergy = PetManager.maxEnergy(this.level);
+ boolean wasResting = this.energy < maxEnergy;
+
+ // Nest gives faster regeneration than resting on floor
+ int energyGain = (this.task == PetTasks.NEST) ? 5 : 2;
+ this.addEnergy(energyGain);
this.addHappiness(1);
- if (this.energy == PetManager.maxEnergy(this.level)) {
+ // Wake up when fully rested
+ if (wasResting && this.energy >= maxEnergy) {
this.roomUnit.removeStatus(RoomUnitStatus.LAY);
this.roomUnit.setCanWalk(true);
- this.roomUnit.setGoalLocation(this.room.getRandomWalkableTile());
+ RoomTile tile = this.room.getRandomWalkableTile();
+ if (tile != null) {
+ this.roomUnit.setGoalLocation(tile);
+ }
this.task = null;
this.roomUnit.setStatus(RoomUnitStatus.GESTURE, PetGestures.ENERGY.getKey());
this.gestureTickTimeout = time;
+ this.say(this.petData.randomVocal(PetVocalsType.GENERIC_HAPPY));
}
} else if (this.tickTimeout >= 5) {
if (this.levelHunger < 100)
@@ -319,6 +357,13 @@ public class Pet implements ISerialize, Runnable {
this.say(this.petData.randomVocal(PetVocalsType.GENERIC_HAPPY));
} else if (this.happiness < 15) {
this.say(this.petData.randomVocal(PetVocalsType.GENERIC_SAD));
+ // When bored and has energy, try to find a toy to play with
+ if (this.energy > 40 && this.task == null) {
+ this.findToy();
+ }
+ } else if (this.happiness < 40 && this.energy > 50 && this.task == null && Emulator.getRandom().nextInt(100) < 30) {
+ // 30% chance to seek toy when moderately bored
+ this.findToy();
} else if (this.levelHunger > 50) {
this.say(this.petData.randomVocal(PetVocalsType.HUNGRY));
this.eat();
@@ -363,10 +408,13 @@ public class Pet implements ISerialize, Runnable {
case PLAY_FOOTBALL:
case PLAY_DEAD:
case FOLLOW:
+ case FOLLOW_LEFT:
+ case FOLLOW_RIGHT:
case JUMP:
case STAND:
case NEST:
case RIDE:
+ case STAY:
return false;
default:
break;
@@ -448,6 +496,9 @@ public class Pet implements ISerialize, Runnable {
public void findNest() {
+ if (this.room == null || this.room.getRoomSpecialTypes() == null || this.petData == null) {
+ return;
+ }
HabboItem item = this.petData.randomNest(this.room.getRoomSpecialTypes().getNests());
this.roomUnit.setCanWalk(true);
if (item != null) {
@@ -460,7 +511,37 @@ public class Pet implements ISerialize, Runnable {
}
+ /**
+ * Finds a suitable drink item for this pet in the current room.
+ * @return The drink Item if found, null otherwise
+ */
+ public Item findDrink() {
+ if (this.room == null || this.room.getRoomSpecialTypes() == null || this.petData == null) {
+ return null;
+ }
+ HabboItem drinkItem = this.petData.randomDrinkItem(this.room.getRoomSpecialTypes().getPetDrinks());
+ return drinkItem != null ? drinkItem.getBaseItem() : null;
+ }
+
+ /**
+ * Finds a suitable food item for this pet in the current room.
+ * @return The food Item if found, null otherwise
+ */
+ public Item findFood() {
+ if (this.room == null || this.room.getRoomSpecialTypes() == null || this.petData == null) {
+ return null;
+ }
+ HabboItem foodItem = this.petData.randomFoodItem(this.room.getRoomSpecialTypes().getPetFoods());
+ return foodItem != null ? foodItem.getBaseItem() : null;
+ }
+
+ /**
+ * Makes the pet walk to a drink item and drink from it.
+ */
public void drink() {
+ if (this.room == null || this.room.getRoomSpecialTypes() == null || this.petData == null) {
+ return;
+ }
HabboItem item = this.petData.randomDrinkItem(this.room.getRoomSpecialTypes().getPetDrinks());
if (item != null) {
this.roomUnit.setCanWalk(true);
@@ -468,26 +549,86 @@ public class Pet implements ISerialize, Runnable {
}
}
-
+ /**
+ * Makes the pet walk to a food item and eat from it.
+ */
public void eat() {
+ if (this.room == null || this.room.getRoomSpecialTypes() == null || this.petData == null) {
+ return;
+ }
HabboItem item = this.petData.randomFoodItem(this.room.getRoomSpecialTypes().getPetFoods());
- {
- if (item != null) {
- this.roomUnit.setCanWalk(true);
- this.roomUnit.setGoalLocation(this.room.getLayout().getTile(item.getX(), item.getY()));
- }
+ if (item != null) {
+ this.roomUnit.setCanWalk(true);
+ this.roomUnit.setGoalLocation(this.room.getLayout().getTile(item.getX(), item.getY()));
}
}
public void findToy() {
- HabboItem item = this.petData.randomToyItem(this.room.getRoomSpecialTypes().getPetToys());
- {
- if (item != null) {
- this.roomUnit.setCanWalk(true);
- this.roomUnit.setGoalLocation(this.room.getLayout().getTile(item.getX(), item.getY()));
+ if (this.room == null || this.room.getRoomSpecialTypes() == null || this.petData == null) {
+ return;
+ }
+
+ // Get all pet toys in the room
+ THashSet toys = this.room.getRoomSpecialTypes().getPetToys();
+ if (toys.isEmpty()) {
+ return;
+ }
+
+ // First try to find a toy this pet can use
+ HabboItem item = this.petData.randomToyItem(toys);
+
+ // If no compatible toy found, just pick any toy in the room
+ if (item == null) {
+ for (InteractionPetToy toy : toys) {
+ item = toy;
+ break;
}
}
+
+ if (item != null) {
+ this.roomUnit.setCanWalk(true);
+ this.setTask(PetTasks.PLAY);
+ this.roomUnit.setGoalLocation(this.room.getLayout().getTile(item.getX(), item.getY()));
+ this.say(this.petData.randomVocal(PetVocalsType.PLAYFUL));
+ }
+ }
+
+ /**
+ * Finds a pet tree (for dragons/monkeys) and walks to it.
+ * Used for hang, swing, ring of fire actions.
+ */
+ public void findTree() {
+ this.findPetItem(PetTasks.FREE, InteractionPetTree.class);
+ }
+
+ /**
+ * Finds a pet item of a specific type and walks to it.
+ * Used for trampolines, trees, and other special pet furniture.
+ * @param task The task to set on the pet
+ * @param type The class type of the item to find
+ * @return true if an item was found and pet is walking to it
+ */
+ public boolean findPetItem(PetTasks task, Class extends HabboItem> type) {
+ if (this.room == null || this.room.getRoomSpecialTypes() == null || this.petData == null) {
+ return false;
+ }
+
+ HabboItem item = this.petData.randomToyHabboItem(this.room.getRoomSpecialTypes().getItemsOfType(type));
+
+ if (item != null) {
+ this.roomUnit.setCanWalk(true);
+ this.setTask(task);
+ if (this.getRoomUnit().getCurrentLocation().distance(this.room.getLayout().getTile(item.getX(), item.getY())) == 0) {
+ try {
+ item.onWalkOn(this.getRoomUnit(), this.getRoom(), null);
+ } catch (Exception ignored) {}
+ return true;
+ }
+ this.roomUnit.setGoalLocation(this.room.getLayout().getTile(item.getX(), item.getY()));
+ return true;
+ }
+ return false;
}
@@ -753,4 +894,74 @@ public class Pet implements ISerialize, Runnable {
public void setStayStartedAt(int stayStartedAt) {
this.stayStartedAt = stayStartedAt;
}
+
+ /**
+ * Gets the stats manager for this pet.
+ * @return The PetStatsManager instance
+ */
+ public PetStatsManager getStatsManager() {
+ return this.statsManager;
+ }
+
+ /**
+ * Gets the behavior manager for this pet.
+ * @return The PetBehaviorManager instance
+ */
+ public PetBehaviorManager getBehaviorManager() {
+ return this.behaviorManager;
+ }
+
+ /**
+ * Checks if a command can be executed based on cooldown and spam prevention.
+ * @param commandId The command ID to check
+ * @return true if the command can be executed, false if on cooldown
+ */
+ public boolean canExecuteCommand(int commandId) {
+ long now = System.currentTimeMillis();
+ int globalCooldownMs = Emulator.getConfig().getInt("pet.command.cooldown_ms", 2000);
+ int maxSameCommandSpam = Emulator.getConfig().getInt("pet.command.max_same_spam", 3);
+ int spamResetMs = Emulator.getConfig().getInt("pet.command.spam_reset_ms", 10000);
+
+ // Global cooldown - applies to ALL commands to prevent switching between commands
+ if (now - this.lastCommandTime < globalCooldownMs) {
+ return false;
+ }
+
+ // Reset spam counter if enough time has passed
+ if (now - this.lastCommandTime > spamResetMs) {
+ this.sameCommandCount = 0;
+ }
+
+ // Check if same command is being spammed
+ if (commandId == this.lastCommandId) {
+ this.sameCommandCount++;
+
+ // Pet gets annoyed if same command spammed too much
+ if (this.sameCommandCount > maxSameCommandSpam) {
+ return false;
+ }
+ } else {
+ // Different command - reset counter but still subject to global cooldown
+ this.sameCommandCount = 1;
+ }
+
+ return true;
+ }
+
+ /**
+ * Records that a command was executed.
+ * @param commandId The command ID that was executed
+ */
+ public void recordCommandExecution(int commandId) {
+ this.lastCommandId = commandId;
+ this.lastCommandTime = System.currentTimeMillis();
+ }
+
+ /**
+ * Gets the number of times the same command has been repeated.
+ * @return The spam count
+ */
+ public int getSameCommandCount() {
+ return this.sameCommandCount;
+ }
}
\ No newline at end of file
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/PetBehaviorManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/PetBehaviorManager.java
new file mode 100644
index 00000000..c06bccfd
--- /dev/null
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/PetBehaviorManager.java
@@ -0,0 +1,219 @@
+package com.eu.habbo.habbohotel.pets;
+
+import com.eu.habbo.Emulator;
+import com.eu.habbo.habbohotel.rooms.RoomTile;
+import com.eu.habbo.habbohotel.rooms.RoomUnitStatus;
+
+/**
+ * Manages pet AI behavior using a state machine pattern.
+ * Handles autonomous pet actions and state transitions.
+ */
+public class PetBehaviorManager {
+ private final Pet pet;
+ private PetBehaviorState currentState;
+ private long stateEnteredAt;
+ private long lastAutonomousAction;
+
+ // Configurable delays
+ private int autonomousActionDelay;
+ private int idleWanderMinMs;
+ private int idleWanderMaxMs;
+
+ /**
+ * Represents the various behavioral states a pet can be in.
+ */
+ public enum PetBehaviorState {
+ IDLE, // Standing around
+ WANDERING, // Random walking
+ FOLLOWING, // Following owner/habbo
+ EXECUTING_COMMAND, // Doing a commanded action
+ EATING, // At food bowl
+ DRINKING, // At water bowl
+ PLAYING, // With toy
+ RESTING, // In nest/laying down
+ BREEDING, // In breeding box
+ DEAD // Monsterplant only
+ }
+
+ public PetBehaviorManager(Pet pet) {
+ this.pet = pet;
+ this.currentState = PetBehaviorState.IDLE;
+ this.stateEnteredAt = System.currentTimeMillis();
+ this.lastAutonomousAction = 0;
+ this.loadConfig();
+ }
+
+ /**
+ * Loads configuration values from the emulator config.
+ */
+ private void loadConfig() {
+ this.autonomousActionDelay = Emulator.getConfig().getInt("pet.behavior.autonomous_action_delay", 5000);
+ this.idleWanderMinMs = Emulator.getConfig().getInt("pet.behavior.idle_wander_min_ms", 10000);
+ this.idleWanderMaxMs = Emulator.getConfig().getInt("pet.behavior.idle_wander_max_ms", 30000);
+ }
+
+ /**
+ * Transitions the pet to a new behavior state.
+ * @param newState The new state to transition to
+ */
+ public void transition(PetBehaviorState newState) {
+ if (this.currentState == newState) return;
+
+ this.onExitState(this.currentState);
+ this.currentState = newState;
+ this.stateEnteredAt = System.currentTimeMillis();
+ this.onEnterState(newState);
+ }
+
+ /**
+ * Called when entering a new state to set up the appropriate room unit status.
+ */
+ private void onEnterState(PetBehaviorState state) {
+ if (this.pet.getRoomUnit() == null) return;
+
+ switch (state) {
+ case RESTING -> {
+ this.pet.getRoomUnit().setCanWalk(false);
+ this.pet.getRoomUnit().setStatus(RoomUnitStatus.LAY, "0");
+ }
+ case EATING -> {
+ this.pet.getRoomUnit().setStatus(RoomUnitStatus.EAT, "0");
+ }
+ case PLAYING -> {
+ // Play status handled by specific toy interaction
+ }
+ case IDLE -> {
+ // Clear any lingering action statuses
+ }
+ case FOLLOWING -> {
+ this.pet.getRoomUnit().setCanWalk(true);
+ }
+ default -> {
+ // No special handling needed
+ }
+ }
+ }
+
+ /**
+ * Called when exiting a state to clean up room unit status.
+ */
+ private void onExitState(PetBehaviorState state) {
+ if (this.pet.getRoomUnit() == null) return;
+
+ switch (state) {
+ case RESTING -> {
+ this.pet.getRoomUnit().removeStatus(RoomUnitStatus.LAY);
+ this.pet.getRoomUnit().setCanWalk(true);
+ }
+ case EATING -> {
+ this.pet.getRoomUnit().removeStatus(RoomUnitStatus.EAT);
+ }
+ case PLAYING -> {
+ // Play status cleanup handled by toy interaction
+ }
+ default -> {
+ // No special cleanup needed
+ }
+ }
+ }
+
+ /**
+ * Processes autonomous pet behavior each cycle.
+ * Called every cycle to handle autonomous pet actions based on needs.
+ */
+ public void processAutonomousBehavior() {
+ // Rate limit autonomous actions
+ if (System.currentTimeMillis() - this.lastAutonomousAction < this.autonomousActionDelay) {
+ return;
+ }
+
+ if (this.pet.getRoom() == null) return;
+
+ PetStatsManager stats = this.pet.getStatsManager();
+ if (stats == null) return;
+
+ // Priority-based autonomous behavior
+ if (stats.needsRest() && this.currentState != PetBehaviorState.RESTING) {
+ this.pet.findNest();
+ this.lastAutonomousAction = System.currentTimeMillis();
+ return;
+ }
+
+ if (stats.needsFood() && this.currentState != PetBehaviorState.EATING) {
+ this.pet.eat();
+ this.lastAutonomousAction = System.currentTimeMillis();
+ return;
+ }
+
+ if (stats.needsWater() && this.currentState != PetBehaviorState.DRINKING) {
+ this.pet.drink();
+ this.lastAutonomousAction = System.currentTimeMillis();
+ return;
+ }
+
+ if (stats.needsAttention() && this.currentState == PetBehaviorState.IDLE) {
+ this.pet.findToy();
+ this.lastAutonomousAction = System.currentTimeMillis();
+ return;
+ }
+
+ // Random wandering when idle
+ if (this.currentState == PetBehaviorState.IDLE) {
+ long idleTime = System.currentTimeMillis() - this.stateEnteredAt;
+ int wanderDelay = this.idleWanderMinMs + Emulator.getRandom().nextInt(
+ this.idleWanderMaxMs - this.idleWanderMinMs);
+
+ if (idleTime > wanderDelay) {
+ RoomTile tile = this.pet.getRoom().getRandomWalkableTile();
+ if (tile != null && this.pet.getRoomUnit() != null) {
+ this.pet.getRoomUnit().setGoalLocation(tile);
+ this.transition(PetBehaviorState.WANDERING);
+ }
+ this.lastAutonomousAction = System.currentTimeMillis();
+ }
+ }
+ }
+
+ /**
+ * Checks if the pet can currently accept commands.
+ * @return true if the pet can accept commands
+ */
+ public boolean canAcceptCommand() {
+ return this.currentState != PetBehaviorState.DEAD
+ && this.currentState != PetBehaviorState.BREEDING;
+ }
+
+ /**
+ * Interrupts the current action and returns to idle state.
+ */
+ public void interruptCurrentAction() {
+ if (this.currentState == PetBehaviorState.EXECUTING_COMMAND
+ || this.currentState == PetBehaviorState.WANDERING) {
+ this.transition(PetBehaviorState.IDLE);
+ }
+ }
+
+ /**
+ * Gets the current behavior state.
+ * @return The current PetBehaviorState
+ */
+ public PetBehaviorState getCurrentState() {
+ return this.currentState;
+ }
+
+ /**
+ * Gets the timestamp when the current state was entered.
+ * @return Timestamp in milliseconds
+ */
+ public long getStateEnteredAt() {
+ return this.stateEnteredAt;
+ }
+
+ /**
+ * Gets how long the pet has been in the current state.
+ * @return Duration in milliseconds
+ */
+ public long getTimeInCurrentState() {
+ return System.currentTimeMillis() - this.stateEnteredAt;
+ }
+}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/PetCommand.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/PetCommand.java
index 30b84482..d8f2e8e8 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/PetCommand.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/PetCommand.java
@@ -45,31 +45,75 @@ public class PetCommand implements Comparable {
}
public void handle(Pet pet, Habbo habbo, String[] data) {
- if (Emulator.getRandom().nextInt((pet.level - this.level <= 0 ? 2 : pet.level - this.level) + 2) == 0) {
- pet.say(pet.petData.randomVocal(PetVocalsType.DISOBEY));
+ // Check command cooldown to prevent spam (global cooldown for ALL commands)
+ if (!pet.canExecuteCommand(this.id)) {
+ // Pet ignores spammed commands - maybe give a tired/annoyed response occasionally
+ if (pet.getSameCommandCount() > Emulator.getConfig().getInt("pet.command.max_same_spam", 3)) {
+ if (Emulator.getRandom().nextInt(3) == 0) {
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.DISOBEY));
+ }
+ }
+ return;
+ }
+
+ // Check if pet has enough energy to perform the command
+ int minEnergy = Emulator.getConfig().getInt("pet.command.min_energy", 15);
+ if (pet.getEnergy() < minEnergy || pet.getEnergy() < this.energyCost) {
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.TIRED));
+ pet.recordCommandExecution(this.id);
+ return;
+ }
+
+ // Check if pet is too unhappy to obey
+ int minHappiness = Emulator.getConfig().getInt("pet.command.min_happiness", 10);
+ if (pet.getHappiness() < minHappiness) {
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.GENERIC_SAD));
+ pet.recordCommandExecution(this.id);
+ return;
+ }
+
+ // Improved obedience formula - configurable base chance with level scaling
+ int levelDifference = pet.getLevel() - this.level;
+ int baseChance = Emulator.getConfig().getInt("pet.command.base_obey_chance", 70); // 70% base
+ int levelBonus = Math.max(0, levelDifference * 5); // +5% per level above requirement
+ int obeyChance = Math.min(95, baseChance + levelBonus); // Cap at 95%
+
+ if (Emulator.getRandom().nextInt(100) >= obeyChance) {
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.DISOBEY));
+ pet.recordCommandExecution(this.id);
return;
}
if (this.action != null) {
- if (this.action.petTask != pet.getTask()) {
- if (this.action.stopsPetWalking) {
- pet.getRoomUnit().setGoalLocation(pet.getRoomUnit().getCurrentLocation());
+ // Allow repeating actions - removed the task comparison check
+ if (this.action.stopsPetWalking) {
+ pet.getRoomUnit().setGoalLocation(pet.getRoomUnit().getCurrentLocation());
+ }
+ if (this.action.apply(pet, habbo, data)) {
+ // Set the pet's task from the action
+ if (this.action.petTask != null) {
+ pet.setTask(this.action.petTask);
}
- if (this.action.apply(pet, habbo, data)) {
- for (RoomUnitStatus status : this.action.statusToRemove) {
- pet.getRoomUnit().removeStatus(status);
- }
-
- for (RoomUnitStatus status : this.action.statusToSet) {
- pet.getRoomUnit().setStatus(status, "0");
- }
-
- pet.getRoomUnit().setStatus(RoomUnitStatus.GESTURE, this.action.gestureToSet);
-
- pet.addEnergy(-this.energyCost);
- pet.addHappiness(-this.happinessCost);
- pet.addExperience(this.xp);
+
+ for (RoomUnitStatus status : this.action.statusToRemove) {
+ pet.getRoomUnit().removeStatus(status);
}
+
+ for (RoomUnitStatus status : this.action.statusToSet) {
+ pet.getRoomUnit().setStatus(status, "0");
+ }
+
+ pet.getRoomUnit().setStatus(RoomUnitStatus.GESTURE, this.action.gestureToSet);
+
+ pet.addEnergy(-this.energyCost);
+ pet.addHappiness(-this.happinessCost);
+ pet.addExperience(this.xp);
+
+ // Mark pet for status update so clients see the animation
+ pet.packetUpdate = true;
+
+ // Record successful command execution
+ pet.recordCommandExecution(this.id);
}
}
}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/PetData.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/PetData.java
index b07677e2..cae3274b 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/PetData.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/PetData.java
@@ -97,15 +97,22 @@ public class PetData implements Comparable {
boolean haveNest(Item nest) {
+ // If no nest items are registered, allow all nest items
+ if (this.nestItems.isEmpty() && PetData.generalNestItems.isEmpty()) {
+ return true;
+ }
return PetData.generalNestItems.contains(nest) || this.nestItems.contains(nest);
}
public HabboItem randomNest(THashSet items) {
List nestList = new ArrayList<>();
+
+ // If no nest items are registered, allow all nests in the room
+ boolean allowAll = this.nestItems.isEmpty() && PetData.generalNestItems.isEmpty();
for (InteractionNest nest : items) {
- if (this.haveNest(nest)) {
+ if (allowAll || this.haveNest(nest)) {
nestList.add(nest);
}
}
@@ -136,15 +143,22 @@ public class PetData implements Comparable {
boolean haveFoodItem(Item food) {
+ // If no food items are registered, allow all food items
+ if (this.foodItems.isEmpty() && PetData.generalFoodItems.isEmpty()) {
+ return true;
+ }
return this.foodItems.contains(food) || PetData.generalFoodItems.contains(food);
}
public HabboItem randomFoodItem(THashSet items) {
List foodList = new ArrayList<>();
+
+ // If no food items are registered, allow all food in the room
+ boolean allowAll = this.foodItems.isEmpty() && PetData.generalFoodItems.isEmpty();
for (InteractionPetFood food : items) {
- if (this.haveFoodItem(food)) {
+ if (allowAll || this.haveFoodItem(food)) {
foodList.add(food);
}
}
@@ -174,15 +188,22 @@ public class PetData implements Comparable {
boolean haveDrinkItem(Item item) {
+ // If no drink items are registered, allow all drink items
+ if (this.drinkItems.isEmpty() && PetData.generalDrinkItems.isEmpty()) {
+ return true;
+ }
return this.drinkItems.contains(item) || PetData.generalDrinkItems.contains(item);
}
public HabboItem randomDrinkItem(THashSet items) {
List drinkList = new ArrayList<>();
+
+ // If no drink items are registered, allow all drinks in the room
+ boolean allowAll = this.drinkItems.isEmpty() && PetData.generalDrinkItems.isEmpty();
for (InteractionPetDrink drink : items) {
- if (this.haveDrinkItem(drink)) {
+ if (allowAll || this.haveDrinkItem(drink)) {
drinkList.add(drink);
}
}
@@ -212,15 +233,22 @@ public class PetData implements Comparable {
public boolean haveToyItem(Item toy) {
+ // If no toy items are registered, allow all toy items
+ if (this.toyItems.isEmpty() && PetData.generalToyItems.isEmpty()) {
+ return true;
+ }
return this.toyItems.contains(toy) || PetData.generalToyItems.contains(toy);
}
public HabboItem randomToyItem(THashSet toys) {
List toyList = new ArrayList<>();
+
+ // If no toy items are registered, allow all toys in the room
+ boolean allowAll = this.toyItems.isEmpty() && PetData.generalToyItems.isEmpty();
for (InteractionPetToy toy : toys) {
- if (this.haveToyItem(toy)) {
+ if (allowAll || this.haveToyItem(toy)) {
toyList.add(toy);
}
}
@@ -233,6 +261,30 @@ public class PetData implements Comparable {
return null;
}
+ /**
+ * Finds a random toy item from a generic set of HabboItems.
+ * Used for finding pet items like trampolines, trees, etc.
+ */
+ public HabboItem randomToyHabboItem(THashSet items) {
+ List itemList = new ArrayList<>();
+
+ // If no toy items are registered, allow all toys in the room
+ boolean allowAll = this.toyItems.isEmpty() && PetData.generalToyItems.isEmpty();
+
+ for (HabboItem item : items) {
+ if (allowAll || this.haveToyItem(item)) {
+ itemList.add(item);
+ }
+ }
+
+ if (!itemList.isEmpty()) {
+ Collections.shuffle(itemList);
+ return itemList.get(0);
+ }
+
+ return null;
+ }
+
public PetVocal randomVocal(PetVocalsType type) {
THashSet petTypeVocals = this.petVocals.get(type);
@@ -242,8 +294,10 @@ public class PetData implements Comparable {
int generalSize = generalVocals != null ? generalVocals.size() : 0;
int totalSize = petTypeSize + generalSize;
- if (totalSize == 0)
- return null;
+ if (totalSize == 0) {
+ // Return a default vocal instead of null
+ return getDefaultVocal(type);
+ }
int randomIndex = Emulator.getRandom().nextInt(totalSize);
@@ -262,7 +316,31 @@ public class PetData implements Comparable {
}
}
- return null;
+ return getDefaultVocal(type);
+ }
+
+ /**
+ * Returns a default vocal message when no configured vocals exist for the type.
+ * This prevents null pointer exceptions and silent pets.
+ */
+ private static PetVocal getDefaultVocal(PetVocalsType type) {
+ return switch (type) {
+ case GENERIC_HAPPY -> new PetVocal("*wags tail happily*");
+ case GENERIC_SAD -> new PetVocal("*whimpers*");
+ case GENERIC_NEUTRAL -> new PetVocal("*looks around*");
+ case HUNGRY -> new PetVocal("*stomach growls*");
+ case THIRSTY -> new PetVocal("*pants*");
+ case TIRED -> new PetVocal("*yawns*");
+ case SLEEPING -> new PetVocal("*snores softly*");
+ case PLAYFUL -> new PetVocal("*bounces excitedly*");
+ case DISOBEY -> new PetVocal("*ignores command*");
+ case EATING -> new PetVocal("*munches happily*");
+ case DRINKING -> new PetVocal("*laps up water*");
+ case LEVEL_UP -> new PetVocal("*jumps with joy*");
+ case GREET_OWNER -> new PetVocal("*perks up excitedly*");
+ case MUTED -> new PetVocal("*stays quiet*");
+ case UNKNOWN_COMMAND -> new PetVocal("*tilts head confused*");
+ };
}
@Override
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/PetManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/PetManager.java
index ee71ba50..3eac86e1 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/PetManager.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/PetManager.java
@@ -7,6 +7,7 @@ import com.eu.habbo.habbohotel.items.interactions.pets.InteractionNest;
import com.eu.habbo.habbohotel.items.interactions.pets.InteractionPetDrink;
import com.eu.habbo.habbohotel.items.interactions.pets.InteractionPetFood;
import com.eu.habbo.habbohotel.items.interactions.pets.InteractionPetToy;
+import com.eu.habbo.habbohotel.items.interactions.pets.InteractionPetTrampoline;
import com.eu.habbo.habbohotel.pets.actions.*;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.habbohotel.rooms.RoomTile;
@@ -52,6 +53,12 @@ public class PetManager {
this.put(15, new ActionFollowLeft());
this.put(16, new ActionFollowRight());
this.put(17, new ActionPlayFootball());
+ this.put(18, new ActionTeleport());
+ this.put(19, new ActionBounce());
+ this.put(20, new ActionFlatten());
+ this.put(21, new ActionDance());
+ this.put(22, new ActionSpin());
+ this.put(23, new ActionSwitch());
this.put(24, new ActionMoveForward());
this.put(25, new ActionTurnLeft());
this.put(26, new ActionTurnRight());
@@ -59,10 +66,20 @@ public class PetManager {
this.put(28, new ActionCroak());
this.put(29, new ActionDip());
this.put(30, new ActionWave());
+ this.put(31, new ActionMambo());
+ this.put(32, new ActionHighJump());
+ this.put(33, new ActionChickenDance());
+ this.put(34, new ActionTripleJump());
this.put(35, new ActionWings());
this.put(36, new ActionBreatheFire());
+ this.put(37, new ActionHang());
this.put(38, new ActionTorch());
+ this.put(40, new ActionSwing());
+ this.put(41, new ActionRoll());
+ this.put(42, new ActionRingOfFire());
this.put(43, new ActionEat());
+ this.put(44, new ActionWagTail());
+ this.put(45, new ActionCount());
this.put(46, new ActionBreed());
}
@@ -200,7 +217,7 @@ public class PetManager {
PetData.generalFoodItems.add(baseItem);
else if (baseItem.getInteractionType().getType() == InteractionPetDrink.class)
PetData.generalDrinkItems.add(baseItem);
- else if (baseItem.getInteractionType().getType() == InteractionPetToy.class)
+ else if (baseItem.getInteractionType().getType() == InteractionPetToy.class || baseItem.getInteractionType().getType() == InteractionPetTrampoline.class)
PetData.generalToyItems.add(baseItem);
} else {
PetData data = this.getPetData(set.getInt("pet_id"));
@@ -212,7 +229,7 @@ public class PetManager {
data.addFoodItem(baseItem);
else if (baseItem.getInteractionType().getType() == InteractionPetDrink.class)
data.addDrinkItem(baseItem);
- else if (baseItem.getInteractionType().getType() == InteractionPetToy.class)
+ else if (baseItem.getInteractionType().getType() == InteractionPetToy.class || baseItem.getInteractionType().getType() == InteractionPetTrampoline.class)
data.addToyItem(baseItem);
}
}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/PetMood.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/PetMood.java
new file mode 100644
index 00000000..7eaa119f
--- /dev/null
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/PetMood.java
@@ -0,0 +1,50 @@
+package com.eu.habbo.habbohotel.pets;
+
+/**
+ * Represents the various mood states a pet can be in based on its stats.
+ */
+public enum PetMood {
+ EXHAUSTED("exhausted", 0),
+ STARVING("starving", 1),
+ PARCHED("parched", 2),
+ DEPRESSED("depressed", 3),
+ NEUTRAL("neutral", 4),
+ HAPPY("happy", 5),
+ ECSTATIC("ecstatic", 6);
+
+ private final String key;
+ private final int priority;
+
+ PetMood(String key, int priority) {
+ this.key = key;
+ this.priority = priority;
+ }
+
+ public String getKey() {
+ return this.key;
+ }
+
+ /**
+ * Gets the priority of this mood. Lower values indicate more urgent moods.
+ * @return The priority value
+ */
+ public int getPriority() {
+ return this.priority;
+ }
+
+ /**
+ * Checks if this mood is a negative/urgent mood that needs addressing.
+ * @return true if this is a negative mood
+ */
+ public boolean isNegative() {
+ return this.priority <= 3;
+ }
+
+ /**
+ * Checks if this mood is a positive mood.
+ * @return true if this is a positive mood
+ */
+ public boolean isPositive() {
+ return this.priority >= 5;
+ }
+}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/PetStatsManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/PetStatsManager.java
new file mode 100644
index 00000000..4eb865c3
--- /dev/null
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/PetStatsManager.java
@@ -0,0 +1,150 @@
+package com.eu.habbo.habbohotel.pets;
+
+import com.eu.habbo.Emulator;
+
+/**
+ * Manages all stat-related logic for pets including decay rates, recovery rates,
+ * and mood calculations. This centralizes stat management for better maintainability.
+ */
+public class PetStatsManager {
+ private final Pet pet;
+
+ // Configurable decay rates
+ private int hungerDecayRate;
+ private int thirstDecayRate;
+ private int energyDecayRate;
+ private int happinessDecayRate;
+
+ // Configurable recovery rates
+ private int energyRecoveryRate;
+ private int happinessRecoveryRate;
+
+ // Configurable thresholds
+ private int hungryThreshold;
+ private int thirstyThreshold;
+ private int tiredThreshold;
+ private int sadThreshold;
+
+ public PetStatsManager(Pet pet) {
+ this.pet = pet;
+ this.loadConfig();
+ }
+
+ /**
+ * Loads configuration values from the emulator config.
+ */
+ private void loadConfig() {
+ this.hungerDecayRate = Emulator.getConfig().getInt("pet.stats.hunger_decay", 1);
+ this.thirstDecayRate = Emulator.getConfig().getInt("pet.stats.thirst_decay", 1);
+ this.energyDecayRate = Emulator.getConfig().getInt("pet.stats.energy_decay", 1);
+ this.happinessDecayRate = Emulator.getConfig().getInt("pet.stats.happiness_decay", 1);
+ this.energyRecoveryRate = Emulator.getConfig().getInt("pet.stats.energy_recovery", 5);
+ this.happinessRecoveryRate = Emulator.getConfig().getInt("pet.stats.happiness_recovery", 1);
+
+ this.hungryThreshold = Emulator.getConfig().getInt("pet.threshold.hungry", 50);
+ this.thirstyThreshold = Emulator.getConfig().getInt("pet.threshold.thirsty", 50);
+ this.tiredThreshold = Emulator.getConfig().getInt("pet.threshold.tired", 30);
+ this.sadThreshold = Emulator.getConfig().getInt("pet.threshold.sad", 30);
+ }
+
+ /**
+ * Process stat changes when pet is walking/active.
+ */
+ public void processActiveTick() {
+ this.pet.addHunger(this.hungerDecayRate);
+ this.pet.addThirst(this.thirstDecayRate);
+ this.pet.addEnergy(-this.energyDecayRate);
+ }
+
+ /**
+ * Process stat changes when pet is in nest/down (resting).
+ */
+ public void processRestingTick() {
+ this.pet.addHunger(-1);
+ this.pet.addThirst(-1);
+ this.pet.addEnergy(this.energyRecoveryRate);
+ this.pet.addHappiness(this.happinessRecoveryRate);
+ }
+
+ /**
+ * Process stat changes when pet is standing still/idle.
+ */
+ public void processIdleTick() {
+ this.pet.addHunger(this.hungerDecayRate / 2);
+ this.pet.addThirst(this.thirstDecayRate / 2);
+ }
+
+ /**
+ * Gets the current mood of the pet based on its stats.
+ * @return The current PetMood
+ */
+ public PetMood getCurrentMood() {
+ if (this.pet.getEnergy() < 20) return PetMood.EXHAUSTED;
+ if (this.pet.getLevelHunger() > 80) return PetMood.STARVING;
+ if (this.pet.getLevelThirst() > 80) return PetMood.PARCHED;
+ if (this.pet.getHappiness() < 20) return PetMood.DEPRESSED;
+ if (this.pet.getHappiness() > 80 && this.pet.getEnergy() > 60) return PetMood.ECSTATIC;
+ if (this.pet.getHappiness() > 50) return PetMood.HAPPY;
+ return PetMood.NEUTRAL;
+ }
+
+ /**
+ * Checks if the pet needs food.
+ * @return true if hunger level exceeds the hungry threshold
+ */
+ public boolean needsFood() {
+ return this.pet.getLevelHunger() > this.hungryThreshold;
+ }
+
+ /**
+ * Checks if the pet needs water.
+ * @return true if thirst level exceeds the thirsty threshold
+ */
+ public boolean needsWater() {
+ return this.pet.getLevelThirst() > this.thirstyThreshold;
+ }
+
+ /**
+ * Checks if the pet needs rest.
+ * @return true if energy level is below the tired threshold
+ */
+ public boolean needsRest() {
+ return this.pet.getEnergy() < this.tiredThreshold;
+ }
+
+ /**
+ * Checks if the pet needs attention/play.
+ * @return true if happiness level is below the sad threshold
+ */
+ public boolean needsAttention() {
+ return this.pet.getHappiness() < this.sadThreshold;
+ }
+
+ /**
+ * Gets the overall health score of the pet (0-100).
+ * @return An integer representing overall pet health
+ */
+ public int getOverallHealth() {
+ int maxEnergy = PetManager.maxEnergy(this.pet.getLevel());
+ int energyPercent = (this.pet.getEnergy() * 100) / maxEnergy;
+ int hungerPercent = 100 - this.pet.getLevelHunger();
+ int thirstPercent = 100 - this.pet.getLevelThirst();
+ int happinessPercent = this.pet.getHappiness();
+
+ return (energyPercent + hungerPercent + thirstPercent + happinessPercent) / 4;
+ }
+
+ // Getters for decay/recovery rates
+ public int getHungerDecayRate() { return hungerDecayRate; }
+ public int getThirstDecayRate() { return thirstDecayRate; }
+ public int getEnergyDecayRate() { return energyDecayRate; }
+ public int getHappinessDecayRate() { return happinessDecayRate; }
+ public int getEnergyRecoveryRate() { return energyRecoveryRate; }
+ public int getHappinessRecoveryRate() { return happinessRecoveryRate; }
+
+ // Getters for thresholds
+ public int getHungryThreshold() { return hungryThreshold; }
+ public int getThirstyThreshold() { return thirstyThreshold; }
+ public int getTiredThreshold() { return tiredThreshold; }
+ public int getSadThreshold() { return sadThreshold; }
+}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionBeg.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionBeg.java
index 9c3695b8..0b0c9fff 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionBeg.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionBeg.java
@@ -17,6 +17,9 @@ public class ActionBeg extends PetAction {
public boolean apply(Pet pet, Habbo habbo, String[] data) {
pet.clearPosture();
+ // Begging is fun interaction with owner
+ pet.addHappiness(5);
+
if (pet.getHappiness() > 90)
pet.say(pet.getPetData().randomVocal(PetVocalsType.PLAYFUL));
else
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionBounce.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionBounce.java
new file mode 100644
index 00000000..978a4675
--- /dev/null
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionBounce.java
@@ -0,0 +1,35 @@
+package com.eu.habbo.habbohotel.pets.actions;
+
+import com.eu.habbo.Emulator;
+import com.eu.habbo.habbohotel.pets.Pet;
+import com.eu.habbo.habbohotel.pets.PetAction;
+import com.eu.habbo.habbohotel.pets.PetTasks;
+import com.eu.habbo.habbohotel.pets.PetVocalsType;
+import com.eu.habbo.habbohotel.rooms.RoomUnitStatus;
+import com.eu.habbo.habbohotel.users.Habbo;
+import com.eu.habbo.threading.runnables.PetClearPosture;
+
+public class ActionBounce extends PetAction {
+ public ActionBounce() {
+ super(PetTasks.JUMP, true);
+ this.minimumActionDuration = 3000;
+ this.statusToSet.add(RoomUnitStatus.JUMP);
+ }
+
+ @Override
+ public boolean apply(Pet pet, Habbo habbo, String[] data) {
+ pet.clearPosture();
+
+ Emulator.getThreading().run(new PetClearPosture(pet, RoomUnitStatus.JUMP, null, false), 3000);
+
+ // Bouncing is fun!
+ pet.addHappiness(8);
+
+ if (pet.getHappiness() > 60)
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.PLAYFUL));
+ else
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.GENERIC_NEUTRAL));
+
+ return true;
+ }
+}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionBreed.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionBreed.java
index 83ec6207..e2051141 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionBreed.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionBreed.java
@@ -4,6 +4,7 @@ import com.eu.habbo.habbohotel.items.interactions.pets.InteractionPetBreedingNes
import com.eu.habbo.habbohotel.pets.Pet;
import com.eu.habbo.habbohotel.pets.PetAction;
import com.eu.habbo.habbohotel.pets.PetTasks;
+import com.eu.habbo.habbohotel.pets.PetVocalsType;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboItem;
import com.eu.habbo.messages.outgoing.rooms.pets.breeding.PetBreedingStartFailedComposer;
@@ -28,6 +29,7 @@ public class ActionBreed extends PetAction {
if (nest != null) {
pet.getRoomUnit().setGoalLocation(pet.getRoom().getLayout().getTile(nest.getX(), nest.getY()));
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.GENERIC_HAPPY));
return true;
} else {
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionChickenDance.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionChickenDance.java
new file mode 100644
index 00000000..1c73bbbb
--- /dev/null
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionChickenDance.java
@@ -0,0 +1,33 @@
+package com.eu.habbo.habbohotel.pets.actions;
+
+import com.eu.habbo.Emulator;
+import com.eu.habbo.habbohotel.pets.Pet;
+import com.eu.habbo.habbohotel.pets.PetAction;
+import com.eu.habbo.habbohotel.pets.PetTasks;
+import com.eu.habbo.habbohotel.pets.PetVocalsType;
+import com.eu.habbo.habbohotel.rooms.RoomUnitStatus;
+import com.eu.habbo.habbohotel.users.Habbo;
+import com.eu.habbo.threading.runnables.PetClearPosture;
+
+public class ActionChickenDance extends PetAction {
+ public ActionChickenDance() {
+ super(PetTasks.FREE, true);
+ this.minimumActionDuration = 4000;
+ this.statusToSet.add(RoomUnitStatus.DANCE);
+ }
+
+ @Override
+ public boolean apply(Pet pet, Habbo habbo, String[] data) {
+ pet.clearPosture();
+ pet.getRoomUnit().setStatus(RoomUnitStatus.DANCE, "");
+
+ Emulator.getThreading().run(new PetClearPosture(pet, RoomUnitStatus.DANCE, null, false), 4000);
+
+ if (pet.getHappiness() > 50)
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.PLAYFUL));
+ else
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.GENERIC_NEUTRAL));
+
+ return true;
+ }
+}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionCount.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionCount.java
new file mode 100644
index 00000000..6a0cb3a0
--- /dev/null
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionCount.java
@@ -0,0 +1,27 @@
+package com.eu.habbo.habbohotel.pets.actions;
+
+import com.eu.habbo.habbohotel.pets.Pet;
+import com.eu.habbo.habbohotel.pets.PetAction;
+import com.eu.habbo.habbohotel.pets.PetTasks;
+import com.eu.habbo.habbohotel.pets.PetVocalsType;
+import com.eu.habbo.habbohotel.users.Habbo;
+
+public class ActionCount extends PetAction {
+ public ActionCount() {
+ super(PetTasks.FREE, true);
+ this.minimumActionDuration = 3000;
+ }
+
+ @Override
+ public boolean apply(Pet pet, Habbo habbo, String[] data) {
+ pet.clearPosture();
+
+ // Count by speaking
+ if (pet.getHappiness() > 50)
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.PLAYFUL));
+ else
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.GENERIC_NEUTRAL));
+
+ return true;
+ }
+}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionCroak.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionCroak.java
index d54d1c5f..6a0bbad2 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionCroak.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionCroak.java
@@ -23,6 +23,8 @@ public class ActionCroak extends PetAction {
if (pet.getHappiness() > 80)
pet.say(pet.getPetData().randomVocal(PetVocalsType.PLAYFUL));
+ else
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.GENERIC_NEUTRAL));
return true;
}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionDance.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionDance.java
new file mode 100644
index 00000000..a6339a56
--- /dev/null
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionDance.java
@@ -0,0 +1,36 @@
+package com.eu.habbo.habbohotel.pets.actions;
+
+import com.eu.habbo.Emulator;
+import com.eu.habbo.habbohotel.pets.Pet;
+import com.eu.habbo.habbohotel.pets.PetAction;
+import com.eu.habbo.habbohotel.pets.PetTasks;
+import com.eu.habbo.habbohotel.pets.PetVocalsType;
+import com.eu.habbo.habbohotel.rooms.RoomUnitStatus;
+import com.eu.habbo.habbohotel.users.Habbo;
+import com.eu.habbo.threading.runnables.PetClearPosture;
+
+public class ActionDance extends PetAction {
+ public ActionDance() {
+ super(PetTasks.FREE, true);
+ this.minimumActionDuration = 5000;
+ this.statusToSet.add(RoomUnitStatus.DANCE);
+ }
+
+ @Override
+ public boolean apply(Pet pet, Habbo habbo, String[] data) {
+ pet.clearPosture();
+ pet.getRoomUnit().setStatus(RoomUnitStatus.DANCE, "");
+
+ Emulator.getThreading().run(new PetClearPosture(pet, RoomUnitStatus.DANCE, null, false), 5000);
+
+ // Dancing is fun!
+ pet.addHappiness(10);
+
+ if (pet.getHappiness() > 60)
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.PLAYFUL));
+ else
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.GENERIC_NEUTRAL));
+
+ return true;
+ }
+}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionDip.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionDip.java
index 8f47aad6..71aa04fb 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionDip.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionDip.java
@@ -4,6 +4,7 @@ import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.items.interactions.InteractionWater;
import com.eu.habbo.habbohotel.pets.Pet;
import com.eu.habbo.habbohotel.pets.PetAction;
+import com.eu.habbo.habbohotel.pets.PetVocalsType;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboItem;
import gnu.trove.set.hash.THashSet;
@@ -23,6 +24,8 @@ public class ActionDip extends PetAction {
HabboItem waterPatch = (HabboItem) waterItems.toArray()[Emulator.getRandom().nextInt(waterItems.size())];
pet.getRoomUnit().setGoalLocation(pet.getRoom().getLayout().getTile(waterPatch.getX(), waterPatch.getY()));
+
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.PLAYFUL));
return true;
}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionDown.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionDown.java
index 14d352e3..8931775b 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionDown.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionDown.java
@@ -1,5 +1,6 @@
package com.eu.habbo.habbohotel.pets.actions;
+import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.pets.Pet;
import com.eu.habbo.habbohotel.pets.PetAction;
import com.eu.habbo.habbohotel.pets.PetTasks;
@@ -10,18 +11,33 @@ import com.eu.habbo.habbohotel.users.Habbo;
public class ActionDown extends PetAction {
public ActionDown() {
super(PetTasks.DOWN, true);
+ this.minimumActionDuration = 4000;
+ this.statusToRemove.add(RoomUnitStatus.BEG);
this.statusToRemove.add(RoomUnitStatus.MOVE);
- this.statusToRemove.add(RoomUnitStatus.SIT);
+ this.statusToRemove.add(RoomUnitStatus.DEAD);
}
@Override
public boolean apply(Pet pet, Habbo habbo, String[] data) {
- pet.getRoomUnit().setStatus(RoomUnitStatus.LAY, pet.getRoom().getStackHeight(pet.getRoomUnit().getX(), pet.getRoomUnit().getY(), false) + "");
+ if (pet.getTask() != PetTasks.DOWN && !pet.getRoomUnit().hasStatus(RoomUnitStatus.LAY)) {
+ pet.getRoomUnit().cmdLay = true;
+ pet.getRoomUnit().setStatus(RoomUnitStatus.LAY, pet.getRoomUnit().getCurrentLocation().getStackHeight() + "");
- if (pet.getHappiness() > 50)
- pet.say(pet.getPetData().randomVocal(PetVocalsType.PLAYFUL));
- else
- pet.say(pet.getPetData().randomVocal(PetVocalsType.GENERIC_NEUTRAL));
+ // Lying down is a bit boring but restful
+ pet.addHappiness(-2);
+
+ Emulator.getThreading().run(() -> {
+ pet.getRoomUnit().cmdLay = false;
+ pet.clearPosture();
+ }, this.minimumActionDuration);
+
+ if (pet.getHappiness() > 50)
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.PLAYFUL));
+ else
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.GENERIC_NEUTRAL));
+
+ return true;
+ }
return true;
}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionDrink.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionDrink.java
index 7d56c1c2..5744ec89 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionDrink.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionDrink.java
@@ -1,5 +1,6 @@
package com.eu.habbo.habbohotel.pets.actions;
+import com.eu.habbo.habbohotel.items.Item;
import com.eu.habbo.habbohotel.pets.Pet;
import com.eu.habbo.habbohotel.pets.PetAction;
import com.eu.habbo.habbohotel.pets.PetVocalsType;
@@ -12,17 +13,30 @@ public class ActionDrink extends PetAction {
@Override
public boolean apply(Pet pet, Habbo habbo, String[] data) {
- if (pet.getLevelThirst() > 40) {
- pet.drink();
-
- if (pet.getLevelThirst() > 65)
- pet.say(pet.getPetData().randomVocal(PetVocalsType.THIRSTY));
-
- return true;
+ // Check if pet is thirsty enough to want water (threshold: 35 to match InteractionPetDrink)
+ if (pet.getLevelThirst() >= 35) {
+ // Check if there's water available in the room before sending pet to drink
+ if (pet.getRoom() != null && pet.getRoom().getRoomSpecialTypes() != null) {
+ Item drinkItem = pet.findDrink();
+
+ if (drinkItem != null) {
+ // Water exists - pet goes to drink
+ if (pet.getLevelThirst() > 65) {
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.THIRSTY));
+ }
+ pet.drink();
+ return true;
+ } else {
+ // No suitable water in room - pet complains
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.THIRSTY));
+ return false;
+ }
+ }
+ return false;
} else {
+ // Pet is not thirsty - disobeys command
pet.say(pet.getPetData().randomVocal(PetVocalsType.DISOBEY));
+ return false;
}
-
- return false;
}
}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionEat.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionEat.java
index 0b701c6d..0c372b7d 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionEat.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionEat.java
@@ -1,33 +1,42 @@
package com.eu.habbo.habbohotel.pets.actions;
-import com.eu.habbo.Emulator;
+import com.eu.habbo.habbohotel.items.Item;
import com.eu.habbo.habbohotel.pets.Pet;
import com.eu.habbo.habbohotel.pets.PetAction;
import com.eu.habbo.habbohotel.pets.PetVocalsType;
-import com.eu.habbo.habbohotel.rooms.RoomUnitStatus;
import com.eu.habbo.habbohotel.users.Habbo;
-import com.eu.habbo.threading.runnables.PetClearPosture;
public class ActionEat extends PetAction {
public ActionEat() {
- super(null, true);
-
- this.statusToSet.add(RoomUnitStatus.EAT);
+ // stopsPetWalking=false so pet can walk to food
+ // Don't set EAT status here - let InteractionPetFood.onWalkOn() handle it when pet arrives
+ super(null, false);
}
@Override
public boolean apply(Pet pet, Habbo habbo, String[] data) {
- //Eat
- if (pet.getLevelHunger() > 40) {
- pet.say(pet.getPetData().randomVocal(PetVocalsType.HUNGRY));
- Emulator.getThreading().run(new PetClearPosture(pet, RoomUnitStatus.EAT, null, false), 500);
- pet.eat();
-
- return true;
+ // Check if pet is hungry enough to want food (threshold: 35 to match InteractionPetFood)
+ if (pet.getLevelHunger() >= 35) {
+ // Check if there's food available in the room before sending pet to eat
+ if (pet.getRoom() != null && pet.getRoom().getRoomSpecialTypes() != null) {
+ Item foodItem = pet.findFood();
+
+ if (foodItem != null) {
+ // Food exists - pet goes to eat
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.HUNGRY));
+ pet.eat();
+ return true;
+ } else {
+ // No suitable food in room - pet complains
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.HUNGRY));
+ return false;
+ }
+ }
+ return false;
} else {
+ // Pet is not hungry - disobeys command
pet.say(pet.getPetData().randomVocal(PetVocalsType.DISOBEY));
return false;
}
-
}
}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionFlatten.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionFlatten.java
new file mode 100644
index 00000000..d0c37963
--- /dev/null
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionFlatten.java
@@ -0,0 +1,33 @@
+package com.eu.habbo.habbohotel.pets.actions;
+
+import com.eu.habbo.Emulator;
+import com.eu.habbo.habbohotel.pets.Pet;
+import com.eu.habbo.habbohotel.pets.PetAction;
+import com.eu.habbo.habbohotel.pets.PetTasks;
+import com.eu.habbo.habbohotel.pets.PetVocalsType;
+import com.eu.habbo.habbohotel.rooms.RoomUnitStatus;
+import com.eu.habbo.habbohotel.users.Habbo;
+import com.eu.habbo.threading.runnables.PetClearPosture;
+
+public class ActionFlatten extends PetAction {
+ public ActionFlatten() {
+ super(PetTasks.DOWN, true);
+ this.minimumActionDuration = 3000;
+ this.statusToSet.add(RoomUnitStatus.FLAT);
+ }
+
+ @Override
+ public boolean apply(Pet pet, Habbo habbo, String[] data) {
+ pet.clearPosture();
+ pet.getRoomUnit().setStatus(RoomUnitStatus.FLAT, pet.getRoomUnit().getCurrentLocation().getStackHeight() + "");
+
+ Emulator.getThreading().run(new PetClearPosture(pet, RoomUnitStatus.FLAT, null, false), 3000);
+
+ if (pet.getHappiness() > 50)
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.PLAYFUL));
+ else
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.GENERIC_NEUTRAL));
+
+ return true;
+ }
+}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionFollow.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionFollow.java
index d197b43c..f11ea94a 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionFollow.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionFollow.java
@@ -23,6 +23,9 @@ public class ActionFollow extends PetAction {
Emulator.getThreading().run(new PetFollowHabbo(pet, habbo, 0));
+ // Following owner is enjoyable
+ pet.addHappiness(5);
+
if (pet.getHappiness() > 75)
pet.say(pet.getPetData().randomVocal(PetVocalsType.PLAYFUL));
else
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionFollowLeft.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionFollowLeft.java
index c4c9c573..99dad2b7 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionFollowLeft.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionFollowLeft.java
@@ -10,7 +10,7 @@ import com.eu.habbo.threading.runnables.PetFollowHabbo;
public class ActionFollowLeft extends PetAction {
public ActionFollowLeft() {
- super(PetTasks.FOLLOW, true);
+ super(PetTasks.FOLLOW_LEFT, true);
}
@Override
@@ -18,7 +18,10 @@ public class ActionFollowLeft extends PetAction {
//Follow left.
pet.clearPosture();
- Emulator.getThreading().run(new PetFollowHabbo(pet, habbo, -2));
+ Emulator.getThreading().run(new PetFollowHabbo(pet, habbo, +2));
+
+ // Following owner is enjoyable
+ pet.addHappiness(5);
if (pet.getHappiness() > 75)
pet.say(pet.getPetData().randomVocal(PetVocalsType.PLAYFUL));
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionFollowRight.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionFollowRight.java
index 29e5a41e..1c39c619 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionFollowRight.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionFollowRight.java
@@ -10,7 +10,7 @@ import com.eu.habbo.threading.runnables.PetFollowHabbo;
public class ActionFollowRight extends PetAction {
public ActionFollowRight() {
- super(PetTasks.FOLLOW, true);
+ super(PetTasks.FOLLOW_RIGHT, true);
}
@Override
@@ -18,7 +18,10 @@ public class ActionFollowRight extends PetAction {
//Follow right.
pet.clearPosture();
- Emulator.getThreading().run(new PetFollowHabbo(pet, habbo, +2));
+ Emulator.getThreading().run(new PetFollowHabbo(pet, habbo, -2));
+
+ // Following owner is enjoyable
+ pet.addHappiness(5);
if (pet.getHappiness() > 75)
pet.say(pet.getPetData().randomVocal(PetVocalsType.PLAYFUL));
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionFree.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionFree.java
index 39fc50a1..052f95f9 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionFree.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionFree.java
@@ -3,6 +3,7 @@ package com.eu.habbo.habbohotel.pets.actions;
import com.eu.habbo.habbohotel.pets.Pet;
import com.eu.habbo.habbohotel.pets.PetAction;
import com.eu.habbo.habbohotel.pets.PetTasks;
+import com.eu.habbo.habbohotel.pets.PetVocalsType;
import com.eu.habbo.habbohotel.users.Habbo;
public class ActionFree extends PetAction {
@@ -13,6 +14,9 @@ public class ActionFree extends PetAction {
@Override
public boolean apply(Pet pet, Habbo habbo, String[] data) {
pet.freeCommand();
+
+ if (pet.getHappiness() > 50)
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.GENERIC_HAPPY));
return true;
}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionHang.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionHang.java
new file mode 100644
index 00000000..4819ecc1
--- /dev/null
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionHang.java
@@ -0,0 +1,47 @@
+package com.eu.habbo.habbohotel.pets.actions;
+
+import com.eu.habbo.Emulator;
+import com.eu.habbo.habbohotel.items.interactions.pets.InteractionPetTree;
+import com.eu.habbo.habbohotel.pets.Pet;
+import com.eu.habbo.habbohotel.pets.PetAction;
+import com.eu.habbo.habbohotel.pets.PetTasks;
+import com.eu.habbo.habbohotel.pets.PetVocalsType;
+import com.eu.habbo.habbohotel.rooms.RoomUnitStatus;
+import com.eu.habbo.habbohotel.users.Habbo;
+import com.eu.habbo.habbohotel.users.HabboItem;
+import com.eu.habbo.threading.runnables.PetClearPosture;
+
+public class ActionHang extends PetAction {
+ public ActionHang() {
+ super(PetTasks.FREE, true);
+ this.minimumActionDuration = 4000;
+ }
+
+ @Override
+ public boolean apply(Pet pet, Habbo habbo, String[] data) {
+ // Hang requires pet to be on a pet tree (dragon/monkey tree)
+ if (pet.getRoom() == null) {
+ return false;
+ }
+
+ HabboItem itemBelow = pet.getRoom().getTopItemAt(pet.getRoomUnit().getX(), pet.getRoomUnit().getY());
+ if (!(itemBelow instanceof InteractionPetTree)) {
+ // Pet must go to tree first
+ pet.findTree();
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.DISOBEY));
+ return false;
+ }
+
+ pet.clearPosture();
+ pet.getRoomUnit().setStatus(RoomUnitStatus.HANG, "");
+
+ Emulator.getThreading().run(new PetClearPosture(pet, RoomUnitStatus.HANG, null, false), 4000);
+
+ if (pet.getHappiness() > 50)
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.PLAYFUL));
+ else
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.GENERIC_NEUTRAL));
+
+ return true;
+ }
+}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionHere.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionHere.java
index ce326c0d..3bfdf2a9 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionHere.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionHere.java
@@ -4,6 +4,7 @@ import com.eu.habbo.habbohotel.pets.Pet;
import com.eu.habbo.habbohotel.pets.PetAction;
import com.eu.habbo.habbohotel.pets.PetTasks;
import com.eu.habbo.habbohotel.pets.PetVocalsType;
+import com.eu.habbo.habbohotel.rooms.RoomTile;
import com.eu.habbo.habbohotel.rooms.RoomUnitStatus;
import com.eu.habbo.habbohotel.users.Habbo;
@@ -16,8 +17,27 @@ public class ActionHere extends PetAction {
@Override
public boolean apply(Pet pet, Habbo habbo, String[] data) {
- pet.getRoomUnit().setGoalLocation(pet.getRoom().getLayout().getTileInFront(habbo.getRoomUnit().getCurrentLocation(), habbo.getRoomUnit().getBodyRotation().getValue()));
- pet.getRoomUnit().setCanWalk(true);
+ if (pet.getRoom() == null || habbo.getRoomUnit() == null) {
+ return false;
+ }
+
+ pet.clearPosture();
+
+ // Try tile in front of habbo first
+ RoomTile target = pet.getRoom().getLayout().getTileInFront(
+ habbo.getRoomUnit().getCurrentLocation(),
+ habbo.getRoomUnit().getBodyRotation().getValue()
+ );
+
+ // If not walkable, try habbo's current tile
+ if (target == null || !pet.getRoom().getLayout().tileWalkable(target.x, target.y)) {
+ target = habbo.getRoomUnit().getCurrentLocation();
+ }
+
+ if (target != null) {
+ pet.getRoomUnit().setGoalLocation(target);
+ pet.getRoomUnit().setCanWalk(true);
+ }
if (pet.getHappiness() > 75)
pet.say(pet.getPetData().randomVocal(PetVocalsType.PLAYFUL));
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionHighJump.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionHighJump.java
new file mode 100644
index 00000000..bca198aa
--- /dev/null
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionHighJump.java
@@ -0,0 +1,32 @@
+package com.eu.habbo.habbohotel.pets.actions;
+
+import com.eu.habbo.Emulator;
+import com.eu.habbo.habbohotel.pets.Pet;
+import com.eu.habbo.habbohotel.pets.PetAction;
+import com.eu.habbo.habbohotel.pets.PetTasks;
+import com.eu.habbo.habbohotel.pets.PetVocalsType;
+import com.eu.habbo.habbohotel.rooms.RoomUnitStatus;
+import com.eu.habbo.habbohotel.users.Habbo;
+import com.eu.habbo.threading.runnables.PetClearPosture;
+
+public class ActionHighJump extends PetAction {
+ public ActionHighJump() {
+ super(PetTasks.JUMP, true);
+ this.minimumActionDuration = 3000;
+ this.statusToSet.add(RoomUnitStatus.JUMP);
+ }
+
+ @Override
+ public boolean apply(Pet pet, Habbo habbo, String[] data) {
+ pet.clearPosture();
+
+ Emulator.getThreading().run(new PetClearPosture(pet, RoomUnitStatus.JUMP, null, false), 3000);
+
+ if (pet.getHappiness() > 70)
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.PLAYFUL));
+ else
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.GENERIC_NEUTRAL));
+
+ return true;
+ }
+}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionJump.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionJump.java
index e267382b..53305b34 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionJump.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionJump.java
@@ -22,6 +22,9 @@ public class ActionJump extends PetAction {
Emulator.getThreading().run(new PetClearPosture(pet, RoomUnitStatus.JUMP, null, false), 2000);
+ // Jumping is fun!
+ pet.addHappiness(8);
+
if (pet.getHappiness() > 60)
pet.say(pet.getPetData().randomVocal(PetVocalsType.PLAYFUL));
else
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionMambo.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionMambo.java
new file mode 100644
index 00000000..be4b3ecf
--- /dev/null
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionMambo.java
@@ -0,0 +1,33 @@
+package com.eu.habbo.habbohotel.pets.actions;
+
+import com.eu.habbo.Emulator;
+import com.eu.habbo.habbohotel.pets.Pet;
+import com.eu.habbo.habbohotel.pets.PetAction;
+import com.eu.habbo.habbohotel.pets.PetTasks;
+import com.eu.habbo.habbohotel.pets.PetVocalsType;
+import com.eu.habbo.habbohotel.rooms.RoomUnitStatus;
+import com.eu.habbo.habbohotel.users.Habbo;
+import com.eu.habbo.threading.runnables.PetClearPosture;
+
+public class ActionMambo extends PetAction {
+ public ActionMambo() {
+ super(PetTasks.FREE, true);
+ this.minimumActionDuration = 5000;
+ this.statusToSet.add(RoomUnitStatus.DANCE);
+ }
+
+ @Override
+ public boolean apply(Pet pet, Habbo habbo, String[] data) {
+ pet.clearPosture();
+ pet.getRoomUnit().setStatus(RoomUnitStatus.DANCE, "");
+
+ Emulator.getThreading().run(new PetClearPosture(pet, RoomUnitStatus.DANCE, null, false), 5000);
+
+ if (pet.getHappiness() > 60)
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.PLAYFUL));
+ else
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.GENERIC_NEUTRAL));
+
+ return true;
+ }
+}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionMoveForward.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionMoveForward.java
index abaac92e..2b9abaed 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionMoveForward.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionMoveForward.java
@@ -3,6 +3,7 @@ package com.eu.habbo.habbohotel.pets.actions;
import com.eu.habbo.habbohotel.pets.Pet;
import com.eu.habbo.habbohotel.pets.PetAction;
import com.eu.habbo.habbohotel.pets.PetVocalsType;
+import com.eu.habbo.habbohotel.rooms.RoomTile;
import com.eu.habbo.habbohotel.users.Habbo;
public class ActionMoveForward extends PetAction {
@@ -12,12 +13,23 @@ public class ActionMoveForward extends PetAction {
@Override
public boolean apply(Pet pet, Habbo habbo, String[] data) {
-
- pet.getRoomUnit().setGoalLocation(pet.getRoom().getLayout().getTileInFront(pet.getRoomUnit().getCurrentLocation(), pet.getRoomUnit().getBodyRotation().getValue()));
- pet.getRoomUnit().setCanWalk(true);
-
- pet.say(pet.getPetData().randomVocal(PetVocalsType.GENERIC_NEUTRAL));
-
+ if (pet.getRoom() == null || pet.getRoomUnit() == null) {
+ return false;
+ }
+
+ RoomTile targetTile = pet.getRoom().getLayout().getTileInFront(
+ pet.getRoomUnit().getCurrentLocation(),
+ pet.getRoomUnit().getBodyRotation().getValue()
+ );
+
+ if (targetTile != null && pet.getRoom().getLayout().tileWalkable(targetTile.x, targetTile.y)) {
+ pet.getRoomUnit().setGoalLocation(targetTile);
+ pet.getRoomUnit().setCanWalk(true);
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.GENERIC_NEUTRAL));
+ return true;
+ }
+
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.DISOBEY));
return false;
}
}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionNest.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionNest.java
index f7d994d6..fa05183f 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionNest.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionNest.java
@@ -12,17 +12,14 @@ public class ActionNest extends PetAction {
@Override
public boolean apply(Pet pet, Habbo habbo, String[] data) {
- if (pet.getEnergy() < 65) {
- pet.findNest();
+ // Pet always obeys nest command - will go to nest or lay down if no nest available
+ pet.findNest();
- if (pet.getEnergy() < 30)
- pet.say(pet.getPetData().randomVocal(PetVocalsType.TIRED));
+ if (pet.getEnergy() < 30)
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.TIRED));
+ else
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.GENERIC_NEUTRAL));
- return true;
- } else {
- pet.say(pet.getPetData().randomVocal(PetVocalsType.DISOBEY));
- }
-
- return false;
+ return true;
}
}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionPlay.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionPlay.java
index dffa9b21..c98c6f76 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionPlay.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionPlay.java
@@ -1,25 +1,133 @@
package com.eu.habbo.habbohotel.pets.actions;
-import com.eu.habbo.habbohotel.pets.Pet;
-import com.eu.habbo.habbohotel.pets.PetAction;
-import com.eu.habbo.habbohotel.pets.PetVocalsType;
+import com.eu.habbo.Emulator;
+import com.eu.habbo.habbohotel.items.interactions.pets.InteractionPetToy;
+import com.eu.habbo.habbohotel.pets.*;
+import com.eu.habbo.habbohotel.rooms.RoomTile;
+import com.eu.habbo.habbohotel.rooms.RoomUnitStatus;
import com.eu.habbo.habbohotel.users.Habbo;
+import com.eu.habbo.habbohotel.users.HabboItem;
+import gnu.trove.set.hash.THashSet;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
public class ActionPlay extends PetAction {
+ private static final Logger LOGGER = LoggerFactory.getLogger(ActionPlay.class);
+
public ActionPlay() {
- super(null, false);
+ super(PetTasks.PLAY, false);
+ this.minimumActionDuration = 4000;
}
@Override
public boolean apply(Pet pet, Habbo habbo, String[] data) {
- //Play
- //TODO Implement playing for pets. For example; go to ball, toy etc.
- if (pet.getHappiness() > 75)
- pet.say(pet.getPetData().randomVocal(PetVocalsType.PLAYFUL));
- else {
- pet.say(pet.getPetData().randomVocal(PetVocalsType.DISOBEY));
+ LOGGER.info("[ActionPlay] apply() called for pet: {}", pet.getName());
+
+ // Check if pet has enough energy to play
+ if (pet.getEnergy() < 25) {
+ LOGGER.info("[ActionPlay] Pet too tired, energy: {}", pet.getEnergy());
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.TIRED));
+ return false;
}
-
+
+ if (pet.getRoom() == null || pet.getRoom().getRoomSpecialTypes() == null) {
+ LOGGER.info("[ActionPlay] Room or RoomSpecialTypes is null");
+ return false;
+ }
+
+ // Get all pet toys in the room
+ THashSet toys = pet.getRoom().getRoomSpecialTypes().getPetToys();
+ LOGGER.info("[ActionPlay] Found {} pet toys in room", toys.size());
+
+ // Find a toy to play with
+ HabboItem toy = pet.getPetData().randomToyItem(toys);
+ LOGGER.info("[ActionPlay] randomToyItem returned: {}", toy != null ? toy.getId() : "null");
+
+ // If no compatible toy, just pick any toy in the room
+ if (toy == null && !toys.isEmpty()) {
+ for (InteractionPetToy t : toys) {
+ toy = t;
+ LOGGER.info("[ActionPlay] Using any toy: {}", toy.getId());
+ break;
+ }
+ }
+
+ if (toy != null) {
+ RoomTile toyTile = pet.getRoom().getLayout().getTile(toy.getX(), toy.getY());
+ LOGGER.info("[ActionPlay] Toy at tile: ({}, {}), tile found: {}", toy.getX(), toy.getY(), toyTile != null);
+
+ if (toyTile != null) {
+ pet.clearPosture();
+ pet.getRoomUnit().setCanWalk(true);
+ pet.setTask(PetTasks.PLAY);
+
+ double distance = pet.getRoomUnit().getCurrentLocation().distance(toyTile);
+ LOGGER.info("[ActionPlay] Distance to toy: {}", distance);
+
+ // Check if already at the toy
+ if (distance == 0) {
+ // Already at toy - start playing immediately
+ LOGGER.info("[ActionPlay] Already at toy, starting play");
+ this.startPlaying(pet, toy);
+ } else {
+ // Walk to toy first
+ LOGGER.info("[ActionPlay] Setting goal location to toy");
+ pet.getRoomUnit().setGoalLocation(toyTile);
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.PLAYFUL));
+ // The InteractionPetToy.onWalkOn will handle the actual play when pet arrives
+ }
+ return true;
+ }
+ }
+
+ LOGGER.info("[ActionPlay] No toy found, doing solo play");
+ // No toy found - play solo animation
+ pet.clearPosture();
+ pet.setTask(PetTasks.PLAY);
+ pet.getRoomUnit().setStatus(RoomUnitStatus.PLAY, "0");
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.PLAYFUL));
+ pet.packetUpdate = true;
+
+ // Give smaller rewards for solo play
+ pet.addHappiness(10);
+ pet.addEnergy(-5);
+ pet.addExperience(3);
+
+ Emulator.getThreading().run(() -> {
+ if (pet.getRoomUnit() != null) {
+ pet.getRoomUnit().removeStatus(RoomUnitStatus.PLAY);
+ pet.packetUpdate = true;
+ }
+ pet.setTask(PetTasks.FREE);
+ }, this.minimumActionDuration);
+
return true;
}
+
+ private void startPlaying(Pet pet, HabboItem toy) {
+ pet.getRoomUnit().clearStatus();
+ pet.getRoomUnit().setStatus(RoomUnitStatus.PLAY, pet.getRoomUnit().getCurrentLocation().getStackHeight() + "");
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.PLAYFUL));
+ pet.packetUpdate = true;
+
+ // Playing with toy gives better rewards
+ pet.addHappiness(25);
+ pet.addEnergy(-10);
+ pet.addExperience(10);
+
+ // Update toy state
+ toy.setExtradata("1");
+ pet.getRoom().updateItemState(toy);
+
+ int playDuration = 2500 + (Emulator.getRandom().nextInt(20) * 500);
+ Emulator.getThreading().run(() -> {
+ toy.setExtradata("0");
+ pet.getRoom().updateItem(toy);
+ if (pet.getRoomUnit() != null) {
+ pet.getRoomUnit().removeStatus(RoomUnitStatus.PLAY);
+ pet.packetUpdate = true;
+ }
+ pet.setTask(PetTasks.FREE);
+ }, playDuration);
+ }
}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionPlayDead.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionPlayDead.java
index ecaee9de..532ea1fa 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionPlayDead.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionPlayDead.java
@@ -21,6 +21,9 @@ public class ActionPlayDead extends PetAction {
pet.getRoomUnit().setStatus(RoomUnitStatus.DEAD, pet.getRoom().getStackHeight(pet.getRoomUnit().getX(), pet.getRoomUnit().getY(), false) + "");
+ // Playing dead is not very fun
+ pet.addHappiness(-3);
+
if (pet.getHappiness() > 50)
pet.say(pet.getPetData().randomVocal(PetVocalsType.PLAYFUL));
else
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionPlayFootball.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionPlayFootball.java
index f5f12c94..a7e825c2 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionPlayFootball.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionPlayFootball.java
@@ -3,41 +3,68 @@ package com.eu.habbo.habbohotel.pets.actions;
import com.eu.habbo.habbohotel.items.interactions.InteractionPushable;
import com.eu.habbo.habbohotel.pets.Pet;
import com.eu.habbo.habbohotel.pets.PetAction;
+import com.eu.habbo.habbohotel.pets.PetTasks;
import com.eu.habbo.habbohotel.pets.PetVocalsType;
import com.eu.habbo.habbohotel.rooms.Room;
+import com.eu.habbo.habbohotel.rooms.RoomTile;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboItem;
public class ActionPlayFootball extends PetAction {
public ActionPlayFootball() {
- super(null, false);
+ super(PetTasks.PLAY_FOOTBALL, false);
}
@Override
public boolean apply(Pet pet, Habbo habbo, String[] data) {
-
Room room = pet.getRoom();
- if(room == null || room.getLayout() == null)
+ if (room == null || room.getLayout() == null) {
return false;
+ }
+
+ if (pet.getRoomUnit() == null) {
+ return false;
+ }
- HabboItem foundBall = null;
+ // Find the nearest ball to the pet
+ HabboItem nearestBall = null;
+ double nearestDistance = Double.MAX_VALUE;
+ RoomTile petTile = pet.getRoomUnit().getCurrentLocation();
- for(HabboItem item : room.getFloorItems()) {
- if(item instanceof InteractionPushable) {
- foundBall = item;
+ if (petTile == null) {
+ return false;
+ }
+
+ for (HabboItem item : room.getFloorItems()) {
+ if (item instanceof InteractionPushable) {
+ RoomTile ballTile = room.getLayout().getTile(item.getX(), item.getY());
+ if (ballTile != null) {
+ double distance = petTile.distance(ballTile);
+ if (distance < nearestDistance) {
+ nearestDistance = distance;
+ nearestBall = item;
+ }
+ }
}
}
- if(foundBall == null)
+ if (nearestBall == null) {
+ // No ball in room - disobey
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.DISOBEY));
return false;
+ }
- pet.getRoomUnit().setGoalLocation(room.getLayout().getTile(foundBall.getX(), foundBall.getY()));
+ // Set task and pathfind to the ball
+ pet.setTask(PetTasks.PLAY_FOOTBALL);
+ pet.getRoomUnit().setCanWalk(true);
+ pet.getRoomUnit().setGoalLocation(room.getLayout().getTile(nearestBall.getX(), nearestBall.getY()));
- if (pet.getHappiness() > 75)
+ if (pet.getHappiness() > 75) {
pet.say(pet.getPetData().randomVocal(PetVocalsType.PLAYFUL));
- else
+ } else {
pet.say(pet.getPetData().randomVocal(PetVocalsType.GENERIC_NEUTRAL));
+ }
return true;
}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionRingOfFire.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionRingOfFire.java
new file mode 100644
index 00000000..44f187f6
--- /dev/null
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionRingOfFire.java
@@ -0,0 +1,43 @@
+package com.eu.habbo.habbohotel.pets.actions;
+
+import com.eu.habbo.Emulator;
+import com.eu.habbo.habbohotel.pets.Pet;
+import com.eu.habbo.habbohotel.pets.PetAction;
+import com.eu.habbo.habbohotel.pets.PetVocalsType;
+import com.eu.habbo.habbohotel.rooms.RoomUnitStatus;
+import com.eu.habbo.habbohotel.users.Habbo;
+
+public class ActionRingOfFire extends PetAction {
+ public ActionRingOfFire() {
+ super(null, true);
+ this.minimumActionDuration = 4000;
+ }
+
+ @Override
+ public boolean apply(Pet pet, Habbo habbo, String[] data) {
+ if (pet.getRoom() == null || pet.getRoomUnit() == null) {
+ return false;
+ }
+
+ // Ring of Fire can only be performed while hanging on a tree
+ if (!pet.getRoomUnit().hasStatus(RoomUnitStatus.HANG)) {
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.DISOBEY));
+ return false;
+ }
+
+ // Transition from HANG to RINGOFFIRE
+ pet.getRoomUnit().removeStatus(RoomUnitStatus.HANG);
+ pet.getRoomUnit().setStatus(RoomUnitStatus.RINGOFFIRE, pet.getRoomUnit().getCurrentLocation().getStackHeight() + "");
+ pet.packetUpdate = true;
+
+ // After ring of fire, go back to hanging
+ Emulator.getThreading().run(() -> {
+ pet.getRoomUnit().removeStatus(RoomUnitStatus.RINGOFFIRE);
+ pet.getRoomUnit().setStatus(RoomUnitStatus.HANG, "");
+ pet.packetUpdate = true;
+ }, minimumActionDuration);
+
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.PLAYFUL));
+ return true;
+ }
+}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionRoll.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionRoll.java
new file mode 100644
index 00000000..58dc9ce0
--- /dev/null
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionRoll.java
@@ -0,0 +1,32 @@
+package com.eu.habbo.habbohotel.pets.actions;
+
+import com.eu.habbo.Emulator;
+import com.eu.habbo.habbohotel.pets.Pet;
+import com.eu.habbo.habbohotel.pets.PetAction;
+import com.eu.habbo.habbohotel.pets.PetTasks;
+import com.eu.habbo.habbohotel.pets.PetVocalsType;
+import com.eu.habbo.habbohotel.rooms.RoomUnitStatus;
+import com.eu.habbo.habbohotel.users.Habbo;
+import com.eu.habbo.threading.runnables.PetClearPosture;
+
+public class ActionRoll extends PetAction {
+ public ActionRoll() {
+ super(PetTasks.FREE, true);
+ this.minimumActionDuration = 3000;
+ }
+
+ @Override
+ public boolean apply(Pet pet, Habbo habbo, String[] data) {
+ pet.clearPosture();
+ pet.getRoomUnit().setStatus(RoomUnitStatus.LAY, pet.getRoomUnit().getCurrentLocation().getStackHeight() + "");
+
+ Emulator.getThreading().run(new PetClearPosture(pet, RoomUnitStatus.LAY, null, false), 3000);
+
+ if (pet.getHappiness() > 50)
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.PLAYFUL));
+ else
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.GENERIC_NEUTRAL));
+
+ return true;
+ }
+}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionSit.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionSit.java
index 235ce5ca..0b9886d8 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionSit.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionSit.java
@@ -1,5 +1,6 @@
package com.eu.habbo.habbohotel.pets.actions;
+import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.pets.Pet;
import com.eu.habbo.habbohotel.pets.PetAction;
import com.eu.habbo.habbohotel.pets.PetTasks;
@@ -10,12 +11,26 @@ import com.eu.habbo.habbohotel.users.Habbo;
public class ActionSit extends PetAction {
public ActionSit() {
super(PetTasks.SIT, true);
+ this.minimumActionDuration = 4000;
+ this.statusToRemove.add(RoomUnitStatus.BEG);
+ this.statusToRemove.add(RoomUnitStatus.MOVE);
+ this.statusToRemove.add(RoomUnitStatus.LAY);
+ this.statusToRemove.add(RoomUnitStatus.DEAD);
}
@Override
public boolean apply(Pet pet, Habbo habbo, String[] data) {
if (pet.getTask() != PetTasks.SIT && !pet.getRoomUnit().hasStatus(RoomUnitStatus.SIT)) {
- pet.getRoomUnit().setStatus(RoomUnitStatus.SIT, pet.getRoom().getStackHeight(pet.getRoomUnit().getX(), pet.getRoomUnit().getY(), false) - 0.50 + "");
+ pet.getRoomUnit().cmdSit = true;
+ pet.getRoomUnit().setStatus(RoomUnitStatus.SIT, pet.getRoomUnit().getCurrentLocation().getStackHeight() + "");
+
+ // Sitting is a bit boring
+ pet.addHappiness(-2);
+
+ Emulator.getThreading().run(() -> {
+ pet.getRoomUnit().cmdSit = false;
+ pet.clearPosture();
+ }, this.minimumActionDuration);
if (pet.getHappiness() > 75)
pet.say(pet.getPetData().randomVocal(PetVocalsType.PLAYFUL));
@@ -25,6 +40,6 @@ public class ActionSit extends PetAction {
return true;
}
- return false;
+ return true;
}
}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionSpeak.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionSpeak.java
index ed3207b4..270285f3 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionSpeak.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionSpeak.java
@@ -21,6 +21,9 @@ public class ActionSpeak extends PetAction {
pet.setMuted(false);
Emulator.getThreading().run(new PetClearPosture(pet, RoomUnitStatus.SPEAK, null, false), 2000);
+ // Speaking/expressing itself makes pet happy
+ pet.addHappiness(3);
+
if (pet.getHappiness() > 70)
pet.say(pet.getPetData().randomVocal(PetVocalsType.GENERIC_HAPPY));
else if (pet.getHappiness() < 30)
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionSpin.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionSpin.java
new file mode 100644
index 00000000..b762e8fa
--- /dev/null
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionSpin.java
@@ -0,0 +1,37 @@
+package com.eu.habbo.habbohotel.pets.actions;
+
+import com.eu.habbo.Emulator;
+import com.eu.habbo.habbohotel.pets.Pet;
+import com.eu.habbo.habbohotel.pets.PetAction;
+import com.eu.habbo.habbohotel.pets.PetTasks;
+import com.eu.habbo.habbohotel.pets.PetVocalsType;
+import com.eu.habbo.habbohotel.rooms.RoomUserRotation;
+import com.eu.habbo.habbohotel.users.Habbo;
+
+public class ActionSpin extends PetAction {
+ public ActionSpin() {
+ super(PetTasks.FREE, true);
+ this.minimumActionDuration = 2000;
+ }
+
+ @Override
+ public boolean apply(Pet pet, Habbo habbo, String[] data) {
+ pet.clearPosture();
+
+ // Spin animation - rotate through all directions
+ for (int i = 0; i < 8; i++) {
+ final int rotation = i;
+ Emulator.getThreading().run(() -> {
+ pet.getRoomUnit().setRotation(RoomUserRotation.values()[rotation]);
+ pet.packetUpdate = true;
+ }, i * 250);
+ }
+
+ if (pet.getHappiness() > 50)
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.PLAYFUL));
+ else
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.GENERIC_NEUTRAL));
+
+ return true;
+ }
+}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionStay.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionStay.java
index 7b3851cf..5c2701ad 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionStay.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionStay.java
@@ -22,6 +22,10 @@ public class ActionStay extends PetAction {
pet.getRoomUnit().setCanWalk(false);
pet.setStayStartedAt(Emulator.getIntUnixTimestamp());
+
+ // Staying still is boring
+ pet.addHappiness(-5);
+
pet.say(pet.getPetData().randomVocal(PetVocalsType.GENERIC_NEUTRAL));
return true;
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionSwing.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionSwing.java
new file mode 100644
index 00000000..7fc5c57b
--- /dev/null
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionSwing.java
@@ -0,0 +1,47 @@
+package com.eu.habbo.habbohotel.pets.actions;
+
+import com.eu.habbo.Emulator;
+import com.eu.habbo.habbohotel.items.interactions.pets.InteractionPetTree;
+import com.eu.habbo.habbohotel.pets.Pet;
+import com.eu.habbo.habbohotel.pets.PetAction;
+import com.eu.habbo.habbohotel.pets.PetTasks;
+import com.eu.habbo.habbohotel.pets.PetVocalsType;
+import com.eu.habbo.habbohotel.rooms.RoomUnitStatus;
+import com.eu.habbo.habbohotel.users.Habbo;
+import com.eu.habbo.habbohotel.users.HabboItem;
+import com.eu.habbo.threading.runnables.PetClearPosture;
+
+public class ActionSwing extends PetAction {
+ public ActionSwing() {
+ super(PetTasks.FREE, true);
+ this.minimumActionDuration = 4000;
+ }
+
+ @Override
+ public boolean apply(Pet pet, Habbo habbo, String[] data) {
+ // Swing requires pet to be on a pet tree (dragon/monkey tree)
+ if (pet.getRoom() == null) {
+ return false;
+ }
+
+ HabboItem itemBelow = pet.getRoom().getTopItemAt(pet.getRoomUnit().getX(), pet.getRoomUnit().getY());
+ if (!(itemBelow instanceof InteractionPetTree)) {
+ // Pet must go to tree first
+ pet.findTree();
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.DISOBEY));
+ return false;
+ }
+
+ pet.clearPosture();
+ pet.getRoomUnit().setStatus(RoomUnitStatus.SWING, "");
+
+ Emulator.getThreading().run(new PetClearPosture(pet, RoomUnitStatus.SWING, null, false), 4000);
+
+ if (pet.getHappiness() > 50)
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.PLAYFUL));
+ else
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.GENERIC_NEUTRAL));
+
+ return true;
+ }
+}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionSwitch.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionSwitch.java
new file mode 100644
index 00000000..5d758c0b
--- /dev/null
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionSwitch.java
@@ -0,0 +1,27 @@
+package com.eu.habbo.habbohotel.pets.actions;
+
+import com.eu.habbo.habbohotel.pets.Pet;
+import com.eu.habbo.habbohotel.pets.PetAction;
+import com.eu.habbo.habbohotel.pets.PetTasks;
+import com.eu.habbo.habbohotel.pets.PetVocalsType;
+import com.eu.habbo.habbohotel.users.Habbo;
+
+public class ActionSwitch extends PetAction {
+ public ActionSwitch() {
+ super(PetTasks.FREE, true);
+ this.minimumActionDuration = 1000;
+ }
+
+ @Override
+ public boolean apply(Pet pet, Habbo habbo, String[] data) {
+ pet.clearPosture();
+
+ // Switch/toggle behavior - pet acknowledges command
+ if (pet.getHappiness() > 50)
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.PLAYFUL));
+ else
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.GENERIC_NEUTRAL));
+
+ return true;
+ }
+}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionTeleport.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionTeleport.java
new file mode 100644
index 00000000..626963e5
--- /dev/null
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionTeleport.java
@@ -0,0 +1,39 @@
+package com.eu.habbo.habbohotel.pets.actions;
+
+import com.eu.habbo.habbohotel.pets.Pet;
+import com.eu.habbo.habbohotel.pets.PetAction;
+import com.eu.habbo.habbohotel.pets.PetTasks;
+import com.eu.habbo.habbohotel.pets.PetVocalsType;
+import com.eu.habbo.habbohotel.rooms.RoomTile;
+import com.eu.habbo.habbohotel.users.Habbo;
+
+public class ActionTeleport extends PetAction {
+ public ActionTeleport() {
+ super(PetTasks.FREE, true);
+ this.minimumActionDuration = 1000;
+ }
+
+ @Override
+ public boolean apply(Pet pet, Habbo habbo, String[] data) {
+ // Teleport pet to a random walkable tile near owner
+ if (habbo != null && habbo.getRoomUnit() != null && pet.getRoom() != null) {
+ RoomTile targetTile = pet.getRoom().getLayout().getTileInFront(
+ habbo.getRoomUnit().getCurrentLocation(),
+ habbo.getRoomUnit().getBodyRotation().getValue()
+ );
+
+ if (targetTile != null && targetTile.isWalkable()) {
+ pet.getRoomUnit().setLocation(targetTile);
+ pet.getRoomUnit().setZ(targetTile.getStackHeight());
+ pet.packetUpdate = true;
+ }
+ }
+
+ if (pet.getHappiness() > 50)
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.PLAYFUL));
+ else
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.GENERIC_NEUTRAL));
+
+ return true;
+ }
+}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionTorch.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionTorch.java
index 04407850..f39c4eaf 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionTorch.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionTorch.java
@@ -23,6 +23,7 @@ public class ActionTorch extends PetAction {
return false;
}
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.PLAYFUL));
Emulator.getThreading().run(new PetClearPosture(pet, RoomUnitStatus.EAT, null, false), this.minimumActionDuration);
return true;
}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionTripleJump.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionTripleJump.java
new file mode 100644
index 00000000..52fc5c0e
--- /dev/null
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionTripleJump.java
@@ -0,0 +1,40 @@
+package com.eu.habbo.habbohotel.pets.actions;
+
+import com.eu.habbo.Emulator;
+import com.eu.habbo.habbohotel.pets.Pet;
+import com.eu.habbo.habbohotel.pets.PetAction;
+import com.eu.habbo.habbohotel.pets.PetTasks;
+import com.eu.habbo.habbohotel.pets.PetVocalsType;
+import com.eu.habbo.habbohotel.rooms.RoomUnitStatus;
+import com.eu.habbo.habbohotel.users.Habbo;
+import com.eu.habbo.threading.runnables.PetClearPosture;
+
+public class ActionTripleJump extends PetAction {
+ public ActionTripleJump() {
+ super(PetTasks.JUMP, true);
+ this.minimumActionDuration = 4000;
+ this.statusToSet.add(RoomUnitStatus.JUMP);
+ }
+
+ @Override
+ public boolean apply(Pet pet, Habbo habbo, String[] data) {
+ pet.clearPosture();
+
+ // Triple jump - three jump animations
+ for (int i = 0; i < 3; i++) {
+ Emulator.getThreading().run(() -> {
+ pet.getRoomUnit().setStatus(RoomUnitStatus.JUMP, "");
+ pet.packetUpdate = true;
+ }, i * 1200);
+ }
+
+ Emulator.getThreading().run(new PetClearPosture(pet, RoomUnitStatus.JUMP, null, false), 4000);
+
+ if (pet.getHappiness() > 70)
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.PLAYFUL));
+ else
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.GENERIC_NEUTRAL));
+
+ return true;
+ }
+}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionWagTail.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionWagTail.java
new file mode 100644
index 00000000..76869810
--- /dev/null
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionWagTail.java
@@ -0,0 +1,32 @@
+package com.eu.habbo.habbohotel.pets.actions;
+
+import com.eu.habbo.Emulator;
+import com.eu.habbo.habbohotel.pets.Pet;
+import com.eu.habbo.habbohotel.pets.PetAction;
+import com.eu.habbo.habbohotel.pets.PetTasks;
+import com.eu.habbo.habbohotel.pets.PetVocalsType;
+import com.eu.habbo.habbohotel.rooms.RoomUnitStatus;
+import com.eu.habbo.habbohotel.users.Habbo;
+import com.eu.habbo.threading.runnables.PetClearPosture;
+
+public class ActionWagTail extends PetAction {
+ public ActionWagTail() {
+ super(PetTasks.FREE, true);
+ this.minimumActionDuration = 2000;
+ }
+
+ @Override
+ public boolean apply(Pet pet, Habbo habbo, String[] data) {
+ pet.clearPosture();
+ pet.getRoomUnit().setStatus(RoomUnitStatus.WAG_TAIL, "");
+
+ Emulator.getThreading().run(new PetClearPosture(pet, RoomUnitStatus.WAG_TAIL, null, false), 2000);
+
+ if (pet.getHappiness() > 40)
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.GENERIC_HAPPY));
+ else
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.GENERIC_NEUTRAL));
+
+ return true;
+ }
+}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionWave.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionWave.java
index 18b512b6..8986f3d0 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionWave.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/actions/ActionWave.java
@@ -4,6 +4,7 @@ import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.pets.Pet;
import com.eu.habbo.habbohotel.pets.PetAction;
import com.eu.habbo.habbohotel.pets.PetTasks;
+import com.eu.habbo.habbohotel.pets.PetVocalsType;
import com.eu.habbo.habbohotel.rooms.RoomUnitStatus;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.threading.runnables.PetClearPosture;
@@ -22,9 +23,15 @@ public class ActionWave extends PetAction {
pet.getRoomUnit().setStatus(RoomUnitStatus.WAVE, "0");
Emulator.getThreading().run(new PetClearPosture(pet, RoomUnitStatus.WAVE, null, false), 2000);
+
+ // Waving is a fun trick
+ pet.addHappiness(5);
+
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.PLAYFUL));
return true;
}
-
+
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.GENERIC_NEUTRAL));
return false;
}
}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/breeding/PetBreedingSession.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/breeding/PetBreedingSession.java
new file mode 100644
index 00000000..47d94ffd
--- /dev/null
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/breeding/PetBreedingSession.java
@@ -0,0 +1,175 @@
+package com.eu.habbo.habbohotel.pets.breeding;
+
+import com.eu.habbo.Emulator;
+import com.eu.habbo.habbohotel.items.interactions.pets.InteractionPetBreedingNest;
+import com.eu.habbo.habbohotel.pets.Pet;
+import com.eu.habbo.habbohotel.pets.PetTasks;
+import com.eu.habbo.habbohotel.users.Habbo;
+
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Represents an active breeding session between two pets.
+ * Manages the state and lifecycle of the breeding process.
+ */
+public class PetBreedingSession {
+ private final InteractionPetBreedingNest nest;
+ private final Pet petOne;
+ private Pet petTwo;
+ private final long startTime;
+ private BreedingState state;
+ private ScheduledFuture> timeoutTask;
+
+ /**
+ * Represents the various states of a breeding session.
+ */
+ public enum BreedingState {
+ WAITING_FOR_SECOND_PET,
+ WAITING_FOR_CONFIRMATION,
+ BREEDING_IN_PROGRESS,
+ COMPLETED,
+ CANCELLED
+ }
+
+ /**
+ * Creates a new breeding session with the first pet.
+ * @param nest The breeding nest item
+ * @param firstPet The first pet to enter the nest
+ */
+ public PetBreedingSession(InteractionPetBreedingNest nest, Pet firstPet) {
+ this.nest = nest;
+ this.petOne = firstPet;
+ this.petTwo = null;
+ this.startTime = System.currentTimeMillis();
+ this.state = BreedingState.WAITING_FOR_SECOND_PET;
+
+ // Auto-cancel if second pet doesn't arrive within configured timeout
+ int timeoutSeconds = Emulator.getConfig().getInt("pet.breeding.timeout_seconds", 120);
+ this.timeoutTask = Emulator.getThreading().getService().schedule(() -> {
+ if (this.state == BreedingState.WAITING_FOR_SECOND_PET) {
+ this.cancel("Timeout waiting for second pet");
+ }
+ }, timeoutSeconds, TimeUnit.SECONDS);
+ }
+
+ /**
+ * Attempts to add the second pet to the breeding session.
+ * @param pet The second pet to add
+ * @return true if the pet was successfully added
+ */
+ public boolean addSecondPet(Pet pet) {
+ if (this.state != BreedingState.WAITING_FOR_SECOND_PET) {
+ return false;
+ }
+
+ // Validate compatibility - must be same pet type
+ if (pet.getPetData().getType() != this.petOne.getPetData().getType()) {
+ return false;
+ }
+
+ // Check if breeding is possible for this pet type
+ if (pet.getPetData().getOffspringType() == -1) {
+ return false;
+ }
+
+ // Don't allow breeding with self
+ if (pet.getId() == this.petOne.getId()) {
+ return false;
+ }
+
+ this.petTwo = pet;
+ this.state = BreedingState.WAITING_FOR_CONFIRMATION;
+
+ // Cancel the timeout task since we have both pets
+ if (this.timeoutTask != null && !this.timeoutTask.isDone()) {
+ this.timeoutTask.cancel(false);
+ }
+
+ return true;
+ }
+
+ /**
+ * Confirms the breeding and starts the process.
+ * @param habbo The habbo confirming the breeding
+ * @param offspringName The name for the offspring
+ */
+ public void confirm(Habbo habbo, String offspringName) {
+ if (this.state != BreedingState.WAITING_FOR_CONFIRMATION) {
+ return;
+ }
+
+ this.state = BreedingState.BREEDING_IN_PROGRESS;
+ this.nest.breed(habbo, offspringName, this.petOne.getId(), this.petTwo.getId());
+ this.state = BreedingState.COMPLETED;
+ }
+
+ /**
+ * Cancels the breeding session and releases the pets.
+ * @param reason The reason for cancellation
+ */
+ public void cancel(String reason) {
+ if (this.state == BreedingState.COMPLETED || this.state == BreedingState.CANCELLED) {
+ return;
+ }
+
+ this.state = BreedingState.CANCELLED;
+
+ // Release first pet
+ if (this.petOne != null && this.petOne.getRoomUnit() != null) {
+ this.petOne.getRoomUnit().setCanWalk(true);
+ this.petOne.setTask(PetTasks.FREE);
+ }
+
+ // Release second pet
+ if (this.petTwo != null && this.petTwo.getRoomUnit() != null) {
+ this.petTwo.getRoomUnit().setCanWalk(true);
+ this.petTwo.setTask(PetTasks.FREE);
+ }
+
+ // Reset nest state
+ this.nest.setExtradata("0");
+ if (this.nest.getRoomId() > 0) {
+ com.eu.habbo.habbohotel.rooms.Room room = com.eu.habbo.Emulator.getGameEnvironment().getRoomManager().getRoom(this.nest.getRoomId());
+ if (room != null) {
+ room.updateItem(this.nest);
+ }
+ }
+
+ // Cancel any pending timeout task
+ if (this.timeoutTask != null && !this.timeoutTask.isDone()) {
+ this.timeoutTask.cancel(false);
+ }
+ }
+
+ /**
+ * Checks if the breeding session is still valid.
+ * @return true if both pets are still in the same room
+ */
+ public boolean isValid() {
+ if (this.petOne == null || this.petOne.getRoom() == null) {
+ return false;
+ }
+
+ if (this.petTwo != null && this.petTwo.getRoom() != this.petOne.getRoom()) {
+ return false;
+ }
+
+ return true;
+ }
+
+ // Getters
+ public InteractionPetBreedingNest getNest() { return nest; }
+ public Pet getPetOne() { return petOne; }
+ public Pet getPetTwo() { return petTwo; }
+ public long getStartTime() { return startTime; }
+ public BreedingState getState() { return state; }
+
+ /**
+ * Gets how long the session has been active in milliseconds.
+ * @return Duration in milliseconds
+ */
+ public long getDuration() {
+ return System.currentTimeMillis() - this.startTime;
+ }
+}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/Room.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/Room.java
index ab734e44..6c7c5a5a 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/Room.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/Room.java
@@ -946,8 +946,20 @@ public class Room implements Comparable, ISerialize, Runnable {
}
}
- this.unitManager.clear();
+ // Save ALL remaining pets (including owner's pets) BEFORE clearing
+ TIntObjectIterator petIterator = this.getCurrentPets().iterator();
+ for (int i = this.getCurrentPets().size(); i-- > 0; ) {
+ try {
+ petIterator.advance();
+ petIterator.value().needsUpdate = true;
+ petIterator.value().run(); // Run synchronously to ensure DB is updated before room reload
+ } catch (NoSuchElementException e) {
+ LOGGER.error("Caught exception", e);
+ break;
+ }
+ }
+ this.unitManager.clear();
this.unitManager.clearBots();
this.unitManager.clearPets();
} catch (Exception e) {
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomCycleManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomCycleManager.java
index be8755fd..c4b6eb62 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomCycleManager.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomCycleManager.java
@@ -434,6 +434,12 @@ public class RoomCycleManager {
}
if (!unit.isWalking() && !unit.cmdSit) {
+ // Don't override special pet statuses with SIT
+ boolean hasSpecialPetStatus = unit.hasStatus(RoomUnitStatus.HANG)
+ || unit.hasStatus(RoomUnitStatus.SWING)
+ || unit.hasStatus(RoomUnitStatus.FLAME)
+ || unit.hasStatus(RoomUnitStatus.PLAY);
+
RoomTile thisTile = this.room.getLayout().getTile(unit.getX(), unit.getY());
HabboItem topItem = this.room.getTallestChair(thisTile);
@@ -442,7 +448,7 @@ public class RoomCycleManager {
unit.removeStatus(RoomUnitStatus.SIT);
update = true;
}
- } else if (thisTile.state == RoomTileState.SIT && (!unit.hasStatus(RoomUnitStatus.SIT)
+ } else if (!hasSpecialPetStatus && thisTile.state == RoomTileState.SIT && (!unit.hasStatus(RoomUnitStatus.SIT)
|| unit.sitUpdate)) {
this.room.dance(unit, DanceType.NONE);
unit.setStatus(RoomUnitStatus.SIT, (Item.getCurrentHeight(topItem) * 1.0D) + "");
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomItemManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomItemManager.java
index 2a66275a..5b83b94d 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomItemManager.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomItemManager.java
@@ -13,11 +13,9 @@ import com.eu.habbo.habbohotel.items.interactions.games.battlebanzai.Interaction
import com.eu.habbo.habbohotel.items.interactions.games.freeze.InteractionFreezeExitTile;
import com.eu.habbo.habbohotel.items.interactions.games.tag.InteractionTagField;
import com.eu.habbo.habbohotel.items.interactions.games.tag.InteractionTagPole;
-import com.eu.habbo.habbohotel.items.interactions.pets.InteractionNest;
-import com.eu.habbo.habbohotel.items.interactions.pets.InteractionPetBreedingNest;
-import com.eu.habbo.habbohotel.items.interactions.pets.InteractionPetDrink;
-import com.eu.habbo.habbohotel.items.interactions.pets.InteractionPetFood;
+import com.eu.habbo.habbohotel.items.interactions.pets.*;
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredBlob;
+import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboInfo;
import com.eu.habbo.habbohotel.users.HabboItem;
@@ -25,23 +23,10 @@ import com.eu.habbo.habbohotel.users.HabboManager;
import com.eu.habbo.habbohotel.wired.core.WiredManager;
import com.eu.habbo.habbohotel.wired.tick.WiredTickable;
import com.eu.habbo.messages.outgoing.inventory.AddHabboItemComposer;
-import com.eu.habbo.messages.outgoing.rooms.items.FloorItemUpdateComposer;
-import com.eu.habbo.messages.outgoing.rooms.items.ItemStateComposer;
-import com.eu.habbo.messages.outgoing.rooms.items.RemoveFloorItemComposer;
-import com.eu.habbo.messages.outgoing.rooms.items.RemoveWallItemComposer;
-import com.eu.habbo.messages.outgoing.rooms.items.WallItemUpdateComposer;
-import com.eu.habbo.habbohotel.permissions.Permission;
-import com.eu.habbo.messages.outgoing.rooms.items.AddFloorItemComposer;
-import com.eu.habbo.messages.outgoing.rooms.items.AddWallItemComposer;
-import com.eu.habbo.messages.outgoing.rooms.items.FloorItemOnRollerComposer;
+import com.eu.habbo.messages.outgoing.rooms.items.*;
import com.eu.habbo.plugin.Event;
-import com.eu.habbo.plugin.events.furniture.FurnitureBuildheightEvent;
-import com.eu.habbo.plugin.events.furniture.FurnitureMovedEvent;
-import com.eu.habbo.plugin.events.furniture.FurniturePickedUpEvent;
-import com.eu.habbo.plugin.events.furniture.FurniturePlacedEvent;
-import com.eu.habbo.plugin.events.furniture.FurnitureRotatedEvent;
+import com.eu.habbo.plugin.events.furniture.*;
import gnu.trove.TCollections;
-import org.apache.commons.math3.util.Pair;
import gnu.trove.iterator.TIntObjectIterator;
import gnu.trove.map.TIntIntMap;
import gnu.trove.map.TIntObjectMap;
@@ -49,6 +34,7 @@ import gnu.trove.map.hash.THashMap;
import gnu.trove.map.hash.TIntIntHashMap;
import gnu.trove.map.hash.TIntObjectHashMap;
import gnu.trove.set.hash.THashSet;
+import org.apache.commons.math3.util.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -661,6 +647,10 @@ public class RoomItemManager {
specialTypes.addPetDrink((InteractionPetDrink) item);
} else if (item instanceof InteractionPetFood) {
specialTypes.addPetFood((InteractionPetFood) item);
+ } else if (item instanceof InteractionPetToy) {
+ specialTypes.addPetToy((InteractionPetToy) item);
+ } else if (item instanceof InteractionPetTree) {
+ specialTypes.addPetTree((InteractionPetTree) item);
} else if (item instanceof InteractionMoodLight ||
item instanceof InteractionPyramid ||
item instanceof InteractionMusicDisc ||
@@ -780,6 +770,10 @@ public class RoomItemManager {
specialTypes.removePetDrink((InteractionPetDrink) item);
} else if (item instanceof InteractionPetFood) {
specialTypes.removePetFood((InteractionPetFood) item);
+ } else if (item instanceof InteractionPetToy) {
+ specialTypes.removePetToy((InteractionPetToy) item);
+ } else if (item instanceof InteractionPetTree) {
+ specialTypes.removePetTree((InteractionPetTree) item);
} else if (item instanceof InteractionMoodLight ||
item instanceof InteractionPyramid ||
item instanceof InteractionMusicDisc ||
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomSpecialTypes.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomSpecialTypes.java
index 3cc1745d..d6eabd1f 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomSpecialTypes.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomSpecialTypes.java
@@ -19,6 +19,7 @@ import com.eu.habbo.habbohotel.items.interactions.pets.InteractionNest;
import com.eu.habbo.habbohotel.items.interactions.pets.InteractionPetDrink;
import com.eu.habbo.habbohotel.items.interactions.pets.InteractionPetFood;
import com.eu.habbo.habbohotel.items.interactions.pets.InteractionPetToy;
+import com.eu.habbo.habbohotel.items.interactions.pets.InteractionPetTree;
import com.eu.habbo.habbohotel.users.HabboItem;
import com.eu.habbo.habbohotel.wired.WiredConditionType;
import com.eu.habbo.habbohotel.wired.WiredEffectType;
@@ -45,6 +46,7 @@ public class RoomSpecialTypes {
private final THashMap petDrinks;
private final THashMap petFoods;
private final THashMap petToys;
+ private final THashMap petTrees;
private final THashMap rollers;
// Thread-safe wired collections using ConcurrentHashMap for better concurrency
@@ -73,6 +75,7 @@ public class RoomSpecialTypes {
this.petDrinks = new THashMap<>(0);
this.petFoods = new THashMap<>(0);
this.petToys = new THashMap<>(0);
+ this.petTrees = new THashMap<>(0);
this.rollers = new THashMap<>(0);
this.wiredTriggers = new ConcurrentHashMap<>();
@@ -232,6 +235,28 @@ public class RoomSpecialTypes {
}
+ public InteractionPetTree getPetTree(int itemId) {
+ return this.petTrees.get(itemId);
+ }
+
+ public void addPetTree(InteractionPetTree item) {
+ this.petTrees.put(item.getId(), item);
+ }
+
+ public void removePetTree(InteractionPetTree petTree) {
+ this.petTrees.remove(petTree.getId());
+ }
+
+ public THashSet getPetTrees() {
+ synchronized (this.petTrees) {
+ THashSet petTrees = new THashSet<>();
+ petTrees.addAll(this.petTrees.values());
+
+ return petTrees;
+ }
+ }
+
+
public InteractionRoller getRoller(int itemId) {
synchronized (this.rollers) {
return this.rollers.get(itemId);
@@ -913,6 +938,23 @@ public class RoomSpecialTypes {
public THashSet getItemsOfType(Class extends HabboItem> type) {
THashSet items = new THashSet<>();
+
+ // Check pet trees collection for InteractionPetTree type
+ if (type == InteractionPetTree.class) {
+ synchronized (this.petTrees) {
+ items.addAll(this.petTrees.values());
+ }
+ return items;
+ }
+
+ // Check pet toys collection for InteractionPetToy type
+ if (type == InteractionPetToy.class) {
+ synchronized (this.petToys) {
+ items.addAll(this.petToys.values());
+ }
+ return items;
+ }
+
synchronized (this.undefined) {
for (HabboItem item : this.undefined.values()) {
if (item.getClass() == type)
@@ -959,6 +1001,8 @@ public class RoomSpecialTypes {
this.nests.clear();
this.petDrinks.clear();
this.petFoods.clear();
+ this.petToys.clear();
+ this.petTrees.clear();
this.rollers.clear();
this.wiredTriggers.clear();
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomUnitManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomUnitManager.java
index 90cced32..bfaa427a 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomUnitManager.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomUnitManager.java
@@ -6,6 +6,7 @@ import com.eu.habbo.habbohotel.bots.VisitorBot;
import com.eu.habbo.habbohotel.items.Item;
import com.eu.habbo.habbohotel.pets.Pet;
import com.eu.habbo.habbohotel.pets.PetManager;
+import com.eu.habbo.habbohotel.pets.PetVocalsType;
import com.eu.habbo.habbohotel.pets.RideablePet;
import com.eu.habbo.habbohotel.users.DanceType;
import com.eu.habbo.habbohotel.users.Habbo;
@@ -983,7 +984,7 @@ public class RoomUnitManager {
((RideablePet) pet).setRider(null);
}
- Emulator.getThreading().run(pet);
+ pet.run(); // Run synchronously to ensure DB is updated before returning pet to inventory
habbo.getInventory().getPetsComponent().addPet(pet);
habbo.getClient().sendResponse(new AddPetComposer(pet));
this.currentPets.remove(pet.getId());
@@ -1266,11 +1267,29 @@ public class RoomUnitManager {
// ==================== VISITOR BOT HANDLING ====================
/**
- * Handles Habbo entering the room (visitor bot notification).
+ * Handles Habbo entering the room (visitor bot notification and pet greeting).
*/
public void habboEntered(Habbo habbo) {
habbo.getRoomUnit().animateWalk = false;
+ // Have pets greet their owner
+ synchronized (this.currentPets) {
+ TIntObjectIterator petIterator = this.currentPets.iterator();
+ for (int i = this.currentPets.size(); i-- > 0; ) {
+ try {
+ petIterator.advance();
+ Pet pet = petIterator.value();
+ if (pet.getUserId() == habbo.getHabboInfo().getId()) {
+ // Pet sees its owner - greet them!
+ pet.say(pet.getPetData().randomVocal(PetVocalsType.GREET_OWNER));
+ pet.addHappiness(10);
+ }
+ } catch (Exception e) {
+ break;
+ }
+ }
+ }
+
synchronized (this.currentBots) {
if (habbo.getHabboInfo().getId() != this.room.getOwnerId()) {
return;
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomUnitStatus.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomUnitStatus.java
index 73191762..b64989b7 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomUnitStatus.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomUnitStatus.java
@@ -55,6 +55,11 @@ public enum RoomUnitStatus {
KICK("kck"),
WAG_TAIL("wag"),
DANCE("dan"),
+ RINGOFFIRE("rng"),
+ SWING("swg"),
+ HANG("hg"),
+ ROLL("rll"),
+ FLAT("flt"),
AMS("ams"),
SWIM("swm"),
TURN("trn"),
diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/pets/PetPickupEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/pets/PetPickupEvent.java
index 211375c3..3cce0a34 100644
--- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/pets/PetPickupEvent.java
+++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/pets/PetPickupEvent.java
@@ -37,7 +37,7 @@ public class PetPickupEvent extends MessageHandler {
}
pet.removeFromRoom();
- Emulator.getThreading().run(pet);
+ pet.run(); // Run synchronously to ensure DB is updated before returning pet to inventory
if (this.client.getHabbo().getHabboInfo().getId() == pet.getUserId()) {
this.client.sendResponse(new AddPetComposer(pet));
diff --git a/Emulator/src/main/java/com/eu/habbo/threading/runnables/PetEatAction.java b/Emulator/src/main/java/com/eu/habbo/threading/runnables/PetEatAction.java
index 4d7a4a96..acee6178 100644
--- a/Emulator/src/main/java/com/eu/habbo/threading/runnables/PetEatAction.java
+++ b/Emulator/src/main/java/com/eu/habbo/threading/runnables/PetEatAction.java
@@ -6,6 +6,7 @@ import com.eu.habbo.habbohotel.items.interactions.pets.InteractionPetFood;
import com.eu.habbo.habbohotel.pets.GnomePet;
import com.eu.habbo.habbohotel.pets.Pet;
import com.eu.habbo.habbohotel.pets.PetTasks;
+import com.eu.habbo.habbohotel.pets.PetVocalsType;
import com.eu.habbo.habbohotel.rooms.RoomUnitStatus;
import com.eu.habbo.messages.outgoing.rooms.items.RemoveFloorItemComposer;
import com.eu.habbo.messages.outgoing.rooms.users.RoomUserStatusComposer;
@@ -22,28 +23,45 @@ public class PetEatAction implements Runnable {
@Override
public void run() {
if (this.pet.getRoomUnit() != null && this.pet.getRoom() != null) {
- if (this.pet.levelHunger >= 20 && this.food != null && Integer.parseInt(this.food.getExtradata()) < this.food.getBaseItem().getStateCount()) {
- this.pet.addHunger(-20);
+ // Check if food still has portions left (state < stateCount means food remaining)
+ int currentState = 0;
+ try {
+ currentState = Integer.parseInt(this.food.getExtradata());
+ } catch (NumberFormatException e) {
+ currentState = 0;
+ }
+
+ if (this.pet.levelHunger >= 10 && this.food != null && currentState < this.food.getBaseItem().getStateCount()) {
+ // Say eating vocal on first bite
+ if (currentState == 0 || Emulator.getRandom().nextInt(3) == 0) {
+ this.pet.say(this.pet.getPetData().randomVocal(PetVocalsType.EATING));
+ }
+
+ // Faster eating: reduce 40 hunger per bite instead of 20
+ this.pet.addHunger(-40);
this.pet.setTask(PetTasks.EAT);
this.pet.getRoomUnit().setCanWalk(false);
- this.food.setExtradata(Integer.valueOf(this.food.getExtradata()) + 1 + "");
+ // Advance food state (each bite uses up a portion)
+ this.food.setExtradata((currentState + 1) + "");
this.pet.getRoom().updateItem(this.food);
if (this.pet instanceof GnomePet) {
if (this.pet.getPetData().getType() == 26) {
- AchievementManager.progressAchievement(Emulator.getGameEnvironment().getHabboManager().getHabbo(this.pet.getUserId()), Emulator.getGameEnvironment().getAchievementManager().getAchievement("GnomeFeeding"), 20);
+ AchievementManager.progressAchievement(Emulator.getGameEnvironment().getHabboManager().getHabbo(this.pet.getUserId()), Emulator.getGameEnvironment().getAchievementManager().getAchievement("GnomeFeeding"), 40);
} else {
- AchievementManager.progressAchievement(Emulator.getGameEnvironment().getHabboManager().getHabbo(this.pet.getUserId()), Emulator.getGameEnvironment().getAchievementManager().getAchievement("LeprechaunFeeding"), 20);
+ AchievementManager.progressAchievement(Emulator.getGameEnvironment().getHabboManager().getHabbo(this.pet.getUserId()), Emulator.getGameEnvironment().getAchievementManager().getAchievement("LeprechaunFeeding"), 40);
}
} else {
- AchievementManager.progressAchievement(Emulator.getGameEnvironment().getHabboManager().getHabbo(this.pet.getUserId()), Emulator.getGameEnvironment().getAchievementManager().getAchievement("PetFeeding"), 20);
+ AchievementManager.progressAchievement(Emulator.getGameEnvironment().getHabboManager().getHabbo(this.pet.getUserId()), Emulator.getGameEnvironment().getAchievementManager().getAchievement("PetFeeding"), 40);
}
- Emulator.getThreading().run(this, 1000);
+ // Faster eating: 500ms between bites instead of 1000ms
+ Emulator.getThreading().run(this, 500);
} else {
- if (this.food != null && Integer.parseInt(this.food.getExtradata()) == this.food.getBaseItem().getStateCount()) {
- Emulator.getThreading().run(new QueryDeleteHabboItem(this.food.getId()), 500);
+ // Food is empty - remove it
+ if (this.food != null && currentState >= this.food.getBaseItem().getStateCount()) {
+ Emulator.getThreading().run(new QueryDeleteHabboItem(this.food.getId()), 250);
if (this.pet.getRoom() != null) {
this.pet.getRoom().removeHabboItem(this.food);
this.pet.getRoom().sendComposer(new RemoveFloorItemComposer(this.food, true).compose());
diff --git a/Emulator/src/main/java/com/eu/habbo/threading/runnables/PetFollowHabbo.java b/Emulator/src/main/java/com/eu/habbo/threading/runnables/PetFollowHabbo.java
index b790916f..ddcfb649 100644
--- a/Emulator/src/main/java/com/eu/habbo/threading/runnables/PetFollowHabbo.java
+++ b/Emulator/src/main/java/com/eu/habbo/threading/runnables/PetFollowHabbo.java
@@ -3,6 +3,7 @@ package com.eu.habbo.threading.runnables;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.pets.Pet;
import com.eu.habbo.habbohotel.pets.PetTasks;
+import com.eu.habbo.habbohotel.pets.PetVocalsType;
import com.eu.habbo.habbohotel.rooms.RoomTile;
import com.eu.habbo.habbohotel.users.Habbo;
@@ -19,31 +20,81 @@ public class PetFollowHabbo implements Runnable {
@Override
public void run() {
- if (this.pet != null) {
- if (this.pet.getTask() != PetTasks.FOLLOW)
- return;
-
- if (this.habbo != null) {
- if (this.habbo.getRoomUnit() != null) {
- if (this.pet.getRoomUnit() != null) {
- RoomTile target = this.habbo.getHabboInfo().getCurrentRoom().getLayout().getTileInFront(this.habbo.getRoomUnit().getCurrentLocation(), Math.abs((this.habbo.getRoomUnit().getBodyRotation().getValue() + this.directionOffset + 4) % 8));
-
- if (target != null) {
- if (target.x < 0 || target.y < 0)
- target = this.habbo.getHabboInfo().getCurrentRoom().getLayout().getTileInFront(this.habbo.getRoomUnit().getCurrentLocation(), this.habbo.getRoomUnit().getBodyRotation().getValue());
-
- if (target.x >= 0 && target.y >= 0) {
- if (this.pet.getRoom().getLayout().tileWalkable(target.x, target.y)) {
- this.pet.getRoomUnit().setGoalLocation(target);
- this.pet.getRoomUnit().setCanWalk(true);
- this.pet.setTask(PetTasks.FOLLOW);
- }
- }
- Emulator.getThreading().run(this, 500);
- }
- }
- }
+ // Comprehensive null checks
+ if (this.pet == null || this.pet.getRoom() == null || this.pet.getRoomUnit() == null) {
+ return; // Stop following - pet or room is gone
+ }
+
+ // Check if task is any follow type
+ PetTasks task = this.pet.getTask();
+ if (task != PetTasks.FOLLOW && task != PetTasks.FOLLOW_LEFT && task != PetTasks.FOLLOW_RIGHT) {
+ return; // Task was changed, stop
+ }
+
+ // Check if habbo is still valid
+ if (this.habbo == null || this.habbo.getRoomUnit() == null) {
+ this.pet.setTask(PetTasks.FREE);
+ return; // Owner gone, stop following
+ }
+
+ // Check if habbo is still in the same room as the pet
+ if (this.habbo.getHabboInfo().getCurrentRoom() != this.pet.getRoom()) {
+ this.pet.setTask(PetTasks.FREE);
+ this.pet.say(this.pet.getPetData().randomVocal(PetVocalsType.GENERIC_SAD));
+ return;
+ }
+
+ // Calculate target position
+ RoomTile habboTile = this.habbo.getRoomUnit().getCurrentLocation();
+ if (habboTile == null) {
+ Emulator.getThreading().run(this, 500);
+ return;
+ }
+
+ int targetRotation = Math.abs((this.habbo.getRoomUnit().getBodyRotation().getValue()
+ + this.directionOffset + 4) % 8);
+
+ RoomTile target = this.pet.getRoom().getLayout().getTileInFront(habboTile, targetRotation);
+
+ // Validate target tile - try alternative positions if needed
+ if (target == null || target.x < 0 || target.y < 0
+ || !this.pet.getRoom().getLayout().tileWalkable(target.x, target.y)) {
+ // Try directly behind habbo
+ target = this.pet.getRoom().getLayout().getTileInFront(
+ habboTile,
+ (this.habbo.getRoomUnit().getBodyRotation().getValue() + 4) % 8
+ );
+ }
+
+ // Try other adjacent positions if still invalid
+ if (target == null || target.x < 0 || target.y < 0
+ || !this.pet.getRoom().getLayout().tileWalkable(target.x, target.y)) {
+ // Try to the left
+ target = this.pet.getRoom().getLayout().getTileInFront(
+ habboTile,
+ (this.habbo.getRoomUnit().getBodyRotation().getValue() + 2) % 8
+ );
+ }
+
+ if (target == null || target.x < 0 || target.y < 0
+ || !this.pet.getRoom().getLayout().tileWalkable(target.x, target.y)) {
+ // Try to the right
+ target = this.pet.getRoom().getLayout().getTileInFront(
+ habboTile,
+ (this.habbo.getRoomUnit().getBodyRotation().getValue() + 6) % 8
+ );
+ }
+
+ // If we found a valid target, move there
+ if (target != null && target.x >= 0 && target.y >= 0) {
+ if (this.pet.getRoom().getLayout().tileWalkable(target.x, target.y)) {
+ this.pet.getRoomUnit().setGoalLocation(target);
+ this.pet.getRoomUnit().setCanWalk(true);
}
}
+
+ // Continue following with slight randomization for natural behavior
+ int nextDelay = 400 + Emulator.getRandom().nextInt(200);
+ Emulator.getThreading().run(this, nextDelay);
}
}
diff --git a/Emulator/src/main/java/com/eu/habbo/threading/runnables/freeze/FreezeHandleSnowballExplosion.java b/Emulator/src/main/java/com/eu/habbo/threading/runnables/freeze/FreezeHandleSnowballExplosion.java
index 973234a6..c8ba4612 100644
--- a/Emulator/src/main/java/com/eu/habbo/threading/runnables/freeze/FreezeHandleSnowballExplosion.java
+++ b/Emulator/src/main/java/com/eu/habbo/threading/runnables/freeze/FreezeHandleSnowballExplosion.java
@@ -75,7 +75,8 @@ class FreezeHandleSnowballExplosion implements Runnable {
habbos.addAll(this.thrownData.room.getHabbosAt(freezeTile.getX(), freezeTile.getY()));
for (Habbo habbo : habbos) {
- if (habbo.getHabboInfo().getGamePlayer() != null && habbo.getHabboInfo().getGamePlayer() instanceof FreezeGamePlayer hPlayer) {
+ if (habbo.getHabboInfo().getGamePlayer() != null && habbo.getHabboInfo().getGamePlayer() instanceof FreezeGamePlayer) {
+ FreezeGamePlayer hPlayer = (FreezeGamePlayer) habbo.getHabboInfo().getGamePlayer();
if (!hPlayer.canGetFrozen())
continue;