@@ -97,6 +97,12 @@ contract TournamentGame is BaseGame, ReentrancyGuard {
9797
9898 }
9999
100+ /// @notice Reasons why a player might be replaced in a tournament.
101+ enum ReplacementReason {
102+ PLAYER_RETIRED,
103+ SKIN_OWNERSHIP_LOST
104+ }
105+
100106 //==============================================================//
101107 // STRUCTS //
102108 //==============================================================//
@@ -261,7 +267,10 @@ contract TournamentGame is BaseGame, ReentrancyGuard {
261267 );
262268 /// @notice Emitted when a player is replaced during tournament execution.
263269 event PlayerReplaced (
264- uint256 indexed tournamentId , uint32 indexed originalPlayerId , uint32 indexed replacementPlayerId , string reason
270+ uint256 indexed tournamentId ,
271+ uint32 indexed originalPlayerId ,
272+ uint32 indexed replacementPlayerId ,
273+ ReplacementReason reason
265274 );
266275 /// @notice Emitted when a tournament is auto-recovered due to blockhash expiration.
267276 event TournamentAutoRecovered (uint256 commitBlock , uint256 currentBlock , TournamentPhase phase );
@@ -335,16 +344,17 @@ contract TournamentGame is BaseGame, ReentrancyGuard {
335344 // Check retirement status (single external call)
336345 if (playerContract.isPlayerRetired (loadout.playerId)) revert PlayerIsRetired ();
337346
338- // Cache equipment requirements to avoid repeated external calls
347+ // Cache equipment requirements and skin registry to avoid repeated external calls
339348 IEquipmentRequirements equipmentReqs = playerContract.equipmentRequirements ();
349+ IPlayerSkinRegistry skinRegistry = playerContract.skinRegistry ();
340350
341- // Validate skin and equipment requirements via Player contract registries
342- try playerContract. skinRegistry () .validateSkinOwnership (loadout.skin, owner) {}
351+ // Validate skin and equipment requirements via cached registry reference
352+ try skinRegistry.validateSkinOwnership (loadout.skin, owner) {}
343353 catch {
344354 revert InvalidSkin ();
345355 }
346- try playerContract. skinRegistry () .validateSkinRequirements (loadout.skin, playerStats.attributes, equipmentReqs)
347- {} catch {
356+ try skinRegistry.validateSkinRequirements (loadout.skin, playerStats.attributes, equipmentReqs) {}
357+ catch {
348358 revert InvalidLoadout ();
349359 }
350360
@@ -803,23 +813,24 @@ contract TournamentGame is BaseGame, ReentrancyGuard {
803813 // Check if real player needs replacement
804814 if (! _isDefaultPlayerId (regPlayer.playerId)) {
805815 bool shouldReplace = false ;
806- string memory reason = "" ;
816+ ReplacementReason reason;
807817
808818 // Check if player is retired
809819 if (playerContract.isPlayerRetired (regPlayer.playerId)) {
810820 shouldReplace = true ;
811- reason = " PLAYER_RETIRED " ;
821+ reason = ReplacementReason. PLAYER_RETIRED;
812822 }
813823
814824 // Check if player still owns their skin
815825 if (! shouldReplace) {
816- address playerOwner = playerContract.getPlayerOwner (regPlayer.playerId);
817- try playerContract.skinRegistry ().validateSkinOwnership (regPlayer.loadout.skin, playerOwner) {
826+ try playerContract.skinRegistry ().validateSkinOwnership (
827+ regPlayer.loadout.skin, playerContract.getPlayerOwner (regPlayer.playerId)
828+ ) {
818829 // Skin validation passed
819830 } catch {
820831 // Skin validation failed - player no longer owns the skin
821832 shouldReplace = true ;
822- reason = " SKIN_OWNERSHIP_LOST " ;
833+ reason = ReplacementReason. SKIN_OWNERSHIP_LOST;
823834 }
824835 }
825836
@@ -1003,7 +1014,7 @@ contract TournamentGame is BaseGame, ReentrancyGuard {
10031014
10041015 // Award ratings and distribute rewards
10051016 _awardTournamentRatings (tournament, eliminatedByRound);
1006- _distributeRewards (tournament, eliminatedByRound);
1017+ _distributeRewards (tournament, eliminatedByRound, randomness );
10071018
10081019 // Extract participant IDs for event emission
10091020 uint256 participantCount = participants.length ;
@@ -1104,15 +1115,17 @@ contract TournamentGame is BaseGame, ReentrancyGuard {
11041115 }
11051116
11061117 /// @notice Distributes rewards to tournament winners.
1107- function _distributeRewards (Tournament storage tournament , uint32 [] memory eliminatedByRound ) private {
1118+ function _distributeRewards (Tournament storage tournament , uint32 [] memory eliminatedByRound , uint256 randomness )
1119+ private
1120+ {
11081121 // Reward champion
11091122 if (_getFighterType (tournament.championId) == Fighter.FighterType.PLAYER) {
1110- _distributeReward (tournament.id, tournament.championId, winnerRewards);
1123+ _distributeReward (tournament.id, tournament.championId, winnerRewards, randomness );
11111124 }
11121125
11131126 // Reward runner-up
11141127 if (_getFighterType (tournament.runnerUpId) == Fighter.FighterType.PLAYER) {
1115- _distributeReward (tournament.id, tournament.runnerUpId, runnerUpRewards);
1128+ _distributeReward (tournament.id, tournament.runnerUpId, runnerUpRewards, randomness );
11161129 }
11171130
11181131 // Reward 3rd-4th place (semi-final losers)
@@ -1122,16 +1135,19 @@ contract TournamentGame is BaseGame, ReentrancyGuard {
11221135 for (uint256 i = semiFinalistStart; i < semiFinalistEnd; i++ ) {
11231136 uint32 playerId = eliminatedByRound[i];
11241137 if (_getFighterType (playerId) == Fighter.FighterType.PLAYER) {
1125- _distributeReward (tournament.id, playerId, thirdFourthRewards);
1138+ _distributeReward (tournament.id, playerId, thirdFourthRewards, randomness );
11261139 }
11271140 }
11281141 }
11291142
11301143 /// @notice Distributes a single reward based on configured percentages.
1131- function _distributeReward (uint256 tournamentId , uint32 playerId , IPlayerTickets.RewardConfig memory config )
1132- private
1133- {
1134- uint256 random = uint256 (keccak256 (abi.encodePacked (tournamentId, playerId, block .timestamp )));
1144+ function _distributeReward (
1145+ uint256 tournamentId ,
1146+ uint32 playerId ,
1147+ IPlayerTickets.RewardConfig memory config ,
1148+ uint256 randomness
1149+ ) private {
1150+ uint256 random = uint256 (keccak256 (abi.encodePacked (randomness, tournamentId, playerId)));
11351151 uint256 roll = random % 10000 ; // 0-9999 for percentage precision
11361152
11371153 IPlayerTickets.RewardType rewardType;
@@ -1164,20 +1180,30 @@ contract TournamentGame is BaseGame, ReentrancyGuard {
11641180 ) {
11651181 rewardType = IPlayerTickets.RewardType.ARMOR_SPECIALIZATION_TICKET;
11661182 ticketId = playerTickets.ARMOR_SPECIALIZATION_TICKET ();
1167- } else {
1183+ } else if (
1184+ roll
1185+ < config.attributeSwapPercent + config.createPlayerPercent + config.playerSlotPercent
1186+ + config.weaponSpecPercent + config.armorSpecPercent + config.duelTicketPercent
1187+ ) {
11681188 rewardType = IPlayerTickets.RewardType.DUEL_TICKET;
11691189 ticketId = playerTickets.DUEL_TICKET ();
1190+ } else {
1191+ // Must be name change ticket (nameChangePercent is the remainder)
1192+ rewardType = IPlayerTickets.RewardType.NAME_CHANGE_TICKET;
11701193 }
11711194
1172- // Get owner once and handle both reward types
1173- address owner = playerContract.getPlayerOwner (playerId);
1174-
1195+ // Handle all reward types - no separate owner variable
11751196 if (rewardType == IPlayerTickets.RewardType.ATTRIBUTE_SWAP) {
11761197 // Award attribute swap charge directly to player
1177- playerContract.awardAttributeSwap (owner);
1198+ playerContract.awardAttributeSwap (playerContract.getPlayerOwner (playerId));
1199+ } else if (rewardType == IPlayerTickets.RewardType.NAME_CHANGE_TICKET) {
1200+ // Mint name change NFT with VRF randomness
1201+ ticketId = playerTickets.mintNameChangeNFT (playerContract.getPlayerOwner (playerId), random);
1202+ emit RewardDistributed (tournamentId, playerId, rewardType, ticketId);
1203+ return ; // Early return for name change tickets
11781204 } else if (ticketId > 0 ) {
1179- // Mint ticket for other reward types
1180- playerTickets.mintFungibleTicket (owner , ticketId, 1 );
1205+ // Mint fungible ticket for other reward types
1206+ playerTickets.mintFungibleTicket (playerContract. getPlayerOwner (playerId) , ticketId, 1 );
11811207 }
11821208
11831209 emit RewardDistributed (tournamentId, playerId, rewardType, ticketId);
@@ -1255,7 +1281,8 @@ contract TournamentGame is BaseGame, ReentrancyGuard {
12551281 returns (ActiveParticipant[] memory )
12561282 {
12571283 // True Fisher-Yates shuffle - clean and simple!
1258- for (uint256 i = participants.length - 1 ; i > 0 ; i-- ) {
1284+ uint256 participantCount = participants.length ;
1285+ for (uint256 i = participantCount - 1 ; i > 0 ; i-- ) {
12591286 seed = uint256 (keccak256 (abi.encodePacked (seed, i)));
12601287 uint256 j = seed % (i + 1 );
12611288
0 commit comments