diff --git a/gameserver/src/main/java/brainwine/gameserver/GameServer.java b/gameserver/src/main/java/brainwine/gameserver/GameServer.java index e8b1ffe0..9264c02c 100644 --- a/gameserver/src/main/java/brainwine/gameserver/GameServer.java +++ b/gameserver/src/main/java/brainwine/gameserver/GameServer.java @@ -7,6 +7,7 @@ import brainwine.gameserver.androidshop.AndroidShop; import brainwine.gameserver.androidshop.AndroidShopPerIpHistory; +import brainwine.gameserver.minigame.GroupDungeon; import brainwine.gameserver.scrapmarket.ScrapMarket; import brainwine.gameserver.anticheat.AnticheatManager; import brainwine.gameserver.chat.ProfanityManager; @@ -73,6 +74,7 @@ public GameServer() { EntityManager.loadEntitySpawns(); GrowthManager.loadGrowthData(); Pandora.loadConfig(); + GroupDungeon.loadConfig(); Quests.loadQuests(); dailyRewardManager = new DailyRewardManager(); AndroidShop.getInstance().loadShopData(); diff --git a/gameserver/src/main/java/brainwine/gameserver/item/ItemUseType.java b/gameserver/src/main/java/brainwine/gameserver/item/ItemUseType.java index 55fb797e..efc48a9a 100644 --- a/gameserver/src/main/java/brainwine/gameserver/item/ItemUseType.java +++ b/gameserver/src/main/java/brainwine/gameserver/item/ItemUseType.java @@ -69,6 +69,7 @@ public enum ItemUseType { PET, PILE(new PileInteraction()), PLENTY, + POTENCY_BUMP, PROTECTED, PUBLIC, REVENANT_DISH, diff --git a/gameserver/src/main/java/brainwine/gameserver/item/interactions/MinigameInteraction.java b/gameserver/src/main/java/brainwine/gameserver/item/interactions/MinigameInteraction.java index 6691fcd2..3b5a5924 100644 --- a/gameserver/src/main/java/brainwine/gameserver/item/interactions/MinigameInteraction.java +++ b/gameserver/src/main/java/brainwine/gameserver/item/interactions/MinigameInteraction.java @@ -5,6 +5,7 @@ import brainwine.gameserver.entity.Entity; import brainwine.gameserver.item.Item; import brainwine.gameserver.item.Layer; +import brainwine.gameserver.minigame.GroupDungeon; import brainwine.gameserver.minigame.Minigame; import brainwine.gameserver.minigame.Pandora; import brainwine.gameserver.player.Player; @@ -57,6 +58,18 @@ public void interact(Zone zone, Entity entity, int x, int y, Layer layer, Item i player.notify("Sorry, custom minigames are not supported yet."); return; } + + String startCondition = MapHelper.getString(configMap, "start_condition"); + if(startCondition != null) { + switch(startCondition) { + case "dungeon-raided": + case "dungeon-is-raided": + if(metaBlock != null && zone.isDungeonIntact(metaBlock.getStringProperty("@"))) { + player.notify("You must destroy all the enemy protectors first."); + return; + } + } + } Map startDialog = MapHelper.getMap(configMap, "start_dialog"); @@ -82,6 +95,9 @@ public void interact(Zone zone, Entity entity, int x, int y, Layer layer, Item i case "pandora": minigame = new Pandora(zone, player, x, y); break; + case "group-dungeon": + minigame = new GroupDungeon(zone, player, x, y); + break; default: player.notify(String.format("Sorry, minigame type '%s' is not supported.", type)); return; diff --git a/gameserver/src/main/java/brainwine/gameserver/minigame/GroupDungeon.java b/gameserver/src/main/java/brainwine/gameserver/minigame/GroupDungeon.java new file mode 100644 index 00000000..b98c9707 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/minigame/GroupDungeon.java @@ -0,0 +1,463 @@ +package brainwine.gameserver.minigame; + +import brainwine.gameserver.Fake; +import brainwine.gameserver.GameServer; +import brainwine.gameserver.dialog.Dialog; +import brainwine.gameserver.dialog.DialogSection; +import brainwine.gameserver.entity.Entity; +import brainwine.gameserver.entity.EntityAttack; +import brainwine.gameserver.entity.npc.Npc; +import brainwine.gameserver.item.Item; +import brainwine.gameserver.item.ItemRegistry; +import brainwine.gameserver.item.ItemUseType; +import brainwine.gameserver.item.Layer; +import brainwine.gameserver.loot.Loot; +import brainwine.gameserver.player.NotificationType; +import brainwine.gameserver.player.Player; +import brainwine.gameserver.player.TradeSession; +import brainwine.gameserver.resource.ResourceFinder; +import brainwine.gameserver.util.MapHelper; +import brainwine.gameserver.util.WeightedMap; +import brainwine.gameserver.zone.Block; +import brainwine.gameserver.zone.MetaBlock; +import brainwine.gameserver.zone.Zone; +import brainwine.shared.JsonHelper; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static brainwine.shared.LogMarkers.SERVER_MARKER; + +public class GroupDungeon extends Minigame { + private static final Logger logger = LogManager.getLogger(); + private static GroupDungeonConfig config = new GroupDungeonConfig(); + private static Set allSpeakerItems = new HashSet<>(config.getAllSpeakerItems()); + private static Set allDoorItems = new HashSet<>(config.getAllDoorItems()); + private static final String sirenOpenId = "mechanical/siren-open"; + + int potencyLevel = 0; + Map potencyBumps = new HashMap<>(); + List speakers = new ArrayList<>(); + List doors = new ArrayList<>(); + int totalWaves; + int currentWave; + int enemiesLeftInWave = 0; + int enemyInterval; + long lastSpawnedAt; + private final List spawns = new ArrayList<>(); + boolean raidStarted = false; + int prefabLeft; + int prefabRight; + int prefabTop; + int prefabBottom; + + public static void loadConfig() { + logger.info(SERVER_MARKER, "Loading group dungeon configuration ..."); + + try { + GroupDungeon.config = JsonHelper.readValue(ResourceFinder.getResourceUrl("group-dungeon.json"), GroupDungeonConfig.class); + GroupDungeon.allSpeakerItems = new HashSet<>(config.getAllSpeakerItems()); + GroupDungeon.allDoorItems = new HashSet<>(config.getAllDoorItems()); + } catch(Exception e) { + logger.error(SERVER_MARKER, "Failed to load group dungeon config", e); + } + } + + public static GroupDungeonConfig getConfig() { + return config; + } + + public GroupDungeon(Zone zone, Player initiator, int x, int y) { + super(zone, initiator, x, y); + } + + @Override + public void tick(float deltaTime) { + super.tick(deltaTime); + long now = System.currentTimeMillis(); + + if(!raidStarted) { + if(now >= startedAt + config.getGracePeriod()) { + raidStarted = true; + this.currentWave++; + enemiesLeftInWave = getTotalEnemiesInWave(currentWave); + setDoorsOpen(false); + lastSpawnedAt = System.currentTimeMillis(); + enemyInterval = (int) (500 + Math.random() * 2000); + for(Player player : zone.getPlayers()) { + if(participants.containsKey(player)) { + player.notify("Oh no, the doors are shut! Now your only way out is to end these pesky brains."); + } else { + player.notify(String.format("The group dungeon at %s has locked down. You can't help raid it anymore.", zone.getReadableCoordinates(x, y))); + } + } + } + return; + } + + final int currentWave = this.currentWave; + + if(enemiesLeftInWave > 0 && now > enemyInterval + lastSpawnedAt) { + // If there are still enemies left to spawn this wave, and it is time to spawn another one + lastSpawnedAt = System.currentTimeMillis(); + enemyInterval = (int)(500 + Math.random() * 2000); + // Pick the enemy table that is only as hard as the current wave or easier + WeightedMap currentEnemyTable = config.getEnemies().get(config.getEnemies().keySet().stream().filter(wave -> currentWave >= wave).max(Integer::compareTo).orElse(1)); + MetaBlock speaker = !speakers.isEmpty() ? Fake.pickFromList(speakers) : null; + String entityType = currentEnemyTable.next(); + + Npc npc = null; + if(speaker != null) { + npc = zone.spawnEntity(entityType, speaker.getX(), speaker.getY()); + } else { + // Make multiple attempts to spawn a raid enemy + for(int attempt = 0; attempt < 20; attempt++) { + int x = prefabLeft + (int)Math.floor(Math.random() * (prefabRight - prefabLeft)); + int y = prefabTop + (int)Math.floor(Math.random() * (prefabBottom - prefabTop)); + + if(!zone.isBlockSolid(x, y)) { + npc = zone.spawnEntity(entityType, x, y); + break; + } + } + + if(npc == null) { + npc = zone.spawnEntity(entityType, this.x, this.y); + } + } + if(npc == null) { + logger.error("Couldn't spawn entity {}!", entityType); + } else { + npc.setMinigame(this); + spawns.add(npc); + enemiesLeftInWave--; + } + } + + if(spawns.isEmpty()) { + // Remove stale speakers + for(int i = 0; i < speakers.size(); i++) { + MetaBlock current = zone.getMetaBlock(speakers.get(i).getX(), speakers.get(i).getY()); + if(current == null || !allSpeakerItems.contains(current.getItem())) { + speakers.remove(i); + i--; + } + } + + if(currentWave >= totalWaves) { + // If all speakers have been destroyed, complete + complete(); + return; + } + + if(enemiesLeftInWave == 0) { + // If the wave is over, set up for the next wave + if(currentWave < totalWaves) { + notifyParticipants(String.format("Wave %d is starting!", currentWave + 1)); + } + if(!speakers.isEmpty()) { + MetaBlock speakerToRemove = Fake.pickFromList(speakers); + Item speakerItem = speakerToRemove.getItem(); + speakers.remove(speakerToRemove); + zone.updateBlock(speakerToRemove.getX(), speakerToRemove.getY(), Layer.FRONT, Item.AIR); + zone.spawnEffect(speakerToRemove.getX() + speakerItem.getBlockWidth() / 2.0f - 0.5f, speakerToRemove.getY() - speakerItem.getBlockHeight() / 2.0f + 0.5f, "bomb-electric", 1); + } + this.currentWave++; + enemiesLeftInWave = getTotalEnemiesInWave(currentWave + 1); + lastSpawnedAt = System.currentTimeMillis(); + enemyInterval = (int)(2000 + Math.random() * 8000); + } + } + } + + private int getTotalEnemiesInWave(int wave) { + return (int)(totalWaves + (potencyLevel * totalWaves * wave * wave) / 10.0); + } + + @Override + public void onInteract(Player player) { + addParticipant(player); + + // Increase potency if the minigame hasn't started yet + if(!raidStarted) { + boolean totalBumpsLimited = config.getMaxTotalBumps() > 0; + int totalBumpsLeft = config.getMaxTotalBumps() - potencyLevel; + if(totalBumpsLimited && totalBumpsLeft <= 0) { + player.notify("Sorry, no more players can participate in this raid."); + return; + } + if(!potencyBumps.containsKey(player.getDocumentId()) || player.isGodMode()) { + potencyBumps.put(player.getDocumentId(), 1); + zone.notifyPlayers(String.format("%s increased the group dungeon's potency level to %s!", player.getName(), ++potencyLevel), NotificationType.PEER_ACCOMPLISHMENT); + } else { + boolean playerBumpsLimited = config.getMaxPlayerBumps() > 0; + int playerBumpsLeft = config.getMaxPlayerBumps() - potencyBumps.getOrDefault(player.getDocumentId(), 0); + boolean bumpsLimited = totalBumpsLimited || playerBumpsLimited; + int bumpsLeft = totalBumpsLimited && playerBumpsLimited ? Math.min(totalBumpsLeft, playerBumpsLeft) : totalBumpsLimited ? totalBumpsLeft : playerBumpsLeft; + if(bumpsLimited && bumpsLeft <= 0) { + player.notify("Sorry, you can't make this raid harder anymore. Maybe ask more players to join?"); + return; + } + + Block blockInteractingWith = zone.getBlock(x, y); + Item interactingWith = blockInteractingWith != null ? blockInteractingWith.getFrontItem() : Item.AIR; + List whistles = new ArrayList<>(); + try { + if(interactingWith.getUse(ItemUseType.POTENCY_BUMP) instanceof List) { + ((List) interactingWith.getUse(ItemUseType.POTENCY_BUMP)).stream() + .map(ItemRegistry::getItem) + .filter(item -> !item.isAir()) + .forEach(whistles::add); + } + } catch (Exception e) { + logger.error("Error while listing the potency bump items", e); + } + if(whistles.isEmpty()) { + player.notify("Don't know what items you can use to bump potency.", NotificationType.SYSTEM); + return; + } + List whistleCounts = whistles.stream().map(item -> player.getInventory().getQuantity(item)).collect(Collectors.toList()); + if(!whistleCounts.stream().anyMatch(x -> x != 0)) { + player.notify("You need whistles to increase the chaos level more. Maybe invite more players to this dungeon instead."); + return; + } + Dialog dialog = new Dialog().setTitle("Group Dungeon Chaos Level"); + dialog.addSection(new DialogSection().setText("You can increase this dungeon's chaos level even more using whistles.")); + for(int i = 0; i < whistleCounts.size(); i++) { + if(whistleCounts.get(i) > 0) { + Item whistle = whistles.get(i); + int allowed = Math.max(1, bumpsLeft / (int)Math.round(whistle.getPower())); + dialog.addSection(TradeSession.Dialogs.createQuantitySelector(whistle.getId(), allowed, 1, true).setTitle("Use how many " + whistle.getFancyTitle() + "? Increases level by " + whistle.getPower() + ".")); + } + } + + player.showDialog(dialog, ans -> { + if(ans.length >= 1 && "cancel".equals(ans[0])) { + return; + } + int ansI = 0; + int bumps = 0; + Map used = new HashMap<>(); + try { + for(DialogSection section : dialog.getSections()) { + if(section.getInput() != null) { + String itemId = section.getInput().getKey(); + if(ansI >= ans.length) { + player.notify("Bad input. Too few choices submitted."); + return; + } + Object val = ans[ansI++]; + if(!(val instanceof String)) { + player.notify("Bad input type."); + return; + } + int qty = Integer.parseInt((String)val); + Item item = ItemRegistry.getItem(itemId); + if(!player.getInventory().hasItem(item, qty)) { + player.notify("You don't have that many " + item.getFancyTitle() + " anymore."); + return; + } + bumps = Math.min(bumpsLeft, qty * (int)Math.round(item.getPower())); + used.put(item, Math.max(0, Math.min(qty, (bumpsLeft - bumps) / (int)Math.round(item.getPower())))); + } + if(bumps >= bumpsLeft) break; + } + // Validation complete, now remove the whistles and increase the potency level + potencyLevel += bumps; + potencyBumps.put(player.getDocumentId(), potencyBumps.getOrDefault(player.getDocumentId(), 0) + 1); + for(Map.Entry usedItem : used.entrySet()) { + player.getInventory().removeItem(usedItem.getKey(), usedItem.getValue(), true); + } + zone.notifyPlayers(String.format("%s increased the group dungeon's potency level to %s!", player.getName(), potencyLevel), NotificationType.PEER_ACCOMPLISHMENT); + } catch (Exception e) { + player.notify("Error while parsing your input."); + logger.error("Error while handling potency bump dialog answers", e); + } + }); + } + } + } + + @Override + protected void onStart() { + // Validations that don't interact with the world. + if(config.getEnemies().isEmpty()) { + notifyParticipants("Don't know what to spawn! Ending the raid now."); + finish(); + return; + } + + // Validations that do interact with the world + MetaBlock siren = zone.getMetaBlock(x, y); + if(siren == null) { + initiator.notify("Siren metadata not found"); + finish(); + return; + } + + String dungeonId = siren.getStringProperty("@"); + if(dungeonId == null) { + initiator.notify("Siren doesn't appear to be inside a dungeon."); + finish(); + return; + } + + // Read prefab size from the siren's meta-block + Object maybePrefab = siren.getProperty("pre"); + if(maybePrefab instanceof Map) { + Map prefab = (Map)maybePrefab; + prefabTop = MapHelper.getInt(prefab, "t"); + prefabBottom = MapHelper.getInt(prefab, "b"); + prefabLeft = MapHelper.getInt(prefab, "l"); + prefabRight = MapHelper.getInt(prefab, "r"); + } + + // Index meta-blocks in this dungeon + for(MetaBlock mb : zone.getMetaBlocks()) { + if(mb != siren && dungeonId.equals(mb.getStringProperty("@"))) { + if(allSpeakerItems.contains(mb.getItem())) { + speakers.add(mb); + } else if(allDoorItems.contains(mb.getItem())) { + doors.add(mb); + } + } + } + + // Compute the total number of waves needed + if(allSpeakerItems.isEmpty()) { + // Assume roughly each 5 by 5 square is a speaker + totalWaves = Math.max(3, Math.abs(prefabRight - prefabLeft) * Math.abs(prefabBottom - prefabTop) / 100); + } else { + if(speakers.size() < 3) { + initiator.notify("You can't raid this dungeon anymore because it has been tampered with."); + finish(); + return; + } + + totalWaves = speakers.size(); + } + + zone.updateBlock(x, y, Layer.FRONT, ItemRegistry.getItem(sirenOpenId)); + + // Everything went well + zone.spawnEffect(x, y, "match start", 1); + zone.updateBlock(x, y, Layer.FRONT, sirenOpenId, 1); + potencyBumps.put(initiator.getDocumentId(), 1); + potencyLevel++; + + // Notify all players in the zone + for(Player player : zone.getPlayers()) { + player.notifyProfile(String.format("%s is raiding a Group Dungeon at %s! Join them before the gates close.", initiator.getName(), zone.getReadableCoordinates(x, y)), String.format("Tap on and/or use whistles on its siren in the next %d seconds to build chaos!", config.getGracePeriod() / 1000)); + } + } + + @Override + protected void onFinish() { + setDoorsOpen(true); + // Kill all spawned entities + for(Npc entity : spawns) { + entity.setMinigame(null); + entity.setHealth(0.0F); + } + } + + protected void complete() { + finish(); + double luckMultiplier = Math.min(10.0, potencyLevel); + int baseLuck = Math.min(12, participants.size() * 4); + int position = 0; + + // Give out rewards + Item sirenOpen = ItemRegistry.getItem(sirenOpenId); + String[] rewardLootCategories = sirenOpen.getLootCategories(); + for(Participant participant : leaderboard) { + if(participant.isParticipating()) { + int luck = (int)(Math.max(1, baseLuck - position * 4) * luckMultiplier); + Player player = participant.getPlayer(); + + Loot loot = GameServer.getInstance().getLootManager().getRandomLoot(luck, zone.getBiome(), player.getInventory().getWardrobe(), rewardLootCategories); + + if(loot != null) { + player.awardLoot(loot, String.format("You won %s place!", ordinalizeNumber(position + 1))); + } else { + player.notify("Sorry, we couldn't find a suitable reward for you."); + } + } + + position++; + } + + // Broadcast leader's score + zone.notifyPlayers(String.format("Dungeon has been raided! %s showed mastery with %s!", currentLeader.getPlayer().getName(), describeScore(currentLeader.getScore()))); + + // Explode the siren + zone.updateBlock(x, y, Layer.FRONT, Item.AIR); + zone.spawnEffect(x + sirenOpen.getBlockWidth() / 2.0f - 0.5f, y - sirenOpen.getBlockHeight() / 2.0f + 0.5f, "bomb-electric", 2); + } + + protected void setDoorsOpen(boolean open) { + Map toLookFor = new HashMap<>(); + for(GroupDungeonConfig.DoorState doorState: config.getDoors()) { + if(open) { + // closed to open + toLookFor.put(doorState.getClosed(), doorState.getOpen()); + } else { + // open to closed + toLookFor.put(doorState.getOpen(), doorState.getClosed()); + } + } + for(int i = 0; i < doors.size(); i++) { + MetaBlock door = doors.get(i); + Block block = zone.getBlock(door.getX(), door.getY()); + if(block != null) { + GroupDungeonConfig.BlockState wantedState = toLookFor.get( + new GroupDungeonConfig.BlockState( + block.getFrontItem(), + block.getFrontMod(), + door.getMetadata() + ) + ); + if(wantedState != null) { + int currentMod = block.getFrontMod(); + if(wantedState.getItem() != block.getFrontItem() || wantedState.getMod() != currentMod) { + Map newMetadata = new HashMap<>(door.getMetadata()); + if(wantedState.getMetadata() != null) { + newMetadata.putAll(wantedState.getMetadata()); + } + zone.updateBlock(door.getX(), door.getY(), Layer.FRONT, wantedState.getItem(), wantedState.getMod(), null, newMetadata); + zone.spawnEffect(door.getX() + block.getFrontItem().getBlockWidth() / 2.0f - 0.5f, door.getY(), "steam", 4); + } + } + } + } + } + + public void entityKilled(Entity entity, EntityAttack cause) { + spawns.remove(entity); + } + + public void entityAttacked(Entity entity, EntityAttack attack, float damage) { + // Do nothing if entity is not a wave enemy + if(!spawns.contains(entity)) { + return; + } + + Entity attacker = attack.getAttacker(); + + // Check if attacker is present + if(attacker == null || !attacker.isPlayer()) { + return; + }; + + Player player = (Player)attacker; + Participant participant = addParticipant(player); + participant.incrementScore(damage); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/minigame/GroupDungeonConfig.java b/gameserver/src/main/java/brainwine/gameserver/minigame/GroupDungeonConfig.java new file mode 100644 index 00000000..aa567f14 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/minigame/GroupDungeonConfig.java @@ -0,0 +1,178 @@ +package brainwine.gameserver.minigame; + +import brainwine.gameserver.entity.EntityRegistry; +import brainwine.gameserver.item.Item; +import brainwine.gameserver.item.ItemRegistry; +import brainwine.gameserver.util.MapHelper; +import brainwine.gameserver.util.WeightedMap; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonSetter; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class GroupDungeonConfig { + private static final Logger logger = LogManager.getLogger(); + + private long gracePeriod = 120000; + private int maxPlayerBumps = 4; + private int maxTotalBumps = 0; + private List doors = Arrays.asList( + new DoorState( + new BlockState("mechanical/door-beefy", 1, null), + new BlockState("mechanical/door-beefy", 0, null) + ) + ); + private List speakers = Arrays.asList( + new BlockState("mechanical/speaker", 0, null) + ); + + private Map> enemies = MapHelper.map(1, new WeightedMap<>(MapHelper.map( + String.class, Double.class, + "brains/small-minion", 15.0, + "brains/medium-minion", 2.0, + "brains/medium-dire-minion", 1.0 + ))); + + public long getGracePeriod() { + return gracePeriod; + } + + public int getMaxPlayerBumps() { + return maxPlayerBumps; + } + + public int getMaxTotalBumps() { + return maxTotalBumps; + } + + @JsonSetter + public void setEnemies(Map> enemies) { + if(enemies.isEmpty()) { + throw new IllegalArgumentException("No enemy tables for the group dungeon are configured."); + } + + Map> newEnemies = new HashMap<>(); + + for(Map.Entry> waveEntry : enemies.entrySet()) { + if(waveEntry.getKey() <= 0) continue; + Map original = enemies.get(waveEntry.getKey()); + Map filtered = original.entrySet().stream() + .filter(ent -> EntityRegistry.getEntityConfig(ent.getKey()) != null + && ent.getValue() != null && ent.getValue() > 0.0 + ).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + if(filtered.isEmpty()) { + throw new IllegalArgumentException(String.format("None of the entity IDs in the enemy table for the group dungeon wave %d are valid.", waveEntry.getKey())); + } + newEnemies.put(waveEntry.getKey(), new WeightedMap(filtered)); + } + + if(!newEnemies.isEmpty()) { + // Make sure the first wave config is present + if(!this.enemies.containsKey(1)) { + int minKey = newEnemies.keySet().stream().min(Integer::compareTo).orElse(1); + newEnemies.put(1, newEnemies.get(minKey)); + } + this.enemies = newEnemies; + } else { + logger.warn("None of the group dungeon waves are positive - not configuring wave enemies."); + } + } + + public Map> getEnemies() { + return enemies; + } + + public List getDoors() { + return doors; + } + + public List getSpeakers() { + return speakers; + } + + @JsonIgnore + public List getAllDoorItems() { + return doors.stream() + .flatMap(door -> Stream.of(door.getOpen().getItem(), door.getClosed().getItem())) + .collect(Collectors.toList()); + } + + @JsonIgnore + public List getAllSpeakerItems() { + return speakers.stream().map(BlockState::getItem).collect(Collectors.toList()); + } + + public static class BlockState { + public Item item; + public int mod; + public Map metadata; + + public BlockState(@JsonSetter String itemId, @JsonSetter int mod, @JsonSetter Map metadata) { + this(ItemRegistry.getItem(itemId), mod, metadata); + } + + public BlockState(Item item, int mod, Map metadata) { + if(item == null) { + throw new IllegalArgumentException("Item null not found."); + } + + if(item.isAir()) { + throw new IllegalArgumentException("Item " + item.getId() + " not found."); + } + + this.item = item; + this.mod = mod; + this.metadata = metadata; + } + + public Item getItem() { + return item; + } + + public int getMod() { + return mod; + } + + public Map getMetadata() { + return metadata; + } + + @Override + public boolean equals(Object o) { + if(!(o instanceof BlockState)) return false; + BlockState that = (BlockState)o; + return mod == that.mod && Objects.equals(item, that.item); + } + + @Override + public int hashCode() { + return Objects.hash(item, mod); + } + } + + public static class DoorState { + private BlockState open; + private BlockState closed; + + public DoorState(@JsonSetter BlockState open, @JsonSetter BlockState closed) { + this.open = open; + this.closed = closed; + } + + public BlockState getOpen() { + return open; + } + + public BlockState getClosed() { + return closed; + } + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/player/TradeSession.java b/gameserver/src/main/java/brainwine/gameserver/player/TradeSession.java index 57e9bbb0..1e850f80 100644 --- a/gameserver/src/main/java/brainwine/gameserver/player/TradeSession.java +++ b/gameserver/src/main/java/brainwine/gameserver/player/TradeSession.java @@ -558,16 +558,23 @@ public static DialogSection createQuantitySelector(int maxQuantity) { } public static DialogSection createQuantitySelector(int maxQuantity, int unit) { + return createQuantitySelector("quantity", maxQuantity, unit, false); + } + + public static DialogSection createQuantitySelector(String key, int maxQuantity, int unit, boolean allowZero) { // Get quantity options that are available to the player List quantityOptions = ITEM_QUANTITY_OPTIONS.stream() .filter(quantity -> Integer.parseInt(quantity) * unit <= maxQuantity) .map(quantity -> Integer.toString(Integer.parseInt(quantity) * unit)) .collect(Collectors.toList()); + if(allowZero) { + quantityOptions.add(0, "0"); + } return new DialogSection() .setInput(new DialogSelectInput() .setOptions(quantityOptions) - .setKey("quantity")); + .setKey(key != null ? key : "quantity")); } public static DialogSection createQuantitySelector(Player offerer, Item item) { diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java b/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java index c24f2b7c..20d8a922 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java @@ -22,6 +22,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import brainwine.gameserver.minigame.GroupDungeon; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; @@ -965,6 +966,9 @@ public void placePrefab(Prefab prefab, int x, int y, Random random, boolean mirr } }); + Set allGroupDungeonItems = new HashSet<>(); + allGroupDungeonItems.addAll(GroupDungeon.getConfig().getAllDoorItems()); + allGroupDungeonItems.addAll(GroupDungeon.getConfig().getAllSpeakerItems()); boolean[] ruinMask = new boolean[width]; for(int j = 0; j < height; j++) { for(int i = 0; i < width; i++) { @@ -1072,6 +1076,13 @@ public void placePrefab(Prefab prefab, int x, int y, Random random, boolean mirr entityManager.updateRevenantDish(x, y, true); } + if(dungeonId != null && (frontItem.getUse(ItemUseType.MINIGAME) instanceof Map && "group-dungeon".equals(((Map)frontItem.getUse(ItemUseType.MINIGAME)).get("type")) || allGroupDungeonItems.contains(frontItem))) { + metadata.put("@", dungeonId); + + // Record prefab bounds, used for entity spawning if dungeon speakers aren't configured + metadata.put("pre", MapHelper.map(String.class, Object.class, "l", x, "t", y, "r", Math.min(getWidth(), x + prefab.getWidth()), "b", Math.min(getHeight(), y + prefab.getHeight()), "m", mirrored)); + } + if(dungeonId != null && frontItem.hasId("mechanical/spawner-brain")) { metadata.put("@", dungeonId); // dungeonType = DungeonType.morePrior(dungeonType, DungeonType.EVOKER);