CREATE TABLE IF NOT EXISTS "cities" (
"id" string,
"owner" string,
+ "icon" string,
"totalSpace" int,
"usedSpace" int,
"gold" int,
INSERT INTO "units" VALUES ('attackers','Attackers',5,2,0,1,0,0,3,4,1);
INSERT INTO "units" VALUES ('defenders','Defenders',4,2,0,1,0,0,5,1,4);
INSERT INTO "units" VALUES ('sp_attackers','Sp. Attacker',9,4,0,0,1,0,7,7,3);
-INSERT INTO "units" VALUES ('sp_defenders','Sp. Defender',11,5,0,0,0,1,10,2,9);
\ No newline at end of file
+INSERT INTO "units" VALUES ('sp_defenders','Sp. Defender',11,5,0,0,0,1,10,2,9);
"dev": "npx nodemon src/api.ts",
"setup:rebels": "npx ts-node scripts/generate-cities.ts",
"setup:database": "npx ts-node scripts/setup.ts",
- "setup": "npm run setup:database && npm run setup:rebels"
+ "setup": "npm run setup:rebels"
},
"dependencies": {
"@bull-board/api": "^3.11.0",
<a href="#" hx-target="#main" hx-get="/poll/unit-training" hx-trigger="click">Unit Training</a>
</li>
<li>
- <a href="#" hx-target="#main" hx-get="/poll/map" hx-trigger="click">Map</a>
+ <a href="#" hx-target="#main" hx-post="/poll/map" hx-trigger="click">Map</a>
</li>
<li>
<a href="#" hx-target="#main" hx-get="/poll/mailroom" hx-trigger="click">Mail</a>
A project by <a href="https://xangelo.ca">xangelo</a>
</footer>
</body>
-</html>
\ No newline at end of file
+</html>
--page-bg: #061619;
--green-bg: #193818;
--green-border: #32821c;
+ --red-border: #821c1c;
}
input {
}
.danger {
background-color: #381818;
- border-color: #821c1c;
+ border-color: var(--red-border);
}
.danger::after {
color: #821c1c
padding: 10px 25px;
background-color: var(--page-bg);
justify-content: space-evenly;
-}
\ No newline at end of file
+}
+
+#overworld-map {
+ width: auto;
+ margin: 0 auto;
+}
+#overworld-map td {
+ overflow: hidden;
+ padding: 0;
+ background: transparent;
+ border: solid 1px #000;
+}
+#overworld-map td div {
+ width: 32px;
+ height: 32px;
+ overflow: hidden;
+}
+
+#overworld-map td div.city {
+ cursor: pointer;
+}
+#overworld-map td div.city.yours {
+ border: solid 1px var(--green-border);
+}
+#overworld-map td div.city.yours img {
+ width: 100%;
+}
+#overworld-map td div.city.others {
+ border: solid 1px var(--red-border);
+}
+#overworld-map td div.city.others img {
+ width: 100%;
+}
+
+#overworld-map .grid-numbers {
+ color: #7b7b7b;
+ text-align: center;
+}
+
+#sector-selector {
+ margin: 20px 50px;
+}
+#sector-selector select {
+ margin-left: 20px;
+}
-#home form > div {
- line-height: 3rem;
-}
table form {
margin-bottom: 0;
color: #fff;
width: 100%;
}
-
-#overworld-map td {
- overflow: hidden;
- padding: 0;
-}
-
-#overworld-map td div {
- width: 5px;
- height: 8px;
- overflow: hidden;
-}
-
-#overworld-map td div.city {
- cursor: pointer;
-}
-
-#overworld-map td div.city.yours {
- background-color: #eee;
-}
-#overworld-map td div.city.others {
- background-color: #a00;
-}
-
.unread td{
background-color: #373737;
-}
\ No newline at end of file
+}
(async () => {
for(let i = 0; i < 100; ++i) {
- await cityRepo.create(uuid());
+ await cityRepo.create(uuid(), true);
}
console.log('Created 100 cities');
process.exit(0);
-})();
\ No newline at end of file
+})();
import { unitTraining } from './tasks/unit-training';
import { fight } from './tasks/fight';
import { renderUnitTraining } from './render/unit-training';
-import { launchOffensive, listOperations, renderOverworldMap } from './render/fight';
+import { launchOffensive, listOperations, renderOverworldMap } from './render/map';
import { createBullBoard } from '@bull-board/api';
import { BullAdapter } from '@bull-board/api/bullAdapter';
import _ from 'lodash';
return renderUnitTraining(city, units, unitTrainingQueues);
});
-server.get<{}, string>('/poll/map', async req => {
+server.post<{body: {sector: string}}, string>('/poll/map', async req => {
const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
const city = await cityRepo.getUsersCity(account.id);
- return renderOverworldMap(await cityRepo.findAllInSector(city.sector_id), city);
+ let sector = city.sector_id;
+ if(req.body.sector) {
+ try {
+ sector = parseInt(req.body.sector);
+ }
+ catch(e) {
+ sector = city.sector_id;
+ }
+ }
+
+ console.log('Checking cities in sector', sector);
+
+ return renderOverworldMap(await cityRepo.findAllInSector(sector), city, sector);
});
server.get<{}, string>('/poll/mailroom', async req => {
});
-server.start();
\ No newline at end of file
+server.start();
export const TICK_LENGTH = process.env.TICK_LENGTH || 1000 * 60 * 5;
export const API_PORT = process.env.API_PORT || 9090;
-export const AVAILABLE_SECTORS = 1
+export const AVAILABLE_SECTORS = 2
+++ /dev/null
-import _ from "lodash";
-import { DateTime } from "luxon";
-import { Account } from "../repository/accounts";
-import { ArmyQueueWithAttackedOwner } from "../repository/army";
-import { City, CityRepository, CityWithLocation } from "../repository/city";
-import { topbar } from "./topbar";
-
-const cityRepo = new CityRepository();
-
-export function renderOverworldMap(cities: CityWithLocation[], yourCity: CityWithLocation): string {
- let map: CityWithLocation[][] = new Array(25);
- for(let i = 0; i < cities.length; ++i) {
- if(!map[cities[i].location_y]) {
- map[cities[i].location_y] = new Array(25);
- }
-
- map[cities[i].location_y][cities[i].location_x] = cities[i];
- }
-
- let rows: string[] = [];
- for(let y = 0; y < 25; ++y) {
- rows[y] = '<tr>';
- for(let x = 0; x < 25; ++x) {
- if(!map[y] || !map[y][x]) {
- rows[y] += '<td><div class="empty"></div></td>' ;
- }
- else if(map[y][x]) {
- if(map[y][x].id === yourCity.id)
- rows[y] += '<td><div class="city yours"></div></td>';
- else
- rows[y] += `<td><div class="city others" hx-trigger="click" hx-get="/city/${map[y][x].id}" hx-target="#city-offence"></div></td>`;
- }
- else {
- rows[y] += '<td><div class="empty"></div></td>';
- }
- }
- rows[y] += '</tr>';
- }
- let html = `
- <h2 data-augmented-ui="tl-clip bl-clip none">Map</h2>
- <div id="city-offence"></div>
- <div id="outgoing-attacks" hx-trigger="reload-outgoing-attacks, every 600s, load" hx-get="/attacks/outgoing"></div>
- <table id="overworld-map">
- ${rows.join("\n")}
- </table>
-
- `;
-
- return html;
-
-}
-
-export function listOperations(outgoingOps: ArmyQueueWithAttackedOwner[]): string {
- let html = `
- <h4>Outgoing Attacks</h4>
- <table>
- <tr>
- <th>Destination</th>
- <th>Sol.</th>
- <th>Atk.</th>
- <th>Def.</th>
- <th>Sp. Atk.</th>
- <th>Sp. Def.</th>
- <th>Est. Arrival</th>
- </tr>
-
- ${outgoingOps.map(op => {
- return `
- <tr>
- <td>${op.attacked_account} - (${op.location_x},${op.location_y})</td>
- <td>${op.soldiers.toLocaleString()}</td>
- <td>${op.attackers.toLocaleString()}</td>
- <td>${op.defenders.toLocaleString()}</td>
- <td>${op.sp_attackers.toLocaleString()}</td>
- <td>${op.sp_defenders.toLocaleString()}</td>
- <td>${ _.round(DateTime.fromMillis(op.due).diffNow().as('hours'), 2)} hours</td>
- </tr>
- `;
- }).join("\n")}
- </table>
- `;
-
- return html;
-}
-
-export async function launchOffensive(city: CityWithLocation, owner: Account, yourCity: CityWithLocation, you: Account): Promise<string> {
- const distance = cityRepo.distanceInHours(city, yourCity);
- const power = await cityRepo.power({
- soldiers: yourCity.soldiers,
- attackers: yourCity.attackers,
- defenders: yourCity.defenders,
- sp_attackers: yourCity.sp_attackers,
- sp_defenders: yourCity.sp_defenders
- })
- let html = `
- <h3 data-augmented-ui="tl-clip bl-clip none">Viewing (${city.location_x},${city.location_y}) owned by ${owner.username}</h3>
- <h4>Report</h4>
- <p>This city is ${distance} hours away.</p>
- <form hx-post="/attack">
- <input type="hidden" name="city" value="${city.id}">
- <table>
- <tr>
- <th>Soldiers</th>
- <td>
- <input type="number" class="potential-attack" name="soldiers" max="${yourCity.soldiers}" value="${yourCity.soldiers}" hx-target="#total-attack-power" hx-post="/attack-power" hx-include=".potential-attack">
- (${yourCity.soldiers})
- </td>
- </tr>
- <tr>
- <th>Attackers</th>
- <td>
- <input type="number" class="potential-attack" name="attackers" max="${yourCity.attackers}" value="${yourCity.attackers}" hx-target="#total-attack-power" hx-post="/attack-power" hx-include=".potential-attack">
- (${yourCity.attackers})
- </td>
- </tr>
- <tr>
- <th>Defenders</th>
- <td>
- <input type="number" class="potential-attack" name="defenders" max="${yourCity.defenders}" value="${yourCity.defenders}" hx-target="#total-attack-power" hx-post="/attack-power" hx-include=".potential-attack">
- (${yourCity.defenders})
- </td>
- </tr>
- <tr>
- <th>Sp. Attack</th>
- <td>
- <input type="number" class="potential-attack" name="sp_attackers" max="${yourCity.sp_attackers}" value="${yourCity.sp_attackers}" hx-target="#total-attack-power" hx-post="/attack-power" hx-include=".potential-attack">
- (${yourCity.sp_attackers})
- </td>
- </tr>
- <tr>
- <th>Sp. Defenders</th>
- <td>
- <input type="number" class="potential-attack" name="sp_defenders" max="${yourCity.sp_defenders}" value="${yourCity.sp_defenders}" hx-target="#total-attack-power" hx-post="/attack-power" hx-include=".potential-attack">
- (${yourCity.sp_defenders})
- </td>
- </tr>
- <tr>
- <th>Total Power</th>
- <td id="total-attack-power">${power}</td>
- </tr>
- <tr>
- <td colspan="2" style="text-align: right">
- <button type="submit">Attack (${city.location_x},${city.location_y})</button>
- </td>
- </tr>
- </table>
- </form>
- `;
-
- return html + topbar(yourCity);
-}
\ No newline at end of file
</tr>
<tr>
<th>Location</th>
- <td>${city.location_x},${city.location_y}</td>
+ <td>${city.sector_id} - (${city.location_x},${city.location_y})</td>
<th>Attackers</th>
<td>${city.attackers.toLocaleString()}</td>
</tr>
</table>
</div>
${topbar(city)}`;
-}
\ No newline at end of file
+}
--- /dev/null
+import _ from "lodash";
+import { DateTime } from "luxon";
+import { Account } from "../repository/accounts";
+import { ArmyQueueWithAttackedOwner } from "../repository/army";
+import { City, CityRepository, CityWithLocation } from "../repository/city";
+import { topbar } from "./topbar";
+import { AVAILABLE_SECTORS } from '../config';
+import {initArray} from "../lib/util";
+
+const cityRepo = new CityRepository();
+
+export function renderOverworldMap(cities: CityWithLocation[], yourCity: CityWithLocation, activeSector: number): string {
+ let map: CityWithLocation[][] = new Array(25);
+ for(let i = 0; i < cities.length; ++i) {
+ if(!map[cities[i].location_y]) {
+ map[cities[i].location_y] = new Array(25);
+ }
+
+ map[cities[i].location_y][cities[i].location_x] = cities[i];
+ }
+
+ let rows: string[] = [];
+ let headerRow: string[] = [];
+ for(let y = 0; y <= 25; ++y) {
+ rows[y] = '<tr>';
+ if(y === 0) {
+ // first row! add the X coors
+ for(let x = 0; x <= 25; ++x) {
+ headerRow.push(`<td class="grid-numbers">${x}</td>`);
+ }
+ }
+ else {
+ for(let x = 0; x <= 25; ++x) {
+ if(x === 0) {
+ rows[y] += `<td class="grid-numbers">${y}</td>`;
+ }
+ else {
+ if(!map[y] || !map[y][x]) {
+ rows[y] += `<td title="(${x},${y})"><div class="empty"></div></td>` ;
+ }
+ else if(map[y][x]) {
+ if(map[y][x].id === yourCity.id)
+ rows[y] += `<td title="You (${x},${y})"><div class="city yours"><img src="/assets/${map[y][x].icon}"></div></td>`;
+ else
+ rows[y] += `<td title="(${x},${y})"><div class="city others" hx-trigger="click" hx-get="/city/${map[y][x].id}" hx-target="#city-offence">
+ <img src="/assets/${map[y][x].icon}">
+ </div></td>`;
+ }
+ else {
+ rows[y] += '<td><div class="empty"></div></td>';
+ }
+ }
+ }
+ }
+ rows[y] += '</tr>';
+ }
+ let sectorSwitch: string[] = [];
+ for(let i = 1; i <= AVAILABLE_SECTORS; ++i) {
+ sectorSwitch.push(
+ `<option value="${i}" ${activeSector === i ? 'selected': ''}>Sector ${i}</option>`
+ );
+ }
+ let html = `
+ <h2 data-augmented-ui="tl-clip bl-clip none">Map</h2>
+ <div id="city-offence"></div>
+ <div id="outgoing-attacks" hx-trigger="reload-outgoing-attacks, every 600s, load" hx-get="/attacks/outgoing"></div>
+ <div id="sector-selector" class="text-right">
+ Switch Scanned Sectors:
+ <select name="sector" hx-trigger="change" hx-post="/poll/map" hx-target="#main">
+ ${sectorSwitch.join("\n")}
+ </select>
+ </div>
+ <table id="overworld-map" style="background-image:url('/assets/bg/mapoverview_${activeSector}.png');">
+ <tr>
+ ${headerRow.join("\n")}
+ </tr>
+ ${rows.join("\n")}
+ </table>
+ <br>
+ `;
+
+
+ return html;
+
+}
+
+export function listOperations(outgoingOps: ArmyQueueWithAttackedOwner[]): string {
+ let html = `
+ <h4>Outgoing Attacks</h4>
+ <table>
+ <tr>
+ <th>Destination</th>
+ <th>Sol.</th>
+ <th>Atk.</th>
+ <th>Def.</th>
+ <th>Sp. Atk.</th>
+ <th>Sp. Def.</th>
+ <th>Est. Arrival</th>
+ </tr>
+
+ ${outgoingOps.map(op => {
+ return `
+ <tr>
+ <td>${op.attacked_account} - (${op.location_x},${op.location_y})</td>
+ <td>${op.soldiers.toLocaleString()}</td>
+ <td>${op.attackers.toLocaleString()}</td>
+ <td>${op.defenders.toLocaleString()}</td>
+ <td>${op.sp_attackers.toLocaleString()}</td>
+ <td>${op.sp_defenders.toLocaleString()}</td>
+ <td>${ _.round(DateTime.fromMillis(op.due).diffNow().as('hours'), 2)} hours</td>
+ </tr>
+ `;
+ }).join("\n")}
+ </table>
+ `;
+
+ return html;
+}
+
+export async function launchOffensive(city: CityWithLocation, owner: Account, yourCity: CityWithLocation, you: Account): Promise<string> {
+ const distance = cityRepo.distanceInHours(city, yourCity);
+ const power = await cityRepo.power({
+ soldiers: yourCity.soldiers,
+ attackers: yourCity.attackers,
+ defenders: yourCity.defenders,
+ sp_attackers: yourCity.sp_attackers,
+ sp_defenders: yourCity.sp_defenders
+ })
+ let html = `
+ <h3 data-augmented-ui="tl-clip bl-clip none">Viewing (${city.location_x},${city.location_y}) owned by ${owner.username}</h3>
+ <h4>Report</h4>
+ <p>This city is ${distance} hours away.</p>
+ <form hx-post="/attack">
+ <input type="hidden" name="city" value="${city.id}">
+ <table>
+ <tr>
+ <th>Soldiers</th>
+ <td>
+ <input type="number" class="potential-attack" name="soldiers" max="${yourCity.soldiers}" value="${yourCity.soldiers}" hx-target="#total-attack-power" hx-post="/attack-power" hx-include=".potential-attack">
+ (${yourCity.soldiers})
+ </td>
+ </tr>
+ <tr>
+ <th>Attackers</th>
+ <td>
+ <input type="number" class="potential-attack" name="attackers" max="${yourCity.attackers}" value="${yourCity.attackers}" hx-target="#total-attack-power" hx-post="/attack-power" hx-include=".potential-attack">
+ (${yourCity.attackers})
+ </td>
+ </tr>
+ <tr>
+ <th>Defenders</th>
+ <td>
+ <input type="number" class="potential-attack" name="defenders" max="${yourCity.defenders}" value="${yourCity.defenders}" hx-target="#total-attack-power" hx-post="/attack-power" hx-include=".potential-attack">
+ (${yourCity.defenders})
+ </td>
+ </tr>
+ <tr>
+ <th>Sp. Attack</th>
+ <td>
+ <input type="number" class="potential-attack" name="sp_attackers" max="${yourCity.sp_attackers}" value="${yourCity.sp_attackers}" hx-target="#total-attack-power" hx-post="/attack-power" hx-include=".potential-attack">
+ (${yourCity.sp_attackers})
+ </td>
+ </tr>
+ <tr>
+ <th>Sp. Defenders</th>
+ <td>
+ <input type="number" class="potential-attack" name="sp_defenders" max="${yourCity.sp_defenders}" value="${yourCity.sp_defenders}" hx-target="#total-attack-power" hx-post="/attack-power" hx-include=".potential-attack">
+ (${yourCity.sp_defenders})
+ </td>
+ </tr>
+ <tr>
+ <th>Total Power</th>
+ <td id="total-attack-power">${power}</td>
+ </tr>
+ <tr>
+ <td colspan="2" style="text-align: right">
+ <button type="submit">Attack (${city.location_x},${city.location_y})</button>
+ </td>
+ </tr>
+ </table>
+ </form>
+ `;
+
+ return html + topbar(yourCity);
+}
barracks: number;
special_attacker_trainer: number;
special_defender_trainer: number;
+ icon: string;
}
export type CityWithLocation = {
this.armyRepository = new ArmyRepository();
}
- async create(accountId: string): Promise<CityWithLocation> {
+ async create(accountId: string, rebel: boolean = false): Promise<CityWithLocation> {
+ const icon = rebel ? `/colony-ships/rebels/${random(1, 6)}.png` : '/colony-ships/01.png';
const info: City = {
id: uuid(),
owner: accountId,
barracks: 0,
special_attacker_trainer: 0,
special_defender_trainer: 0,
+ icon
};
await this.Insert(info);
Math.pow((city2.location_y - city1.location_y), 2)
);
- return _.round(dist/4, 2);
+ // sectors always add 4 hours
+ const sector_dist = Math.abs(city1.sector_id - city2.sector_id) * 6;
+
+ return _.round(dist/4, 2) + sector_dist;
}