Wednesday, July 15, 2026 4:59:07 AM
> territory.rb
# frozen_string_literal: true

module ESM
  module Exile
    class Territory
      # Days-left window within which #payment_due_soon? flags a territory.
      PAYMENT_DUE_SOON_DAYS = 3

      # So I don't build a bad URL
      VALID_FLAGS = %w[
        flag_blue_co flag_country_at_co flag_country_au_co flag_country_be_co flag_country_by_co flag_country_cn_co
        flag_country_cz_co flag_country_de_co flag_country_dk_co flag_country_ee_co flag_country_fi_co flag_country_fr_co
        flag_country_ir_co flag_country_it_co flag_country_nl_co flag_country_pl2_co flag_country_pl_co flag_country_pt_co
        flag_country_ru_co flag_country_sct_co flag_country_se_co flag_cp_co flag_exile_city_co flag_green_co flag_mate_21dmd_co
        flag_mate_bis_co flag_mate_commandermalc_co flag_mate_hollow_co flag_mate_jankon_co flag_mate_legion_ca flag_mate_secretone_co
        flag_mate_spawny_co flag_mate_stitchmoonz_co flag_mate_vish_co flag_mate_zanders_streched_co flag_misc_alkohol_co flag_misc_battleye_co
        flag_misc_beardageddon_co flag_misc_brunswik_co flag_misc_bss_co flag_misc_dickbutt_co flag_misc_dorset_co flag_misc_dream_cat_co
        flag_misc_eraser1_co flag_misc_exile_co flag_misc_kickass_co flag_misc_kiwifern_co flag_misc_knuckles_co flag_misc_lazerkiwi_co
        flag_misc_lemonparty_co flag_misc_nuclear_co flag_misc_pedobear_co flag_misc_petoria_co flag_misc_pirate_co
        flag_misc_privateproperty_co flag_misc_rainbow_co flag_misc_rma_co flag_misc_skippy_co flag_misc_smashing_co flag_misc_snake_co
        flag_misc_svarog_co flag_misc_trololol_co flag_misc_utcity_co flag_misc_weeb_co flag_misc_willbeeaten_co flag_red_co
        flag_trouble2_co flag_uk_co flag_us_co flag_white_co
      ].freeze

      def initialize(server:, territory:)
        @server = server
        @server_settings = server.server_setting
        @territory = normalize_territory(territory)
      end

      def id
        @territory[:esm_custom_id].presence || @territory[:id]
      end

      def name
        @territory[:name]
      end

      def owner
        "#{@territory[:owner_name]} (#{@territory[:owner_uid]})"
      end

      def level
        @territory[:level]
      end

      def object_count
        @territory[:object_count]
      end

      def radius
        # Radius comes in as a decimal
        @territory[:radius].to_i
      end

      def flag_path
        @flag_path ||= convert_flag_path(@territory[:flag_texture])
      end

      def stolen?
        @territory[:flag_stolen]
      end

      def flag_status
        stolen? ? "Stolen!" : "Secure"
      end

      def status_color
        days = days_left_until_payment_due

        return ESM::Color::Toast::RED if stolen?
        return ESM::Color::Toast::GREEN if days.nil?

        if days <= 2
          ESM::Color::Toast::RED
        elsif days <= 5
          ESM::Color::Toast::YELLOW
        else
          ESM::Color::Toast::GREEN
        end
      end

      def last_paid_at
        return if @territory[:last_paid_at].blank?

        @last_paid_at ||= ESM::Time.parse(@territory[:last_paid_at])
      end

      def next_due_date
        return if last_paid_at.nil?

        @next_due_date ||= last_paid_at + @server_settings.territory_lifetime.days
      end

      def max_object_count
        current_level_territory.territory_object_count
      end

      def upgrade_level
        next_level_territory.territory_level
      end

      def current_level_territory
        @current_level_territory ||= @server.territories.find_by(territory_level: @territory[:level])
      end

      def next_level_territory
        @next_level_territory ||= @server.territories.find_by(territory_level: @territory[:level] + 1)
      end

      def renew_cost
        cost = @territory[:level] *
          @territory[:object_count] *
          @server_settings.territory_price_per_object

        return cost if @server_settings.territory_payment_tax.zero?

        # Add the server's payment tax on top of the base cost
        cost + (cost * (@server_settings.territory_payment_tax.to_f / 100)).round
      end

      def renew_price
        return "#{renew_cost.to_delimitated_s} poptabs" if @server_settings.territory_payment_tax.zero?

        "#{renew_cost.to_delimitated_s} poptabs (#{@server_settings.territory_payment_tax}% tax added)"
      end

      def upgradeable?
        !next_level_territory.nil?
      end

      def upgrade_price
        price = next_level_territory.territory_purchase_price
        return "#{price.to_delimitated_s} poptabs" if @server_settings.territory_upgrade_tax.zero?

        # If the server has tax, add it to the price
        price += (price * (@server_settings.territory_upgrade_tax.to_f / 100)).round

        "#{price.to_delimitated_s} poptabs (#{@server_settings.territory_upgrade_tax}% tax added)"
      end

      def upgrade_radius
        next_level_territory.territory_radius
      end

      def upgrade_object_count
        next_level_territory.territory_object_count
      end

      def moderators
        if @server.v2?
          @territory[:moderators]
            .sort_by { |a| a[:name].downcase }
            .join_map("\n") do |account|
              next if account[:owner]

              "#{account[:name]} (#{account[:uid]})"
            end
        else
          # V1
          @territory[:moderators].map { |name, uid| "#{name} (#{uid})" }
        end
      end

      def builders
        if @server.v2?
          @territory[:build_rights]
            .join_map("\n") do |account|
              next if account[:owner] || account[:moderator]

              "#{account[:name]} (#{account[:uid]})"
            end
        else
          # V1
          @territory[:build_rights].map { |name, uid| "#{name} (#{uid})" }
        end
      end

      def days_left_until_payment_due
        return if next_due_date.nil?

        @days_left_until_payment_due ||= (next_due_date.to_date - ::Time.zone.today).to_i
      end

      # Whether a protection payment is close enough to surface a call to action.
      # Drives the website's pay panels and its "payment due" grouping.
      def payment_due_soon?
        days = days_left_until_payment_due
        return false if days.nil?

        days <= PAYMENT_DUE_SOON_DAYS
      end

      def payment_reminder_message
        time_left_message = "You have `#{ESM::Time.distance_of_time_in_words(next_due_date, precise: false)}` until your next payment is due."

        case days_left_until_payment_due
        when 0..2
          # 0 to 2 days left
          ":alarm_clock: **You should make a base payment ASAP to avoid losing your base!**\n#{time_left_message}"
        when 3..5
          # 3 to 5 days left
          ":warning: **You should consider making a base payment soon.**\n#{time_left_message}"
        else
          # Don't show anything
          ""
        end
      end

      def to_embed
        ESM::Embed.build do |e|
          e.title = "#{I18n.t(:territory)} \"#{name}\""
          e.thumbnail = flag_path
          e.color = status_color
          e.description = payment_reminder_message

          e.add_field(name: I18n.t(:territory_id), value: "```#{id}```", inline: true)
          e.add_field(name: I18n.t(:flag_status), value: "```#{flag_status}```", inline: true)

          e.add_field(
            name: I18n.t(:next_due_date),
            value: "```#{next_due_date.strftime(ESM::Time::Format::TIME)}```"
          )

          e.add_field(
            name: I18n.t(:last_paid),
            value: "```#{last_paid_at.strftime(ESM::Time::Format::TIME)}```"
          )

          e.add_field(name: I18n.t(:price_to_renew_protection), value: renew_price, inline: true)

          e.add_field(value: I18n.t("commands.territories.current_territory_stats"))
          e.add_field(name: I18n.t(:level), value: level, inline: true)
          e.add_field(name: I18n.t(:radius), value: "#{radius}m", inline: true)

          e.add_field(
            name: "#{I18n.t(:current)} / #{I18n.t(:max_objects)}",
            value: "#{object_count}/#{max_object_count}",
            inline: true
          )

          if upgradeable?
            e.add_field(value: I18n.t("commands.territories.next_territory_stats"))
            e.add_field(name: I18n.t(:level), value: upgrade_level, inline: true)
            e.add_field(name: I18n.t(:radius), value: "#{upgrade_radius}m", inline: true)
            e.add_field(name: I18n.t(:max_objects), value: upgrade_object_count, inline: true)
            e.add_field(name: I18n.t(:price), value: upgrade_price, inline: true)
          end

          e.add_field(value: I18n.t("commands.territories.territory_members"))
          e.add_field(name: ":crown: #{I18n.t(:owner)}", value: owner)

          if (value = moderators) && value.present?
            e.add_field(name: ":shield: #{I18n.t(:moderators)}", value:)
          end

          if (value = builders) && value.present?
            e.add_field(name: ":construction_site: #{I18n.t(:build_rights)}", value:)
          end
        end
      end

      private

      def normalize_territory(territory)
        territory = territory.to_h unless territory.is_a?(Hash)
        territory = transform_territory(territory) if @server.v2?

        territory[:name] = territory[:territory_name] if territory.key?(:territory_name)
        territory[:esm_custom_id] ||= nil

        territory
      end

      def transform_territory(territory)
        moderator_uids = territory[:moderators]&.map { |a| a[:uid] } || []
        builder_uids = territory[:build_rights]&.map { |a| a[:uid] } || []

        label_accounts = lambda do |account|
          account[:owner] = account[:uid] == territory[:owner_uid]
          account[:moderator] = moderator_uids.include?(account[:uid])
          account[:builder] = builder_uids.include?(account[:uid])
        end

        sort_accounts = ->(account) { account[:name].downcase }

        territory[:moderators]&.each(&label_accounts)&.sort_by!(&sort_accounts)
        territory[:build_rights]&.each(&label_accounts)&.sort_by!(&sort_accounts)

        territory
      end

      def convert_flag_path(arma_path)
        flag_base_path = "https://exile-server-manager.s3.amazonaws.com/flags"
        default_flag = "#{flag_base_path}/flag_white_co.jpg"
        return default_flag if arma_path.blank?

        flag_name = arma_path.match(ESM::Regex::FLAG_NAME)
        return default_flag if flag_name.blank?

        flag_path = "#{flag_base_path}/#{flag_name[1]}.jpg"

        # If we have a version of this flag, return the path, otherwise, just return the default flag
        VALID_FLAGS.include?(flag_name[1]) ? flag_path : default_flag
      end
    end
  end
end
All opinions represented herein are my own
- © 2024 - 2026 itsthedevman
- build 43aa0d7