import { Observable } from "./observable";
import counters from './assets/images/counters.svg';
const weapons = {
rifle: {
name: 'Rifle',
damage: '4L',
shortRange: '1-27',
longRange: '28-75'
},
smg: {
name: 'SMG',
damage: '3L',
shortRange: '1-15',
longRange: '16-25'
},
blazer: {
name: 'Blazer',
damage: '4L',
shortRange: '1-17',
longRange: '18-105'
},
hsplaser: {
name: 'Hvy Semi-Portable Laser',
damage: '14L',
shortRange: '1-100',
longRange: '101-280'
},
lmg: {
name: 'Light MG',
damage: '5L',
shortRange: '1-30',
longRange: '31-84'
},
srm: {
name: 'SRM',
damage: '8/4/2 L',
shortRange: '1-44',
longRange: '45-108'
},
smggl: {
name: 'SMG w/Grenade Launcher',
damage: '4/2/1 L',
shortRange: '1-10',
longRange: '11-24'
},
riflegl: {
name: 'Rifle w/Grenade Launcher',
damage: '4/2/1 L',
shortRange: '1-10',
longRange: '11-24'
}
}
function createIcon(number) {
const [icon, use, text] = ['svg', 'use', 'text'].map(t => document.createElementNS(svgns, t));
icon.setAttributeNS(null, 'viewBox', '-6 -6 12 12');
icon.setAttribute('xmlns', svgns);
use.setAttributeNS(null, 'href', `./${counters}#counter-base`);
text.textContent = number;
icon.appendChild(use);
icon.appendChild(text);
return icon;
}
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random#getting_a_random_integer_between_two_values_inclusive
function getRandomIntInclusive(min, max) {
const minCeiled = Math.ceil(min);
const maxFloored = Math.floor(max);
return Math.floor(Math.random() * (maxFloored - minCeiled + 1) + minCeiled); // The maximum is inclusive and the minimum is inclusive
}
function createWeaponIcon(type) {
const [icon, use] = ['svg', 'use'].map(t => document.createElementNS(svgns, t));
icon.setAttributeNS(null, 'viewBox', '-6 -6 12 12');
icon.setAttribute('xmlns', svgns);
icon.classList.add('weapon-icon');
use.setAttributeNS(null, 'href', `${counters}#${type}`);
icon.appendChild(use);
return icon;
}
function makeInactiveDivider(parent) {
const div = document.createElement('div');
div.classList.add('inactive-divider');
div.textContent = 'Inactive';
parent.append(div);
return div;
}
function deactivationHandler(e) {
e.preventDefault();
if (!this.classList.contains('inactive')) {
const inactiveDivider = this.parentElement.querySelector('.inactive-divider') || makeInactiveDivider(this.parentElement);
this.addEventListener('transitionend', e => {
inactiveDivider.after(this);
inactiveDivider.scrollIntoView({ behavior: 'smooth' });
});
this.classList.add('inactive');
this.setAttributeNS(null, 'style', 'transform: scale(0.9);');
} else {
const squadRecords = this.parentElement.querySelectorAll(`.soldier-record:not(.inactive)[data-squad="${this.dataset.squad}"]`);
const sorted = [...squadRecords, this].sort(({ dataset: { number: a }}, { dataset: { number: b }}) => +a > +b);
const index = sorted.findIndex(record => record === this);
if (index === 0)
this.parentElement.prepend(this);
else if (index === sorted.length - 1)
sorted[sorted.length - 2].after(this)
else
sorted[index - 1].after(this)
this.classList.remove('inactive');
this.removeAttributeNS(null, 'style');
this.scrollIntoView({ behavior: 'smooth' });
}
}
function configArmor(unit, record) {
const s = `damage-block:nth-of-type(n + 1):nth-of-type(-n + ${unit.dataset.armor})`;
const armorBlocks = record.shadowRoot.querySelectorAll(s);
armorBlocks.forEach(el => el.classList.add('armor'));
const ls = 'damage-block:nth-child(1 of .armor):not(:first-child)';
const rs = 'damage-block:nth-last-child(1 of .armor):not(:last-child)';
const moveArmorEl = record.shadowRoot.querySelectorAll(`${ls}, ${rs}`);
function moveArmorHandler(e) {
e.stopPropagation();
this.removeEventListener('click', moveArmorHandler);
if (!this.previousElementSibling.classList.contains('armor')) {
this.previousElementSibling.classList.add('armor');
this.previousElementSibling.addEventListener('click', moveArmorHandler);
let current = this.nextElementSibling;
while (current.nextElementSibling && current.nextElementSibling.classList.contains('armor')) {
current = current.nextElementSibling;
}
current.classList.remove('armor');
current.removeEventListener('click', moveArmorHandler);
current.previousElementSibling.addEventListener('click', moveArmorHandler);
} else if (!this.nextElementSibling.classList.contains('armor')) {
this.nextElementSibling.classList.add('armor');
this.nextElementSibling.addEventListener('click', moveArmorHandler);
let current = this.previousElementSibling;
while (current.previousElementSibling && current.previousElementSibling.classList.contains('armor')) {
current = current.previousElementSibling;
}
current.classList.remove('armor');
current.removeEventListener('click', moveArmorHandler);
current.nextElementSibling.addEventListener('click', moveArmorHandler);
}
}
moveArmorEl.forEach(el => el.addEventListener('click', moveArmorHandler));
}
function createRecord(unit) {
const { dataset: { allegiance, number, squad }} = unit,
pw = unit.classList && unit.classList.contains('counter')
? unit.querySelector('.primary-weapon').getAttributeNS(null, 'href').split('#').pop()
: unit.dataset.weapon || 'rifle',
div = document.createElement('soldier-record-block'),
spans = Array(6).fill('span').map(t => document.createElement(t)),
[tn, sn, pwt, pwd, pwrs, pwrl] = spans;
div.classList.add('soldier-record');
if (unit.classList && unit.classList.contains('selected')) div.classList.add('selected');
div.dataset.number = number;
div.dataset.squad = squad;
div.dataset.allegiance = allegiance;
tn.setAttribute('slot', 'troop-number');
tn.appendChild(createIcon(number));
sn.setAttribute('slot', 'squad-number');
sn.appendChild(createIcon(squad || 1));
pwt.setAttribute('slot', 'primary-weapon-type');
pwt.textContent = ' ' + weapons[pw].name;
pwt.prepend(createWeaponIcon(pw));
pwd.setAttribute('slot', 'primary-weapon-damage');
pwd.textContent = weapons[pw].damage;
pwrs.setAttribute('slot', 'primary-weapon-range-short');
pwrs.textContent = weapons[pw].shortRange;
pwrl.setAttribute('slot', 'primary-weapon-range-long');
pwrl.textContent = weapons[pw].longRange;
spans.forEach(el => div.appendChild(el));
if (unit.dataset.armor) configArmor(unit, div);
div.addEventListener('contextmenu', deactivationHandler);
return div;
}
function createRecords(units) {
return Array.from(units).reduce((acc, unit) => {
const record = createRecord(unit),
{ allegiance, squad } = unit.dataset;
if (acc[allegiance]) {
acc[allegiance][squad]?.push(record) || (acc[allegiance][squad] = [record])
} else {
acc[allegiance] = { [squad]: [record] }
}
return acc;
}, {});
}
function getRecord({ dataset: { allegiance: al, number: n, squad: s }}) {
const selector = `.soldier-record[data-number="${n}"][data-allegiance="${al}"][data-squad="${s}"]`;
return document.querySelector(selector);
}
function deselect() {
const selected = getSelected();
if (selected) {
selected.classList.remove('selected');
}
}
function clear() {
document.querySelectorAll('#record-sheet > *').forEach(el => {
//el.querySelectorAll('.squad-number').forEach(sn => sn.remove());
const records = el.querySelector('.records');
records.dataset.viewSquadNumber = 1;
[...records.children].forEach(c => c.remove());
});
//document.querySelector('#attacker-record .name').textContent = 'attacker';
//document.querySelector('#defender-record .name').textContent = 'defender';
}
function reveal(record) {
const currentSquadView = document.querySelector(`#record-sheet #${record.dataset.allegiance}-record .records-header .squad-number text`);
const records = document.querySelector(`#record-sheet #${record.dataset.allegiance}-record .records`);
const target = records.querySelector(`.squad-${record.dataset.squad}`);
const currentSquad = records.querySelector(`.squad-${currentSquadView.textContent}`);
let direction;
let next = prev = currentSquad;
while (!direction && (next || prev)) {
next = next?.nextElementSibling;
prev = prev?.previousElementSibling;
if (next === target) direction = 'next';
else if (prev === target) direction = 'previous';
}
function showSquad(current, target, direction) {
current.addEventListener('transitionend', e => {
const toSquad = current[`${direction}ElementSibling`];
currentSquadView.textContent = +toSquad.className.match(/\d+/);
current.style.display = 'none';
// There needs to be a delay between making it visible and the
// transformation. ScrollTo seems to create enough delay.
toSquad.style.display = 'block';
records.scrollTo(0, 0);
if (toSquad[`${direction}ElementSibling`] && toSquad !== target) {
showSquad(toSquad, target, direction);
} else {
toSquad.style.transform = 'translateX(0)';
toSquad.addEventListener('transitionend', e => {
record.classList.add('selected');
record.scrollIntoView({ behavior: 'smooth' });
}, { once: true });
}
}, { once: true });
current.style.transform = `translateX(${direction === 'next' ? '-' : ''}100%)`;
}
if (currentSquad !== target)
showSquad(currentSquad, target, direction);
else
record.scrollIntoView({ behavior: 'smooth' });
}
function select(data) {
const record = data && getRecord(data);
const isSelected = record?.classList.contains('selected');
deselect();
if (isSelected || !data) return;
reveal(record);
record.classList.add('selected');
}
function endMove() {
const selected = getSelected();
if (selected) {
const list = selected.closest('.records').querySelectorAll('.soldier-record:not(.movement-ended, .inactive)');
const index = [...list].findIndex(s => s === selected);
const next = list.length > 1 ? list[(index + 1) % list.length] : null;
selected.classList.toggle('movement-ended');
deselect();
if (next) Observable.notify('select', next);
}
}
export function extractWeaponFromRecord(recordEl) {
return recordEl
.querySelector('[slot="primary-weapon-type"] use')
.getAttributeNS(null, 'href')
.split('#')
.pop();
}
export function isRecord(el) {
return el.classList && el.classList.contains('soldier-record');
}
export function getSelected() {
return document.querySelector('.soldier-record.selected');
}
export function start(startLoc, units) {
clear();
const forces = createRecords(units);
for (const affiliation in forces) {
const container = document.querySelector(`#${affiliation}-record`);
const records = container.querySelector('.records');
const viewSquadIndicator = container.querySelector('.squad-number svg text');
for (const squadNumber in forces[affiliation]) {
const squadContainer = document.createElement('div');
squadContainer.classList.add(`squad-${squadNumber}`);
forces[affiliation][squadNumber].forEach(r => squadContainer.append(r));
records.append(squadContainer);
}
viewSquadIndicator.textContent = Object.keys(forces[affiliation])[0];
}
document.querySelectorAll('.soldier-record').forEach(el =>
el.addEventListener('click', () => Observable.notify('select', el))
);
Observable.subscribe('select', select);
Observable.subscribe('endmove', endMove);
console.log('records created');
}
export function stop() {
Observable.unsubscribe('select', select);
Observable.unsubscribe('endmove', endMove);
}