|
// 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);
|