// retoor <retoor@molodetz.nl>
import { Component } from "./Component.js";
import { Http } from "../Http.js";
import { Poller } from "../Poller.js";
const POLL_INTERVAL_MS = 15000;
const COUNTDOWN_INTERVAL_MS = 30000;
const TICKER_CAP = 5;
export class AppOpinionWar extends Component {
connectedCallback() {
this._uid = this.attr("uid");
this._topic = this.attr("topic");
this._lastSeq = this.intAttr("seq", 0);
this._endsAt = this.attr("ends-at");
this._status = this.attr("status", "active");
this._countdown = this.querySelector("[data-war-countdown]");
this._ticker = this.querySelector("[data-war-ticker]");
this._victory = this.querySelector("[data-war-victory]");
this._victoryTitle = this.querySelector("[data-war-victory-title]");
this._actions = this.querySelector("[data-war-actions]");
this._field = this.querySelector("[data-war-field]");
this._subscribed = false;
this._onSubmitBound = (event) => this._onSubmit(event);
this.addEventListener("submit", this._onSubmitBound);
if (this._status === "active") {
this._subscribe();
this._poller = new Poller(() => this._poll(), POLL_INTERVAL_MS, { immediate: false, pauseHidden: true });
this._timer = window.setInterval(() => this._tickCountdown(), COUNTDOWN_INTERVAL_MS);
this._tickCountdown();
}
}
disconnectedCallback() {
this._teardown();
this.removeEventListener("submit", this._onSubmitBound);
}
_teardown() {
if (this._poller) {
this._poller.stop();
this._poller = null;
}
if (this._timer) {
window.clearInterval(this._timer);
this._timer = null;
}
if (this._subscribed && window.app && window.app.pubsub) {
window.app.pubsub.unsubscribe(this._topic, this._onFrame);
this._subscribed = false;
}
}
_subscribe() {
const app = window.app;
if (!this._topic || !app || !app.pubsub || typeof app.pubsub.subscribe !== "function") return;
this._onFrame = (frame) => this._apply(frame);
app.pubsub.subscribe(this._topic, this._onFrame);
this._subscribed = true;
}
async _poll() {
if (this._status !== "active") return;
let payload;
try {
payload = await Http.getJson(`/battles/${this._uid}/events?after=${this._lastSeq}`);
} catch (error) {
return;
}
(payload.events || []).forEach((event) => this._apply(event));
if (payload.status === "resolved" && this._status === "active") {
this._refreshState();
}
}
_apply(event) {
if (!event || typeof event.seq !== "number") return;
if (event.seq > 0 && event.seq <= this._lastSeq) return;
if (event.seq > 0) this._lastSeq = event.seq;
if (typeof event.hp_a === "number" && typeof event.hp_b === "number") {
this._renderTotals(event.hp_a, event.hp_b);
}
this._prependEvent(event);
if (event.kind === "result") {
this._refreshState();
}
}
async _refreshState() {
let war;
try {
war = await Http.getJson(`/battles/${this._uid}`);
} catch (error) {
return;
}
this._renderWar(war);
}
async _onSubmit(event) {
const form = event.target.closest("form[data-war-action]");
if (!form || !this.contains(form)) return;
event.preventDefault();
event.stopPropagation();
const action = form.dataset.warAction;
const confirmText = form.dataset.warConfirm;
if (confirmText && window.app && window.app.dialog) {
const accepted = await window.app.dialog.confirm({ message: confirmText });
if (!accepted) return;
}
const button = form.querySelector("button[type=submit]");
if (button) button.disabled = true;
const params = {};
new FormData(form).forEach((value, key) => {
params[key] = value;
});
try {
const result = await Http.send(form.getAttribute("action"), params);
const war = result && result.data ? result.data.war : null;
if (action === "fight" && war) {
this._renderWar(war);
this._toast(`You dealt ${result.data.damage} HP!`);
} else {
window.location.reload();
}
} catch (error) {
this._toast(String(error && error.message ? error.message : error));
if (button) button.disabled = false;
}
}
_toast(message) {
if (window.app && window.app.toast && typeof window.app.toast.show === "function") {
window.app.toast.show(message, { type: "info" });
}
}
_renderTotals(hpA, hpB) {
const total = Math.max(0, hpA) + Math.max(0, hpB);
const pctA = total ? Math.round((Math.max(0, hpA) * 100) / total) : 50;
const pctB = total ? 100 - pctA : 50;
this._setText("[data-war-hp-a]", hpA.toLocaleString());
this._setText("[data-war-hp-b]", hpB.toLocaleString());
this._setText("[data-war-pct-a]", `${pctA}%`);
this._setText("[data-war-pct-b]", `${pctB}%`);
const barA = this.querySelector("[data-war-bar-a]");
const barB = this.querySelector("[data-war-bar-b]");
if (barA) barA.style.setProperty("--war-pct", `${pctA}%`);
if (barB) barB.style.setProperty("--war-pct", `${pctB}%`);
if (this._field) {
this._field.classList.toggle("war-field-lead-a", hpA > hpB);
this._field.classList.toggle("war-field-lead-b", hpB > hpA);
}
}
_setText(selector, value) {
const node = this.querySelector(selector);
if (node) node.textContent = value;
}
_renderTicker(events) {
if (!this._ticker || !events.length) return;
this._ticker.textContent = "";
events.slice(0, TICKER_CAP).forEach((event) => {
this._ticker.appendChild(this._buildEvent(event, ""));
});
}
_buildEvent(event, timeLabel) {
const item = document.createElement("li");
item.className = `war-event war-event-${event.kind}`;
const dot = document.createElement("span");
dot.className = `war-event-dot war-dot-${event.faction || "n"}`;
dot.setAttribute("aria-hidden", "true");
const message = document.createElement("span");
message.className = "war-event-message";
message.textContent = event.message;
item.append(dot, message);
if (timeLabel) {
const time = document.createElement("span");
time.className = "war-event-time";
time.textContent = timeLabel;
item.appendChild(time);
}
return item;
}
_prependEvent(event) {
if (!this._ticker || !event.message) return;
const empty = this._ticker.querySelector(".war-event-empty");
if (empty) empty.remove();
this._ticker.prepend(this._buildEvent(event, "just now"));
while (this._ticker.children.length > TICKER_CAP) {
this._ticker.removeChild(this._ticker.lastChild);
}
}
_renderWar(war) {
if (!war || !war.uid) return;
this._renderTotals(war.hp_a, war.hp_b);
if (typeof war.last_seq === "number" && war.last_seq > this._lastSeq) {
this._lastSeq = war.last_seq;
this._renderTicker(war.recent_events || []);
}
if (war.viewer) {
this._setText("[data-war-mine-hp]", war.viewer.hp.toLocaleString());
const fight = this.querySelector("[data-war-fight]");
if (fight) {
fight.disabled = !war.viewer.can_fight || war.status !== "active";
fight.textContent = war.viewer.can_fight
? `Fight (${war.fight_cost} coins)`
: "On cooldown";
}
}
if (war.status === "resolved" && this._status === "active") {
this._finishResolved(war);
}
if (war.ends_at) this._endsAt = war.ends_at;
}
_finishResolved(war) {
this._status = "resolved";
this._teardown();
if (this._actions) this._actions.hidden = true;
if (this._countdown) this._countdown.textContent = "Battle ended";
if (this._victoryTitle) {
this._victoryTitle.textContent =
war.winner === "draw"
? `It's a draw at ${war.hp_a.toLocaleString()} HP each`
: `${war.winner_label} wins the war!`;
}
if (this._victory) this._victory.hidden = false;
}
_tickCountdown() {
if (!this._countdown || this._status !== "active") return;
const ends = Date.parse(this._endsAt);
if (Number.isNaN(ends)) return;
const remaining = ends - Date.now();
if (remaining <= 0) {
this._countdown.textContent = "Battle ended";
this._refreshState();
return;
}
const minutes = Math.floor(remaining / 60000);
const days = Math.floor(minutes / 1440);
const hours = Math.floor((minutes % 1440) / 60);
const mins = minutes % 60;
const label = days ? `${days}d ${hours}h ${mins}m` : hours ? `${hours}h ${mins}m` : `${Math.max(mins, 1)}m`;
this._countdown.textContent = `Battle ends in ${label}`;
}
}
customElements.define("dp-opinion-war", AppOpinionWar);