Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ We will seek to implement CI/CD in the repository so that developers have a good
- Simple Galaxy Gate: Supports ABG, Delta, Epsilon, Zeta, Hades, Kuiper, LoW, Invasion, Mimesis, EBG, GoP, Treacherous, DSE, Fiesta, Trinity and Voyagers. By @do-gamer
- Autobuy: Automatically buys boosters and special items from the shop at a configured interval. By @do-gamer
- Log Overlay: Displays the latest in-game log messages on the canvas (top-center, auto-fade after 5s). Built-in keyword whitelist limits the displayed lines to gains and errors so the canvas does not get cluttered. By @Halizeur
- DailyTaskAPI: Detects supported daily quests from DarkBot's typed quest data, executes multi-step NPC, map, collection, ore and opt-in player-combat objectives, applies configurable reward and safety policies before accepting them, and uses DarkBot's NPC metadata and PET Enemy Locator while hunting. By Berke

## Contributing

Expand Down
8 changes: 7 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ description = "SharedPlugin"
dependencies {
api("eu.darkbot.DarkBotAPI", "darkbot-impl", "0.9.8")
api("eu.darkbot", "DarkBot", "dc48506543")
testImplementation("org.junit.jupiter:junit-jupiter:5.11.4")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

tasks.test {
useJUnitPlatform()
}


Expand All @@ -40,4 +46,4 @@ tasks.named<Jar>("jar") {
tasks.register<Exec>("signFile") {
dependsOn("build")
commandLine("cmd", "/c", "sign.bat")
}
}
93 changes: 93 additions & 0 deletions src/main/java/dev/shared/berke/dailytaskapi/DailyNpcCombat.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package dev.shared.berke.dailytaskapi;

import eu.darkbot.api.PluginAPI;
import eu.darkbot.api.game.entities.Npc;
import eu.darkbot.shared.modules.LootModule;

import java.util.Comparator;

/**
* DarkBot's standard NPC combat engine with target selection restricted to
* the NPC required by the currently displayed daily quest.
*
* <p>The inherited implementation supplies the saved NPC radius, circle and
* run-configuration behaviour, ammunition selection, target validation and
* SafetyFinder logic used by NPC Kill and Collect. DailyTaskAPI remains the
* owner of quest and map selection.</p>
*/
final class DailyNpcCombat extends LootModule {
private String targetDescription;
private int targetRadius = 590;

DailyNpcCombat(PluginAPI api) {
super(api);
}

void tick(String description, int attackRadius) {
targetDescription = description;
targetRadius = attackRadius;
super.onTickModule();
}

void stopCombat() {
attack.stopAttack();
attack.setTarget(null);
targetDescription = null;
}

void shutdownPet() {
stopCombat();
if (pet.isEnabled()) pet.setEnabled(false);
}

boolean isPetActive() {
return pet.isActive();
}

@Override
protected boolean checkMap() {
// DailyTaskAPI resolves and navigates to the quest's required map.
// Do not let the saved working-map setting override that choice.
return true;
}

@Override
protected boolean findTarget() {
Npc current = attack.getTargetAs(Npc.class);
if (isUsableTarget(current)) return true;

if (current != null) {
attack.stopAttack();
attack.setTarget(null);
}

Npc selected = npcs.stream()
.filter(this::isUsableTarget)
.min(Comparator.comparingDouble(hero::distanceTo))
.orElse(null);
attack.setTarget(selected);
// MovementAPI.moveRandom follows the saved preferred-zone route for
// this map and falls back to a random map point when no route exists.
if (selected == null && !movement.isMoving()) {
movement.moveRandom();
}
return selected != null;
}

private boolean isUsableTarget(Npc npc) {
if (npc == null || targetDescription == null ||
!npc.isValid() || !npc.isSelectable() || npc.isBlacklisted()) {
return false;
}
String name = npc.getInfo() == null ? null : npc.getInfo().getName();
if (name == null || movement.getClosestDistance(npc) >= targetRadius) return false;

String target = DailyTaskPlanner.normalizeNpcName(targetDescription);
String candidate = DailyTaskPlanner.normalizeNpcName(name);
boolean wantsBoss = target.contains("boss");
boolean wantsUber = target.contains("uber");
return !candidate.isBlank() && target.contains(candidate) &&
wantsBoss == candidate.startsWith("boss ") &&
wantsUber == candidate.startsWith("uber");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package dev.shared.berke.dailytaskapi;

import eu.darkbot.api.config.ConfigSetting;
import eu.darkbot.api.config.types.NpcInfo;
import eu.darkbot.api.game.other.GameMap;
import eu.darkbot.api.managers.StarSystemAPI;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;

/** Resolves NPC maps from DarkBot's own NPC metadata instead of a static list. */
final class DailyNpcMapResolver {
private final ConfigSetting<Map<String, NpcInfo>> npcInfos;
private final StarSystemAPI starSystem;

DailyNpcMapResolver(ConfigSetting<Map<String, NpcInfo>> npcInfos, StarSystemAPI starSystem) {
this.npcInfos = npcInfos;
this.starSystem = starSystem;
}

Optional<String> resolve(String description, String companyPrefix) {
try {
Map<String, NpcInfo> configured = npcInfos.getValue();
if (configured == null || configured.isEmpty()) return Optional.empty();
return resolve(description, companyPrefix, new ArrayList<>(configured.values()));
} catch (RuntimeException ignored) {
return Optional.empty();
}
}

private Optional<String> resolve(String description, String companyPrefix, List<NpcInfo> configured) {
Optional<NpcInfo> target = configured.stream()
.filter(info -> info != null && DailyTaskPlanner.matchesNpcName(description, info.getName()))
.max(Comparator.comparingInt(info ->
DailyTaskPlanner.normalizeNpcName(info.getName()).length()));
if (target.isEmpty()) return Optional.empty();

Collection<Integer> mapIds = target.get().getMapIds();
if (mapIds == null || mapIds.isEmpty()) return Optional.empty();

GameMap current = starSystem.getCurrentMap();
if (current != null && mapIds.contains(current.getId()) && starSystem.isAccessible(current)) {
return Optional.of(current.getName());
}

String prefix = companyPrefix != null && companyPrefix.matches("[1-3]")
? companyPrefix + "-"
: "";
return mapIds.stream()
.map(starSystem::findMap)
.flatMap(Optional::stream)
.filter(starSystem::isAccessible)
.filter(map -> !map.isGG())
.min(Comparator
.comparingInt((GameMap map) -> map.getName().startsWith(prefix) ? 0 : 1)
.thenComparing(GameMap::getName))
.map(GameMap::getName);
}
}
159 changes: 159 additions & 0 deletions src/main/java/dev/shared/berke/dailytaskapi/DailyPlayerCombat.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package dev.shared.berke.dailytaskapi;

import eu.darkbot.api.PluginAPI;
import eu.darkbot.api.game.entities.Player;
import eu.darkbot.api.game.other.EntityInfo;
import eu.darkbot.api.managers.AttackAPI;
import eu.darkbot.api.managers.EntitiesAPI;
import eu.darkbot.api.managers.GroupAPI;
import eu.darkbot.api.managers.HeroAPI;
import eu.darkbot.api.managers.MovementAPI;

import java.util.Comparator;

/** Executes opt-in player-combat daily objectives with conservative filters. */
final class DailyPlayerCombat {
private final EntitiesAPI entities;
private final GroupAPI group;
private final MovementAPI movement;
private final AttackAPI attack;
private final HeroAPI hero;

private long searchStartedAt;
private long nextRoamAt;
private int deaths;
private boolean previouslyAlive = true;
private String status = "waiting";

DailyPlayerCombat(PluginAPI api) {
entities = api.requireAPI(EntitiesAPI.class);
group = api.requireAPI(GroupAPI.class);
movement = api.requireAPI(MovementAPI.class);
attack = api.requireAPI(AttackAPI.class);
hero = api.requireAPI(HeroAPI.class);
}

void startQuest() {
searchStartedAt = 0L;
nextRoamAt = 0L;
deaths = 0;
previouslyAlive = hero.getHealth().hpPercent() > 0d;
status = "searching for an eligible player";
}

void tick(String objectiveDescription, DailyTaskConfig config) {
long now = System.currentTimeMillis();
observeHeroState();
if (hero.getHealth().hpPercent() <= 0d) {
stopAttackOnly();
status = "waiting for ship recovery";
return;
}

Player target = attack.getTargetAs(Player.class);
if (!isEligible(target, objectiveDescription, config)) {
stopAttackOnly();
target = entities.getPlayers().stream()
.filter(player -> isEligible(player, objectiveDescription, config))
.min(playerComparator(config))
.orElse(null);
attack.setTarget(target);
}

if (target == null) {
if (searchStartedAt == 0L) searchStartedAt = now;
if (!movement.isMoving() || now >= nextRoamAt) {
movement.moveRandom();
nextRoamAt = now + 2_500L;
}
status = "searching for an eligible enemy player";
return;
}

searchStartedAt = 0L;
double distance = hero.distanceTo(target);
if (distance > config.playerAttackRadius) {
hero.setRoamMode();
movement.moveTo(target);
status = "approaching player at " + (int) Math.round(distance);
return;
}

movement.stop(false);
attack.tryLockAndAttack();
status = "attacking eligible enemy player";
}

boolean exceededSafetyLimits(DailyTaskConfig config) {
boolean deathLimit = config.maxPlayerDeaths > 0 && deaths >= config.maxPlayerDeaths;
boolean searchLimit = searchStartedAt > 0L &&
System.currentTimeMillis() - searchStartedAt >= config.playerSearchTimeoutSeconds * 1_000L;
return deathLimit || searchLimit;
}

void observeHeroState() {
observeDeath();
}

String failureReason(DailyTaskConfig config) {
if (config.maxPlayerDeaths > 0 && deaths >= config.maxPlayerDeaths) {
return "player-combat death limit reached (" + deaths + ")";
}
return "no eligible player found before the search timeout";
}

String getStatus() {
return status + " | deaths: " + deaths;
}

void stopCombat() {
stopAttackOnly();
searchStartedAt = 0L;
}

private void stopAttackOnly() {
if (attack.getTargetAs(Player.class) != null) {
attack.stopAttack();
attack.setTarget(null);
}
}

private void observeDeath() {
boolean alive = hero.getHealth().hpPercent() > 0d;
if (!alive && previouslyAlive) deaths++;
previouslyAlive = alive;
}

private Comparator<Player> playerComparator(DailyTaskConfig config) {
Comparator<Player> distance = Comparator.comparingDouble(hero::distanceTo);
if (!config.prioritizePlayersAttackingMe) return distance;
return Comparator.comparing((Player player) -> !player.isAttacking(hero)).thenComparing(distance);
}

private boolean isEligible(Player player, String objectiveDescription, DailyTaskConfig config) {
if (player == null || !player.isValid() || !player.isSelectable() ||
player.isInvisible() || player.isBlacklisted() || player.getId() == hero.getId()) {
return false;
}
EntityInfo info = player.getEntityInfo();
if (info == null) return false;
if (!matchesRequiredFaction(info, objectiveDescription)) return false;
if (config.avoidAlliedAndGroupPlayers && isProtected(player, info)) return false;
return !config.enemyPlayersOnly || info.isEnemy() ||
info.getClanDiplomacy() == EntityInfo.Diplomacy.WAR;
}

private boolean matchesRequiredFaction(EntityInfo info, String description) {
String normalized = DailyTaskPlanner.normalize(description);
if (normalized.contains("mmo")) return info.getFaction() == EntityInfo.Faction.MMO;
if (normalized.contains("eic")) return info.getFaction() == EntityInfo.Faction.EIC;
if (normalized.contains("vru")) return info.getFaction() == EntityInfo.Faction.VRU;
return true;
}

private boolean isProtected(Player player, EntityInfo info) {
EntityInfo.Diplomacy diplomacy = info.getClanDiplomacy();
return group.getMember(player) != null || diplomacy == EntityInfo.Diplomacy.ALLIED ||
diplomacy == EntityInfo.Diplomacy.NOT_ATTACK_PACT;
}
}
Loading
Loading