mirror of
https://codeberg.org/demostf/frontend.git
synced 2026-06-03 18:24:12 +02:00
some basic viewer
This commit is contained in:
parent
2dee28d022
commit
5910b2f35a
45 changed files with 1089 additions and 1436 deletions
103
script/api.js
103
script/api.js
|
|
@ -1,103 +0,0 @@
|
|||
"use strict";
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __generator = (this && this.__generator) || function (thisArg, body) {
|
||||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
||||
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
||||
function verb(n) { return function (v) { return step([n, v]); }; }
|
||||
function step(op) {
|
||||
if (f) throw new TypeError("Generator is already executing.");
|
||||
while (_) try {
|
||||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
||||
if (y = 0, t) op = [op[0] & 2, t.value];
|
||||
switch (op[0]) {
|
||||
case 0: case 1: t = op; break;
|
||||
case 4: _.label++; return { value: op[1], done: false };
|
||||
case 5: _.label++; y = op[1]; op = [0]; continue;
|
||||
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
||||
default:
|
||||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
||||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
||||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
||||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
||||
if (t[2]) _.ops.pop();
|
||||
_.trys.pop(); continue;
|
||||
}
|
||||
op = body.call(thisArg, _);
|
||||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
||||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
||||
}
|
||||
};
|
||||
exports.__esModule = true;
|
||||
exports.Api = void 0;
|
||||
var Api = /** @class */ (function () {
|
||||
function Api(base) {
|
||||
this.base = base;
|
||||
}
|
||||
Api.prototype.getApiUrl = function (url) {
|
||||
return this.base + url;
|
||||
};
|
||||
Api.prototype.request = function (url, params, json) {
|
||||
if (params === void 0) { params = {}; }
|
||||
if (json === void 0) { json = true; }
|
||||
var queryParams = new URLSearchParams(params);
|
||||
return fetch(this.getApiUrl(url) + '?' + queryParams)
|
||||
.then(function (response) {
|
||||
if (json) {
|
||||
return response.json();
|
||||
}
|
||||
else {
|
||||
return response.text();
|
||||
}
|
||||
});
|
||||
};
|
||||
Api.prototype.searchPlayer = function (query) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var players, _i, players_1, player;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0:
|
||||
if (query.length < 2) {
|
||||
return [2 /*return*/, []];
|
||||
}
|
||||
return [4 /*yield*/, this.request('users/search', { query: query })];
|
||||
case 1:
|
||||
players = _a.sent();
|
||||
for (_i = 0, players_1 = players; _i < players_1.length; _i++) {
|
||||
player = players_1[_i];
|
||||
localStorage.setItem("player.".concat(player.steamid), JSON.stringify(player));
|
||||
}
|
||||
return [2 /*return*/, players];
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
Api.prototype.getPlayer = function (id) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var cached, player;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0:
|
||||
cached = localStorage.getItem("player.".concat(id));
|
||||
if (cached) {
|
||||
return [2 /*return*/, JSON.parse(cached)];
|
||||
}
|
||||
return [4 /*yield*/, this.request("users/".concat(id), {})];
|
||||
case 1:
|
||||
player = _a.sent();
|
||||
localStorage.setItem("player.".concat(id), JSON.stringify(player));
|
||||
return [2 /*return*/, player];
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
return Api;
|
||||
}());
|
||||
exports.Api = Api;
|
||||
28
script/download.ts
Normal file
28
script/download.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
export async function download(url: string, progress: (number) => void): Promise<ArrayBuffer> {
|
||||
const response = await fetch(url, {mode: 'cors'});
|
||||
|
||||
if (!response.body || !response.headers) {
|
||||
throw new Error("invalid response");
|
||||
}
|
||||
const contentLength = +(response.headers.get('Content-Length') || 0);
|
||||
let receivedLength = 0;
|
||||
|
||||
let data = new Uint8Array(contentLength);
|
||||
|
||||
const reader = response.body.getReader();
|
||||
|
||||
while(true) {
|
||||
const {done, value} = await reader.read();
|
||||
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
|
||||
data.set(value, receivedLength);
|
||||
receivedLength += value.length;
|
||||
|
||||
progress((receivedLength / contentLength) * 100);
|
||||
}
|
||||
|
||||
return data.buffer;
|
||||
}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
"use strict";
|
||||
var __assign = (this && this.__assign) || function () {
|
||||
__assign = Object.assign || function(t) {
|
||||
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
||||
s = arguments[i];
|
||||
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
||||
t[p] = s[p];
|
||||
}
|
||||
return t;
|
||||
};
|
||||
return __assign.apply(this, arguments);
|
||||
};
|
||||
exports.__esModule = true;
|
||||
exports.queryForFilter = exports.Reset = exports.FilterBar = void 0;
|
||||
var solid_select_1 = require("@thisbeyond/solid-select");
|
||||
var solid_js_1 = require("solid-js");
|
||||
var FilterBar = function (_a) {
|
||||
var maps = _a.maps, api = _a.api, onChange = _a.onChange, initialFilter = _a.initialFilter;
|
||||
var modes = (0, solid_select_1.createOptions)(["4v4", "6v6", "Highlander"]);
|
||||
var mapOptions = (0, solid_select_1.createOptions)(maps, {
|
||||
createable: true
|
||||
});
|
||||
var playerOptions = (0, solid_select_1.createAsyncOptions)(function (search) { return api.searchPlayer(search); });
|
||||
var playerFormat = function (player) { return player.name; };
|
||||
var _b = (0, solid_js_1.createSignal)(initialFilter.mode, { equals: false }), initialMode = _b[0], setInitialMode = _b[1];
|
||||
var _c = (0, solid_js_1.createSignal)(initialFilter.map, { equals: false }), initialMap = _c[0], setInitialMap = _c[1];
|
||||
var _d = (0, solid_js_1.createSignal)(initialFilter), filterSet = _d[0], setFilterSet = _d[1];
|
||||
(0, solid_js_1.createEffect)(function () { return onChange(filterSet()); });
|
||||
return <div class="filter-bar">
|
||||
<solid_select_1.Select class="mode" onChange={function (mode) { return setFilterSet(__assign(__assign({}, filterSet()), { mode: mode })); }} initialValue={initialMode()} placeholder="All Types" {...modes}/>
|
||||
<exports.Reset reset={function () {
|
||||
setInitialMode("");
|
||||
onChange(__assign(__assign({}, filterSet()), { mode: "" }));
|
||||
}}/>
|
||||
<solid_select_1.Select class="maps" onChange={function (map) { return setFilterSet(__assign(__assign({}, filterSet()), { map: map })); }} initialValue={initialMap()} placeholder="All Maps" {...mapOptions}/>
|
||||
<exports.Reset reset={function () {
|
||||
setInitialMap("");
|
||||
console.log(__assign(__assign({}, filterSet()), { map: "" }));
|
||||
onChange(__assign(__assign({}, filterSet()), { map: "" }));
|
||||
}}/>
|
||||
<solid_select_1.Select class="players" initialValue={initialFilter.players} onChange={function (players) { return setFilterSet(__assign(__assign({}, filterSet()), { players: players })); }} multiple placeholder="All Players" format={playerFormat} {...playerOptions}/>
|
||||
</div>;
|
||||
};
|
||||
exports.FilterBar = FilterBar;
|
||||
var Reset = function (_a) {
|
||||
var reset = _a.reset;
|
||||
return <button onMouseDown={reset} class="reset">X</button>;
|
||||
};
|
||||
exports.Reset = Reset;
|
||||
function queryForFilter(filter) {
|
||||
var queryParams = new URLSearchParams({
|
||||
players: filter.players.map(function (player) { return player.steamid; }).join(','),
|
||||
mode: (filter.mode || "").toLowerCase(),
|
||||
map: filter.map || ""
|
||||
});
|
||||
if (filter.uploader) {
|
||||
queryParams.set("uploader", filter.uploader);
|
||||
}
|
||||
return queryParams;
|
||||
}
|
||||
exports.queryForFilter = queryForFilter;
|
||||
|
|
@ -58,7 +58,7 @@ export const FilterBar = ({maps, api, onChange, initialFilter}: FilterBarProps)
|
|||
}
|
||||
|
||||
export const Reset = ({reset}) => {
|
||||
return <button onMouseDown={reset} class="reset">X</button>;
|
||||
return <button onClick={reset} class="reset">X</button>;
|
||||
}
|
||||
|
||||
export interface FilterSet {
|
||||
|
|
|
|||
|
|
@ -1,88 +0,0 @@
|
|||
"use strict";
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __generator = (this && this.__generator) || function (thisArg, body) {
|
||||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
||||
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
||||
function verb(n) { return function (v) { return step([n, v]); }; }
|
||||
function step(op) {
|
||||
if (f) throw new TypeError("Generator is already executing.");
|
||||
while (_) try {
|
||||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
||||
if (y = 0, t) op = [op[0] & 2, t.value];
|
||||
switch (op[0]) {
|
||||
case 0: case 1: t = op; break;
|
||||
case 4: _.label++; return { value: op[1], done: false };
|
||||
case 5: _.label++; y = op[1]; op = [0]; continue;
|
||||
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
||||
default:
|
||||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
||||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
||||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
||||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
||||
if (t[2]) _.ops.pop();
|
||||
_.trys.pop(); continue;
|
||||
}
|
||||
op = body.call(thisArg, _);
|
||||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
||||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
||||
}
|
||||
};
|
||||
exports.__esModule = true;
|
||||
exports.parseHeader = void 0;
|
||||
DataView.prototype["getString"] = function (offset, length) {
|
||||
var end = typeof length == 'number' ? offset + length : this.byteLength;
|
||||
var text = '';
|
||||
var val = -1;
|
||||
while (offset < this.byteLength && offset < end) {
|
||||
val = this.getUint8(offset++);
|
||||
if (val === 0)
|
||||
break;
|
||||
text += String.fromCharCode(val);
|
||||
}
|
||||
return text;
|
||||
};
|
||||
function parseHeader(file) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var data, view;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0: return [4 /*yield*/, readFile(file)];
|
||||
case 1:
|
||||
data = _a.sent();
|
||||
view = new DataView(data);
|
||||
return [2 /*return*/, {
|
||||
'type': view.getString(0, 8),
|
||||
'server': view.getString(16, 260),
|
||||
'nick': view.getString(276, 260),
|
||||
'map': view.getString(536, 260),
|
||||
'game': view.getString(796, 260),
|
||||
'duration': view.getFloat32(1056, true),
|
||||
'ticks': view.getUint32(1060, true)
|
||||
}];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
exports.parseHeader = parseHeader;
|
||||
function readFile(file) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
return __generator(this, function (_a) {
|
||||
return [2 /*return*/, new Promise(function (resolve, reject) {
|
||||
var reader = new FileReader();
|
||||
reader.onload = function () {
|
||||
resolve(reader.result);
|
||||
};
|
||||
reader.onerror = reject;
|
||||
reader.readAsArrayBuffer(file);
|
||||
})];
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -26,8 +26,12 @@ export interface GetStringDataView extends DataView {
|
|||
getString: (offset: number, length: number) => string;
|
||||
}
|
||||
|
||||
export async function parseHeader(file): Promise<DemoHead> {
|
||||
export async function parseHeader(file: File): Promise<DemoHead> {
|
||||
const data = await readFile(file);
|
||||
return parseHeaderFromBuffer(data);
|
||||
}
|
||||
|
||||
export function parseHeaderFromBuffer(data: ArrayBuffer): DemoHead {
|
||||
const view = new DataView(data) as GetStringDataView;
|
||||
return {
|
||||
'type': view.getString(0, 8),
|
||||
|
|
@ -40,7 +44,7 @@ export async function parseHeader(file): Promise<DemoHead> {
|
|||
};
|
||||
}
|
||||
|
||||
async function readFile(file: File): Promise<ArrayBuffer> {
|
||||
export async function readFile(file: File): Promise<ArrayBuffer> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
"use strict";
|
||||
exports.__esModule = true;
|
||||
exports.ready = void 0;
|
||||
function ready(cb) {
|
||||
if (document.readyState === "complete") {
|
||||
cb();
|
||||
}
|
||||
else {
|
||||
document.addEventListener("DOMContentLoaded", cb);
|
||||
}
|
||||
}
|
||||
exports.ready = ready;
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
"use strict";
|
||||
exports.__esModule = true;
|
||||
exports.formatDuration = void 0;
|
||||
function formatDuration(input) {
|
||||
if (!input) {
|
||||
return '0:00';
|
||||
}
|
||||
var hours = Math.floor(input / 3600);
|
||||
var minutes = Math.floor((input - (hours * 3600)) / 60);
|
||||
var seconds = Math.floor(input - (hours * 3600) - (minutes * 60));
|
||||
var hourString = (hours < 10) ? "0" + hours : "" + hours;
|
||||
var minuteString = (minutes < 10) ? "0" + minutes : "" + minutes;
|
||||
var secondString = (seconds < 10) ? "0" + seconds : "" + seconds;
|
||||
if (hourString !== '00') {
|
||||
return hourString + ':' + minuteString + ':' + secondString;
|
||||
}
|
||||
else {
|
||||
return minuteString + ':' + secondString;
|
||||
}
|
||||
}
|
||||
exports.formatDuration = formatDuration;
|
||||
144
script/upload.js
144
script/upload.js
|
|
@ -1,144 +0,0 @@
|
|||
"use strict";
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __generator = (this && this.__generator) || function (thisArg, body) {
|
||||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
||||
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
||||
function verb(n) { return function (v) { return step([n, v]); }; }
|
||||
function step(op) {
|
||||
if (f) throw new TypeError("Generator is already executing.");
|
||||
while (_) try {
|
||||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
||||
if (y = 0, t) op = [op[0] & 2, t.value];
|
||||
switch (op[0]) {
|
||||
case 0: case 1: t = op; break;
|
||||
case 4: _.label++; return { value: op[1], done: false };
|
||||
case 5: _.label++; y = op[1]; op = [0]; continue;
|
||||
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
||||
default:
|
||||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
||||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
||||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
||||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
||||
if (t[2]) _.ops.pop();
|
||||
_.trys.pop(); continue;
|
||||
}
|
||||
op = body.call(thisArg, _);
|
||||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
||||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
||||
}
|
||||
};
|
||||
exports.__esModule = true;
|
||||
var ready_1 = require("./ready");
|
||||
var header_1 = require("./header");
|
||||
var time_1 = require("./time");
|
||||
(0, ready_1.ready)(function () {
|
||||
var red_name = document.querySelector(".red input");
|
||||
var blue_name = document.querySelector(".blue input");
|
||||
var file = document.querySelector(".dropzone input[type=\"file\"]");
|
||||
var drop_text = document.querySelector(".dropzone .text");
|
||||
var button = document.querySelector(".upload > button");
|
||||
var map = document.querySelector(".demo-info .map");
|
||||
var time = document.querySelector(".demo-info .time");
|
||||
var apiBase = document.querySelector("input[name=\"api\"]").value;
|
||||
var key = document.querySelector(".key").textContent;
|
||||
var selectedFile = null;
|
||||
console.log(key);
|
||||
file.addEventListener("change", function (event) { return __awaiter(void 0, void 0, void 0, function () {
|
||||
var file, header;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0:
|
||||
file = event.target.files[0];
|
||||
drop_text.textContent = file.name;
|
||||
return [4 /*yield*/, (0, header_1.parseHeader)(file)];
|
||||
case 1:
|
||||
header = _a.sent();
|
||||
if (header.type === "HL2DEMO" && header.game === "tf") {
|
||||
map.textContent = header.map;
|
||||
time.textContent = (0, time_1.formatDuration)(header.duration);
|
||||
button.removeAttribute("disabled");
|
||||
selectedFile = file;
|
||||
}
|
||||
else {
|
||||
drop_text.textContent = "Malformed demo or not a TF2 demo";
|
||||
map.textContent = "";
|
||||
time.textContent = "";
|
||||
button.setAttribute("disabled", "disabled");
|
||||
selectedFile = null;
|
||||
}
|
||||
return [2 /*return*/];
|
||||
}
|
||||
});
|
||||
}); });
|
||||
button.addEventListener("click", function () { return __awaiter(void 0, void 0, void 0, function () {
|
||||
var _a, e_1;
|
||||
return __generator(this, function (_b) {
|
||||
switch (_b.label) {
|
||||
case 0:
|
||||
button.setAttribute("disabled", "disabled");
|
||||
if (!selectedFile) {
|
||||
return [2 /*return*/];
|
||||
}
|
||||
drop_text.textContent = "Uploading...";
|
||||
_b.label = 1;
|
||||
case 1:
|
||||
_b.trys.push([1, 3, , 4]);
|
||||
_a = window.location;
|
||||
return [4 /*yield*/, uploadDemo(apiBase, key, red_name.value || 'RED', blue_name.value || 'BLU', selectedFile.name, selectedFile)];
|
||||
case 2:
|
||||
_a.href = _b.sent();
|
||||
return [3 /*break*/, 4];
|
||||
case 3:
|
||||
e_1 = _b.sent();
|
||||
drop_text.textContent = "Error ".concat(e_1.message);
|
||||
return [3 /*break*/, 4];
|
||||
case 4: return [2 /*return*/];
|
||||
}
|
||||
});
|
||||
}); });
|
||||
});
|
||||
function uploadDemo(apiBase, key, red, blue, name, demo) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var data, response, _a, body, matches;
|
||||
return __generator(this, function (_b) {
|
||||
switch (_b.label) {
|
||||
case 0:
|
||||
data = new FormData();
|
||||
data.append('key', key);
|
||||
data.append('red', red);
|
||||
data.append('blu', blue);
|
||||
data.append('name', name);
|
||||
data.append('demo', demo, demo.name);
|
||||
return [4 /*yield*/, fetch(apiBase + "upload", {
|
||||
method: 'POST',
|
||||
body: data
|
||||
})];
|
||||
case 1:
|
||||
response = _b.sent();
|
||||
if (!(response.status >= 400)) return [3 /*break*/, 3];
|
||||
_a = Error.bind;
|
||||
return [4 /*yield*/, response.text()];
|
||||
case 2: throw new (_a.apply(Error, [void 0, _b.sent()]))();
|
||||
case 3: return [4 /*yield*/, response.text()];
|
||||
case 4:
|
||||
body = _b.sent();
|
||||
matches = body.match(/STV available at: https?:\/\/[^/]+\/(\d+)/);
|
||||
if (matches) {
|
||||
return [2 /*return*/, matches[1]];
|
||||
}
|
||||
else {
|
||||
throw new Error(body);
|
||||
}
|
||||
return [2 /*return*/];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
45
script/viewer.tsx
Normal file
45
script/viewer.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import {ready} from './ready';
|
||||
import {parseHeaderFromBuffer, readFile} from './header';
|
||||
import {download} from "./download";
|
||||
import {AsyncParser} from "./viewer/Analyse/Data/AsyncParser";
|
||||
import {render} from "solid-js/web";
|
||||
import {Analyser} from "./viewer/Analyse/Analyser";
|
||||
|
||||
ready(async () => {
|
||||
const fileInput: HTMLInputElement | null = document.querySelector(`.dropzone input[type="file"]`);
|
||||
const urlInput: HTMLInputElement | null = document.querySelector(`.viewer-page input[name="url"]`);
|
||||
const drop_text = document.querySelector(`.dropzone .text`);
|
||||
const downloadProgress: HTMLProgressElement = document.querySelector(`progress.download`);
|
||||
const parseProgress: HTMLProgressElement = document.querySelector(`progress.parse`);
|
||||
|
||||
if (fileInput) {
|
||||
fileInput.addEventListener("change", async (event: InputEvent) => {
|
||||
let file = (event.target as HTMLInputElement).files[0];
|
||||
drop_text.textContent = file.name;
|
||||
const data = await readFile(file);
|
||||
const header = parseHeaderFromBuffer(data);
|
||||
|
||||
if (header.type === "HL2DEMO" && header.game === "tf") {
|
||||
drop_text.textContent = "Parsing...";
|
||||
parse(data, parseProgress, false);
|
||||
} else {
|
||||
drop_text.textContent = "Malformed demo or not a TF2 demo";
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const url = urlInput.value;
|
||||
console.log(url);
|
||||
const data = await download(url, (progress) => downloadProgress.value = progress);
|
||||
parse(data, parseProgress, true);
|
||||
}
|
||||
})
|
||||
|
||||
const parse = async (data: ArrayBuffer, parseProgress: HTMLProgressElement, stored: boolean) => {
|
||||
const header = parseHeaderFromBuffer(data);
|
||||
const parser = new AsyncParser(data, (progress) => parseProgress.value = progress);
|
||||
await parser.cache();
|
||||
|
||||
const page = document.querySelector('.viewer-page');
|
||||
|
||||
render(() => <Analyser parser={parser} header={header} isStored={stored}/>, page);
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
.analyse-menu {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0.5;
|
||||
transition: all 0.5s;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.share-session {
|
||||
|
||||
background: transparent;
|
||||
color: var(--primary-color);
|
||||
font-size: 200%;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin: 10px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
background-image: url("../images/link_white.svg");
|
||||
background-size: contain;
|
||||
|
||||
&:active, &:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.share-text {
|
||||
color: var(--primary-color);
|
||||
background-color: var(--text-secondary);
|
||||
padding: 5px;
|
||||
margin-top: 0;
|
||||
border: 1px #888 solid;
|
||||
border-radius: 5px;
|
||||
|
||||
&:active, &:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
.map-holder {
|
||||
position: fixed;
|
||||
top: 32px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: calc(100% - 32px - 100px);
|
||||
}
|
||||
|
||||
.time-control {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
background-color: var(--primary-color-accent);
|
||||
|
||||
.timeline {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 64px;
|
||||
width: calc(100% - 64px);
|
||||
}
|
||||
|
||||
.play-pause-button {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 100px;
|
||||
width: 64px;
|
||||
background-color: transparent;
|
||||
color: black;
|
||||
font-size: 200%;
|
||||
border: none;
|
||||
|
||||
&:focus, &:active {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.error-holder {
|
||||
.error-image {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
font-size: 250%;
|
||||
|
||||
&::after {
|
||||
display: block;
|
||||
content: '';
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 50% 50%;
|
||||
background-image: url('../images/teleporter.png');
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
margin: 50px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
background-color: #FF9494;
|
||||
line-height: 32px;
|
||||
margin: 0 -30px;
|
||||
padding: 32px;
|
||||
padding-left: 74px;
|
||||
background-image: url('../images/error.png');
|
||||
background-size: 32px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 32px 32px;
|
||||
|
||||
.error-hint {
|
||||
margin-top: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ import {throttle, debounce} from 'throttle-debounce';
|
|||
import {Timeline} from './Render/Timeline';
|
||||
import {SpecHUD} from './Render/SpecHUD';
|
||||
import {AnalyseMenu} from './AnalyseMenu'
|
||||
import {Header, WorldBoundaries} from "@demostf/parser-worker";
|
||||
import {Header, WorldBoundaries} from "./Data/Parser";
|
||||
|
||||
import {AsyncParser} from "./Data/AsyncParser";
|
||||
import {getMapBoundaries} from "./MapBoundries";
|
||||
|
|
@ -21,10 +21,10 @@ export const Analyser = (props: AnalyseProps) => {
|
|||
const parser = props.parser;
|
||||
const intervalPerTick = props.header.interval_per_tick;
|
||||
|
||||
const [tick, setTick] = createSignal<number>();
|
||||
const [scale, setScale] = createSignal<number>();
|
||||
const [playing, setPlaying] = createSignal<boolean>();
|
||||
const [sessionName, setSessionName] = createSignal<string>();
|
||||
const [tick, setTick] = createSignal<number>(0);
|
||||
const [scale, setScale] = createSignal<number>(1);
|
||||
const [playing, setPlaying] = createSignal<boolean>(false);
|
||||
const [sessionName, setSessionName] = createSignal<string>("");
|
||||
|
||||
let lastFrameTime = 0;
|
||||
let playStartTick = 0;
|
||||
|
|
@ -165,9 +165,9 @@ export const Analyser = (props: AnalyseProps) => {
|
|||
<div class="time-control"
|
||||
title={`${tickToTime(tick(), intervalPerTick)} (tick ${tick()})`}>
|
||||
<input class="play-pause-button" type="button"
|
||||
onClick={togglePlay}
|
||||
value={playButtonText()}
|
||||
disabled={disabled}
|
||||
onClick={togglePlay}
|
||||
/>
|
||||
<Timeline parser={parser} tick={tick()}
|
||||
onSetTick={throttle(50, (tick) => {
|
||||
|
|
|
|||
|
|
@ -1,58 +0,0 @@
|
|||
"use strict";
|
||||
exports.__esModule = true;
|
||||
exports.AsyncParser = void 0;
|
||||
var parser_worker_1 = require("@demostf/parser-worker");
|
||||
var AsyncParser = /** @class */ (function () {
|
||||
function AsyncParser(buffer, progressCallback) {
|
||||
this.buffer = buffer;
|
||||
this.progressCallback = progressCallback;
|
||||
}
|
||||
AsyncParser.prototype.cache = function () {
|
||||
var _this = this;
|
||||
return new Promise(function (resolve, reject) {
|
||||
var worker = new Worker(new URL('./ParseWorker.ts', import.meta.url));
|
||||
worker.postMessage({
|
||||
buffer: _this.buffer
|
||||
}, [_this.buffer]);
|
||||
worker.onmessage = function (event) {
|
||||
if (event.data.error) {
|
||||
reject(event.data.error);
|
||||
return;
|
||||
}
|
||||
else if (event.data.progress) {
|
||||
_this.progressCallback(event.data.progress);
|
||||
return;
|
||||
}
|
||||
else if (event.data.demo) {
|
||||
var cachedData = event.data.demo;
|
||||
console.log("packed data: ".concat((cachedData.data.length / (1024 * 1024)).toFixed(1), "MB"));
|
||||
_this.world = cachedData.world;
|
||||
_this.demo = new parser_worker_1.ParsedDemo(cachedData.playerCount, cachedData.buildingCount, cachedData.world, cachedData.header, cachedData.data, cachedData.kills, cachedData.playerInfo, cachedData.tickCount);
|
||||
resolve(_this.demo);
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
AsyncParser.prototype.getPlayersAtTick = function (tick) {
|
||||
var players = [];
|
||||
for (var i = 0; i < this.demo.playerCount; i++) {
|
||||
players.push(this.demo.getPlayer(tick, i));
|
||||
}
|
||||
return players;
|
||||
};
|
||||
AsyncParser.prototype.getBuildingsAtTick = function (tick) {
|
||||
var buildings = [];
|
||||
for (var i = 0; i < this.demo.buildingCount; i++) {
|
||||
var building = this.demo.getBuilding(tick, i);
|
||||
if (building.health > 0) {
|
||||
buildings.push(building);
|
||||
}
|
||||
}
|
||||
return buildings;
|
||||
};
|
||||
AsyncParser.prototype.getKills = function () {
|
||||
return this.demo.kills;
|
||||
};
|
||||
return AsyncParser;
|
||||
}());
|
||||
exports.AsyncParser = AsyncParser;
|
||||
|
|
@ -1,5 +1,9 @@
|
|||
import {ParsedDemo, PlayerState, WorldBoundaries, Header, Kill, BuildingState} from "@demostf/parser-worker";
|
||||
import {getMapBoundaries} from "../MapBoundries";
|
||||
import {ParsedDemo, PlayerState, WorldBoundaries, Kill, BuildingState} from "./Parser";
|
||||
|
||||
function getCacheBuster(): string {
|
||||
const url = document.querySelector('script[src*="viewer"]').attributes.src.value;
|
||||
return url.substring("/viewer.js".length);
|
||||
}
|
||||
|
||||
export class AsyncParser {
|
||||
buffer: ArrayBuffer;
|
||||
|
|
@ -14,7 +18,7 @@ export class AsyncParser {
|
|||
|
||||
cache(): Promise<ParsedDemo> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const worker = new Worker(new URL('./ParseWorker.ts', import.meta.url));
|
||||
const worker = new Worker(`/parse-worker.js${getCacheBuster()}`);
|
||||
worker.postMessage({
|
||||
buffer: this.buffer
|
||||
}, [this.buffer]);
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
"use strict";
|
||||
exports.__esModule = true;
|
||||
var parser_worker_1 = require("@demostf/parser-worker");
|
||||
/**
|
||||
* @global postMessage
|
||||
* @param event
|
||||
*/
|
||||
onmessage = function (event) {
|
||||
var buffer = event.data.buffer;
|
||||
var bytes = new Uint8Array(buffer);
|
||||
(0, parser_worker_1.parseDemo)(bytes, function (progress) {
|
||||
postMessage({
|
||||
progress: progress
|
||||
});
|
||||
}).then(function (parsed) {
|
||||
postMessage({
|
||||
demo: parsed
|
||||
}, [parsed.data.buffer]);
|
||||
})["catch"](function (e) {
|
||||
console.error(e);
|
||||
postMessage({
|
||||
error: e.message
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import {parseDemo} from "@demostf/parser-worker";
|
||||
import {parseDemo} from "./Parser";
|
||||
|
||||
declare function postMessage(message: any, transfer?: any[]): void;
|
||||
|
||||
|
|
@ -23,5 +23,4 @@ onmessage = (event: MessageEvent) => {
|
|||
error: e.message
|
||||
});
|
||||
});
|
||||
|
||||
};
|
||||
|
|
|
|||
259
script/viewer/Analyse/Data/Parser.ts
Normal file
259
script/viewer/Analyse/Data/Parser.ts
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
import {
|
||||
get_assister_ids,
|
||||
get_attacker_ids, get_data,
|
||||
get_kill_ticks,
|
||||
get_map, get_player_entity_id,
|
||||
get_player_name, get_player_steam_id, get_player_user_id, get_victim_ids, get_weapon,
|
||||
parse_demo,
|
||||
XY
|
||||
} from '@demostf/tf-demos-viewer';
|
||||
import viewer from "@demostf/tf-demos-viewer";
|
||||
|
||||
function getCacheBuster(): string {
|
||||
const url = self.location.href;
|
||||
return url.substring(url.indexOf('?'));
|
||||
}
|
||||
|
||||
export async function parseDemo(bytes: Uint8Array, progressCallback: (progress: number) => void): Promise<ParsedDemo> {
|
||||
await viewer(`/tf-demo-viewer.wasm${getCacheBuster()}`);
|
||||
const state = parse_demo(bytes, progressCallback);
|
||||
|
||||
let playerCount = state.player_count;
|
||||
let buildingCount = state.building_count;
|
||||
let boundaries = state.boundaries;
|
||||
let interval_per_tick = state.interval_per_tick;
|
||||
let tickCount = state.tick_count;
|
||||
let kill_ticks = get_kill_ticks(state);
|
||||
let attackers = get_attacker_ids(state);
|
||||
let assisters = get_assister_ids(state);
|
||||
let victims = get_victim_ids(state);
|
||||
|
||||
let playerInfo = [];
|
||||
|
||||
for (let i = 0; i < playerCount; i++) {
|
||||
playerInfo.push({
|
||||
name: get_player_name(state, i),
|
||||
steamId: get_player_steam_id(state, i),
|
||||
entityId: get_player_entity_id(state, i),
|
||||
userId: get_player_user_id(state, i),
|
||||
})
|
||||
}
|
||||
|
||||
let kills = [];
|
||||
for (let i = 0; i < kill_ticks.length; i++) {
|
||||
kills.push({
|
||||
tick: kill_ticks[i],
|
||||
attacker: attackers[i],
|
||||
assister: assisters[i],
|
||||
victim: victims[i],
|
||||
weapon: get_weapon(state, i),
|
||||
})
|
||||
}
|
||||
|
||||
let map = get_map(state);
|
||||
let data = get_data(state);
|
||||
|
||||
return new ParsedDemo(
|
||||
playerCount,
|
||||
buildingCount,
|
||||
{
|
||||
boundary_min: {
|
||||
x: boundaries.boundary_min.x,
|
||||
y: boundaries.boundary_min.y,
|
||||
},
|
||||
boundary_max: {
|
||||
x: boundaries.boundary_max.x,
|
||||
y: boundaries.boundary_max.y,
|
||||
}
|
||||
},
|
||||
{
|
||||
map,
|
||||
interval_per_tick
|
||||
},
|
||||
data,
|
||||
kills,
|
||||
playerInfo,
|
||||
tickCount,
|
||||
);
|
||||
}
|
||||
|
||||
export interface PlayerInfo {
|
||||
entityId: number,
|
||||
name: string,
|
||||
steamId: string,
|
||||
userId: number,
|
||||
}
|
||||
|
||||
export enum Team {
|
||||
Other = 0,
|
||||
Spectator = 1,
|
||||
Red = 2,
|
||||
Blue = 3,
|
||||
}
|
||||
|
||||
export enum Class {
|
||||
Other = 0,
|
||||
Scout = 1,
|
||||
Sniper = 2,
|
||||
Solder = 3,
|
||||
Demoman = 4,
|
||||
Medic = 5,
|
||||
Heavy = 6,
|
||||
Pyro = 7,
|
||||
Spy = 8,
|
||||
Engineer = 9,
|
||||
}
|
||||
|
||||
export enum BuildingType {
|
||||
TeleporterEntrance = 0,
|
||||
TeleporterExit = 1,
|
||||
Dispenser = 2,
|
||||
Level1Sentry = 3,
|
||||
Level2Sentry = 4,
|
||||
Level3Sentry = 5,
|
||||
MiniSentry = 6,
|
||||
Unknown = 7,
|
||||
}
|
||||
|
||||
export interface WorldBoundaries {
|
||||
boundary_min: {
|
||||
x: number,
|
||||
y: number
|
||||
},
|
||||
boundary_max: {
|
||||
x: number,
|
||||
y: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface PlayerState {
|
||||
position: {
|
||||
x: number,
|
||||
y: number
|
||||
},
|
||||
angle: number,
|
||||
health: number,
|
||||
team: Team,
|
||||
playerClass: Class,
|
||||
info: PlayerInfo,
|
||||
charge: number,
|
||||
}
|
||||
|
||||
export interface BuildingState {
|
||||
position: {
|
||||
x: number,
|
||||
y: number
|
||||
},
|
||||
angle: number,
|
||||
health: number,
|
||||
level: number,
|
||||
team: Team,
|
||||
buildingType: BuildingType,
|
||||
}
|
||||
|
||||
export interface Header {
|
||||
interval_per_tick: number,
|
||||
map: string
|
||||
}
|
||||
|
||||
export interface Kill {
|
||||
tick: number,
|
||||
attacker: number,
|
||||
assister: number,
|
||||
victim: number,
|
||||
weapon: string,
|
||||
}
|
||||
|
||||
function unpack_f32(val: number, min: number, max: number): number {
|
||||
const ratio = val / (Math.pow(2, 16) - 1);
|
||||
return ratio * (max - min) + min;
|
||||
}
|
||||
|
||||
function unpack_angle(val: number): number {
|
||||
const ratio = val / (Math.pow(2, 8) - 1);
|
||||
return ratio * 360;
|
||||
}
|
||||
|
||||
export class ParsedDemo {
|
||||
public readonly playerCount: number;
|
||||
public readonly buildingCount: number;
|
||||
public readonly world: WorldBoundaries;
|
||||
public readonly data: Uint8Array;
|
||||
public readonly header: Header;
|
||||
public readonly tickCount: number;
|
||||
public readonly kills: Kill[];
|
||||
public readonly playerInfo: PlayerInfo[];
|
||||
|
||||
constructor(playerCount: number, buildingCount: number, world: WorldBoundaries, header: Header, data: Uint8Array, kills: Kill[], playerInfo: PlayerInfo[], tickCount: number) {
|
||||
this.playerCount = playerCount;
|
||||
this.buildingCount = buildingCount;
|
||||
this.world = world;
|
||||
this.header = header;
|
||||
this.data = data;
|
||||
this.kills = kills;
|
||||
this.playerInfo = playerInfo;
|
||||
this.tickCount = tickCount;
|
||||
}
|
||||
|
||||
getPlayer(tick: number, playerIndex: number): PlayerState {
|
||||
if (playerIndex >= this.playerCount) {
|
||||
throw new Error("Player out of bounds");
|
||||
}
|
||||
|
||||
const base = ((playerIndex * this.tickCount) + tick) * PLAYER_PACK_SIZE;
|
||||
return unpackPlayer(this.data, base, this.world, this.playerInfo[playerIndex]);
|
||||
}
|
||||
|
||||
getBuilding(tick: number, buildingIndex: number): BuildingState {
|
||||
if (buildingIndex >= this.buildingCount) {
|
||||
throw new Error("Building out of bounds");
|
||||
}
|
||||
|
||||
const base = (this.playerCount * this.tickCount * PLAYER_PACK_SIZE) + ((buildingIndex * this.tickCount) + tick) * BUILDING_PACK_SIZE;
|
||||
return unpackBuilding(this.data, base, this.world);
|
||||
}
|
||||
}
|
||||
|
||||
const PLAYER_PACK_SIZE = 8;
|
||||
const BUILDING_PACK_SIZE = 7;
|
||||
|
||||
function unpackPlayer(bytes: Uint8Array, base: number, world: WorldBoundaries, info: PlayerInfo): PlayerState {
|
||||
const x = unpack_f32(bytes[base] + (bytes[base + 1] << 8), world.boundary_min.x, world.boundary_max.x);
|
||||
const y = unpack_f32(bytes[base + 2] + (bytes[base + 3] << 8), world.boundary_min.y, world.boundary_max.y);
|
||||
const team_class_health = bytes[base + 4] + (bytes[base + 5] << 8);
|
||||
const angle = unpack_angle(bytes[base + 6]);
|
||||
const health = team_class_health & 1013;
|
||||
const team = (team_class_health >> 14) as Team;
|
||||
const playerClass = ((team_class_health >> 10) & 15) as Class;
|
||||
const charge = bytes[base + 7];
|
||||
|
||||
return {
|
||||
position: {x, y},
|
||||
angle,
|
||||
health,
|
||||
team,
|
||||
playerClass,
|
||||
info,
|
||||
charge
|
||||
}
|
||||
}
|
||||
|
||||
function unpackBuilding(bytes: Uint8Array, base: number, world: WorldBoundaries): BuildingState {
|
||||
const x = unpack_f32(bytes[base] + (bytes[base + 1] << 8), world.boundary_min.x, world.boundary_max.x);
|
||||
const y = unpack_f32(bytes[base + 2] + (bytes[base + 3] << 8), world.boundary_min.y, world.boundary_max.y);
|
||||
const team_type_health = bytes[base + 4] + (bytes[base + 5] << 8);
|
||||
const angle = unpack_angle(bytes[base + 6]);
|
||||
const health = team_type_health & 1013;
|
||||
const team = (((team_type_health >> 13) & 1) === 0) ? Team.Blue : Team.Red;
|
||||
const level = (team_type_health >> 14);
|
||||
const buildingType = ((team_type_health >> 10) & 7) as BuildingType;
|
||||
|
||||
return {
|
||||
position: {x, y},
|
||||
angle,
|
||||
health,
|
||||
team,
|
||||
buildingType,
|
||||
level,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
.map-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: black;
|
||||
}
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import {Panner} from "../Panner/Panner";
|
||||
import {createEffect, createSignal, ParentProps} from "solid-js";
|
||||
import {createEffect, createSignal, ParentProps, Show} from "solid-js";
|
||||
import { createElementSize } from "@solid-primitives/resize-observer";
|
||||
|
||||
export class MapContainerProps {
|
||||
export interface MapContainerProps {
|
||||
contentSize: {
|
||||
width: number;
|
||||
height: number;
|
||||
|
|
@ -9,25 +10,31 @@ export class MapContainerProps {
|
|||
onScale?: (scale: number) => any;
|
||||
}
|
||||
|
||||
export const MapContainer = ({children, contentSize, onScale}: ParentProps<MapContainerProps>) => {
|
||||
export const MapContainer = (props: ParentProps<MapContainerProps>) => {
|
||||
const [container, setContainer] = createSignal<Element>();
|
||||
const scale = () => Math.min(
|
||||
container().clientWidth / contentSize.width,
|
||||
container().clientHeight / contentSize.height
|
||||
);
|
||||
const size = createElementSize(container);
|
||||
const scale = () => {
|
||||
if (size.width && size.height) {
|
||||
return Math.min(size.width / props.contentSize.width, size.height / props.contentSize.height);
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
createEffect(() => {
|
||||
if (isFinite(scale())) {
|
||||
onScale && onScale(scale());
|
||||
props.onScale && props.onScale(scale());
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div class="map-container" ref={setContainer}>
|
||||
<Panner width={container().clientWidth} height={container().clientHeight}
|
||||
scale={scale()} contentSize={contentSize}
|
||||
onScale={onScale}>
|
||||
{children}
|
||||
</Panner>
|
||||
<Show when={size.width}>
|
||||
<Panner width={size.width} height={size.height}
|
||||
scale={scale()} contentSize={props.contentSize}
|
||||
onScale={props.onScale}>
|
||||
{props.children}
|
||||
</Panner>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
.map-background {
|
||||
/*width: 1000px;*/
|
||||
/*height: 1000px;*/
|
||||
min-height: 500px;
|
||||
min-width: 700px;
|
||||
background: black no-repeat;
|
||||
background-position: bottom left;
|
||||
background-size: contain;
|
||||
}
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import {Player as PlayerDot} from './Render/Player';
|
||||
import {Building as BuildingDot} from './Render/Building';
|
||||
import {findMapAlias} from './MapBoundries';
|
||||
import {PlayerState, Header, WorldBoundaries, BuildingState} from "@demostf/parser-worker";
|
||||
import {PlayerState, Header, WorldBoundaries, BuildingState} from "./Data/Parser";
|
||||
import {splitProps} from "solid-js";
|
||||
|
||||
export interface MapRenderProps {
|
||||
header: Header;
|
||||
|
|
@ -15,37 +16,31 @@ export interface MapRenderProps {
|
|||
scale: number;
|
||||
}
|
||||
|
||||
declare const require: {
|
||||
<T>(path: string): T;
|
||||
(paths: string[], callback: (...modules: any[]) => void): void;
|
||||
ensure: (paths: string[], callback: (require: <T>(path: string) => T) => void) => void;
|
||||
};
|
||||
|
||||
export function MapRender({header, players, size, world, scale, buildings}: MapRenderProps) {
|
||||
const mapAlias = findMapAlias(header.map);
|
||||
export function MapRender(props: MapRenderProps) {
|
||||
const mapAlias = findMapAlias(props.header.map);
|
||||
const image = `images/leveloverview/dist/${mapAlias}.webp`;
|
||||
const background = `url(${image})`;
|
||||
|
||||
const playerDots = players
|
||||
const playerDots = () => props.players
|
||||
.filter((player: PlayerState) => player.health)
|
||||
.map((player: PlayerState) => {
|
||||
return <PlayerDot player={player} mapBoundary={world}
|
||||
targetSize={size} scale={scale} />
|
||||
return <PlayerDot player={player} mapBoundary={props.world}
|
||||
targetSize={props.size} scale={props.scale} />
|
||||
});
|
||||
|
||||
const buildingDots = buildings
|
||||
const buildingDots = () => props.buildings
|
||||
.filter((building: PlayerState) => building.position.x)
|
||||
.map((building: PlayerState) => {
|
||||
return <BuildingDot building={building}
|
||||
mapBoundary={world}
|
||||
targetSize={size} scale={scale}/>
|
||||
mapBoundary={props.world}
|
||||
targetSize={props.size} scale={props.scale}/>
|
||||
});
|
||||
|
||||
return (
|
||||
<svg class="map-background" width={size.width} height={size.height}
|
||||
<svg class="map-background" width={props.size.width} height={props.size.height}
|
||||
style={{"background-image": background}}>
|
||||
{playerDots}
|
||||
{buildingDots}
|
||||
{playerDots()}
|
||||
{buildingDots()}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import {BuildingState, WorldBoundaries, BuildingType, Team} from "@demostf/parser-worker";
|
||||
import {BuildingState, WorldBoundaries, BuildingType, Team} from "../Data/Parser";
|
||||
import {Show} from "solid-js";
|
||||
|
||||
export interface BuildingProp {
|
||||
building: BuildingState;
|
||||
|
|
@ -54,17 +55,15 @@ export function Building({building, mapBoundary, targetSize, scale}: BuildingPro
|
|||
const alpha = building.health / maxHealth;
|
||||
try {
|
||||
const image = getIcon(building);
|
||||
|
||||
const angle = (building.angle) ?
|
||||
<polygon points="-6,14 0, 16 6,14 0,24" fill="white"
|
||||
transform={`rotate(${270 - building.angle})`}/> : '';
|
||||
|
||||
return <g transform={`translate(${scaledX} ${scaledY}) scale(${1 / scale})`}
|
||||
opacity={alpha}>
|
||||
{angle}
|
||||
<image href={image} class={"player-icon"} height={32}
|
||||
width={32}
|
||||
transform={`translate(-16 -16)`}/>
|
||||
<image href={image} className={"player-icon"} height={32}
|
||||
width={32}
|
||||
transform={`translate(-16 -16)`}/>
|
||||
<Show when={building.angle}>
|
||||
<polygon points="-6,14 0, 16 6,14 0,24" fill="white"
|
||||
transform={`rotate(${270 - building.angle})`}/>
|
||||
</Show>
|
||||
</g>
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
|
|
|
|||
|
|
@ -1,45 +0,0 @@
|
|||
.killfeed {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: 15px;
|
||||
user-select: none;
|
||||
|
||||
.kill {
|
||||
.red {
|
||||
color: #a75d50;
|
||||
}
|
||||
.blue {
|
||||
color: #5b818f;
|
||||
}
|
||||
|
||||
&.active-player {
|
||||
background-color: #ddcfb2;
|
||||
}
|
||||
|
||||
display: inline-block;
|
||||
margin: 3px;
|
||||
white-space: nowrap;
|
||||
background-color: #2d2727cc;
|
||||
border-radius: 5px;
|
||||
padding: 5px 15px;
|
||||
width: auto;
|
||||
text-align: right;
|
||||
color: var(--primary-color);
|
||||
font-weight: bold;
|
||||
float: right;
|
||||
clear: both;
|
||||
|
||||
.player {
|
||||
padding: 0 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.kill-icon {
|
||||
display: inline-block;
|
||||
height: 22px;
|
||||
width: auto;
|
||||
vertical-align: middle;
|
||||
filter: brightness(600%);
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import {Kill, PlayerState} from "@demostf/parser-worker";
|
||||
import {Kill, PlayerState} from "../Data/Parser";
|
||||
import {killAlias} from "./killAlias";
|
||||
|
||||
export interface KillFeedProps {
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
.player-icon {
|
||||
mask-size: cover;
|
||||
mask-repeat: no-repeat;
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import {PlayerState, WorldBoundaries, Team} from "@demostf/parser-worker";
|
||||
import {PlayerState, WorldBoundaries, Team} from "../Data/Parser";
|
||||
|
||||
export interface PlayerProp {
|
||||
player: PlayerState;
|
||||
|
|
|
|||
|
|
@ -1,218 +0,0 @@
|
|||
.blueSpecHolder {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translate(0, -50%);
|
||||
}
|
||||
|
||||
.redSpecHolder {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translate(0, -50%);
|
||||
}
|
||||
|
||||
.playerspec {
|
||||
background-color: black;
|
||||
color: white;
|
||||
height: 42px;
|
||||
width: 200px;
|
||||
position: relative;
|
||||
font-family: sans-serif;
|
||||
margin-bottom: 2px;
|
||||
user-select: none;
|
||||
|
||||
&.uber {
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.class-icon, .steam-avatar {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-position: top left;
|
||||
background-size: 100% 100%;
|
||||
|
||||
&.uber {
|
||||
height: 28px;
|
||||
background-size: 28px 28px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 50% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.player-name {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
padding: 0 5px;
|
||||
white-space: nowrap;
|
||||
width: 120px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.health-container {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
left: 42px;
|
||||
top: 0;
|
||||
height: 28px;
|
||||
width: calc(100% - 42px);
|
||||
line-height: 28px;
|
||||
font-weight: bold;
|
||||
.health {
|
||||
position: relative;
|
||||
float: right;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.healthbar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
&.red {
|
||||
.health-container {
|
||||
background-color: #a75d50aa;
|
||||
}
|
||||
|
||||
.healthbar {
|
||||
background-color: #a75d50;
|
||||
}
|
||||
|
||||
.class-icon.scout {
|
||||
background-image: url('../../images/class_portraits/Icon_scout.jpg');
|
||||
}
|
||||
.class-icon.soldier {
|
||||
background-image: url('../../images/class_portraits/Icon_soldier.jpg');
|
||||
}
|
||||
.class-icon.pyro {
|
||||
background-image: url('../../images/class_portraits/Icon_pyro.jpg');
|
||||
}
|
||||
.class-icon.demoman {
|
||||
background-image: url('../../images/class_portraits/Icon_demoman.jpg');
|
||||
}
|
||||
.class-icon.engineer {
|
||||
background-image: url('../../images/class_portraits/Icon_engineer.jpg');
|
||||
}
|
||||
.class-icon.heavy {
|
||||
background-image: url('../../images/class_portraits/Icon_heavy.jpg');
|
||||
}
|
||||
.class-icon.medic {
|
||||
background-image: url('../../images/class_portraits/Icon_medic.jpg');
|
||||
}
|
||||
.class-icon.sniper {
|
||||
background-image: url('../../images/class_portraits/Icon_sniper.jpg');
|
||||
}
|
||||
.class-icon.spy{
|
||||
background-image: url('../../images/class_portraits/Icon_spy.jpg');
|
||||
}
|
||||
.class-icon.uber {
|
||||
background-image: url('../../images/charge_red.svg');
|
||||
}
|
||||
|
||||
.class-icon, .steam-avatar {
|
||||
right: 0;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
.health-container {
|
||||
right: 42px;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
.health {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.player-name {
|
||||
float: right;
|
||||
direction: ltr;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
&.blue {
|
||||
.health-container {
|
||||
background-color: #5b818faa;
|
||||
}
|
||||
|
||||
.healthbar {
|
||||
background-color: #5b818f;
|
||||
}
|
||||
|
||||
.class-icon.scout {
|
||||
background-image: url('../../images/class_portraits/Icon_scout_blue.jpg');
|
||||
}
|
||||
.class-icon.soldier {
|
||||
background-image: url('../../images/class_portraits/Icon_soldier_blue.jpg');
|
||||
}
|
||||
.class-icon.pyro {
|
||||
background-image: url('../../images/class_portraits/Icon_pyro_blue.jpg');
|
||||
}
|
||||
.class-icon.demoman {
|
||||
background-image: url('../../images/class_portraits/Icon_demoman_blue.jpg');
|
||||
}
|
||||
.class-icon.engineer {
|
||||
background-image: url('../../images/class_portraits/Icon_engineer_blue.jpg');
|
||||
}
|
||||
.class-icon.heavy {
|
||||
background-image: url('../../images/class_portraits/Icon_heavy_blue.jpg');
|
||||
}
|
||||
.class-icon.medic {
|
||||
background-image: url('../../images/class_portraits/Icon_medic_blue.jpg');
|
||||
}
|
||||
.class-icon.sniper {
|
||||
background-image: url('../../images/class_portraits/Icon_sniper_blue.jpg');
|
||||
}
|
||||
.class-icon.spy {
|
||||
background-image: url('../../images/class_portraits/Icon_spy_blue.jpg');
|
||||
}
|
||||
.class-icon.uber {
|
||||
background-image: url('../../images/charge_blue.svg');
|
||||
}
|
||||
}
|
||||
|
||||
&.overhealed {
|
||||
.health {
|
||||
color: #79d297;
|
||||
}
|
||||
|
||||
.health:after {
|
||||
position: absolute;
|
||||
top: 21px;
|
||||
right: 0;
|
||||
padding: 0 5px;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
content: 'OVERHEALED'
|
||||
}
|
||||
|
||||
&.red .health:after {
|
||||
position: absolute;
|
||||
top: 21px;
|
||||
left: 0;
|
||||
right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&.dead {
|
||||
.healthbar, .health {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.health-container {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.class-icon {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import {PlayerState} from "@demostf/parser-worker";
|
||||
import {PlayerState} from "../Data/Parser";
|
||||
|
||||
export interface PlayerSpecProps {
|
||||
player: PlayerState;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import {PlayersSpec} from './PlayerSpec';
|
||||
import {KillFeed} from './KillFeed';
|
||||
import {AsyncParser} from "../Data/AsyncParser";
|
||||
import {PlayerState, Kill} from "@demostf/parser-worker";
|
||||
import {PlayerState, Kill} from "../Data/Parser";
|
||||
|
||||
export interface SpecHUDProps {
|
||||
tick: number;
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
.timeline {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
/*bottom: 0;*/
|
||||
/*position: relative;*/
|
||||
}
|
||||
|
||||
.timeline-progress {
|
||||
position: absolute;
|
||||
bottom: -60px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 160px;
|
||||
}
|
||||
|
||||
.timeline-background {
|
||||
position: absolute;
|
||||
bottom: 22px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 78px;
|
||||
transform: scale(1, -1);
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import {ParsedDemo, PlayerState, Header, WorldBoundaries, Team} from "@demostf/parser-worker";
|
||||
import {ParsedDemo, PlayerState, Header, WorldBoundaries, Team} from "../Data/Parser";
|
||||
|
||||
export interface TimelineProps {
|
||||
parser: AsyncParser;
|
||||
|
|
@ -8,16 +8,13 @@ export interface TimelineProps {
|
|||
}
|
||||
|
||||
export const Timeline = ({parser, tick, onSetTick, disabled}) => {
|
||||
const background = <TimeLineBackground parser={parser}/>;
|
||||
|
||||
return (<div class="timeline">
|
||||
{background}
|
||||
<input class="timeline-progress" type="range" min={0}
|
||||
max={parser.demo.tickCount} value={tick}
|
||||
disabled={disabled}
|
||||
return <div class="timeline">
|
||||
<input max={parser.demo.tickCount} value={tick} class="timeline-progress" type="range" min={0}
|
||||
onChange={(event) => {onSetTick(parseInt(event.target.value, 10))}}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>);
|
||||
<TimeLineBackground parser={parser}/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
import {AsyncParser} from "../Data/AsyncParser";
|
||||
|
|
|
|||
|
|
@ -1,54 +0,0 @@
|
|||
.pan-zoom-element {
|
||||
border: 1px solid #ccc;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
cursor: default;
|
||||
|
||||
.content-container {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
.noselect {
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
.zoommenu {
|
||||
position: fixed;
|
||||
color: #888;
|
||||
|
||||
bottom: 100px;
|
||||
right: 0;
|
||||
margin: 10px;
|
||||
font-size: 200%;
|
||||
|
||||
opacity: 0.5;
|
||||
transition: all 0.5s;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
background-color: #f8f8f8;
|
||||
border-radius: 5px;
|
||||
|
||||
div {
|
||||
transition: all 0.5s;
|
||||
width: 32px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
|
||||
div:first-child {
|
||||
border-bottom: 1px solid #ddd;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -85,9 +85,14 @@ export const Panner = (props: ParentProps<PannerProps>) => {
|
|||
y: Math.floor(panner.screen.height / 2)
|
||||
});
|
||||
|
||||
const pannerStyle = () => {
|
||||
console.log(props.width, props.height, props.scale);
|
||||
return {width: `${props.width}px`, height: `${props.height}px`}
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="pan-zoom-element"
|
||||
style={{width: `${props.width}px`, height: `${props.height}px`}}
|
||||
style={pannerStyle()}
|
||||
onMouseDown={mouseDown}
|
||||
onWheel={mouseWheel}>
|
||||
<div class="content-container noselect"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue