mirror of
https://github.com/smogon/pokemon-showdown-client.git
synced 2026-03-21 17:50:29 -05:00
Preact minor updates batch 16
Minor
- Light mode scrollbar in line with dark mode
- OS default scrollbars all look ugly for some reason, unless you use
macOS's hidden scrollbars, which are nice except they're hidden.
- Add Aurastic to credits
- Fix subtle notifications
- Fix "Copy" button and HTML popups
- These are both for the replay upload popup.
- Fix escaping in teambuilder tables
- They're escaped in Preact but raw HTML in old client, so using
Unicode make them work correctly in both.
- Fix a weird issue where history.replaceState was being called too
often
- I don't know exactly why this is only recently became a problem,
but it's easy enough to fix...
Trivial
- More ARIA roles (for blind users)
- The accessibility tree looks great now.
- Fix an unnecessary closure in AvatarsPanel
- Fix non-ASCII in battle.ts
- Fix class="readmore" to work like class="details" (re: don't do
expand/collapse hover effect if hovering over a link)
- Fix resizing from one-panel to two-panel mode
- This was really just one bug in `focusRoom` left over from an old
architecture where mini-rooms could be panels in vertical tab mode.
- But I took the opportunity to refactor a bunch of panel code to be
clearer.
- Slightly redesign open team sheets
- The yellow and black "construction" message no longer needs nested
divs
This commit is contained in:
parent
b7a953930b
commit
734ce0d71d
|
|
@ -658,7 +658,7 @@ process.stdout.write("Building `data/teambuilder-tables.js`... ");
|
|||
|
||||
const greatItems = [['header', "Popular items"]];
|
||||
const goodItems = [['header', "Items"]];
|
||||
const specificItems = [['header', "Pokémon-specific items"]];
|
||||
const specificItems = [['header', "Pok\u00e9mon-specific items"]];
|
||||
const poorItems = [['header', "Usually useless items"]];
|
||||
const badItems = [['header', "Useless items"]];
|
||||
const unreleasedItems = [];
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ export class DexSearch {
|
|||
article: 9,
|
||||
};
|
||||
static typeName = {
|
||||
pokemon: 'Pokémon',
|
||||
pokemon: 'Pok\u00e9mon',
|
||||
type: 'Type',
|
||||
tier: 'Tiers',
|
||||
move: 'Moves',
|
||||
|
|
@ -460,7 +460,7 @@ export class DexSearch {
|
|||
switch (fType) {
|
||||
case 'type':
|
||||
let type = fId.charAt(0).toUpperCase() + fId.slice(1) as Dex.TypeName;
|
||||
buf.push(['header', `${type}-type Pokémon`]);
|
||||
buf.push(['header', `${type}-type Pok\u00e9mon`]);
|
||||
for (let id in BattlePokedex) {
|
||||
if (!BattlePokedex[id].types) continue;
|
||||
if (this.dex.species.get(id).types.includes(type)) {
|
||||
|
|
@ -470,7 +470,7 @@ export class DexSearch {
|
|||
break;
|
||||
case 'ability':
|
||||
let ability = Dex.abilities.get(fId).name;
|
||||
buf.push(['header', `${ability} Pokémon`]);
|
||||
buf.push(['header', `${ability} Pok\u00e9mon`]);
|
||||
for (let id in BattlePokedex) {
|
||||
if (!BattlePokedex[id].abilities) continue;
|
||||
if (Dex.hasAbility(this.dex.species.get(id), ability)) {
|
||||
|
|
|
|||
|
|
@ -317,7 +317,7 @@ export class BattleLog {
|
|||
}
|
||||
return buf;
|
||||
}).join('');
|
||||
divHTML = `<div class="infobox"><details><summary>Open Team Sheet for ${side.name}</summary>${exportedTeam}</details></div>`;
|
||||
divHTML = `<div class="infobox"><details class="details"><summary>Open team sheet for ${side.name}</summary>${exportedTeam}</details></div>`;
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -592,7 +592,7 @@ export class Pokemon implements PokemonDetails, PokemonHealth {
|
|||
if (pokemon.maxhp === 100) return `${pokemon.hp}%`;
|
||||
if (pokemon.maxhp !== 48) return (100 * pokemon.hp / pokemon.maxhp).toFixed(precision) + '%';
|
||||
let range = Pokemon.getPixelRange(pokemon.hp, pokemon.hpcolor);
|
||||
return Pokemon.getFormattedRange(range, precision, '–');
|
||||
return Pokemon.getFormattedRange(range, precision, '\u2013');
|
||||
}
|
||||
destroy() {
|
||||
if (this.sprite) this.sprite.destroy();
|
||||
|
|
|
|||
|
|
@ -819,6 +819,7 @@ export class PSRoom extends PSStreamModel<Args | null> implements RoomOptions {
|
|||
subtleNotify() {
|
||||
if (PS.isVisible(this)) return;
|
||||
this.isSubtleNotifying = true;
|
||||
PS.update();
|
||||
}
|
||||
dismissNotification(id: string) {
|
||||
this.notifications = this.notifications.filter(notification => notification.id !== id);
|
||||
|
|
@ -1394,26 +1395,6 @@ export const PS = new class extends PSModel {
|
|||
/** Currently active popups, in stack order (bottom to top) */
|
||||
popups: RoomID[] = [];
|
||||
|
||||
/**
|
||||
* Currently active left room.
|
||||
*
|
||||
* In two-panel mode, this will be the visible left panel.
|
||||
*
|
||||
* In one-panel mode, this is the visible room only if it is
|
||||
* `PS.room`. Still tracked when not visible, so we know which
|
||||
* panels to display if PS is resized to two-panel mode.
|
||||
*/
|
||||
leftPanel: PSRoom = null!;
|
||||
/**
|
||||
* Currently active right room.
|
||||
*
|
||||
* In two-panel mode, this will be the visible right panel.
|
||||
*
|
||||
* In one-panel mode, this is the visible room only if it is
|
||||
* `PS.room`. Still tracked when not visible, so we know which
|
||||
* panels to display if PS is resized to two-panel mode.
|
||||
*/
|
||||
rightPanel: PSRoom | null = null;
|
||||
/**
|
||||
* The currently focused room. Should always be the topmost popup
|
||||
* if it exists. If no popups are open, it should be
|
||||
|
|
@ -1425,13 +1406,34 @@ export const PS = new class extends PSModel {
|
|||
*/
|
||||
room: PSRoom = null!;
|
||||
/**
|
||||
* The currently active panel. Should always be either `PS.leftRoom`
|
||||
* or `PS.rightRoom`. If no popups are open, should be `PS.room`.
|
||||
* The currently active panel. Should always be either `PS.leftPanel`
|
||||
* or `PS.leftPanel`. If no popups are open, should be `PS.room`.
|
||||
*
|
||||
* In one-panel mode, determines whether the left or right panel is
|
||||
* visible. Otherwise, no effect.
|
||||
* visible. Otherwise, it just tracks which panel will be in focus
|
||||
* after all popups are closed.
|
||||
*/
|
||||
panel: PSRoom = null!;
|
||||
/**
|
||||
* Currently active left room.
|
||||
*
|
||||
* In two-panel mode, this will be the visible left panel.
|
||||
*
|
||||
* In one-panel mode, this is the visible room only if it is
|
||||
* `PS.panel`. Still tracked when not visible, so we know which
|
||||
* panels to display if PS is resized to two-panel mode.
|
||||
*/
|
||||
leftPanel: PSRoom = null!;
|
||||
/**
|
||||
* Currently active right room.
|
||||
*
|
||||
* In two-panel mode, this will be the visible right panel.
|
||||
*
|
||||
* In one-panel mode, this is the visible room only if it is
|
||||
* `PS.panel`. Still tracked when not visible, so we know which
|
||||
* panels to display if PS is resized to two-panel mode.
|
||||
*/
|
||||
rightPanel: PSRoom | null = null;
|
||||
/**
|
||||
* * 0 = only one panel visible
|
||||
* * null = vertical nav layout
|
||||
|
|
@ -1833,13 +1835,12 @@ export const PS = new class extends PSModel {
|
|||
room.focusNextUpdate = true;
|
||||
}
|
||||
if (PS.isNormalRoom(room)) {
|
||||
if (room.location === 'right' && !this.prefs.onepanel) {
|
||||
this.rightPanel = this.panel = room;
|
||||
if (room.location === 'right') {
|
||||
this.rightPanel = room;
|
||||
} else {
|
||||
this.leftPanel = this.panel = room;
|
||||
this.leftPanel = room;
|
||||
}
|
||||
PS.closeAllPopups(true);
|
||||
this.room = room;
|
||||
this.panel = this.room = room;
|
||||
} else { // popup or mini-window
|
||||
if (room.location === 'mini-window') {
|
||||
this.leftPanel = this.panel = PS.mainmenu;
|
||||
|
|
|
|||
|
|
@ -408,7 +408,7 @@ class NewsPanel extends PSRoomPanel {
|
|||
override render() {
|
||||
const cookieSet = document.cookie.includes('preactalpha=1');
|
||||
return <PSPanelWrapper room={this.props.room} fullSize scrollable>
|
||||
<div class="construction"><div class="construction-inner">
|
||||
<div class="construction">
|
||||
This is the Preact client alpha test.
|
||||
<form>
|
||||
<label class="checkbox">
|
||||
|
|
@ -424,7 +424,7 @@ class NewsPanel extends PSRoomPanel {
|
|||
Back to the old client
|
||||
</label>
|
||||
</form>
|
||||
</div></div>
|
||||
</div>
|
||||
<div class="readable-bg" dangerouslySetInnerHTML={{ __html: PS.newsHTML }}></div>
|
||||
</PSPanelWrapper>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -923,7 +923,7 @@ class AvatarsPanel extends PSRoomPanel {
|
|||
|
||||
return <PSPanelWrapper room={room} width={1210}><div class="pad">
|
||||
<label class="optlabel"><strong>Choose an avatar or </strong>
|
||||
<button class="button" onClick={() => this.close()}> Cancel</button>
|
||||
<button class="button" data-cmd="/close"> Cancel</button>
|
||||
</label>
|
||||
<div class="avatarlist">
|
||||
{avatars.map(([i, avatar]) => (
|
||||
|
|
@ -1590,6 +1590,12 @@ class PopupPanel extends PSRoomPanel<PopupRoom> {
|
|||
if (!textbox) return;
|
||||
textbox.value = this.props.room.args?.value as string || '';
|
||||
}
|
||||
parseMessage(message: string) {
|
||||
if (message.startsWith('|html|')) {
|
||||
return BattleLog.sanitizeHTML(message.slice(6));
|
||||
}
|
||||
return BattleLog.parseMessage(message);
|
||||
}
|
||||
|
||||
override render() {
|
||||
const room = this.props.room;
|
||||
|
|
@ -1601,7 +1607,7 @@ class PopupPanel extends PSRoomPanel<PopupRoom> {
|
|||
return <PSPanelWrapper room={room} width={480}><form class="pad" onSubmit={this.handleSubmit}>
|
||||
{room.args?.message && <p
|
||||
style="white-space:pre-wrap;word-wrap:break-word"
|
||||
dangerouslySetInnerHTML={{ __html: BattleLog.parseMessage(room.args.message as string) }}
|
||||
dangerouslySetInnerHTML={{ __html: this.parseMessage(room.args.message as string || '') }}
|
||||
></p>}
|
||||
{!!type && <p><input name="value" type={type} class="textbox autofocus" style="width:100%;box-sizing:border-box" /></p>}
|
||||
<p class="buttonbar">
|
||||
|
|
|
|||
|
|
@ -129,11 +129,11 @@ export class PSHeader extends preact.Component<{ style: object }> {
|
|||
if (!room) return null;
|
||||
const closable = (id === '' || id === 'rooms' ? '' : ' closable');
|
||||
const cur = PS.isVisible(room) ? ' cur' : '';
|
||||
let notifying = '';
|
||||
let notifying = room.isSubtleNotifying ? ' subtle-notifying' : '';
|
||||
let hoverTitle = '';
|
||||
const notifications = room.notifications;
|
||||
if (notifications.length) {
|
||||
notifying = room.isSubtleNotifying ? ' subtle-notifying' : ' notifying';
|
||||
notifying = ' notifying';
|
||||
for (const notif of notifications) {
|
||||
if (!notif.body) continue;
|
||||
hoverTitle += `${notif.title}\n${notif.body}\n`;
|
||||
|
|
@ -218,7 +218,7 @@ export class PSHeader extends preact.Component<{ style: object }> {
|
|||
alt="Pokémon Showdown! (beta)"
|
||||
width="50" height="50"
|
||||
/>
|
||||
<div class="tablist">
|
||||
<div class="tablist" role="tablist">
|
||||
<ul>
|
||||
{PSHeader.renderRoomTab(PS.leftRoomList[0])}
|
||||
</ul>
|
||||
|
|
@ -255,8 +255,8 @@ export class PSHeader extends preact.Component<{ style: object }> {
|
|||
|
||||
return <div id="header" class="header" style={this.props.style} role="navigation">
|
||||
<div class="maintabbarbottom"></div>
|
||||
<div class="tabbar maintabbar"><div class="inner-1"><div class="inner-2">
|
||||
<ul class="maintabbar-left" style={{ width: `${PS.leftPanelWidth}px` }}>
|
||||
<div class="tabbar maintabbar"><div class="inner-1" role={PS.leftPanelWidth ? 'none' : 'tablist'}><div class="inner-2">
|
||||
<ul class="maintabbar-left" style={{ width: `${PS.leftPanelWidth}px` }} role={PS.leftPanelWidth ? 'tablist' : 'none'}>
|
||||
<li>
|
||||
<img
|
||||
class="logo"
|
||||
|
|
@ -268,7 +268,7 @@ export class PSHeader extends preact.Component<{ style: object }> {
|
|||
{PSHeader.renderRoomTab(PS.leftRoomList[0])}
|
||||
{PS.leftRoomList.slice(1).map(roomid => PSHeader.renderRoomTab(roomid))}
|
||||
</ul>
|
||||
<ul class="maintabbar-right">
|
||||
<ul class="maintabbar-right" role={PS.leftPanelWidth ? 'tablist' : 'none'}>
|
||||
{PS.rightRoomList.map(roomid => PSHeader.renderRoomTab(roomid))}
|
||||
</ul>
|
||||
</div></div></div>
|
||||
|
|
|
|||
|
|
@ -60,7 +60,8 @@ export class PSRouter {
|
|||
|
||||
return url as RoomID;
|
||||
}
|
||||
updatePanelState(): { roomid: RoomID, changed: boolean, newTitle: string } {
|
||||
/** true: roomid changed, false: panelState changed, null: neither changed */
|
||||
updatePanelState(): { roomid: RoomID, changed: boolean | null, newTitle: string } {
|
||||
let room = PS.room;
|
||||
// some popups don't have URLs and don't generate history
|
||||
// there's definitely a better way to do this but I'm lazy
|
||||
|
|
@ -79,9 +80,10 @@ export class PSRouter {
|
|||
PS.leftPanel.id + '..' + PS.rightPanel!.id :
|
||||
room.id);
|
||||
const newTitle = roomid === '' ? 'Showdown!' : `${room.title} - Showdown!`;
|
||||
const changed = (roomid !== this.roomid);
|
||||
let changed: boolean | null = (roomid !== this.roomid);
|
||||
|
||||
this.roomid = roomid;
|
||||
if (this.panelState === panelState) changed = null;
|
||||
this.panelState = panelState;
|
||||
return { roomid, changed, newTitle };
|
||||
}
|
||||
|
|
@ -130,7 +132,7 @@ export class PSRouter {
|
|||
const { roomid, changed, newTitle } = this.updatePanelState();
|
||||
if (changed) {
|
||||
history.pushState(this.panelState, '', `/${roomid}`);
|
||||
} else {
|
||||
} else if (changed !== null) {
|
||||
history.replaceState(this.panelState, '', `/${roomid}`);
|
||||
}
|
||||
// n.b. must be done after changing hash, so history entry has the old title
|
||||
|
|
@ -612,6 +614,20 @@ export class PSView extends preact.Component {
|
|||
parentElem: elem,
|
||||
});
|
||||
return true;
|
||||
case 'copyText':
|
||||
const dummyInput = document.createElement("input");
|
||||
// This is a hack. You can only "select" an input field.
|
||||
// The trick is to create a short lived input element and destroy it after a copy.
|
||||
// (stolen from the replay code, obviously --mia)
|
||||
dummyInput.id = "dummyInput";
|
||||
dummyInput.value = elem.value || (elem as any).href || "";
|
||||
dummyInput.style.position = 'absolute';
|
||||
elem.appendChild(dummyInput);
|
||||
dummyInput.select();
|
||||
document.execCommand("copy");
|
||||
elem.removeChild(dummyInput);
|
||||
elem.innerText = 'Copied!';
|
||||
return true;
|
||||
case 'send':
|
||||
case 'cmd':
|
||||
const room = PS.getRoom(elem) || PS.mainmenu;
|
||||
|
|
@ -785,7 +801,7 @@ export class PSView extends preact.Component {
|
|||
rooms.push(this.renderRoom(room));
|
||||
}
|
||||
}
|
||||
return <div class="ps-frame">
|
||||
return <div class="ps-frame" role="none">
|
||||
<PSHeader style={{}} />
|
||||
<PSMiniHeader />
|
||||
{rooms}
|
||||
|
|
|
|||
|
|
@ -745,10 +745,11 @@ details.readmore summary, details.details summary {
|
|||
details.readmore summary::-webkit-details-marker details.details summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
details.readmore summary:after {
|
||||
details.readmore summary:after, details.readmore summary:hover:has(a:hover):after {
|
||||
content: '[read more]';
|
||||
font-family: Verdana, sans-serif;
|
||||
margin-left: 0.5em;
|
||||
text-decoration: none;
|
||||
color: #888;
|
||||
}
|
||||
details.readmore summary:hover:after {
|
||||
|
|
@ -782,7 +783,7 @@ details[open] .details-preview {
|
|||
.details summary:hover, .readmore summary:hover {
|
||||
background: rgba(119, 153, 187, 0.15);
|
||||
}
|
||||
.details summary:hover:has(a:hover) {
|
||||
.details summary:hover:has(a:hover), .readmore summary:hover:has(a:hover) {
|
||||
background: transparent;
|
||||
}
|
||||
.fa.expandbutton, summary:hover:has(a:hover) .fa.expandbutton {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ License: GPLv2
|
|||
|
||||
*/
|
||||
|
||||
@import url(./battle-log.css?v8.1);
|
||||
@import url(./battle-log.css?v9);
|
||||
|
||||
.battle {
|
||||
position: absolute;
|
||||
|
|
|
|||
|
|
@ -56,17 +56,19 @@ pre {
|
|||
}
|
||||
|
||||
.construction {
|
||||
background: repeating-linear-gradient(
|
||||
border: 15px solid transparent;
|
||||
padding: 5px 10px;
|
||||
background: #d9ca28;
|
||||
background-image: linear-gradient(#d9ca28), repeating-linear-gradient(
|
||||
-45deg,
|
||||
#d9ca28,
|
||||
#d9ca28 10px,
|
||||
#292824 10px,
|
||||
#292824 20px
|
||||
);
|
||||
padding: 15px;
|
||||
}
|
||||
.construction-inner {
|
||||
background: #d9ca28;
|
||||
background-origin: border-box;
|
||||
background-clip: padding-box, border-box;
|
||||
|
||||
padding: 5px 10px;
|
||||
font-weight: bold;
|
||||
color: black;
|
||||
|
|
@ -1520,6 +1522,60 @@ pre.textbox.textbox-empty[placeholder]:before {
|
|||
margin: 0 0 0 -1px;
|
||||
}
|
||||
|
||||
html:not(.native-scrollbars) *::-webkit-scrollbar {
|
||||
-webkit-appearance: none
|
||||
}
|
||||
html:not(.native-scrollbars) *::-webkit-scrollbar:vertical {
|
||||
width: 16px
|
||||
}
|
||||
html:not(.native-scrollbars) *::-webkit-scrollbar:horizontal {
|
||||
height: 16px
|
||||
}
|
||||
html:not(.native-scrollbars) *::-webkit-scrollbar-button,html:not(.native-scrollbars) *::-webkit-scrollbar-corner {
|
||||
display: none
|
||||
}
|
||||
html:not(.native-scrollbars) *::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
html:not(.native-scrollbars) *::-webkit-scrollbar-track:hover {
|
||||
background: #c5cbce;
|
||||
border-color: #959595;
|
||||
}
|
||||
html:not(.native-scrollbars) *::-webkit-scrollbar-track:vertical {
|
||||
border-width: 0 0 0 1px;
|
||||
}
|
||||
html:not(.native-scrollbars) *::-webkit-scrollbar-track:vertical:corner-present {
|
||||
border-width: 0 0 1px 1px;
|
||||
border-radius: 0 0 0 2px;
|
||||
}
|
||||
html:not(.native-scrollbars) *::-webkit-scrollbar-track:horizontal {
|
||||
border-width: 1px 1px 0 1px;
|
||||
border-radius: 2px 2px 0 0;
|
||||
}
|
||||
html:not(.native-scrollbars) *::-webkit-scrollbar-thumb {
|
||||
min-height: 2rem;
|
||||
background: #acaeaf;
|
||||
background-clip: padding-box;
|
||||
border: 5px solid transparent;
|
||||
border-radius: 10px;
|
||||
}
|
||||
html:not(.native-scrollbars) *::-webkit-scrollbar-thumb:hover,html:not(.native-scrollbars) *::-webkit-scrollbar-thumb:active {
|
||||
background-color: #6c6c6f;
|
||||
border-width: 4px;
|
||||
}
|
||||
|
||||
html.dark:not(.native-scrollbars) *::-webkit-scrollbar-track:hover {
|
||||
background: #24282a;
|
||||
border-color: #000000;
|
||||
}
|
||||
html.dark:not(.native-scrollbars) *::-webkit-scrollbar-thumb {
|
||||
background: #6c6c6f;
|
||||
}
|
||||
html.dark:not(.native-scrollbars) *::-webkit-scrollbar-thumb:hover,html.dark:not(.native-scrollbars) *::-webkit-scrollbar-thumb:active {
|
||||
background-color: #949697;
|
||||
}
|
||||
|
||||
/*********************************************************
|
||||
* Battle
|
||||
*********************************************************/
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ includeHeader();
|
|||
<li><p><a href="http://sailorwanda.deviantart.com/" target="_blank" class="subtle"><strong>Catherine D.</strong> [SailorCosmos, Matryoshkat]</a> <small>– Art (battle animations)</small></p></li>
|
||||
<li><p><strong>Cody Thompson</strong> [Rising_Dusk] <small>– Development</small></p></li>
|
||||
<li><p>[<strong>Enigami</strong>] <small>– Development</small></p></li>
|
||||
<li><p>[<strong>Hisuian Zoroark, HiZo</strong>] <small>– Development</small></p></li>
|
||||
<li><p>[<strong>Hisuian Zoroark</strong>, HiZo] <small>– Development</small></p></li>
|
||||
<li><p>[<strong>Honko</strong>] <small>– Development (damage calculator)</small></p></li>
|
||||
<li><p><strong>Ian Clail</strong> [Layell] <small>– Art (battle graphics, sprites)</small></p></li>
|
||||
<li><p><strong>Jacob McLemore</strong> <small>– Development</small></p></li>
|
||||
|
|
@ -110,6 +110,7 @@ includeHeader();
|
|||
<li><p><a href="http://leparagon.deviantart.com/" target="_blank" class="subtle">[<strong>leparagon</strong>]</a> <small>– Art (sprites, minisprite resizing)</small></p></li>
|
||||
<li><p>[<strong>livid washed</strong>] <small>– Development</small></p></li>
|
||||
<li><p><strong>Luke Harmon-Vellotti</strong> [moo, CheeseMuffin] <small>– Development</small></p></li>
|
||||
<li><p><a href="https://mayurhiwale.me/" target="_blank" class="subtle"><strong>Mayur H.</strong> [Aurastic]</a> <small>– Development (client)</small></p></li>
|
||||
<li><p><strong>Parth Mane</strong> [PartMan] <small>– Development</small></p></li>
|
||||
<li><p>[<strong>Plague von Karma</strong>] <small>– Development (Gen 1), Research</small></p></li>
|
||||
<li><p><strong>Russell Jones</strong> [SadisticMystic] <small>– Research (game mechanics)</small></p></li>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user