Initial Release of IGNCore version 2.5
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
from core.chat_blob import ChatBlob
|
||||
from core.command_alias_service import CommandAliasService
|
||||
from core.command_param_types import Any, Int, Const
|
||||
from core.db import DB
|
||||
from core.decorators import command, instance
|
||||
from core.lookup.character_service import CharacterService
|
||||
from core.setting_service import SettingService
|
||||
from core.text import Text
|
||||
from core.tyrbot import Tyrbot
|
||||
from core.util import Util
|
||||
from modules.core.accounting.services.account_service import AccountService
|
||||
|
||||
|
||||
@instance()
|
||||
class PresetController:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def inject(self, registry):
|
||||
self.bot: Tyrbot = registry.get_instance("bot")
|
||||
self.db: DB = registry.get_instance("db")
|
||||
self.text: Text = registry.get_instance("text")
|
||||
self.character_service: CharacterService = registry.get_instance("character_service")
|
||||
self.util: Util = registry.get_instance("util")
|
||||
self.setting_service: SettingService = registry.get_instance("setting_service")
|
||||
self.command_alias_service: CommandAliasService = registry.get_instance("command_alias_service")
|
||||
self.account_service: AccountService = registry.get_instance("account_service")
|
||||
|
||||
def start(self):
|
||||
self.command_alias_service.add_alias("startauc", "auction start")
|
||||
self.command_alias_service.add_alias("bid", "auction bid")
|
||||
if self.db.query_single("SELECT COUNT(*) AS count FROM points_presets").count < 1:
|
||||
# Populate with pre-made presets if empty
|
||||
sql = "INSERT INTO points_presets (name, points) VALUES " \
|
||||
"('Killed Sideboss', 1), " \
|
||||
"('Killed TNH', 2), " \
|
||||
"('Killed Beast', 3), " \
|
||||
"('Alien Playfield', 3), " \
|
||||
"('Wipe: Clan', 1), " \
|
||||
"('Wipe: Omni', 1), " \
|
||||
"('Bonus', 1), " \
|
||||
"('Tara', 2), " \
|
||||
"('S42/Boss', 2), " \
|
||||
"('S42/Endboss', 4)"
|
||||
self.db.exec(sql, [])
|
||||
|
||||
@command(command="presets", params=[Const("add"), Any("name"), Int("points")], access_level="superadmin",
|
||||
description="Add new points preset", sub_command="mod")
|
||||
def presets_add_cmd(self, _1, _2, name: str, points: int):
|
||||
count = self.db.query_single("SELECT COUNT(*) AS count FROM points_presets WHERE name = ?", [name]).count
|
||||
|
||||
if count > 0:
|
||||
return "A preset already exists with the name <highlight>%s<end>." % name
|
||||
|
||||
sql = "INSERT INTO points_presets (name, points) VALUES (?,?)"
|
||||
if self.db.exec(sql, [name, points]) > 0:
|
||||
return f"A preset with the name <highlight>{name}<end> was added, worth <green>{points:d}<end> points."
|
||||
|
||||
return "Failed to insert new preset in DB."
|
||||
|
||||
@command(command="presets", params=[Const("rem"), Int("preset_id")], access_level="superadmin",
|
||||
description="Delete preset", sub_command="mod")
|
||||
def presets_rem_cmd(self, _1, _2, preset_id: int):
|
||||
if self.db.exec("DELETE FROM points_presets WHERE preset_id = ?", [preset_id]) > 0:
|
||||
return "Successfully removed preset with ID <highlight>%d<end>." % preset_id
|
||||
|
||||
return "No preset with given ID <highlight>%d<end>." % preset_id
|
||||
|
||||
@command(command="presets",
|
||||
params=[Const("alter"), Int("preset_id"), Int("new_points")],
|
||||
access_level="superadmin",
|
||||
description="Alter the points dished out by given preset", sub_command="mod")
|
||||
def presets_alter_cmd(self, _1, _2, preset_id: int, new_points: int):
|
||||
preset = self.db.query_single("SELECT * FROM points_presets WHERE preset_id = ?", [preset_id])
|
||||
|
||||
if preset:
|
||||
if self.db.exec("UPDATE points_presets SET points = ? WHERE preset_id = ?", [new_points, preset_id]) > 0:
|
||||
return f"Successfully updated the preset, <highlight>{preset.name}<end>, " \
|
||||
f"to dish out <green>{new_points:d}<end> points instead of <red>{preset.points:d}<end>."
|
||||
|
||||
return "Failed to update preset with ID <highlight>%d<end>." % preset_id
|
||||
|
||||
@command(command="presets", params=[], access_level="admin",
|
||||
description="See list of points presets")
|
||||
def presets_cmd(self, _):
|
||||
return ChatBlob("Points presets", self.build_preset_list)
|
||||
|
||||
@property
|
||||
def build_preset_list(self):
|
||||
presets = self.db.query("SELECT * FROM points_presets ORDER BY name, points DESC")
|
||||
|
||||
if presets:
|
||||
blob = ""
|
||||
|
||||
for preset in presets:
|
||||
add_points_link = self.text.make_chatcmd("Add pts", f"/tell <myname> raid addpts {preset.name}")
|
||||
delete_link = self.text.make_chatcmd("Delete", f"/tell <myname> presets rem {preset.preset_id:d}")
|
||||
blob += f"<highlight>{preset.name}<end> worth <green>{preset.points:d}<end> points " \
|
||||
f"[id: {preset.preset_id:d}]\n | [{add_points_link}] [{delete_link}]\n\n"
|
||||
|
||||
return blob
|
||||
|
||||
return "No presets available. " \
|
||||
"To add new presets use <highlight><symbol>presets add preset_name preset_points<end>."
|
||||
@@ -0,0 +1,592 @@
|
||||
import time
|
||||
from typing import Union
|
||||
|
||||
from core.aochat import server_packets
|
||||
from core.aochat.BaseModule import BaseModule
|
||||
from core.buddy_service import BuddyService
|
||||
from core.chat_blob import ChatBlob
|
||||
from core.command_param_types import Const, Int, Any, Options, Character
|
||||
from core.command_request import CommandRequest
|
||||
from core.db import DB
|
||||
from core.decorators import instance, command, timerevent, event
|
||||
from core.event_service import EventService
|
||||
from core.lookup.character_service import CharacterService
|
||||
from core.lookup.pork_service import PorkService
|
||||
from core.private_channel_service import PrivateChannelService
|
||||
from core.sender_obj import SenderObj
|
||||
from core.setting_service import SettingService
|
||||
from core.text import Text
|
||||
from core.tyrbot import Tyrbot
|
||||
from core.util import Util
|
||||
from modules.core.accounting.services.account_service import AccountService
|
||||
from modules.raidbot.raid.preset_controller import PresetController
|
||||
from modules.standard.raid.leader_controller import LeaderController
|
||||
|
||||
|
||||
class Raider:
|
||||
def __init__(self, alts, active):
|
||||
self.main_id = alts[0].char_id
|
||||
self.alts = alts
|
||||
self.active_id = active
|
||||
self.accumulated_points = 0
|
||||
self.is_active = True
|
||||
self.left_raid = None
|
||||
self.was_kicked = None
|
||||
self.was_kicked_reason = None
|
||||
|
||||
def get_active_char(self):
|
||||
for alt in self.alts:
|
||||
if self.active_id == alt.char_id:
|
||||
return alt
|
||||
return None
|
||||
|
||||
|
||||
class Raid:
|
||||
def __init__(self, raid_name, started_by, raiders=None):
|
||||
self.raid_name = raid_name
|
||||
self.desc = raid_name
|
||||
self.started_at = int(time.time())
|
||||
self.started_by = started_by
|
||||
self.raiders = raiders or []
|
||||
self.is_open = True
|
||||
self.raid_orders = None
|
||||
self.level = 180
|
||||
|
||||
|
||||
@instance()
|
||||
class RaidbotController(BaseModule):
|
||||
NO_RAID_RUNNING_RESPONSE = "No raid is running."
|
||||
|
||||
def __init__(self):
|
||||
self.raid = None
|
||||
|
||||
def inject(self, registry):
|
||||
self.bot: Tyrbot = registry.get_instance("bot")
|
||||
self.db: DB = registry.get_instance("db")
|
||||
self.text: Text = registry.get_instance("text")
|
||||
self.setting_service: SettingService = registry.get_instance("setting_service")
|
||||
self.character_service: CharacterService = registry.get_instance("character_service")
|
||||
self.preset_controller: PresetController = registry.get_instance("preset_controller")
|
||||
self.util: Util = registry.get_instance("util")
|
||||
self.event_service: EventService = registry.get_instance("event_service")
|
||||
self.leader: LeaderController = registry.get_instance("leader_controller")
|
||||
self.getresp = registry.get_instance("translation_service").get_response
|
||||
self.pork: PorkService = registry.get_instance("pork_service")
|
||||
self.private_channel_service: PrivateChannelService = registry.get_instance("private_channel_service")
|
||||
self.account_service: AccountService = registry.get_instance("account_service")
|
||||
self.buddy_service: BuddyService = registry.get_instance("buddy_service")
|
||||
|
||||
def pre_start(self):
|
||||
self.event_service.register_event_type("RAID_END")
|
||||
|
||||
@event("connect", "Adds all raiders to buddylist")
|
||||
def connect(self, _, _1):
|
||||
query = self.db.query("SELECT char_id, member from account where member != -1 and disabled = 0")
|
||||
if query:
|
||||
for player in query:
|
||||
self.buddy_service.add_buddy(player.char_id, "member")
|
||||
|
||||
@command(command="raid", params=[], access_level="member",
|
||||
description="Show the current raid status")
|
||||
def raid_cmd(self, _):
|
||||
if not self.raid:
|
||||
return self.NO_RAID_RUNNING_RESPONSE
|
||||
|
||||
t = int(time.time())
|
||||
|
||||
blob = ""
|
||||
blob += f"Name: <highlight>{self.raid.desc}<end>\n"
|
||||
blob += f"Minimum Level: <highlight>{self.raid.level}</highlight>\n"
|
||||
blob += f"Started By: <highlight>{self.raid.started_by.name}<end>\n"
|
||||
blob += f"Raidleader: " \
|
||||
f"<highlight>" \
|
||||
f"{self.leader.leader.name if self.leader.leader else '<red>not set - use !leader set<end>'}" \
|
||||
f"<end>"
|
||||
blob += f"Started At: <highlight>{self.util.format_datetime(self.raid.started_at)}<end> " \
|
||||
f"({self.util.time_to_readable(t - self.raid.started_at)} ago)\n"
|
||||
if self.raid.is_open:
|
||||
blob += f"Status: <green>Open<end> [{self.text.make_chatcmd('Join', '/tell <myname> raid join')}] " \
|
||||
f"<red>[{self.text.make_chatcmd('Lock', '/tell <myname> raid lock')}]<end>"
|
||||
else:
|
||||
blob += f"Status: <red>Closed<end> " \
|
||||
f"<green>[{self.text.make_chatcmd('Unlock', '/tell <myname> raid open')}]<end>"
|
||||
blob += "\n\n"
|
||||
|
||||
if self.raid.raid_orders:
|
||||
blob += "<header2>Orders<end>\n"
|
||||
blob += self.raid.raid_orders + "\n\n"
|
||||
blob += "<header2>Raiders<end>\n"
|
||||
for raider in self.raid.raiders:
|
||||
if raider.is_active:
|
||||
blob += self.format_row(raider.get_active_char())
|
||||
return ChatBlob("Raid Status", blob)
|
||||
|
||||
def format_row(self, user):
|
||||
return f" {self.util.get_prof_icon(user.profession)} " \
|
||||
f"<{user.faction.lower()}>{user.name}</{user.faction.lower()}>" \
|
||||
f" ({user.level}/<green>{user.ai_level}</green>) " \
|
||||
f"[<{user.faction.lower()}>{user.org_name}</{user.faction.lower()}>|<cyan>{user.org_rank_name}</cyan>]\n"
|
||||
|
||||
@event(event_type="private_channel_left", description="Autokick inactive characters")
|
||||
def left_channel(self, _1, event_data: server_packets.PrivateChannelClientLeft):
|
||||
self.raid_kick_cmd(None, None, self.pork.get_character_info(event_data.char_id), "Left private channel")
|
||||
|
||||
@command(command="raid", params=[Const("start"), Any("raid_name")],
|
||||
description="Start new raid", access_level="leader", sub_command="manage")
|
||||
def raid_start_cmd(self, request: CommandRequest, _, raid_name: str):
|
||||
if self.raid:
|
||||
return "The <highlight>%s<end> raid is already running." % self.raid.raid_name
|
||||
|
||||
self.raid = Raid(raid_name, request.sender)
|
||||
|
||||
leader_alts = self.account_service.get_alts(request.sender.char_id)
|
||||
self.raid.raiders.append(Raider(leader_alts, request.sender.char_id))
|
||||
join_link = self.get_raid_join_blob("Click here")
|
||||
|
||||
msg = "\n" \
|
||||
f"<highlight>────────[ Raid starting ]────────<end>\n" \
|
||||
f"Initiator: <highlight>{request.sender.name}<end>\n" \
|
||||
f"Raid Name: <highlight>{raid_name}<end>\n" \
|
||||
f"{join_link} to join\n" \
|
||||
f"<highlight>────────[ Raid starting ]────────<end>"
|
||||
self.leader.leader = None
|
||||
self.leader.set_raid_leader(request.sender, request.sender)
|
||||
# self.bot.send_org_message(msg)
|
||||
self.bot.send_private_channel_message(msg)
|
||||
self.account_service.add_log(leader_alts[0].char_id, "raid",
|
||||
f"[<notice>STARTED</notice>] Raid with desc: <highlight>{raid_name}</highlight>",
|
||||
request.sender.char_id)
|
||||
if request.channel == "msg":
|
||||
return "You have been set as the <notice>active<end> Raidleader. " \
|
||||
"Set another Raidleader by using <notice>!leader set <name><end>"
|
||||
else:
|
||||
return f"{request.sender.name} has been set as the <notice>active<end> Raidleader automatically."
|
||||
|
||||
@command(command="raid",
|
||||
params=[Const("desc"), Any("description")],
|
||||
description="Change the raid description",
|
||||
access_level="leader", sub_command="manage")
|
||||
def raid_desc_cmd(self, _1, _2, desc):
|
||||
if not self.raid:
|
||||
return self.NO_RAID_RUNNING_RESPONSE
|
||||
else:
|
||||
self.raid.desc = desc
|
||||
return f"Changed raid description to <highlight>{desc}</highlight> successfully."
|
||||
|
||||
@command(command="raid",
|
||||
params=[Const("level"), Int("level")],
|
||||
description="Change the minimum raider level",
|
||||
access_level="leader",
|
||||
sub_command="manage")
|
||||
def raid_level_cmd(self, _1, _2, level):
|
||||
if not self.raid:
|
||||
return self.NO_RAID_RUNNING_RESPONSE
|
||||
else:
|
||||
self.raid.level = level
|
||||
return f"Changed minimum raider level to <highlight>{level}</highlight> successfully."
|
||||
|
||||
@command(command="raid",
|
||||
params=[Const("add"), Character("character_name")],
|
||||
description="add an player to the raid",
|
||||
access_level="leader", sub_command="manage")
|
||||
def raid_add_cmd(self, request, _, char: SenderObj):
|
||||
if not self.raid:
|
||||
return self.NO_RAID_RUNNING_RESPONSE
|
||||
if not char.char_id:
|
||||
return self.getresp("global", "char_not_found", {"char": char.name})
|
||||
if not self.private_channel_service.in_private_channel(char.char_id):
|
||||
return f"The player <highlight>{char.name}</highlight> is not in my private channel. Could not add him."
|
||||
main_id = self.account_service.get_main(char.char_id).char_id
|
||||
in_raid = self.is_in_raid(main_id)
|
||||
if in_raid is not None:
|
||||
if in_raid.active_id == char.char_id:
|
||||
if in_raid.is_active:
|
||||
return f"The Character <highlight>{char.name}<end> is already participating in the Raid."
|
||||
else:
|
||||
in_raid.is_active = True
|
||||
in_raid.was_kicked = None
|
||||
in_raid.was_kicked_reason = None
|
||||
in_raid.left_raid = None
|
||||
self.account_service.add_log(main_id, "raid",
|
||||
f"[<notice>ADD</notice>] Added to the raid by "
|
||||
f"<highlight>{request.sender.name}</highlight>",
|
||||
request.sender.char_id)
|
||||
self.send_raid_msg("<green>READD</green>",
|
||||
f"<highlight>{char.name}</highlight> by "
|
||||
f"<highlight>{request.sender.name}</highlight>")
|
||||
elif in_raid.is_active:
|
||||
former_active_name = self.character_service.resolve_char_to_name(in_raid.active_id)
|
||||
return f"<highlight>{char.name}<end> is already participating in the raid with the char " \
|
||||
f"<highlight>{former_active_name}<end>"
|
||||
else:
|
||||
alts = self.account_service.get_alts(char.char_id)
|
||||
self.raid.raiders.append(Raider(alts, char.char_id))
|
||||
self.account_service.add_log(main_id, "raid", f"[<notice>ADD</notice>] Added to the raid by "
|
||||
f"<highlight>{request.sender.name}</highlight>",
|
||||
request.sender.char_id)
|
||||
self.send_raid_msg("<green>ADD</green>", f"<highlight>{char.name}</highlight> by "
|
||||
f"<highlight>{request.sender.name}</highlight>")
|
||||
|
||||
@command(command="raid", params=[Const("join")], description="Join the ongoing raid", access_level="member")
|
||||
def raid_join_cmd(self, request, _):
|
||||
if not self.raid:
|
||||
return self.NO_RAID_RUNNING_RESPONSE
|
||||
if not self.private_channel_service.in_private_channel(request.sender.char_id):
|
||||
return "You are not in my private channel. you cant join the raid."
|
||||
main_id = self.account_service.get_main(request.sender.char_id).char_id
|
||||
in_raid = self.is_in_raid(main_id)
|
||||
user = self.pork.get_character_info(request.sender.char_id)
|
||||
if user.level < self.raid.level:
|
||||
return f"You need to be at least level <highlight>{self.raid.level}</highlight> " \
|
||||
f"to participate in this raid."
|
||||
if in_raid is not None:
|
||||
if in_raid.active_id == request.sender.char_id:
|
||||
if in_raid.is_active:
|
||||
return "You are already participating in the raid."
|
||||
else:
|
||||
if not self.raid.is_open:
|
||||
return "Raid is closed."
|
||||
in_raid.is_active = True
|
||||
in_raid.was_kicked = None
|
||||
in_raid.was_kicked_reason = None
|
||||
in_raid.left_raid = None
|
||||
self.account_service.add_log(main_id, "raid", f"[<notice>JOIN</notice>] Raid: "
|
||||
f"<highlight>{self.raid.raid_name}</highlight>",
|
||||
request.sender.char_id)
|
||||
self.send_raid_msg("<green>REJOIN</green>", f"<green>{request.sender.name}</green>")
|
||||
|
||||
elif in_raid.is_active:
|
||||
former_active_name = self.character_service.resolve_char_to_name(in_raid.active_id)
|
||||
in_raid.active_id = request.sender.char_id
|
||||
self.account_service.add_log(main_id, "raid", f"[<notice>JOIN</notice>] Raid: "
|
||||
f"<highlight>{self.raid.raid_name}</highlight>",
|
||||
request.sender.char_id)
|
||||
self.send_raid_msg("<green>ALT</green>", f"<highlight>{request.sender.name}</highlight> "
|
||||
f"[before: <highlight>{former_active_name}</highlight>]")
|
||||
|
||||
elif not in_raid.is_active:
|
||||
if not self.raid.is_open:
|
||||
return "Raid is closed."
|
||||
former_active_name = self.character_service.resolve_char_to_name(in_raid.active_id)
|
||||
in_raid.active_id = request.sender.char_id
|
||||
in_raid.was_kicked = None
|
||||
in_raid.was_kicked_reason = None
|
||||
in_raid.left_raid = None
|
||||
self.account_service.add_log(main_id, "raid",
|
||||
f"[<notice>JOIN</notice>] Raid: "
|
||||
f"<highlight>{self.raid.raid_name}</highlight>",
|
||||
request.sender.char_id)
|
||||
self.send_raid_msg("<green>ALT</green>", f"<highlight>{request.sender.name}</highlight> "
|
||||
f"[before: <highlight>{former_active_name}</highlight>]")
|
||||
|
||||
elif self.raid.is_open:
|
||||
alts = self.account_service.get_alts(request.sender.char_id)
|
||||
self.raid.raiders.append(Raider(alts, request.sender.char_id))
|
||||
self.account_service.add_log(main_id, "raid",
|
||||
f"[<notice>JOIN</notice>] Raid: <highlight>{self.raid.raid_name}</highlight>",
|
||||
request.sender.char_id)
|
||||
self.send_raid_msg("<green>JOIN</green>", f"<green>{request.sender.name}</green>")
|
||||
|
||||
# self.bot.send_private_channel_message("<highlight>%s<end> joined the raid." % request.sender.name)
|
||||
else:
|
||||
return "Raid is closed."
|
||||
|
||||
@command(command="raid", params=[Const("leave")], description="Leave the ongoing raid", access_level="member")
|
||||
def raid_leave_cmd(self, request, _):
|
||||
main = self.account_service.get_main(request.sender.char_id).char_id
|
||||
in_raid = self.is_in_raid(main)
|
||||
if in_raid:
|
||||
if not in_raid.is_active:
|
||||
return "You are not active in the raid."
|
||||
|
||||
in_raid.is_active = False
|
||||
in_raid.left_raid = int(time.time())
|
||||
self.account_service.add_log(main, "raid",
|
||||
f"[<notice>LEAVE</notice>] Raid: <highlight>{self.raid.raid_name}</highlight>",
|
||||
request.sender.char_id)
|
||||
self.send_raid_msg("<red>LEAVE</red>", f"<red>{request.sender.name}</red>")
|
||||
# self.bot.send_private_channel_message("<highlight>%s<end> left the raid." % request.sender.name)
|
||||
else:
|
||||
return "You are not in the raid."
|
||||
|
||||
@command(command="raid",
|
||||
params=[Const("addpts"), Any("name")],
|
||||
description="Add points to all active participants",
|
||||
access_level="leader",
|
||||
sub_command="manage")
|
||||
def points_add_cmd(self, request, _, name: str):
|
||||
if not self.raid:
|
||||
return self.NO_RAID_RUNNING_RESPONSE
|
||||
|
||||
preset = self.db.query_single("SELECT * FROM points_presets WHERE name = ?", [name])
|
||||
if not preset:
|
||||
return ChatBlob("No such preset - see list of presets", self.preset_controller.build_preset_list)
|
||||
count = 0
|
||||
|
||||
for raider in self.raid.raiders:
|
||||
current_points = self.account_service.get_account(raider.main_id)
|
||||
if raider.is_active:
|
||||
if current_points and current_points.disabled == 0:
|
||||
self.account_service.add_pts(raider.main_id,
|
||||
preset.points,
|
||||
f"{preset.name}",
|
||||
request.sender.char_id)
|
||||
raider.accumulated_points += preset.points
|
||||
count += 1
|
||||
self.bot.send_private_channel_message(f"<highlight>{preset.points}</highlight> points added "
|
||||
f"to all active raiders (<highlight>{count}</highlight>).")
|
||||
|
||||
@command(command="raid", params=[Const("check")], description="Get a list of raiders to do active check",
|
||||
access_level="leader", sub_command="manage")
|
||||
def raid_active_cmd(self, _1, _):
|
||||
if not self.raid:
|
||||
return self.NO_RAID_RUNNING_RESPONSE
|
||||
|
||||
blob = ""
|
||||
raider_names = []
|
||||
for raider in self.raid.raiders:
|
||||
if not raider.is_active:
|
||||
continue
|
||||
raider_name = self.character_service.resolve_char_to_name(raider.active_id)
|
||||
akick_link = self.text.make_chatcmd("Active kick", f"/tell <myname> raid kick {raider.main_id} inactive")
|
||||
warn_link = self.text.make_chatcmd("Warn",
|
||||
f"/tell <myname> cmd {raider_name} missed active check, "
|
||||
f"please give notice.")
|
||||
blob += "<highlight>%s<end> [%s] [%s]\n" % (raider_name, akick_link, warn_link)
|
||||
raider_names.append(raider_name)
|
||||
active_check_names = "/assist "
|
||||
active_check_names += "\\n /assist ".join(raider_names)
|
||||
blob += "[<a href='chatcmd://%s'>Active check</a>]\n\n" % active_check_names
|
||||
raider_names.clear()
|
||||
return ChatBlob("Active check", blob)
|
||||
|
||||
@command(command="raid", params=[Const("kick"), Character("char"), Any("reason")],
|
||||
description="Set raider as kicked with a reason", access_level="leader", sub_command="manage")
|
||||
def raid_kick_cmd(self, request, _2, char: SenderObj, reason: str):
|
||||
if self.raid is None:
|
||||
return self.NO_RAID_RUNNING_RESPONSE
|
||||
if not char.char_id:
|
||||
return self.getresp("global", "char_not_found", {"char": char.name})
|
||||
main_id = self.account_service.get_main(char.char_id).char_id
|
||||
in_raid = self.is_in_raid(main_id)
|
||||
|
||||
if in_raid is not None:
|
||||
if not in_raid.is_active:
|
||||
return "<highlight>%s<end> is not an active participant of the raid." % char.name
|
||||
|
||||
in_raid.is_active = False
|
||||
in_raid.was_kicked = int(time.time())
|
||||
in_raid.was_kicked_reason = reason
|
||||
name = self.character_service.resolve_char_to_name(in_raid.active_id)
|
||||
if request:
|
||||
self.account_service.add_log(main_id, "raid", f"[<notice>KICKED</notice>] by "
|
||||
f"<highlight>{request.sender.name}</highlight> "
|
||||
f"for {reason}",
|
||||
request.sender.char_id)
|
||||
self.account_service.add_log(self.account_service.get_main(request.sender.char_id).char_id, "raid",
|
||||
f"[<notice>KICKED</notice>] <highlight>{char.name}</highlight> "
|
||||
f"for {reason}",
|
||||
request.sender.char_id)
|
||||
else:
|
||||
self.account_service.add_log(main_id, "raid",
|
||||
f"[<notice>KICKED</notice>] by "
|
||||
f"<highlight>{self.bot.get_char_name()}</highlight> for {reason}",
|
||||
self.bot.get_char_id())
|
||||
self.send_raid_msg("KICKED", f"<highlight>{name}</highlight> for: <notice>{reason}</notice>")
|
||||
else:
|
||||
return "<highlight>%s<end> is not participating." % char.name
|
||||
|
||||
@command(command="raid",
|
||||
params=[Options(["unlock", "open", "lock", "close"])],
|
||||
description="Open/close raid for new participants",
|
||||
access_level="leader",
|
||||
sub_command="manage")
|
||||
def raid_open_close_cmd(self, request, action):
|
||||
if not self.raid:
|
||||
return self.NO_RAID_RUNNING_RESPONSE
|
||||
|
||||
if action in ["unlock", "open"]:
|
||||
if self.raid.is_open:
|
||||
return "Raid is already open."
|
||||
self.raid.is_open = True
|
||||
self.send_raid_msg("<green>unlocked</green>", f"by <highlight>{request.sender.name}</highlight>")
|
||||
return
|
||||
elif action in ["lock", "close"]:
|
||||
if self.raid.is_open:
|
||||
self.raid.is_open = False
|
||||
self.send_raid_msg("<red>locked</red>", f"by <highlight>{request.sender.name}</highlight>")
|
||||
|
||||
return
|
||||
return "Raid is already closed."
|
||||
|
||||
@command(command="raid",
|
||||
params=[Options(["save", "end", "stop"])],
|
||||
description="Save and log running raid",
|
||||
access_level="leader",
|
||||
sub_command="manage")
|
||||
def raid_save_cmd(self, _1, _2):
|
||||
if not self.raid:
|
||||
return self.NO_RAID_RUNNING_RESPONSE
|
||||
|
||||
sql = "INSERT INTO raid_log (raid_name, started_by, raid_start, raid_end) VALUES (?,?,?,?)"
|
||||
num_rows = self.db.exec(sql, [self.raid.raid_name,
|
||||
self.raid.started_by.char_id,
|
||||
self.raid.started_at,
|
||||
int(time.time())])
|
||||
if num_rows > 0:
|
||||
raid_id = self.db.query_single("SELECT raid_id FROM raid_log ORDER BY raid_id DESC LIMIT 1").raid_id
|
||||
|
||||
for raider in self.raid.raiders:
|
||||
sql = "INSERT INTO raid_log_participants (" \
|
||||
"raid_id, raider_id, accumulated_points, " \
|
||||
"left_raid, was_kicked, was_kicked_reason" \
|
||||
") " \
|
||||
"VALUES (?,?,?,?,?,?)"
|
||||
self.db.exec(sql, [raid_id, raider.active_id,
|
||||
raider.accumulated_points,
|
||||
raider.left_raid,
|
||||
raider.was_kicked,
|
||||
raider.was_kicked_reason])
|
||||
|
||||
self.raid = None
|
||||
self.leader.set_raid_leader(self.leader.leader, None)
|
||||
self.event_service.fire_event("RAID_ENDED")
|
||||
return "Raid has ended, logs got saved. <notice>Raidleader cleared.<end>"
|
||||
|
||||
else:
|
||||
return "Failed to end raid. Try again."
|
||||
|
||||
@command(command="raid", params=[Const("logentry"), Int("raid_id"), Character("char", is_optional=True)],
|
||||
description="Show log entry for raid, with possibility of narrowing down the log for character in raid",
|
||||
access_level="member")
|
||||
def raid_log_entry_cmd(self, _1, _2, raid_id: int, char: SenderObj):
|
||||
log_entry_spec = None
|
||||
if char:
|
||||
sql = "SELECT * FROM raid_log r " \
|
||||
"LEFT JOIN raid_log_participants p ON r.raid_id = p.raid_id " \
|
||||
"WHERE r.raid_id = ? AND p.raider_id = ?"
|
||||
log_entry_spec = self.db.query_single(sql, [raid_id, char.char_id])
|
||||
|
||||
sql = "SELECT * FROM raid_log r " \
|
||||
"LEFT JOIN raid_log_participants p ON r.raid_id = p.raid_id " \
|
||||
"WHERE r.raid_id = ? ORDER BY p.accumulated_points DESC"
|
||||
log_entry = self.db.query(sql, [raid_id])
|
||||
pts_sum = self.db.query_single("SELECT SUM(p.accumulated_points) AS sum FROM raid_log_participants p "
|
||||
"WHERE p.raid_id = ?", [raid_id]).sum
|
||||
|
||||
if not log_entry:
|
||||
return "No such log entry."
|
||||
|
||||
blob = f"Raid name: <highlight>{log_entry[0].raid_name}<end>\n"
|
||||
blob += f"Started by: <highlight>{self.character_service.resolve_char_to_name(log_entry[0].started_by)}<end>\n"
|
||||
blob += f"Start time: <highlight>{self.util.format_datetime(log_entry[0].raid_start)}<end>\n"
|
||||
blob += f"End time: <highlight>{self.util.format_datetime(log_entry[0].raid_end)}<end>\n"
|
||||
blob += f"Run time: " \
|
||||
f"<highlight>{self.util.time_to_readable(log_entry[0].raid_end - log_entry[0].raid_start)}<end>\n"
|
||||
blob += f"Total points: <highlight>{pts_sum:d}<end>\n\n"
|
||||
|
||||
if char and log_entry_spec:
|
||||
raider_name = self.character_service.resolve_char_to_name(log_entry_spec.raider_id)
|
||||
main_info = self.account_service.get_main(log_entry_spec.raider_id)
|
||||
alt_link = "Alt of %s" % main_info.name if main_info.char_id != log_entry_spec.raider_id else "Alts"
|
||||
alt_link = self.text.make_chatcmd(alt_link, "/tell <myname> alts %s" % main_info.name)
|
||||
blob += "<header2>Log entry for %s<end>\n" % raider_name
|
||||
blob += "Raider: <highlight>%s<end> [%s]\n" % (raider_name, alt_link)
|
||||
blob += "Left raid: %s\n" % ("n/a"
|
||||
if log_entry_spec.left_raid is None
|
||||
else self.util.format_datetime(log_entry_spec.left_raid))
|
||||
blob += "Was kicked: %s\n" % ("No"
|
||||
if log_entry_spec.was_kicked is None
|
||||
else "Yes [%s]" % (self.util.format_datetime(log_entry_spec.was_kicked)))
|
||||
blob += "Kick reason: %s\n\n" % ("n/a"
|
||||
if log_entry_spec.was_kicked_reason is None
|
||||
else log_entry_spec.was_kicked_reason)
|
||||
|
||||
blob += "<header2>Participants<end>\n"
|
||||
for raider in log_entry:
|
||||
raider_name = self.character_service.resolve_char_to_name(raider.raider_id)
|
||||
main_info = self.account_service.get_main(raider.raider_id)
|
||||
alt_link = "Alt of %s" % main_info.name if main_info.char_id != raider.raider_id else "Alts"
|
||||
alt_link = self.text.make_chatcmd(alt_link, "/tell <myname> alts %s" % main_info.name)
|
||||
log_link = self.text.make_chatcmd("Log", "/tell <myname> raid logentry %d %s" % (raid_id, raider_name))
|
||||
account_link = self.text.make_chatcmd("Account", "/tell <myname> account %s" % raider_name)
|
||||
blob += f"{raider_name} - {raider.accumulated_points:d} points earned " \
|
||||
f"[{log_link}] [{account_link}] [{alt_link}]\n"
|
||||
|
||||
log_entry_reference = "the raid %s" % log_entry[0].raid_name \
|
||||
if char is None \
|
||||
else "%s in raid %s" \
|
||||
% (self.character_service.resolve_char_to_name(char.char_id), log_entry[0].raid_name)
|
||||
return ChatBlob("Log entry for %s" % log_entry_reference, blob)
|
||||
|
||||
@command(command="raid", params=[Const("history")], description="Show a list of recent raids",
|
||||
access_level="member")
|
||||
def raid_history_cmd(self, _1, _2):
|
||||
sql = "SELECT * FROM raid_log ORDER BY raid_end DESC LIMIT 30"
|
||||
raids = self.db.query(sql)
|
||||
|
||||
blob = ""
|
||||
for raid in raids:
|
||||
participant_link = self.text.make_chatcmd("Log", "/tell <myname> raid logentry %d" % raid.raid_id)
|
||||
timestamp = self.util.format_datetime(raid.raid_start)
|
||||
leader_name = self.character_service.resolve_char_to_name(raid.started_by)
|
||||
blob += f"[{raid.raid_id:d}] [{timestamp}] <highlight>{raid.raid_name}<end> " \
|
||||
f"started by <highlight>{leader_name}<end> [{participant_link}]\n"
|
||||
|
||||
return ChatBlob("Raid history", blob)
|
||||
|
||||
def is_in_raid(self, main_id: int) -> Union[bool, Raider]:
|
||||
if self.raid is None:
|
||||
return True
|
||||
|
||||
for raider in self.raid.raiders:
|
||||
if raider.main_id == main_id:
|
||||
return raider
|
||||
|
||||
def send_raid_msg(self, msg_type, msg):
|
||||
self.bot.send_private_channel_message(f"[<notice>Raid: {msg_type}</notice>] {msg}")
|
||||
|
||||
def get_raid_join_blob(self, link_txt: str):
|
||||
blob = "<header2>1. Join the raid<end>\n" \
|
||||
"To join the current raid <highlight>%s<end>, send the following tell to <myname>\n" \
|
||||
"<tab><tab><a href='chatcmd:///tell <myname> <symbol>raid join'>/tell <myname> raid join</a>\n" \
|
||||
"\n" \
|
||||
"<header2>2. Enable LFT<end>\n" \
|
||||
"When you have joined the raid, go lft with \"<myname>\" as description\n" \
|
||||
"<tab><tab><a href='chatcmd:///lft <font color=#FFFF00>» <myname><end>'>/lft <myname></a>\n" \
|
||||
"\n" \
|
||||
"<header2>3. Announce<end>\n" \
|
||||
"You could announce to the raid leader, that you have enabled LFT\n" \
|
||||
"<tab><tab><a href='chatcmd:///group <myname> I am on lft'>Announce</a> that you have enabled lft\n" \
|
||||
"\n" \
|
||||
"<header2>4. Rally with yer mateys<end>\n" \
|
||||
"Finally, move towards the starting location of the raid.\n" \
|
||||
"<highlight>Ask for help<end> if you're in doubt of where to go." % self.raid.raid_name
|
||||
|
||||
return self.text.paginate_single(ChatBlob(link_txt, blob))
|
||||
|
||||
@timerevent(budatime="5m", description="announce raid")
|
||||
def announce_raid(self, event_type, event_data):
|
||||
if self.raid:
|
||||
if self.raid.is_open:
|
||||
self.bot.send_private_channel_message("<highlight>Raid Running<end> "
|
||||
"<yellow>::<end> <green>%s<end> <yellow>::<end> %s" %
|
||||
(self.raid.desc,
|
||||
f"<a href=\"text://{self.raid.raid_name}\">"
|
||||
f"{self.get_raid_join_blob('Click to Join')}"
|
||||
f"</a>"))
|
||||
else:
|
||||
self.bot.send_private_channel_message("<highlight>Raid Running<end> <yellow>::<end> <green>%s<end> "
|
||||
"<yellow>::<end> <red>Closed<end>" % self.raid.desc)
|
||||
|
||||
@timerevent(budatime="1m", description="No leader reminder")
|
||||
def leader_auto_remove(self, _1, _2):
|
||||
if self.raid:
|
||||
leader = self.leader.leader
|
||||
if not leader:
|
||||
self.bot.send_private_channel_message(f"<red>::</red> No Leader set <red>::</red> "
|
||||
f"use <highlight>!leader set</highlight>")
|
||||
return
|
||||
if not self.is_in_raid(leader.char_id):
|
||||
self.bot.send_private_channel_message(f"<red>::</red> Raidleader <highlight>{leader.name}</highlight> "
|
||||
f"is not in raid <red>::</red>")
|
||||
@@ -0,0 +1,124 @@
|
||||
from core.aochat.BaseModule import BaseModule
|
||||
from core.chat_blob import ChatBlob
|
||||
from core.command_param_types import Any
|
||||
from core.db import DB
|
||||
from core.decorators import instance, command
|
||||
from core.lookup.character_service import CharacterService
|
||||
from core.lookup.pork_service import PorkService
|
||||
from core.private_channel_service import PrivateChannelService
|
||||
from core.setting_service import SettingService
|
||||
from core.text import Text
|
||||
from core.tyrbot import Tyrbot
|
||||
from core.util import Util
|
||||
from modules.core.accounting.services.account_service import AccountService
|
||||
|
||||
|
||||
@instance()
|
||||
class MassinviteController(BaseModule):
|
||||
NO_RAID_RUNNING_RESPONSE = "No raid is running."
|
||||
|
||||
def __init__(self):
|
||||
self.count = 0
|
||||
|
||||
def inject(self, registry):
|
||||
self.bot: Tyrbot = registry.get_instance("bot")
|
||||
self.db: DB = registry.get_instance("db")
|
||||
self.text: Text = registry.get_instance("text")
|
||||
self.setting_service: SettingService = registry.get_instance("setting_service")
|
||||
self.character_service: CharacterService = registry.get_instance("character_service")
|
||||
self.util: Util = registry.get_instance("util")
|
||||
self.pork: PorkService = registry.get_instance("pork_service")
|
||||
self.private_channel_service: PrivateChannelService = registry.get_instance("private_channel_service")
|
||||
self.account_service: AccountService = registry.get_instance("account_service")
|
||||
|
||||
@command(command="massinvite",
|
||||
params=[Any("text")],
|
||||
description="Send a Massmessage combined with a massinvite",
|
||||
access_level="leader")
|
||||
def mass_invite(self, request, msg):
|
||||
if not self.raid:
|
||||
return self.NO_RAID_RUNNING_RESPONSE
|
||||
main = ""
|
||||
blob = ""
|
||||
spam = [self.format_subtile_spam(
|
||||
f"<highlight>{request.sender.name}</highlight> invited you to join a raid: <yellow>{msg}</yellow>"),
|
||||
self.format_spam(f"<highlight>{request.sender.name}</highlight> invited you to join a raid:\n"
|
||||
f"<red>{msg}</red>")]
|
||||
self.bot.send_private_channel_message(spam[1])
|
||||
invites = self.account_service.get_all_members(online_only=True)
|
||||
request.reply(f"Attempting to send {len(invites)} Raid invites....")
|
||||
for player in invites:
|
||||
if player.main != main:
|
||||
main = player.main
|
||||
blob += f"\n<notice>{self.character_service.resolve_char_to_name(main)}<notice> as \n"
|
||||
blob += self.handle_player(player, spam, True)
|
||||
|
||||
msg = ChatBlob("Info", blob)
|
||||
msg.page_prefix = f"{self.count} Massinvites sent! ["
|
||||
msg.page_postfix = "]"
|
||||
self.count = 0
|
||||
if request.channel == "msg":
|
||||
self.bot.send_mass_message(request.sender.char_id, msg)
|
||||
else:
|
||||
return msg
|
||||
|
||||
@command(command="massmessage", params=[Any("text")], description="Send a Massmessage to all online players",
|
||||
access_level="leader")
|
||||
def mass_message(self, request, msg):
|
||||
# if not self.raid:
|
||||
# return self.NO_RAID_RUNNING_RESPONSE
|
||||
main = ""
|
||||
blob = ""
|
||||
spam = [self.format_subtile_spam(
|
||||
f"<highlight>{request.sender.name}</highlight> sent you a message: <yellow>{msg}</yellow>"),
|
||||
self.format_spam(f"<highlight>{request.sender.name}</highlight> sent you a message:\n"
|
||||
f"<red>{msg}</red>")]
|
||||
self.bot.send_private_channel_message(spam[1])
|
||||
invites = self.account_service.get_all_members(online_only=True)
|
||||
request.reply(f"Attempting to send {len(invites)} Massmessage's...")
|
||||
for player in invites:
|
||||
if player.main != main:
|
||||
main = player.main
|
||||
blob += f"\n<notice>{self.character_service.resolve_char_to_name(main)}<notice> as \n"
|
||||
blob += self.handle_player(player, spam, False)
|
||||
|
||||
msg = ChatBlob("Info", blob)
|
||||
msg.page_prefix = f"{self.count} Massmessage's have been sent! ["
|
||||
msg.page_postfix = "]"
|
||||
self.count = 0
|
||||
if request.channel == "msg":
|
||||
self.bot.send_mass_message(request.sender.char_id, msg)
|
||||
else:
|
||||
return msg
|
||||
|
||||
def handle_player(self, user, msg, invite=False):
|
||||
pg = " [<green>PG</green>]" if self.private_channel_service.in_private_channel(
|
||||
user.char_id) else " [<red>MSG sent</red>]"
|
||||
if not self.private_channel_service.in_private_channel(user.char_id):
|
||||
count = 0
|
||||
if user.raid_spam == 1:
|
||||
if user.subtile_spam == 1:
|
||||
self.bot.send_mass_message(user.char_id, msg[0])
|
||||
else:
|
||||
self.bot.send_mass_message(user.char_id, msg[1])
|
||||
count += 1
|
||||
if user.raid_invite == 1 and invite:
|
||||
self.private_channel_service.invite(user.char_id)
|
||||
count += 1
|
||||
if count == 0:
|
||||
return ""
|
||||
self.count += 1
|
||||
return f"<tab>{self.util.get_prof_icon(user.profession)} " \
|
||||
f"({self.text.zfill(user.level, 220)}/<green>{self.text.zfill(user.ai_level, 30)}</green>) " \
|
||||
f"<{user.faction.lower()}>{user.name}</{user.faction.lower()}> " \
|
||||
f"[<{user.faction.lower()}>{user.org_name}</{user.faction.lower()}> - " \
|
||||
f"<highlight>{user.org_rank_id + 1}</highlight>]{pg}\n"
|
||||
|
||||
def format_spam(self, msg):
|
||||
return f"\n" \
|
||||
f"<yellow>────────[ Raid invite ]────────</yellow>\n" \
|
||||
f"{msg}\n" \
|
||||
f"<yellow>────────[ Raid invite ]────────</yellow>"
|
||||
|
||||
def format_subtile_spam(self, msg):
|
||||
return f"[ Raid invite ] {msg}"
|
||||
@@ -0,0 +1,81 @@
|
||||
import math
|
||||
|
||||
from core.aochat.BaseModule import BaseModule
|
||||
from core.chat_blob import ChatBlob
|
||||
from core.command_alias_service import CommandAliasService
|
||||
from core.command_param_types import Int, NamedParameters
|
||||
from core.db import DB
|
||||
from core.decorators import instance, command
|
||||
from core.event_service import EventService
|
||||
from core.lookup.pork_service import PorkService
|
||||
from core.public_channel_service import PublicChannelService
|
||||
from core.text import Text
|
||||
from core.tyrbot import Tyrbot
|
||||
from core.util import Util
|
||||
from modules.raidbot.tower.tower_service import TowerService
|
||||
from modules.standard.helpbot.playfield_controller import PlayfieldController
|
||||
|
||||
|
||||
# noinspection DuplicatedCode
|
||||
@instance()
|
||||
class ContractController(BaseModule):
|
||||
PAGE_SIZE = 40
|
||||
|
||||
# noinspection DuplicatedCode
|
||||
def inject(self, registry):
|
||||
self.bot: Tyrbot = registry.get_instance("bot")
|
||||
self.db: DB = registry.get_instance("db")
|
||||
self.util: Util = registry.get_instance("util")
|
||||
self.text: Text = registry.get_instance("text")
|
||||
self.event_service: EventService = registry.get_instance("event_service")
|
||||
self.pork_service: PorkService = registry.get_instance("pork_service")
|
||||
self.playfield_controller: PlayfieldController = registry.get_instance("playfield_controller")
|
||||
self.public_channel_service: PublicChannelService = registry.get_instance("public_channel_service")
|
||||
self.towercache: TowerService = registry.get_instance("tower_service")
|
||||
self.command_alias_service: CommandAliasService = registry.get_instance("command_alias_service")
|
||||
|
||||
@command(command="contracts",
|
||||
params=[Int('mininum', is_optional=True), NamedParameters(['page'])],
|
||||
access_level="member",
|
||||
description="Shows contracts")
|
||||
def cotracts(self, _, min_ql, named_params):
|
||||
if not min_ql:
|
||||
min_ql = 200
|
||||
data = self.db.query("SELECT CAST(SUM(ql)*2 AS INTEGER) AS contracts, "
|
||||
"COUNT(*) as sites, org_name, org_id, faction FROM towers "
|
||||
"where org_name IS NOT NULL GROUP BY org_name ORDER BY contracts desc", [])
|
||||
page = int(named_params.page or "1")
|
||||
offset = (page - 1) * self.PAGE_SIZE
|
||||
data = [x for x in data if x.contracts > min_ql]
|
||||
return self.format_pagination(data, offset, page, f"Tower Contracts ({len(data)})",
|
||||
f"There are no orgs with more than "
|
||||
f"<highlight>{min_ql}</highlight> contract points.",
|
||||
f'contracts {min_ql}')
|
||||
|
||||
def format_pagination(self, data, offset, page, title, nullmsg, cmd):
|
||||
selected = data[offset:offset + self.PAGE_SIZE]
|
||||
count = len(selected)
|
||||
pages = ""
|
||||
if page > 1:
|
||||
pages += "Pages: " + self.text.make_tellcmd("«« Page %d" % (page - 1), f'{cmd} --page={page - 1}')
|
||||
if offset + self.PAGE_SIZE < len(data):
|
||||
pages += f" Page {page}/{math.ceil(len(data) / self.PAGE_SIZE)}"
|
||||
pages += " " + self.text.make_tellcmd("Page %d »»" % (page + 1), f'{cmd} --page={page + 1}')
|
||||
pages += "\n"
|
||||
if count == 0:
|
||||
return nullmsg
|
||||
else:
|
||||
blob = "<font color=CCInfoText>"
|
||||
blob += "" + pages + "\n"
|
||||
index = offset
|
||||
for entry in selected:
|
||||
index += 1
|
||||
blob += self.row_formatter(entry, index, data)
|
||||
blob += "\n" + pages
|
||||
blob += "</font>"
|
||||
return ChatBlob(title, blob)
|
||||
|
||||
def row_formatter(self, entry, index, data):
|
||||
return f"{self.text.zfill(index, len(data))}. " \
|
||||
f"<white>{self.text.zfill(entry.contracts, data[0].contracts)}</white> " \
|
||||
f"<{entry.faction.lower()}>{entry.org_name}</{entry.faction.lower()}>\n"
|
||||
@@ -0,0 +1,76 @@
|
||||
import time
|
||||
|
||||
from core.aochat.BaseModule import BaseModule
|
||||
from core.chat_blob import ChatBlob
|
||||
from core.command_alias_service import CommandAliasService
|
||||
from core.db import DB
|
||||
from core.decorators import instance, command
|
||||
from core.event_service import EventService
|
||||
from core.lookup.pork_service import PorkService
|
||||
from core.public_channel_service import PublicChannelService
|
||||
from core.text import Text
|
||||
from core.tyrbot import Tyrbot
|
||||
from core.util import Util
|
||||
from modules.raidbot.tower.tower_service import TowerService
|
||||
from modules.standard.helpbot.playfield_controller import PlayfieldController
|
||||
|
||||
|
||||
@instance()
|
||||
class PlantController(BaseModule):
|
||||
# noinspection DuplicatedCode
|
||||
def inject(self, registry):
|
||||
self.bot: Tyrbot = registry.get_instance("bot")
|
||||
self.db: DB = registry.get_instance("db")
|
||||
self.util: Util = registry.get_instance("util")
|
||||
self.text: Text = registry.get_instance("text")
|
||||
self.event_service: EventService = registry.get_instance("event_service")
|
||||
self.pork_service: PorkService = registry.get_instance("pork_service")
|
||||
self.playfield_controller: PlayfieldController = registry.get_instance("playfield_controller")
|
||||
self.public_channel_service: PublicChannelService = registry.get_instance("public_channel_service")
|
||||
self.towercache: TowerService = registry.get_instance("tower_service")
|
||||
self.command_alias_service: CommandAliasService = registry.get_instance("command_alias_service")
|
||||
|
||||
@command(command="penalty", params=[], access_level="member", description="Shows planttimers")
|
||||
def penalty(self, _):
|
||||
blob = "<font color=CCInfoText>"
|
||||
self.towercache.attack_hot.sort(key=lambda k: k['org_name'])
|
||||
rem = []
|
||||
for key, value in enumerate(sorted(self.towercache.attack_hot, key=lambda k: k['org_name'])):
|
||||
if value["hot"] < time.time():
|
||||
rem.append(key)
|
||||
continue
|
||||
|
||||
lca = self.towercache.get_towers_by_org_name(value["org_name"])
|
||||
for site in lca:
|
||||
blob += self.towercache.format_entry(site, 10)
|
||||
|
||||
blob += "</font>"
|
||||
for i in reversed(rem):
|
||||
self.towercache.attack_hot.pop(i)
|
||||
if len(self.towercache.attack_hot) == 0:
|
||||
blob = ""
|
||||
return ChatBlob(f"Warhot tower sites", blob.strip("\n")) if blob != "" else f"There are no orgs in penalty."
|
||||
|
||||
@command(command="plant", params=[], access_level="member", description="Shows planttimers")
|
||||
def plant(self, _):
|
||||
blob = "<font color=CCInfoText>"
|
||||
rem = []
|
||||
for key, value in enumerate(sorted(self.towercache.plant, key=lambda k: k['pf'])):
|
||||
print(key, value)
|
||||
if value["plant"] < time.time():
|
||||
rem.append(key)
|
||||
continue
|
||||
|
||||
lca = self.playfield_controller.get_playfield_by_id(value['pf'])
|
||||
blob += f"[<cyan>{lca.short_name}</cyan>] <cyan>x{value['site']}</cyan> in " \
|
||||
f"<white>{self.util.format_time(value['plant'] - time.time())}</white>\n"
|
||||
|
||||
blob += "</font>"
|
||||
for i in reversed(rem):
|
||||
self.towercache.plant.pop(i)
|
||||
if len(self.towercache.plant) == 0:
|
||||
blob = ""
|
||||
if blob != "":
|
||||
return ChatBlob(f"Awaiting plants ({len(self.towercache.plant)})", blob.strip("\n"))
|
||||
else:
|
||||
return f"There are no sites awaiting plants."
|
||||
@@ -0,0 +1,358 @@
|
||||
import time
|
||||
|
||||
from core.chat_blob import ChatBlob
|
||||
from core.command_param_types import Const, Int, NamedParameters
|
||||
from core.decorators import instance, command, event
|
||||
from core.job_scheduler import JobScheduler
|
||||
from core.logger import Logger
|
||||
from core.public_channel_service import PublicChannelService
|
||||
from core.setting_service import SettingService
|
||||
from core.text import Text
|
||||
from core.tyrbot import Tyrbot
|
||||
from modules.core.accounting.services.account_service import AccountService
|
||||
from modules.raidbot.tower.tower_controller import TowerController
|
||||
from modules.raidbot.tower.tower_service import TowerService
|
||||
from modules.standard.helpbot.playfield_controller import PlayfieldController
|
||||
|
||||
|
||||
@instance()
|
||||
class TowerAttackController:
|
||||
def __init__(self):
|
||||
self.logger = Logger(__name__)
|
||||
|
||||
def inject(self, registry):
|
||||
self.bot: Tyrbot = registry.get_instance("bot")
|
||||
self.db = registry.get_instance("db")
|
||||
self.text: Text = registry.get_instance("text")
|
||||
self.settings: SettingService = registry.get_instance("setting_service")
|
||||
self.util = registry.get_instance("util")
|
||||
self.tower: TowerController = registry.get_instance("tower_controller")
|
||||
self.towerservice: TowerService = registry.get_instance("tower_service")
|
||||
self.event_service = registry.get_instance("event_service")
|
||||
self.command_alias_service = registry.get_instance("command_alias_service")
|
||||
self.public_channel_service: PublicChannelService = registry.get_instance("public_channel_service")
|
||||
self.job_scheduler: JobScheduler = registry.get_instance("job_scheduler")
|
||||
self.playfield_controller: PlayfieldController = registry.get_instance("playfield_controller")
|
||||
self.account_service: AccountService = registry.get_instance("account_service")
|
||||
|
||||
def start(self):
|
||||
self.db.exec(
|
||||
"CREATE TABLE IF NOT EXISTS tower_attacker ("
|
||||
"id INT PRIMARY KEY AUTO_INCREMENT, "
|
||||
"att_org_name VARCHAR(50) NOT NULL, "
|
||||
"att_faction VARCHAR(10) NOT NULL, "
|
||||
"att_char_id INT, att_char_name VARCHAR(20) NOT NULL, "
|
||||
"att_level INT NOT NULL, "
|
||||
"att_ai_level INT NOT NULL, "
|
||||
"att_profession VARCHAR(15) NOT NULL, "
|
||||
"x_coord INT NOT NULL, "
|
||||
"y_coord INT NOT NULL, "
|
||||
"is_victory SMALLINT NOT NULL, "
|
||||
"tower_battle_id INT NOT NULL, "
|
||||
"created_at INT NOT NULL)")
|
||||
self.db.exec(
|
||||
"CREATE TABLE IF NOT EXISTS tower_battle ("
|
||||
"id INT PRIMARY KEY AUTO_INCREMENT, "
|
||||
"playfield_id INT NOT NULL, "
|
||||
"site_number INT NOT NULL, "
|
||||
"def_org_name VARCHAR(50) NOT NULL, "
|
||||
"def_faction VARCHAR(10) NOT NULL, "
|
||||
"is_finished INT NOT NULL, "
|
||||
"battle_type VARCHAR(20) NOT NULL, "
|
||||
"last_updated INT NOT NULL)")
|
||||
|
||||
self.command_alias_service.add_alias("victory", "attacks")
|
||||
|
||||
@command(command="attacks", params=[NamedParameters(["page"])], access_level="member",
|
||||
description="Show recent tower attacks and victories")
|
||||
def attacks_cmd(self, _, named_params):
|
||||
page = int(named_params.page or "1")
|
||||
|
||||
page_size = 30
|
||||
offset = (page - 1) * page_size
|
||||
|
||||
sql = """
|
||||
SELECT
|
||||
b.*,
|
||||
a.*,
|
||||
COALESCE(a.att_level, 0) AS att_level,
|
||||
COALESCE(a.att_ai_level, 0) AS att_ai_level,
|
||||
p.short_name,
|
||||
b.id AS battle_id
|
||||
FROM
|
||||
tower_battle b
|
||||
LEFT JOIN tower_attacker a ON
|
||||
a.tower_battle_id = b.id
|
||||
LEFT JOIN playfields p ON
|
||||
p.id = b.playfield_id
|
||||
ORDER BY
|
||||
b.last_updated DESC,
|
||||
a.created_at DESC
|
||||
LIMIT %d, %d
|
||||
""" % (offset, page_size)
|
||||
|
||||
data = self.db.query(sql)
|
||||
t = int(time.time())
|
||||
|
||||
blob = self.check_for_all_towers_channel()
|
||||
|
||||
if page > 1:
|
||||
blob += " " + self.text.make_chatcmd("<< Page %d" % (page - 1), self.get_chat_command(page - 1))
|
||||
if len(data) > 0:
|
||||
blob += " Page " + str(page)
|
||||
blob += " " + self.text.make_chatcmd("Page %d >>" % (page + 1), self.get_chat_command(page + 1))
|
||||
blob += "\n"
|
||||
|
||||
current_battle_id = -1
|
||||
for row in data:
|
||||
if current_battle_id != row.battle_id:
|
||||
blob += "\n<pagebreak>"
|
||||
current_battle_id = row.battle_id
|
||||
blob += self.format_battle_info(row, t)
|
||||
blob += self.text.make_tellcmd("More Info", "attacks battle %d" % row.battle_id) + "\n"
|
||||
blob += "<header2>Attackers:</header2>\n"
|
||||
|
||||
blob += "<tab>" + self.format_attacker(row) + "\n"
|
||||
|
||||
return ChatBlob("Tower Attacks", blob)
|
||||
|
||||
@command(command="attacks", params=[Const("battle"), Int("battle_id")], access_level="member",
|
||||
description="Show battle info for a specific battle")
|
||||
def attacks_battle_cmd(self, _, _1, battle_id):
|
||||
battle = self.db.query_single(
|
||||
"SELECT b.*, p.short_name FROM tower_battle b "
|
||||
"LEFT JOIN playfields p ON p.id = b.playfield_id WHERE b.id = ?",
|
||||
[battle_id])
|
||||
if not battle:
|
||||
return "Could not find battle with ID <highlight>%d</highlight>." % battle_id
|
||||
|
||||
t = int(time.time())
|
||||
|
||||
attackers = self.db.query("SELECT * FROM tower_attacker WHERE tower_battle_id = ? ORDER BY created_at DESC",
|
||||
[battle_id])
|
||||
|
||||
first_activity = attackers[-1].created_at if len(attackers) > 0 else battle.last_updated
|
||||
|
||||
blob = self.check_for_all_towers_channel()
|
||||
blob += self.format_battle_info(battle, t)
|
||||
blob += f"Duration: <highlight>" \
|
||||
f"{self.util.time_to_readable(battle.last_updated - first_activity)}</highlight>\n\n"
|
||||
blob += "<header2>Attackers:</header2>\n"
|
||||
|
||||
for row in attackers:
|
||||
blob += "<tab>" + self.format_attacker(row)
|
||||
blob += " " + self.format_timestamp(row.created_at, t)
|
||||
blob += "\n"
|
||||
|
||||
return ChatBlob("Battle Info %d" % battle_id, blob)
|
||||
|
||||
@event(event_type=TowerController.TOWER_ATTACK_EVENT, description="Warn on Tower attacks", is_hidden=True)
|
||||
def tower_attack_event(self, _, event_data):
|
||||
t = int(time.time())
|
||||
site_number = self.find_closest_site_number(event_data.location.playfield.id, event_data.location.x_coord,
|
||||
event_data.location.y_coord)
|
||||
|
||||
attacker = event_data.attacker or {}
|
||||
defender = event_data.defender
|
||||
|
||||
battle = self.find_or_create_battle(event_data.location.playfield.id, site_number, defender.org_name,
|
||||
defender.faction, "attack", t)
|
||||
# print(battle)
|
||||
self.db.exec(
|
||||
"INSERT INTO tower_attacker (att_org_name, att_faction, att_char_id, att_char_name, "
|
||||
"att_level, att_ai_level, att_profession, "
|
||||
"x_coord, y_coord, is_victory, tower_battle_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
[attacker.get("org_name", ""), attacker.get("faction", ""), attacker.get("char_id", 0),
|
||||
attacker.get("name", ""), attacker.get("level", 0),
|
||||
attacker.get("ai_level", 0), attacker.get("profession", ""), event_data.location.x_coord,
|
||||
event_data.location.y_coord, 0, battle.id, t])
|
||||
|
||||
@event(event_type=TowerController.TOWER_ATTACK_EVENT, description="Notify Tower attacks on own org")
|
||||
def tower_def_event(self, _, event_data):
|
||||
if event_data.attacker.get("name", None) is not None:
|
||||
field_id = self.find_closest_site_number(event_data.location.playfield.id, event_data.location.x_coord,
|
||||
event_data.location.y_coord)
|
||||
row = self.db.query_single(
|
||||
"SELECT t.*, p.short_name, p.long_name FROM tower_site t "
|
||||
"JOIN playfields p ON t.playfield_id = p.id WHERE t.playfield_id = ? AND site_number = ?",
|
||||
[event_data.location.playfield.id, field_id])
|
||||
lca = self.text.format_page("%s - %d" % (event_data.location.playfield.long_name, field_id),
|
||||
self.tower.format_site_info(row))
|
||||
attacker = self.text.format_char_info(event_data.attacker)
|
||||
add = ""
|
||||
if account := self.account_service.get_account(event_data.attacker.char_id):
|
||||
if account.disabled == 0 and account.member != 0:
|
||||
add = " :: <red>He's a <myname> Raider!</red>"
|
||||
self.bot.send_private_channel_message(
|
||||
f"[<cyan>NW</cyan>] "
|
||||
f"<{event_data.defender.faction.lower()}>"
|
||||
f"{event_data.defender.org_name}"
|
||||
f"</{event_data.defender.faction.lower()}> "
|
||||
f"attacked by {attacker} in {lca}{add}")
|
||||
|
||||
@event(event_type=TowerController.TOWER_VICTORY_EVENT, description="Record tower victories", is_hidden=True)
|
||||
def tower_victory_event(self, _, event_data):
|
||||
t = int(time.time())
|
||||
if event_data.type == "attack":
|
||||
row = self.get_last_attack(event_data.winner.faction, event_data.winner.org_name, event_data.loser.faction,
|
||||
event_data.loser.org_name, event_data.location.playfield.id, t)
|
||||
|
||||
if not row:
|
||||
site_number = 0
|
||||
is_finished = 1
|
||||
self.db.exec(
|
||||
"INSERT INTO tower_battle (playfield_id, site_number, def_org_name, def_faction, "
|
||||
"is_finished, battle_type, last_updated) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
[event_data.location.playfield.id, site_number, event_data.loser.org_name, event_data.loser.faction,
|
||||
is_finished, event_data.type, t])
|
||||
battle_id = self.db.last_insert_id()
|
||||
|
||||
attacker = event_data.winner or {}
|
||||
self.db.exec(
|
||||
"INSERT INTO tower_attacker (att_org_name, att_faction, att_char_id, "
|
||||
"att_char_name, att_level, att_ai_level, att_profession, "
|
||||
"x_coord, y_coord, is_victory, tower_battle_id, created_at) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
[attacker.get("org_name", ""), attacker.get("faction", ""), attacker.get("char_id", 0),
|
||||
attacker.get("name", ""), attacker.get("level", 0),
|
||||
attacker.get("ai_level", 0), attacker.get("profession", ""), 0, 0, 0, battle_id, t])
|
||||
else:
|
||||
is_victory = 1
|
||||
self.db.exec("UPDATE tower_attacker SET is_victory = ? WHERE id = ?", [is_victory, row.attack_id])
|
||||
|
||||
is_finished = 1
|
||||
self.db.exec("UPDATE tower_battle SET is_finished = ?, last_updated = ? WHERE id = ?",
|
||||
[is_finished, t, row.battle_id])
|
||||
elif event_data.type == "terminated":
|
||||
site_number = 0
|
||||
is_finished = 1
|
||||
self.db.exec(
|
||||
"INSERT INTO tower_battle (playfield_id, site_number, def_org_name, def_faction, "
|
||||
"is_finished, battle_type, last_updated) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
[event_data.location.playfield.id, site_number, event_data.loser.org_name, event_data.loser.faction,
|
||||
is_finished, event_data.type, t])
|
||||
else:
|
||||
raise Exception("Unknown victory event type: '%s'" % event_data.type)
|
||||
|
||||
def format_attacker(self, row):
|
||||
level = ("%d/<green>%d</green>" % (
|
||||
row.att_level, row.att_ai_level)) if row.att_ai_level > 0 else "%d" % row.att_level
|
||||
org = row.att_org_name + " " if row.att_org_name else ""
|
||||
victor = " - <notice>Winner!</notice>" if row.is_victory else ""
|
||||
return "%s (%s %s) %s(%s)%s" % (
|
||||
row.att_char_name or "Unknown attacker", level, row.att_profession, org, row.att_faction, victor)
|
||||
|
||||
def find_closest_site_number(self, playfield_id, x_coord, y_coord):
|
||||
# noinspection SqlUnused
|
||||
sql = """
|
||||
SELECT
|
||||
site_number,
|
||||
((x_distance * x_distance) + (y_distance * y_distance)) radius
|
||||
FROM
|
||||
(SELECT
|
||||
playfield_id,
|
||||
site_number,
|
||||
min_ql,
|
||||
max_ql,
|
||||
x_coord,
|
||||
y_coord,
|
||||
site_name,
|
||||
(x_coord - ?) as x_distance,
|
||||
(y_coord - ?) as y_distance
|
||||
FROM
|
||||
tower_site
|
||||
WHERE
|
||||
playfield_id = ?) t
|
||||
ORDER BY
|
||||
radius
|
||||
LIMIT 1"""
|
||||
|
||||
row = self.db.query_single(sql, [x_coord, y_coord, playfield_id])
|
||||
if row:
|
||||
return row.site_number
|
||||
else:
|
||||
return 0
|
||||
|
||||
def find_or_create_battle(self, playfield_id, site_number, org_name, faction, battle_type, t):
|
||||
last_updated = t - (8 * 3600)
|
||||
is_finished = 0
|
||||
|
||||
sql = """
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
tower_battle
|
||||
WHERE
|
||||
playfield_id = ?
|
||||
AND site_number = ?
|
||||
AND is_finished = ?
|
||||
AND def_org_name = ?
|
||||
AND def_faction = ?
|
||||
AND last_updated >= ?
|
||||
"""
|
||||
|
||||
battle = self.db.query_single(sql, [playfield_id, site_number, is_finished, org_name, faction, last_updated])
|
||||
|
||||
if battle:
|
||||
self.db.exec("UPDATE tower_battle SET last_updated = ? WHERE id = ?", [t, battle.id])
|
||||
return battle
|
||||
else:
|
||||
self.db.exec(
|
||||
"INSERT INTO tower_battle (playfield_id, site_number, def_org_name, def_faction, "
|
||||
"is_finished, battle_type, last_updated) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
[playfield_id, site_number, org_name, faction, is_finished, battle_type, t])
|
||||
time.sleep(0.5)
|
||||
return self.db.query_single("SELECT * FROM tower_battle WHERE id = ?", [self.db.last_insert_id()])
|
||||
|
||||
def get_last_attack(self, att_faction, att_org_name, def_faction, def_org_name, playfield_id, t):
|
||||
last_updated = t - (8 * 3600)
|
||||
is_finished = 0
|
||||
|
||||
sql = """
|
||||
SELECT
|
||||
b.id AS battle_id,
|
||||
a.id AS attack_id,
|
||||
b.playfield_id as pf_id,
|
||||
b.site_number as site
|
||||
FROM
|
||||
tower_battle b
|
||||
JOIN tower_attacker a ON
|
||||
a.tower_battle_id = b.id
|
||||
WHERE
|
||||
a.att_faction = ?
|
||||
AND a.att_org_name = ?
|
||||
AND b.def_faction = ?
|
||||
AND b.def_org_name = ?
|
||||
AND b.playfield_id = ?
|
||||
AND b.is_finished = ?
|
||||
AND b.last_updated >= ?
|
||||
ORDER BY
|
||||
last_updated DESC
|
||||
LIMIT 1"""
|
||||
|
||||
return self.db.query_single(sql,
|
||||
[att_faction, att_org_name, def_faction, def_org_name, playfield_id, is_finished,
|
||||
last_updated])
|
||||
|
||||
def format_battle_info(self, row, t):
|
||||
blob = ""
|
||||
defeated = " - <notice>Defeated!</notice>" if row.is_finished else ""
|
||||
blob += "Site: <highlight>%s %s</highlight>\n" % (row.short_name, row.site_number or "?")
|
||||
blob += "Defender: <highlight>%s</highlight> (%s)%s\n" % (row.def_org_name, row.def_faction, defeated)
|
||||
blob += "Last Activity: %s\n" % self.format_timestamp(row.last_updated, t)
|
||||
return blob
|
||||
|
||||
def format_timestamp(self, t, current_t):
|
||||
return "<highlight>%s</highlight> (%s ago)" % (
|
||||
self.util.format_datetime(t), self.util.time_to_readable(current_t - t))
|
||||
|
||||
def get_chat_command(self, page):
|
||||
return "/tell <myname> attacks --page=%d" % page
|
||||
|
||||
def check_for_all_towers_channel(self):
|
||||
if not self.public_channel_service.get_channel_name(TowerController.ALL_TOWERS_ID):
|
||||
return "Notice: The bot must belong to an org and be promoted to a rank that is high enough " \
|
||||
"to have the All Towers channel (e.g., Squad Commander) in order for the " \
|
||||
"<symbol>attacks command to work correctly.\n\n"
|
||||
else:
|
||||
return ""
|
||||
@@ -0,0 +1,207 @@
|
||||
import re
|
||||
|
||||
from core.aochat import server_packets
|
||||
from core.conn import Conn
|
||||
from core.db import DB
|
||||
from core.decorators import instance, event
|
||||
from core.dict_object import DictObject
|
||||
from core.event_service import EventService
|
||||
from core.logger import Logger
|
||||
from core.lookup.pork_service import PorkService
|
||||
from core.public_channel_service import PublicChannelService
|
||||
from core.text import Text
|
||||
from core.tyrbot import Tyrbot
|
||||
from modules.standard.helpbot.playfield_controller import PlayfieldController
|
||||
|
||||
|
||||
@instance()
|
||||
class TowerController:
|
||||
TOWER_ATTACK_EVENT = "tower_attack"
|
||||
TOWER_VICTORY_EVENT = "tower_victory"
|
||||
|
||||
TOWER_BATTLE_OUTCOME_ID = 42949672962
|
||||
ALL_TOWERS_ID = 42949672960
|
||||
|
||||
# The %s organization %s just entered a state of war!
|
||||
# %s attacked the %s organization %s's tower in %s at location (%d,%d).
|
||||
ATTACK_1 = [506, 12753364]
|
||||
ATTACK_2 = re.compile(r"^(.+) just attacked the (clan|neutral|omni) organization (.+)'s tower in (.+) "
|
||||
r"at location \((\d+), (\d+)\).\n$")
|
||||
|
||||
VICTORY_1 = re.compile(r"^Notum Wars Update: Victory to the (Clan|Neutral|Omni)s!!!$")
|
||||
VICTORY_2 = re.compile(r"^The (Clan|Neutral|Omni) organization (.+) attacked the (Clan|Neutral|Omni) (.+) "
|
||||
r"at their base in (.+). The attackers won!!$")
|
||||
VICTORY_3 = [506, 147506468] # 'Notum Wars Update: The %s organization %s lost their base in %s.'
|
||||
|
||||
def __init__(self):
|
||||
self.logger = Logger(__name__)
|
||||
|
||||
def inject(self, registry):
|
||||
self.bot: Tyrbot = registry.get_instance("bot")
|
||||
self.db: DB = registry.get_instance("db")
|
||||
self.text: Text = registry.get_instance("text")
|
||||
self.event_service: EventService = registry.get_instance("event_service")
|
||||
self.pork_service: PorkService = registry.get_instance("pork_service")
|
||||
self.playfield_controller: PlayfieldController = registry.get_instance("playfield_controller")
|
||||
self.public_channel_service: PublicChannelService = registry.get_instance("public_channel_service")
|
||||
|
||||
def pre_start(self):
|
||||
self.event_service.register_event_type(self.TOWER_ATTACK_EVENT)
|
||||
self.event_service.register_event_type(self.TOWER_VICTORY_EVENT)
|
||||
self.bot.register_packet_handler(server_packets.PublicChannelMessage.id, self.handle_public_channel_message)
|
||||
self.db.load_sql_file(self.module_dir + "/" + "tower_site.sql", pre_optimized=True)
|
||||
self.db.create_view("tower_site")
|
||||
|
||||
@event(event_type="connect", description="Check if All Towers channel is available", is_hidden=True)
|
||||
def handle_connect_event(self, _, _1):
|
||||
if self.public_channel_service.org_id and not self.public_channel_service.get_channel_id("All Towers"):
|
||||
self.logger.warning("This bot is a member of an org but does not have access to 'All Towers' channel and "
|
||||
"therefore will not receive tower attack messages")
|
||||
|
||||
def format_site_info(self, row):
|
||||
blob = f"Short name: <highlight>{row.short_name} {row.site_number:d}</highlight>\n"
|
||||
blob += f"Long name: <highlight>{row.site_name}, {row.long_name}</highlight>\n"
|
||||
blob += f"Level range: <highlight>{row.min_ql:d}-{row.max_ql:d}</highlight>\n"
|
||||
blob += "Coordinates: %s\n" % self.text.make_chatcmd(f"{row.x_coord:d}x{row.y_coord:d}",
|
||||
f"/waypoint {row.x_coord:d} "
|
||||
f"{row.y_coord:d} "
|
||||
f"{row.playfield_id:d}")
|
||||
|
||||
return blob
|
||||
|
||||
def handle_public_channel_message(self, conn: Conn, packet: server_packets.PublicChannelMessage):
|
||||
if conn.id != "main":
|
||||
return
|
||||
|
||||
if packet.channel_id == self.TOWER_BATTLE_OUTCOME_ID:
|
||||
victory = self.get_victory_event(packet)
|
||||
|
||||
if victory:
|
||||
# self.logger.debug("tower victory packet: %s" % str(packet))
|
||||
|
||||
# lookup playfield
|
||||
playfield_name = victory.location.playfield.long_name
|
||||
victory.location.playfield = self.playfield_controller.get_playfield_by_name(playfield_name) or \
|
||||
DictObject()
|
||||
victory.location.playfield.long_name = playfield_name
|
||||
print(victory)
|
||||
self.event_service.fire_event(self.TOWER_VICTORY_EVENT, victory)
|
||||
elif packet.channel_id == self.ALL_TOWERS_ID:
|
||||
attack = self.get_attack_event(packet)
|
||||
|
||||
if attack:
|
||||
# self.logger.debug("tower attack packet: %s" % str(packet))
|
||||
|
||||
# lookup playfield
|
||||
playfield_name = attack.location.playfield.long_name
|
||||
attack.location.playfield = self.playfield_controller.get_playfield_by_name(playfield_name) or \
|
||||
DictObject()
|
||||
attack.location.playfield.long_name = playfield_name
|
||||
|
||||
# lookup attacker
|
||||
name = attack.attacker.name
|
||||
faction = attack.attacker.faction
|
||||
org_name = attack.attacker.org_name
|
||||
char_info = self.pork_service.get_character_info(name)
|
||||
attack.attacker = char_info or DictObject()
|
||||
attack.attacker.name = name
|
||||
attack.attacker.faction = faction or attack.attacker.get("faction", "Unknown")
|
||||
attack.attacker.org_name = org_name
|
||||
|
||||
self.event_service.fire_event(self.TOWER_ATTACK_EVENT, attack)
|
||||
|
||||
def get_attack_event(self, packet: server_packets.PublicChannelMessage):
|
||||
if packet.extended_message and \
|
||||
[packet.extended_message.category_id, packet.extended_message.instance_id] == self.ATTACK_1:
|
||||
params = packet.extended_message.params
|
||||
return DictObject({
|
||||
"attacker": {
|
||||
"name": params[2],
|
||||
"faction": params[0].capitalize(),
|
||||
"org_name": params[1]
|
||||
},
|
||||
"defender": {
|
||||
"faction": params[3].capitalize(),
|
||||
"org_name": params[4]
|
||||
},
|
||||
"location": {
|
||||
"playfield": {
|
||||
"long_name": params[5]
|
||||
},
|
||||
"x_coord": params[6],
|
||||
"y_coord": params[7]
|
||||
}
|
||||
})
|
||||
else:
|
||||
match = self.ATTACK_2.match(packet.message)
|
||||
if match:
|
||||
return DictObject({
|
||||
"attacker": {
|
||||
"name": match.group(1),
|
||||
"faction": "",
|
||||
"org_name": ""
|
||||
},
|
||||
"defender": {
|
||||
"faction": match.group(2).capitalize(),
|
||||
"org_name": match.group(3)
|
||||
},
|
||||
"location": {
|
||||
"playfield": {
|
||||
"long_name": match.group(4)
|
||||
},
|
||||
"x_coord": match.group(5),
|
||||
"y_coord": match.group(6)
|
||||
}
|
||||
})
|
||||
|
||||
# Unknown attack
|
||||
self.logger.warning("Unknown tower attack: " + str(packet))
|
||||
return None
|
||||
|
||||
def get_victory_event(self, packet: server_packets.PublicChannelMessage):
|
||||
match = self.VICTORY_1.match(packet.message)
|
||||
if match:
|
||||
return None
|
||||
|
||||
match = self.VICTORY_2.match(packet.message)
|
||||
if match:
|
||||
return DictObject({
|
||||
"type": "attack",
|
||||
"winner": {
|
||||
"faction": match.group(1).capitalize(),
|
||||
"org_name": match.group(2)
|
||||
},
|
||||
"loser": {
|
||||
"faction": match.group(3).capitalize(),
|
||||
"org_name": match.group(4)
|
||||
},
|
||||
"location": {
|
||||
"playfield": {
|
||||
"long_name": match.group(5)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if packet.extended_message and \
|
||||
[packet.extended_message.category_id, packet.extended_message.instance_id] == self.VICTORY_3:
|
||||
params = packet.extended_message.params
|
||||
return DictObject({
|
||||
"type": "terminated",
|
||||
"winner": {
|
||||
"faction": params[0].capitalize(),
|
||||
"org_name": params[1]
|
||||
},
|
||||
"loser": {
|
||||
"faction": params[0].capitalize(),
|
||||
"org_name": params[1]
|
||||
},
|
||||
"location": {
|
||||
"playfield": {
|
||||
"long_name": params[2]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
# Unknown victory
|
||||
self.logger.warning("Unknown tower victory: " + str(packet))
|
||||
return None
|
||||
@@ -0,0 +1,204 @@
|
||||
from core.aochat.BaseModule import BaseModule
|
||||
from core.chat_blob import ChatBlob
|
||||
from core.command_alias_service import CommandAliasService
|
||||
from core.command_param_types import Options, Int, Any, Const, NamedParameters
|
||||
from core.db import DB
|
||||
from core.decorators import instance, command
|
||||
from core.dict_object import DictObject
|
||||
from core.event_service import EventService
|
||||
from core.lookup.pork_service import PorkService
|
||||
from core.public_channel_service import PublicChannelService
|
||||
from core.text import Text
|
||||
from core.tyrbot import Tyrbot
|
||||
from core.util import Util
|
||||
from modules.raidbot.tower.tower_service import TowerService
|
||||
from modules.standard.helpbot.playfield_controller import PlayfieldController
|
||||
|
||||
|
||||
@instance()
|
||||
class TowerHotController(BaseModule):
|
||||
PAGE_SIZE = 9
|
||||
|
||||
# noinspection DuplicatedCode
|
||||
|
||||
def inject(self, registry):
|
||||
self.bot: Tyrbot = registry.get_instance("bot")
|
||||
self.db: DB = registry.get_instance("db")
|
||||
self.util: Util = registry.get_instance("util")
|
||||
self.text: Text = registry.get_instance("text")
|
||||
self.event_service: EventService = registry.get_instance("event_service")
|
||||
self.pork_service: PorkService = registry.get_instance("pork_service")
|
||||
self.playfield_controller: PlayfieldController = registry.get_instance("playfield_controller")
|
||||
self.public_channel_service: PublicChannelService = registry.get_instance("public_channel_service")
|
||||
self.towercache: TowerService = registry.get_instance("tower_service")
|
||||
self.command_alias_service: CommandAliasService = registry.get_instance("command_alias_service")
|
||||
|
||||
def pre_start(self):
|
||||
self.command_alias_service.add_alias('towers', "lc")
|
||||
self.command_alias_service.add_alias('tower', "lc")
|
||||
|
||||
@command(command="hot",
|
||||
params=[Options(['tl1', 'tl2', 'tl3', 'tl4', 'tl5', 'tl6', 'tl7']), Any('faction', is_optional=True),
|
||||
NamedParameters(["page"])],
|
||||
access_level="member",
|
||||
description="Shows hot playfields")
|
||||
def hot_tl(self, _, tl, faction: str, named_params):
|
||||
if faction:
|
||||
if faction.startswith("--page="):
|
||||
named_params = DictObject({'page': faction[7:]})
|
||||
faction = None
|
||||
if faction is not None and faction.lower() not in ['omni', 'clan', 'neut', 'neutral']:
|
||||
return f"Unknown faction: {faction}"
|
||||
tl = tl[2:]
|
||||
page = int(named_params.page or "1")
|
||||
offset = (page - 1) * self.PAGE_SIZE
|
||||
towers = self.towercache.get_towers_hot_tl(int(tl), faction)
|
||||
return self.text.format_pagination(towers, offset, page, self.formatter, f"Hot Sites TL{tl} ({len(towers)})",
|
||||
f"There are no hot sites for TL <highlight>{tl}</highlight>.",
|
||||
f'hot tl{tl} {faction or ""}', 9)
|
||||
|
||||
def formatter(self, row, _, data):
|
||||
return self.towercache.format_entry(row, len(data))
|
||||
|
||||
@command(command="hot",
|
||||
params=[Int('level', is_optional=True), Any('faction', is_optional=True), NamedParameters(["page"])],
|
||||
access_level="member",
|
||||
description="Shows hot playfields by level")
|
||||
def hot_level(self, _, level, faction, named_params):
|
||||
if faction:
|
||||
if faction.startswith("--page="):
|
||||
named_params = DictObject({'page': faction[7:]})
|
||||
faction = None
|
||||
if faction is not None and faction.lower() not in ['omni', 'clan', 'neut', 'neutral']:
|
||||
return f"Unknown faction: {faction}"
|
||||
if level:
|
||||
if level < 0 | level > 220:
|
||||
return f"Level out of range: {level}"
|
||||
page = int(named_params.page or "1")
|
||||
offset = (page - 1) * self.PAGE_SIZE
|
||||
towers = self.towercache.get_towers_hot_level(level, faction)
|
||||
level = f"{level}" if level else ""
|
||||
faction = f"{faction} " if faction else ""
|
||||
return self.text.format_pagination(towers, offset, page, self.formatter, f"Hot Towersites ({len(towers)})",
|
||||
f"There are no hot sites.", f'hot {level}{faction}', 9)
|
||||
|
||||
@command(command="free", params=[],
|
||||
access_level="member",
|
||||
description="Shows hot playfields by level")
|
||||
def free(self, _, ):
|
||||
blob = ""
|
||||
towers = [x for x in self.towercache.get_free() if x.short_name not in ['AND', 'GTC']]
|
||||
for row in towers:
|
||||
blob += self.towercache.format_entry(row, len(towers))
|
||||
|
||||
return ChatBlob(f"FREE Towersites ({len(towers)})", blob) if blob else f"No free towersites found."
|
||||
|
||||
@command(command="lc", params=[], access_level="member",
|
||||
description="See a list of land control tower sites in a particular playfield")
|
||||
def lc(self, _):
|
||||
hot, cold, unplanted = 0, 0, 0
|
||||
clan, omni, neut = 0, 0, 0
|
||||
last_pf = 0
|
||||
previous = {}
|
||||
blob = ""
|
||||
|
||||
def number(numb):
|
||||
if numb < 10:
|
||||
return f"<black>0</black>{numb}"
|
||||
return numb
|
||||
|
||||
for tower in self.towercache.get_towers_all():
|
||||
if tower.id != last_pf:
|
||||
if last_pf == 0:
|
||||
previous = tower
|
||||
last_pf = tower.id
|
||||
continue
|
||||
blob += f"<red>{number(hot)}</red> <cyan>{number(cold)}</cyan> <grey>{number(unplanted)}</grey> :: " \
|
||||
f"<clan>{number(clan)}</clan> <neutral>{number(neut)}</neutral> <omni>{number(omni)}</omni> " \
|
||||
f"[{self.text.make_tellcmd(previous.short_name, f'lc {previous.long_name}')}] " \
|
||||
f"{previous.long_name}\n"
|
||||
hot, cold, unplanted = 0, 0, 0
|
||||
clan, omni, neut = 0, 0, 0
|
||||
|
||||
previous = tower
|
||||
last_pf = tower.id
|
||||
|
||||
site = self.towercache.is_hot(tower)
|
||||
if site == 0:
|
||||
cold += 1
|
||||
elif site == -1:
|
||||
unplanted += 1
|
||||
elif site == 1:
|
||||
hot += 1
|
||||
faction = tower.get('faction', None)
|
||||
if faction:
|
||||
faction = faction.lower
|
||||
if faction == "omni":
|
||||
omni += 1
|
||||
elif faction == "clan":
|
||||
clan += 1
|
||||
else:
|
||||
neut += 1
|
||||
|
||||
return ChatBlob('All Tower Sites', blob)
|
||||
|
||||
@command(command="lc", params=[Const("org"), Any("org_name", is_optional=True)], access_level="member",
|
||||
description="See a list of land control tower sites in a particular playfield")
|
||||
def lc_org(self, _, _1, org):
|
||||
towers = []
|
||||
try:
|
||||
org = int(org)
|
||||
except ValueError:
|
||||
pass
|
||||
if type(org) == str:
|
||||
orgs = self.db.query(
|
||||
"SELECT * from all_orgs where org_name LIKE ? and org_id in "
|
||||
"(SELECT org_id from towers group by org_id)",
|
||||
[f"%{org.replace(' ', '%')}%"])
|
||||
if len(orgs) == 1:
|
||||
towers = self.towercache.get_towers_by_org(orgs[0].org_id)
|
||||
|
||||
elif len(orgs) == 0:
|
||||
return "Your search returned no orgs."
|
||||
else:
|
||||
blob = "Your search had multiple results; please pick an org:<br>"
|
||||
for org in orgs:
|
||||
blob += "[%s] <highlight>%s<end> (<highlight>%s<end>) <%s>%s<end> [<highlight>%s<end> " \
|
||||
"members]<br><pagebreak>" \
|
||||
% (self.text.make_chatcmd("Towers", "/tell <myname> lc org %s" % org.org_id),
|
||||
org.org_name, org.org_id, org.faction.lower(), org.faction, org.member_count)
|
||||
return ChatBlob("Pick an Org", blob)
|
||||
elif type(org) == int:
|
||||
if len(self.db.query("SELECT org_id from all_orgs where org_id=?", [int(org)])) == 0:
|
||||
return "Your search returned no orgs."
|
||||
else:
|
||||
towers = self.towercache.get_towers_by_org(org)
|
||||
|
||||
title = f"Towersites of the org {org}"
|
||||
|
||||
blob = ""
|
||||
for tower in towers:
|
||||
blob += self.towercache.format_entry(tower, len(towers))
|
||||
|
||||
return ChatBlob(title, blob)
|
||||
|
||||
@command(command="lc", params=[Any("playfield"), Int("site_number", is_optional=True)], access_level="member",
|
||||
description="See a list of land control tower sites in a particular playfield")
|
||||
def lc_playfield(self, _, playfield_name, site_number):
|
||||
playfield = self.playfield_controller.get_playfield_by_name(playfield_name)
|
||||
if not playfield:
|
||||
return "Could not find playfield <highlight>%s</highlight>." % playfield_name
|
||||
if site_number:
|
||||
title = f"Tower site x{site_number} in {playfield.long_name}"
|
||||
towers = self.towercache.get_towers_by_pf_site(playfield.id, site_number)
|
||||
else:
|
||||
title = f"Tower sites in {playfield.long_name}"
|
||||
towers = self.towercache.get_towers_by_pf(playfield.id)
|
||||
|
||||
blob = ""
|
||||
for tower in towers:
|
||||
blob += self.towercache.format_entry(tower, len(towers))
|
||||
if site_number:
|
||||
blob += "More to come... stay tuned."
|
||||
|
||||
return ChatBlob(title, blob)
|
||||
@@ -0,0 +1,342 @@
|
||||
import sys
|
||||
import time
|
||||
|
||||
from mysql.connector.cursor import CursorBase
|
||||
from requests import Session
|
||||
|
||||
from conf.config import BotConfig
|
||||
from core.aochat.BaseModule import BaseModule
|
||||
from core.db import DB
|
||||
from core.decorators import instance, timerevent, event
|
||||
from core.job_scheduler import JobScheduler
|
||||
from core.text import Text
|
||||
from core.tyrbot import Tyrbot
|
||||
from core.util import Util
|
||||
from modules.raidbot.tower.tower_controller import TowerController
|
||||
from modules.standard.helpbot.playfield_controller import PlayfieldController
|
||||
|
||||
|
||||
@instance()
|
||||
class TowerService(BaseModule):
|
||||
# [{'org_name': "Dragons", 'hot': time.time() + 60 * 60}, {'org_name': "Most Wanted", 'hot': time.time() + 60 * 60}]
|
||||
attack_hot = []
|
||||
plant = []
|
||||
|
||||
def inject(self, registry):
|
||||
self.bot: Tyrbot = registry.get_instance("bot")
|
||||
self.db: DB = registry.get_instance("db")
|
||||
self.util: Util = registry.get_instance("util")
|
||||
self.text: Text = registry.get_instance("text")
|
||||
self.playfield_controller: PlayfieldController = registry.get_instance("playfield_controller")
|
||||
self.job_scheduler: JobScheduler = registry.get_instance("job_scheduler")
|
||||
|
||||
mod = __import__(f'conf.{sys.argv[1]}', fromlist=['BotConfig'])
|
||||
config: BotConfig = getattr(mod, 'BotConfig')
|
||||
if hasattr(config, "tower_url"):
|
||||
self.tower_url = config.tower_url
|
||||
else:
|
||||
self.tower_url = None
|
||||
|
||||
def pre_start(self):
|
||||
self.db.shared.exec("CREATE TABLE IF NOT EXISTS towers("
|
||||
"pf_id int not null, "
|
||||
"site_number int not null,"
|
||||
"ql int,"
|
||||
"x_coord int not null,"
|
||||
"y_coord int not null,"
|
||||
"org_id int,"
|
||||
"org_name varchar(255), "
|
||||
"faction varchar(32), "
|
||||
"close_time int,"
|
||||
"planted int,"
|
||||
"enabled tinyint, "
|
||||
"PRIMARY KEY (pf_id, site_number), "
|
||||
"INDEX ql(ql), INDEX close(close_time), "
|
||||
"INDEX planted(planted), INDEX enabled(enabled)) ENGINE MEMORY")
|
||||
self.db.create_view("towers")
|
||||
|
||||
@event(event_type=TowerController.TOWER_ATTACK_EVENT, description="Track planthot", is_hidden=True)
|
||||
def tower_attack(self, _, event_data):
|
||||
if event_data.attacker.get("org_id", None):
|
||||
self.attack_hot.append({'org_name': event_data.attacker.org_name, 'hot': time.time() + 60 * 60})
|
||||
|
||||
@event(event_type=TowerController.TOWER_VICTORY_EVENT, description="Send NW warnings")
|
||||
def victory(self, _, event_data):
|
||||
t = int(time.time())
|
||||
if event_data.type == "attack":
|
||||
row = self.get_last_attack(event_data.winner.faction, event_data.winner.org_name, event_data.loser.faction,
|
||||
event_data.loser.org_name, event_data.location.playfield.id, t)
|
||||
self.send_nw_warn(0,
|
||||
f'<{event_data.loser.faction.lower()}>'
|
||||
f'{event_data.loser.org_name}'
|
||||
f'</{event_data.loser.faction.lower()}>'
|
||||
f' Lost their Site at {event_data.location.playfield.short_name} x{row.site} - '
|
||||
f'<{event_data.winner.faction.lower()}>'
|
||||
f'{event_data.winner.org_name}'
|
||||
f'</{event_data.winner.faction.lower()}> <highlight>won!!</highlight>')
|
||||
self.plant.append({'pf': row.pf_id, 'site': row.site, 'plant': time.time() + 20 * 60 - 1})
|
||||
self.prepare_nw_warn(row.pf_id, row.site)
|
||||
|
||||
elif event_data.type == "terminated":
|
||||
field = self.db.query("SELECT * FROM towers t where t.org_name=? and t.pf_id=?",
|
||||
[event_data.loser.org_name, event_data.playfield.id])
|
||||
if len(field) == 1:
|
||||
field = field[0]
|
||||
self.plant.append({'pf': event_data.location.playfield.id,
|
||||
'site': field.site_number,
|
||||
'plant': time.time() + 20 * 60 - 1})
|
||||
self.send_nw_warn(0, f'<{event_data.loser.faction.lower()}>'
|
||||
f'{event_data.loser.org_name}'
|
||||
f'</{event_data.loser.faction.lower()}> Lost their Site at '
|
||||
f'{event_data.location.playfield.short_name} x{field.site_number}')
|
||||
self.prepare_nw_warn(event_data.location.playfield.id, field.site_number)
|
||||
return
|
||||
self.plant.append({'pf': event_data.location.playfield.id,
|
||||
'site': f"(<red>UKN</red>) PO: {event_data.loser.org_name}|{event_data.loser.faction}",
|
||||
'plant': time.time() + 20 * 60 - 1})
|
||||
|
||||
self.send_nw_warn(0,
|
||||
f'<{event_data.loser.faction.lower()}>'
|
||||
f'{event_data.loser.org_name}'
|
||||
f'</{event_data.loser.faction.lower()}> '
|
||||
f'Lost their Site in {event_data.location.playfield.long_name}')
|
||||
self.prepare_nw_warn(event_data.playfield.id, "(<red>UKN</red>)",
|
||||
f"(<red>UKN</red>) PO: {event_data.loser.org_name}|{event_data.loser.faction}")
|
||||
|
||||
@timerevent(budatime="4h", description="fetch the towerAPI cache", is_enabled=False)
|
||||
def fetch_tower_update(self, _1, _2):
|
||||
if self.tower_url:
|
||||
#
|
||||
# ONLY Access the API's via Tor... Tyrence is shadow-banning every IP Subnet
|
||||
# accessing it multiple times/hour whenever the limit parameter is being used....
|
||||
# ?limit is a parameter not documented anywhere, but allows pulling the whole list
|
||||
# On other API implementations this parameter has no effect,
|
||||
# as the server always responds with the full list.
|
||||
from torpy.http.requests import TorRequests
|
||||
with TorRequests() as tor_request:
|
||||
with tor_request.get_session(1) as session:
|
||||
session: Session
|
||||
r = session.get(self.tower_url)
|
||||
if not r:
|
||||
return
|
||||
if data := r.json()["results"]:
|
||||
blob = []
|
||||
for row in data:
|
||||
blob.append((row["playfield_id"], row["site_number"], row["ql"],
|
||||
row["x_coord"], row["y_coord"],
|
||||
row["org_id"], row['org_name'], row['faction'],
|
||||
row["close_time"], row["created_at"],
|
||||
row["enabled"]))
|
||||
|
||||
with self.db.lock:
|
||||
with self.db.pool.get_connection() as conn:
|
||||
with conn.cursor(dictionary=True) as cur:
|
||||
cur: CursorBase
|
||||
cur.executemany(
|
||||
"INSERT INTO towers (pf_id, site_number, "
|
||||
"ql, x_coord, y_coord, org_id, org_name, "
|
||||
"faction, close_time, planted, enabled) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE "
|
||||
"org_id=VALUE(org_id), "
|
||||
"org_name=VALUE(org_name), "
|
||||
"faction=VALUE(faction), "
|
||||
"close_time=VALUE(close_time),"
|
||||
"ql=VALUE(ql),"
|
||||
"planted=VALUE(planted) ", blob)
|
||||
rem = []
|
||||
for key, value in enumerate(sorted(self.attack_hot, key=lambda k: k['hot'])):
|
||||
if value['hot'] < time.time():
|
||||
rem.append(key)
|
||||
for key in reversed(rem):
|
||||
self.attack_hot.pop(key)
|
||||
|
||||
def day_time(self, day_t):
|
||||
if day_t > 86400:
|
||||
day_t -= 86400
|
||||
elif day_t < 0:
|
||||
day_t += 86400
|
||||
return day_t
|
||||
|
||||
def get(self, where_order="", param=None):
|
||||
if param is None:
|
||||
param = []
|
||||
return self.db.query(f"SELECT a.org_id, a.planted, a.close_time, a.ql, a.org_name, a.faction, "
|
||||
f"b.min_ql, b.max_ql, b.site_name, b.site_number, b.x_coord, b.y_coord, "
|
||||
f"c.id, c.long_name, c.short_name, "
|
||||
f"e.pvp_min, e.pvp_max "
|
||||
f"FROM towers a "
|
||||
f"INNER JOIN tower_site b ON a.site_number=b.site_number "
|
||||
f"LEFT JOIN playfields c ON a.pf_id = c.id "
|
||||
f"LEFT JOIN level e ON e.level = a.ql "
|
||||
f"WHERE a.pf_id = b.playfield_id and enabled = 1 "
|
||||
f"{where_order}", param)
|
||||
|
||||
def get_towers_by_tl(self, tl, faction=None):
|
||||
min_ql, max_ql = self.util.get_level_range_tl(tl)
|
||||
if faction:
|
||||
return self.get("and ql between ? and ? and faction LIKE ? order by c.short_name",
|
||||
[min_ql, max_ql, faction])
|
||||
return self.get("and ql between ? and ? order by c.short_name", [min_ql, max_ql])
|
||||
|
||||
def get_towers_all(self):
|
||||
return self.get("order by c.long_name", [])
|
||||
|
||||
def get_towers_by_pf(self, pf):
|
||||
return self.get("and a.pf_id=? order by a.site_number", [pf])
|
||||
|
||||
def get_towers_by_pf_site(self, pf, site):
|
||||
return self.get("and a.pf_id=? and a.site_number=?", [pf, site])
|
||||
|
||||
def get_towers_by_org(self, org):
|
||||
return self.get("and a.org_id"
|
||||
"=? order by c.long_name", [org])
|
||||
|
||||
def get_towers_by_org_name(self, org):
|
||||
return self.get("and a.org_name=? order by c.long_name", [org])
|
||||
|
||||
def get_free(self):
|
||||
return self.get("and a.org_id IS NULL order by a.site_number", [])
|
||||
|
||||
def get_towers_hot_tl(self, tl, faction=None):
|
||||
towers = self.get_towers_by_tl(tl, faction)
|
||||
out = []
|
||||
|
||||
for tower in towers:
|
||||
if self.is_hot(tower) in [1, 2]:
|
||||
out.append(tower)
|
||||
return out
|
||||
|
||||
def get_towers_hot_level(self, level, faction=None):
|
||||
out = []
|
||||
|
||||
if level:
|
||||
if faction:
|
||||
towers = self.get("and pvp_min <= ? and pvp_max >= ? and faction LIKE ?", [level, level, faction])
|
||||
else:
|
||||
towers = self.get("and pvp_min <= ? and pvp_max >= ?", [level, level])
|
||||
|
||||
else:
|
||||
if faction:
|
||||
towers = self.get("and faction LIKE ?", [faction])
|
||||
else:
|
||||
towers = self.get()
|
||||
|
||||
for tower in towers:
|
||||
if self.is_hot(tower) in [1, 2]:
|
||||
out.append(tower)
|
||||
return out
|
||||
|
||||
def format_entry(self, entry, _):
|
||||
h3 = ""
|
||||
now = self.day_time(int(time.time()) % 86400)
|
||||
row0 = f"<font color=CCInfoText>Site: <blue>{entry.short_name}</blue> <red>x{entry.site_number}</red> " \
|
||||
f"[R:<red>{entry.min_ql} - {entry.max_ql}</red>] " \
|
||||
f"[{self.text.make_tellcmd('More', f'lc {entry.short_name} {entry.site_number}')}]\n"
|
||||
row1 = f'<grey>UKN</grey> :: <red>No Owner -> Unplanted</red>\n'
|
||||
row2 = ""
|
||||
row3 = "</font>\n"
|
||||
hot = self.is_hot(entry)
|
||||
if hot != -1:
|
||||
h1 = "<cyan>COLD</cyan>"
|
||||
if hot == 2:
|
||||
h1 = "<red>WARHOT</red>"
|
||||
for org in self.attack_hot:
|
||||
if org['org_name'] == entry.org_name:
|
||||
hot_normal = self.is_hot(entry, False)
|
||||
# print(hot, hot_normal, org['hot'] - now)
|
||||
if hot_normal == 1:
|
||||
h3 = f"<cyan>COLD</cyan> in " \
|
||||
f"{self.util.format_time(self.day_time(int(entry.close_time - now)))}"
|
||||
if hot_normal == 0:
|
||||
h3 = f"<cyan>COLD</cyan> in {self.util.format_time(org['hot'] - now)}"
|
||||
|
||||
elif hot == 1:
|
||||
h1 = '<red>HOT</red>'
|
||||
h3 = f"<cyan>COLD</cyan> in {self.util.format_time(self.day_time(int(entry.close_time - now)))}"
|
||||
else:
|
||||
hot_time = self.day_time(entry.close_time - 6 * 60 * 60)
|
||||
h3 = f"<red>HOT</red> in " \
|
||||
f"{self.util.format_time((18 * 80 * 60 - hot_time) if hot_time > 18 * 60 * 60 else hot_time)}"
|
||||
org = f"<{entry.faction.lower()}>{entry.org_name}</{entry.faction.lower()}> " \
|
||||
f"[{self.text.make_tellcmd('View org', f'lc org {entry.org_name}')}]"
|
||||
row1 = f"{h1} :: {h3} :: {org} :: \n"
|
||||
row3 = f" » Planted: <grey>{self.util.format_datetime(entry.planted)}</grey></font>\n\n"
|
||||
if entry.pvp_min:
|
||||
pvp = f"[<red>{entry.pvp_min} - {entry.pvp_max}</red>]"
|
||||
else:
|
||||
pvp = f"[<red>175 - 220</red>]"
|
||||
# noinspection LongLine
|
||||
row2 = f" » QL: <red>{entry.ql}</red> PvP: {pvp} " \
|
||||
f"[{self.text.make_chatcmd(f'{entry.x_coord} x {entry.y_coord}', f'/waypoint {entry.x_coord} {entry.y_coord} {entry.id}')}]\n"
|
||||
return row0 + row1 + row2 + row3 + "<pagebreak>"
|
||||
|
||||
def is_hot(self, entry, with_war=True) -> int:
|
||||
if entry.get("close_time", None):
|
||||
now = self.day_time((time.time()) % 86400)
|
||||
self.attack_hot.sort(key=lambda k: k['org_name'])
|
||||
rem = []
|
||||
inside = False
|
||||
for index, i in enumerate(self.attack_hot):
|
||||
if i['hot'] < time.time():
|
||||
rem.append(index)
|
||||
continue
|
||||
if i['org_name'] == entry.org_name:
|
||||
inside = True
|
||||
for index in reversed(rem):
|
||||
self.attack_hot.pop(index)
|
||||
if inside and with_war:
|
||||
return 2
|
||||
if self.day_time(entry.close_time - int(now)) > 6 * 60 * 60:
|
||||
return 0
|
||||
return 1
|
||||
return -1
|
||||
|
||||
def get_last_attack(self, att_faction, att_org_name, def_faction, def_org_name, playfield_id, t):
|
||||
last_updated = t - (8 * 3600)
|
||||
is_finished = 1
|
||||
|
||||
sql = """
|
||||
SELECT
|
||||
b.id AS battle_id,
|
||||
a.id AS attack_id,
|
||||
b.playfield_id as pf_id,
|
||||
b.site_number as site
|
||||
FROM
|
||||
tower_battle b
|
||||
JOIN tower_attacker a ON
|
||||
a.tower_battle_id = b.id
|
||||
WHERE
|
||||
a.att_faction = ?
|
||||
AND a.att_org_name = ?
|
||||
AND b.def_faction = ?
|
||||
AND b.def_org_name = ?
|
||||
AND b.playfield_id = ?
|
||||
AND b.is_finished = ?
|
||||
AND b.last_updated >= ?
|
||||
ORDER BY
|
||||
last_updated DESC
|
||||
LIMIT 1"""
|
||||
|
||||
return self.db.query_single(sql,
|
||||
[att_faction, att_org_name, def_faction, def_org_name, playfield_id, is_finished,
|
||||
last_updated])
|
||||
|
||||
def prepare_nw_warn(self, pf_id, site, bonus=""):
|
||||
pf = self.playfield_controller.get_playfield_by_id(pf_id)
|
||||
site = f"{pf.short_name} <cyan>»</cyan> x{site}" + bonus
|
||||
self.job_scheduler.delayed_job(self.send_nw_warn, 0, f"{site} plantable in 20 minutes!")
|
||||
self.job_scheduler.delayed_job(self.send_nw_warn, 10 * 60 - 1, f"{site} plantable in 10 minutes!")
|
||||
self.job_scheduler.delayed_job(self.send_nw_warn, 15 * 60 - 1, f"{site} plantable in 5 minutes!")
|
||||
self.job_scheduler.delayed_job(self.send_nw_warn, 19 * 60 - 1, f"{site} plantable in 1 minute!")
|
||||
self.job_scheduler.delayed_job(self.send_nw_warn, 19 * 60 + 30 - 1, f"{site} plantable in 30 seconds!")
|
||||
self.job_scheduler.delayed_job(self.send_nw_warn, 19 * 60 + 45 - 1, f"{site} plantable in 15 seconds!")
|
||||
self.job_scheduler.delayed_job(self.send_nw_warn, 19 * 60 + 50 - 1, f"{site} plantable in 10 seconds!")
|
||||
self.job_scheduler.delayed_job(self.send_nw_warn, 19 * 60 + 55 - 1, f"{site} plantable in 5 seconds!")
|
||||
self.job_scheduler.delayed_job(self.send_nw_warn, 19 * 60 + 56 - 1, f"{site} plantable in 4 seconds!")
|
||||
self.job_scheduler.delayed_job(self.send_nw_warn, 19 * 60 + 57 - 1, f"{site} plantable in 3 seconds!")
|
||||
self.job_scheduler.delayed_job(self.send_nw_warn, 19 * 60 + 58 - 1, f"{site} plantable in 2 seconds!")
|
||||
self.job_scheduler.delayed_job(self.send_nw_warn, 19 * 60 + 59 - 1, f"{site} plantable in 1 second!")
|
||||
self.job_scheduler.delayed_job(self.send_nw_warn, 20 * 60 - 1, f"{site} plantable <green>NOW</green>!")
|
||||
|
||||
def send_nw_warn(self, _, msg):
|
||||
self.bot.send_private_channel_message(f"[<cyan>NW</cyan>] {msg}")
|
||||
@@ -0,0 +1,278 @@
|
||||
# noinspection LongLineForFile
|
||||
|
||||
DROP TABLE IF EXISTS tower_site;
|
||||
CREATE TABLE tower_site
|
||||
(
|
||||
playfield_id INT NOT NULL,
|
||||
site_number SMALLINT NOT NULL,
|
||||
min_ql SMALLINT NOT NULL,
|
||||
max_ql SMALLINT NOT NULL,
|
||||
x_coord SMALLINT NOT NULL,
|
||||
y_coord SMALLINT NOT NULL,
|
||||
site_name varchar(50) NOT NULL,
|
||||
PRIMARY KEY (playfield_id, site_number)
|
||||
);
|
||||
INSERT INTO tower_site (playfield_id, site_number, min_ql, max_ql, x_coord, y_coord, site_name)
|
||||
VALUES (505, 1, 60, 90, 2740, 4260, 'Griffon Frontier'),
|
||||
(505, 2, 80, 110, 540, 4180, 'Draught'),
|
||||
(505, 3, 70, 95, 1740, 3460, 'Dreadfire Volcano'),
|
||||
(505, 4, 80, 120, 2780, 3420, 'Northeast Barren Lands'),
|
||||
(505, 5, 60, 90, 580, 3140, 'Western Desert'),
|
||||
(505, 6, 50, 75, 2420, 1900, 'Waylander Mines'),
|
||||
(505, 7, 70, 100, 1860, 1700, 'North of Main Omni Base'),
|
||||
(505, 8, 61, 82, 460, 1380, 'Dome Ore'),
|
||||
(505, 9, 100, 150, 2700, 620, 'Crystal Forge Volcano'),
|
||||
(505, 10, 100, 150, 660, 460, 'SW Low Plateau'),
|
||||
(550, 1, 10, 20, 2660, 2020, 'Sifter Beach'),
|
||||
(550, 2, 20, 30, 1780, 1780, 'Academy Ore'),
|
||||
(550, 3, 15, 25, 1980, 1340, 'Athen Fault'),
|
||||
(550, 4, 10, 20, 2660, 820, 'Grindmoore'),
|
||||
(550, 5, 15, 23, 1380, 380, 'Gladius Grove'),
|
||||
(551, 1, 40, 90, 1700, 3700, 'Styx Magma'),
|
||||
(551, 2, 35, 50, 2220, 3340, 'Carbon Grove'),
|
||||
(551, 3, 26, 50, 980, 3140, 'Between the Craters'),
|
||||
(551, 4, 25, 35, 340, 2420, 'Powdered Dunes'),
|
||||
(551, 5, 32, 45, 2540, 2060, 'Dust Bank'),
|
||||
(551, 6, 20, 30, 580, 1740, 'Charred Groove'),
|
||||
(551, 7, 12, 45, 940, 1540, 'West of Perdition'),
|
||||
(551, 8, 15, 30, 660, 900, 'North of Yuttos'),
|
||||
(560, 1, 100, 170, 1500, 3420, 'Terraform Edge'),
|
||||
(560, 2, 170, 250, 3060, 3020, 'West Spirals'),
|
||||
(560, 3, 170, 250, 3500, 2980, 'East Spirals'),
|
||||
(560, 4, 130, 170, 1220, 2220, 'Middle Mort Desert'),
|
||||
(560, 5, 1, 100, 900, 1460, 'Green Crater'),
|
||||
(560, 6, 110, 160, 3100, 1460, 'Oasis Ore'),
|
||||
(560, 7, 150, 200, 2740, 700, 'South East Craterwall'),
|
||||
(560, 8, 100, 150, 540, 540, 'South West Craterwall'),
|
||||
(560, 9, 160, 210, 2780, 540, 'Stormshelter'),
|
||||
(565, 1, 25, 40, 2940, 2900, 'Rich Desert Ridge'),
|
||||
(565, 2, 30, 45, 1980, 2580, 'East of Meetmedere'),
|
||||
(565, 3, 50, 75, 540, 2020, 'Middle of Western Desert'),
|
||||
(565, 4, 40, 60, 2580, 1940, 'North of Rhino Village'),
|
||||
(565, 5, 40, 60, 2700, 1260, 'South of Rhino Village'),
|
||||
(567, 1, 12, 20, 1220, 1060, 'In the Newland Desert'),
|
||||
(567, 2, 15, 25, 540, 460, 'West of Newland Lake'),
|
||||
(570, 1, 200, 300, 3220, 3020, 'North of Cyborg Hideout'),
|
||||
(570, 2, 191, 250, 3780, 2540, 'Middle of Liberty'),
|
||||
(570, 3, 120, 180, 980, 2060, 'South of Sabulum'),
|
||||
(570, 4, 190, 230, 3940, 2060, 'Cyborg Border'),
|
||||
(570, 5, 200, 300, 2820, 1820, 'Middle of Perpetual Wastelands'),
|
||||
(570, 6, 200, 300, 3740, 1700, 'South of Cyborg Hideout'),
|
||||
(570, 7, 100, 150, 1500, 1340, 'Lower Plateu Zone'),
|
||||
(570, 8, 100, 150, 2100, 1380, 'The Mid Canyon Crossing'),
|
||||
(570, 9, 120, 180, 3020, 1220, 'Plains of dust'),
|
||||
(570, 10, 100, 150, 900, 1060, 'West of Canyon'),
|
||||
(570, 11, 100, 150, 3180, 940, 'The Canyon Mines'),
|
||||
(570, 12, 190, 230, 2300, 780, 'South of Canyon'),
|
||||
(585, 1, 40, 60, 1220, 2740, 'Northern Wastelands'),
|
||||
(585, 2, 11, 16, 2180, 2580, 'West Wastelands'),
|
||||
(585, 3, 40, 55, 1020, 2460, 'Mid Wastelands'),
|
||||
(585, 4, 30, 45, 2140, 1660, 'Giant Green River Bank North'),
|
||||
(585, 5, 11, 16, 1180, 1340, 'West of the Dead Forest'),
|
||||
(585, 6, 30, 45, 2100, 1340, 'Giant Green River Bank South'),
|
||||
(585, 7, 15, 22, 1420, 1020, 'Canyon East'),
|
||||
(585, 8, 25, 35, 820, 780, 'Canyon South'),
|
||||
(585, 9, 35, 50, 900, 460, 'By the River'),
|
||||
(590, 1, 140, 200, 1740, 3100, 'By the Fisher Village'),
|
||||
(590, 2, 140, 200, 2100, 3060, 'Fisher Village Approach'),
|
||||
(590, 3, 100, 170, 2900, 2820, 'North Forest'),
|
||||
(590, 4, 90, 130, 3340, 2700, 'North-east Forest'),
|
||||
(590, 5, 130, 170, 860, 1220, 'North-west of Lava Ditches'),
|
||||
(590, 6, 100, 150, 3100, 980, 'Mid Clutching Forest'),
|
||||
(590, 7, 130, 170, 860, 780, 'South-west of Lava Ditches'),
|
||||
(590, 8, 100, 150, 3180, 620, 'South Clutching Forest'),
|
||||
(595, 1, 100, 150, 1140, 3380, 'Old ruins'),
|
||||
(595, 2, 100, 150, 3180, 2900, 'Plains of defense'),
|
||||
(595, 3, 130, 180, 1740, 2300, 'The haunted forest outskirt'),
|
||||
(595, 4, 130, 180, 900, 2220, 'Forest of Xzawkaz'),
|
||||
(595, 5, 200, 300, 2260, 1860, 'In the Swamp of Horrors'),
|
||||
(595, 6, 130, 180, 1420, 1500, 'Island of Control'),
|
||||
(595, 7, 130, 180, 1340, 1140, 'The swamp of hope'),
|
||||
(595, 8, 200, 300, 2900, 1100, 'South of the Medusa'),
|
||||
(595, 9, 140, 210, 2140, 780, 'Middle of the Foul Forest'),
|
||||
(595, 10, 200, 300, 540, 540, 'Southern Forest of Xzawkaz'),
|
||||
(600, 1, 30, 45, 2420, 2980, 'By the Rivers Edge'),
|
||||
(600, 2, 50, 75, 620, 2900, 'North Forest Road'),
|
||||
(600, 3, 25, 50, 1300, 2660, 'Along the Rivers Edge'),
|
||||
(600, 4, 30, 45, 3740, 2500, 'East Forest'),
|
||||
(600, 5, 25, 50, 3140, 2020, 'Rhino Hills'),
|
||||
(600, 6, 50, 75, 580, 1700, 'West Forest'),
|
||||
(600, 7, 60, 90, 1940, 1620, 'Crossroads'),
|
||||
(600, 8, 60, 90, 1140, 1500, 'Forestdawn'),
|
||||
(600, 9, 50, 75, 3220, 1140, 'East of Crater'),
|
||||
(605, 1, 160, 200, 2940, 2820, 'Forest Waters'),
|
||||
(605, 2, 110, 120, 1100, 2620, 'Muddy Pools'),
|
||||
(605, 3, 100, 150, 1700, 2300, 'West of Wine'),
|
||||
(605, 4, 120, 180, 2940, 2260, 'East of Wine'),
|
||||
(605, 5, 130, 195, 1900, 1740, 'Central Belial Forest'),
|
||||
(605, 6, 130, 190, 2500, 1660, 'River Delta'),
|
||||
(605, 7, 160, 200, 2540, 1220, 'Junction Forest'),
|
||||
(605, 8, 100, 150, 2340, 860, 'Borderline'),
|
||||
(605, 9, 120, 180, 2020, 420, 'Southern belial Mine'),
|
||||
(605, 10, 140, 200, 620, 380, 'Southwest Belial Mining District'),
|
||||
(610, 1, 60, 90, 1380, 2780, 'Tetlies Land control area'),
|
||||
(610, 2, 80, 120, 2900, 2660, 'East of the Great Marsh'),
|
||||
(610, 3, 60, 90, 660, 2460, 'West of outpost 10-3'),
|
||||
(610, 4, 100, 150, 2300, 2020, 'Defense of Geholva'),
|
||||
(610, 5, 106, 143, 2740, 1180, 'South of Forest of Geholva'),
|
||||
(610, 6, 120, 180, 860, 900, 'Avid Crater'),
|
||||
(610, 7, 120, 180, 1540, 900, 'East of Avid Crater'),
|
||||
(610, 8, 100, 150, 2460, 540, 'Bendelham forest Defense'),
|
||||
(615, 1, 60, 100, 1900, 3020, 'North of Lenne'),
|
||||
(615, 2, 100, 150, 860, 2820, 'Little Hawaii Defense'),
|
||||
(615, 3, 90, 120, 2620, 2660, 'Defense of Zoto'),
|
||||
(615, 4, 60, 100, 900, 2100, 'By the Ocean'),
|
||||
(615, 5, 61, 100, 2300, 1180, 'Birm'),
|
||||
(615, 6, 120, 180, 2700, 660, 'SFH Defense'),
|
||||
(615, 7, 100, 150, 1860, 500, 'South in Nightplain'),
|
||||
(620, 1, 150, 200, 2700, 3860, 'Krud the Lost Valley Defense'),
|
||||
(620, 2, 150, 225, 1900, 3180, 'Pranade'),
|
||||
(620, 3, 120, 180, 620, 2980, 'Plains of Jarga Defense'),
|
||||
(620, 4, 200, 300, 2460, 2260, 'Old Plains'),
|
||||
(620, 5, 200, 300, 1540, 1780, 'Middle of Easter Fouls Plains'),
|
||||
(620, 6, 130, 200, 1540, 1140, 'Clefre Defense'),
|
||||
(620, 7, 100, 150, 2020, 860, 'Central Sharewood'),
|
||||
(620, 8, 200, 300, 820, 540, 'Pegradul'),
|
||||
(625, 1, 90, 130, 1460, 1940, 'The Resilient Forest - North'),
|
||||
(625, 2, 90, 120, 1900, 1540, 'The Resilient Forest - East'),
|
||||
(625, 3, 125, 170, 2780, 1380, 'Central Prowler Waste'),
|
||||
(625, 4, 100, 125, 1380, 1180, 'Central Resilient Forest'),
|
||||
(625, 5, 125, 170, 2860, 1020, 'Southern Prowler Waste'),
|
||||
(625, 6, 100, 150, 4020, 980, 'The Barren Hills'),
|
||||
(625, 7, 100, 125, 1740, 860, 'The Resilient Forest - South'),
|
||||
(625, 8, 50, 75, 2460, 540, 'The Silent Woods - East'),
|
||||
(630, 1, 40, 60, 1540, 2660, 'Pleasant Range Offense Hill'),
|
||||
(630, 2, 60, 90, 2380, 2500, 'Central Pleasant Range'),
|
||||
(630, 3, 50, 75, 580, 2420, 'West of 20K'),
|
||||
(630, 4, 30, 70, 3220, 2220, 'Pleasant Range Defense'),
|
||||
(630, 5, 60, 90, 3220, 1980, 'Pleasant River Defense'),
|
||||
(630, 6, 60, 90, 3260, 1500, 'Pleasant River Offense'),
|
||||
(630, 7, 40, 60, 2260, 1140, 'Central Pleasant Plains'),
|
||||
(630, 8, 30, 70, 3020, 1020, 'East Pleasant Plains'),
|
||||
(630, 9, 30, 45, 740, 460, 'West of Versailles Tower'),
|
||||
(635, 1, 55, 70, 700, 2420, 'Northern River Bank'),
|
||||
(635, 2, 60, 90, 1780, 2460, 'Hawker Trench'),
|
||||
(635, 3, 70, 105, 1460, 1740, 'Klapam Forest Defense'),
|
||||
(635, 4, 55, 70, 2020, 1740, 'Klompfot Defense'),
|
||||
(635, 5, 70, 105, 1900, 1220, 'South of Trench'),
|
||||
(635, 6, 80, 120, 1140, 940, 'Nile Hills'),
|
||||
(635, 7, 55, 70, 1780, 700, 'Aprils Rock Offense'),
|
||||
(635, 8, 80, 150, 820, 420, 'Southern Lower River Bank'),
|
||||
(635, 9, 80, 150, 1700, 340, 'Aprils Rock Defense'),
|
||||
(646, 1, 10, 15, 460, 1300, 'Great W. Forest Vein'),
|
||||
(646, 2, 10, 15, 2940, 980, 'The Hidden Notum Canal'),
|
||||
(646, 3, 20, 30, 3220, 620, 'Mountain Areas'),
|
||||
(646, 4, 10, 15, 580, 580, 'Great W. Forest Dorsal'),
|
||||
(646, 5, 10, 15, 1500, 460, 'Western Mountain Areas'),
|
||||
(647, 1, 90, 135, 1100, 3100, 'The Mineral Mine'),
|
||||
(647, 2, 20, 30, 2900, 2940, 'NE Desert Aperient'),
|
||||
(647, 3, 37, 64, 1900, 2700, 'SurroundingTemple of Three Winds'),
|
||||
(647, 4, 25, 40, 2220, 1900, 'Piercing Thundertube'),
|
||||
(647, 5, 30, 45, 2820, 1940, 'Central Striking Ant'),
|
||||
(647, 6, 25, 40, 620, 1660, 'Tir Prairie'),
|
||||
(647, 7, 25, 40, 1180, 1700, 'Crater Swamp'),
|
||||
(650, 1, 50, 75, 540, 2820, 'West Pass'),
|
||||
(650, 2, 65, 75, 900, 2300, 'Crowning Shallows'),
|
||||
(650, 3, 100, 150, 1660, 2180, 'Haven Notum Crematorium'),
|
||||
(650, 4, 70, 140, 2020, 1740, 'Stret Vale Deux Drilling Field'),
|
||||
(650, 5, 120, 180, 1340, 1620, 'The Flooded Bottomland'),
|
||||
(650, 6, 75, 90, 1820, 740, 'Stret Woods'),
|
||||
(650, 7, 60, 90, 940, 420, 'Greenslopes'),
|
||||
(655, 1, 30, 45, 420, 2700, 'Skop Notum Mine'),
|
||||
(655, 2, 30, 80, 2820, 2340, 'Klor'),
|
||||
(655, 3, 60, 80, 2820, 1660, 'Harstad'),
|
||||
(655, 4, 40, 90, 540, 1580, 'Ubleo'),
|
||||
(655, 5, 40, 60, 1420, 1580, 'Flubu Notum Mine'),
|
||||
(655, 6, 40, 70, 4340, 900, 'Plago'),
|
||||
(655, 7, 60, 80, 2260, 380, 'jucha'),
|
||||
(655, 8, 70, 105, 4380, 380, 'Mune'),
|
||||
(655, 9, 30, 60, 820, 340, 'Mocnuf Notum Mine'),
|
||||
(665, 1, 80, 150, 940, 4820, 'Central Desert north'),
|
||||
(665, 2, 45, 75, 1260, 3860, 'Notum Disruption Mountain'),
|
||||
(665, 3, 75, 110, 1940, 3860, 'The Notum Plains'),
|
||||
(665, 4, 100, 150, 940, 3380, 'Near Clan Outpost'),
|
||||
(665, 5, 45, 80, 1300, 3060, 'Central Mountains'),
|
||||
(665, 6, 55, 150, 380, 2300, 'Surrounding Evil'),
|
||||
(665, 7, 45, 60, 1260, 2140, 'Notum Mountain'),
|
||||
(665, 8, 55, 100, 2020, 1980, 'Near Omni-Tek Outpost'),
|
||||
(665, 9, 100, 150, 420, 820, 'Shores Notum Vein'),
|
||||
(670, 1, 30, 45, 1100, 4340, 'Yukon Source'),
|
||||
(670, 2, 35, 50, 1460, 2540, 'Frisko'),
|
||||
(670, 3, 30, 45, 2140, 2420, 'Round Hills'),
|
||||
(670, 4, 50, 75, 2140, 1900, 'Dense Drewen'),
|
||||
(670, 5, 35, 50, 1260, 1820, 'Borrowed Hill'),
|
||||
(670, 6, 35, 50, 1340, 1340, 'Narrow Lune'),
|
||||
(670, 7, 10, 15, 2500, 1220, 'Micron Slopes Notum Mine'),
|
||||
(670, 8, 50, 75, 2100, 540, 'High Juniper'),
|
||||
(670, 9, 50, 75, 2300, 460, 'High Juniper Notum Vein'),
|
||||
(685, 1, 35, 50, 2140, 2620, 'Nature Reverve - East'),
|
||||
(685, 2, 35, 50, 1900, 2580, 'Nature Reverve - West'),
|
||||
(685, 3, 50, 75, 1300, 1900, 'Poole - West'),
|
||||
(685, 4, 50, 75, 1580, 1820, 'Poole - East'),
|
||||
(685, 5, 15, 25, 1140, 1100, 'V-Hill'),
|
||||
(685, 6, 20, 30, 1580, 700, 'Lunder Hills - North'),
|
||||
(685, 7, 25, 40, 2740, 460, 'Galway hills'),
|
||||
(685, 8, 20, 30, 1220, 380, 'Lunder Hills'),
|
||||
(685, 9, 25, 40, 2260, 380, 'South-east Woods'),
|
||||
(687, 1, 10, 15, 500, 1900, 'Blossom Valley'),
|
||||
(687, 2, 10, 15, 380, 1300, 'Konty Passage Plains'),
|
||||
(687, 3, 17, 28, 900, 1220, 'Vas'' Pass'),
|
||||
(687, 4, 15, 25, 780, 900, 'Arthur''s Pass'),
|
||||
(687, 5, 10, 15, 380, 580, 'Kontys Sixth Passage - West'),
|
||||
(687, 6, 10, 15, 620, 540, 'Kontys Sixth Passage - East'),
|
||||
(695, 1, 30, 45, 940, 3260, 'North West Lush Fields'),
|
||||
(695, 2, 20, 30, 2420, 3180, 'North East Lush Fields'),
|
||||
(695, 3, 10, 40, 3460, 2940, 'Stret River Island'),
|
||||
(695, 4, 40, 60, 1260, 2460, 'West of Outpost'),
|
||||
(695, 5, 35, 60, 1740, 2460, 'East of Outpost'),
|
||||
(695, 6, 20, 30, 1780, 1820, 'Central Lush Fields'),
|
||||
(695, 7, 10, 15, 2860, 420, 'South East Lush Fields'),
|
||||
(695, 8, 30, 45, 980, 380, 'South West Lush Fields'),
|
||||
(696, 1, 15, 25, 780, 1420, 'Mutant Domain North'),
|
||||
(696, 2, 20, 30, 500, 860, 'Mutant Domain Central'),
|
||||
(696, 3, 25, 40, 780, 460, 'Mutant Domain South'),
|
||||
(716, 1, 20, 35, 500, 3220, 'Northern Grassland'),
|
||||
(716, 2, 15, 30, 980, 3020, 'Moderate Grassland'),
|
||||
(716, 3, 10, 20, 460, 2180, 'Dungeon Hilltop'),
|
||||
(716, 4, 10, 15, 700, 2180, 'Rocky Upsurge'),
|
||||
(716, 5, 15, 25, 340, 1420, 'Northern Easy Swamps Notum Field'),
|
||||
(716, 6, 15, 26, 460, 820, 'Ocean Inlet'),
|
||||
(717, 1, 30, 45, 1620, 2660, 'Greater Omni Forest Swamps'),
|
||||
(717, 2, 15, 25, 1180, 2460, 'Dragonback Ridge'),
|
||||
(717, 3, 30, 45, 1900, 1820, 'Mountainous Regions'),
|
||||
(717, 4, 20, 35, 1860, 1340, 'Waterfall Swamp'),
|
||||
(717, 5, 10, 15, 1500, 1300, 'Greater Omni Forest South'),
|
||||
(717, 6, 25, 40, 900, 1220, 'Northern Semi-Barren Area'),
|
||||
(717, 7, 10, 25, 1940, 900, 'Ring Mountain Range'),
|
||||
(717, 8, 14, 25, 940, 460, 'Southern Isle'),
|
||||
(760, 1, 60, 90, 1580, 2380, 'Notum Ore in Buttu'),
|
||||
(760, 2, 35, 50, 940, 2020, 'Mountain of Fourtyone'),
|
||||
(760, 3, 35, 50, 1300, 1980, 'Mountain in 4Holes'),
|
||||
(760, 4, 45, 60, 1660, 1740, 'South of Ahenus'),
|
||||
(760, 5, 70, 100, 1820, 1340, 'Ibreri Woods North'),
|
||||
(760, 6, 35, 50, 1460, 1260, 'Mountain of Fourtytwo'),
|
||||
(760, 7, 100, 150, 1740, 1060, 'Ibreri Woods'),
|
||||
(760, 8, 45, 70, 1180, 500, 'Ibreri'),
|
||||
(760, 9, 45, 70, 460, 420, 'Jall Mountain'),
|
||||
(790, 1, 20, 30, 1700, 3100, 'Hells Courtyard'),
|
||||
(790, 2, 15, 25, 2300, 2860, 'Pondus Beach'),
|
||||
(790, 3, 15, 30, 1700, 2780, 'Hound Land'),
|
||||
(790, 4, 20, 40, 1980, 2780, 'Hound Notum Field'),
|
||||
(790, 5, 20, 30, 1700, 1940, 'East Mutie'),
|
||||
(790, 6, 12, 30, 1340, 1220, 'Omni Outpost'),
|
||||
(790, 7, 20, 30, 660, 1180, 'South Mutie'),
|
||||
(790, 8, 30, 60, 2260, 1140, 'The Beach'),
|
||||
(791, 1, 15, 26, 420, 2020, 'Populous Mountain'),
|
||||
(791, 2, 12, 22, 660, 1500, 'Hound Land Mining'),
|
||||
(791, 3, 12, 20, 220, 1060, 'Stret West Notum Ore'),
|
||||
(791, 4, 10, 15, 740, 820, 'Snake Mountain'),
|
||||
(791, 5, 20, 40, 780, 460, 'Southern Empty Wastes and Roads'),
|
||||
(791, 6, 10, 20, 380, 340, 'Transit Valley Ore'),
|
||||
(795, 1, 40, 60, 4220, 1580, 'Illuminati'),
|
||||
(795, 2, 100, 150, 500, 1540, 'Northern Forest of Illuminations'),
|
||||
(795, 3, 25, 50, 3420, 1540, 'Fate Notum Field'),
|
||||
(795, 4, 71, 120, 580, 820, 'Pegrama'),
|
||||
(795, 5, 84, 120, 1220, 700, 'Grazeland Notum Field'),
|
||||
(795, 6, 50, 75, 4020, 620, 'Winterbottom'),
|
||||
(795, 7, 90, 120, 540, 500, 'Southern Forest of Illuminations'),
|
||||
(795, 8, 60, 90, 2900, 500, 'Summer');
|
||||
Reference in New Issue
Block a user