Order entry
Clicks on the board, because that is where the question is. Select a unit, pick a verb, and the provinces the next click will accept light up. Nobody writes a move the rules were never going to allow, and nobody has to learn a notation to find that out. What is offered comes from the rules rather than from the map, which is the same split the map is built on: a fleet on the north coast of Spain is drawn identically to one on the south coast and can go to entirely different places. A support is only offered where both units could have gone, since you cannot support a move you could not have made yourself, and a fleet is only asked which coast it means when more than one is reachable. Orders are drawn where they are given -- an arrow to the destination, a dashed line to what a support is holding up, a ring round a unit staying put -- and written out beside the board in the rules' own words, because an arrow cannot tell you that the support you meant for Vienna is going to Budapest. The written list runs through the validator on every keystroke, so an order the rules will not take is struck through while it is being written rather than after the turn.
This commit is contained in:
@@ -178,6 +178,25 @@ refused to make that offer would be playing a politer game than this one. It
|
||||
will not, however, ask the power it is attacking to *help*, or offer it peace
|
||||
while the orders say otherwise.
|
||||
|
||||
## Giving orders
|
||||
|
||||
Clicks on the board, because that is where the question is. Select a unit,
|
||||
pick a verb, and the provinces the next click will accept light up -- so
|
||||
nobody writes a move the rules were never going to allow, and nobody has to
|
||||
learn a notation to find that out.
|
||||
|
||||
Everything offered comes from the rules rather than from the map, which is
|
||||
the same split the map itself is built on. A fleet on the north coast of
|
||||
Spain is drawn identically to one on the south coast and can go to entirely
|
||||
different places; no shape can express that, and the highlight has to.
|
||||
|
||||
Orders are drawn where they are given: an arrow to where a unit is going, a
|
||||
dashed line to what a support is holding up, a ring round a unit that is
|
||||
staying. The list beside the board says the same thing in the rules' own
|
||||
words, because an arrow cannot tell you that the support you meant for Vienna
|
||||
is being given to Budapest. Anything the rules will not take is struck
|
||||
through in the list as it is written, rather than after the turn.
|
||||
|
||||
## Still to build
|
||||
|
||||
- the map, the orders, the music, the battle sound, the four endings
|
||||
|
||||
+100
@@ -31,6 +31,16 @@ h1 {
|
||||
|
||||
header p { margin: 2px 0 0; font-size: 0.9rem; }
|
||||
|
||||
header select {
|
||||
font: inherit;
|
||||
font-weight: 700;
|
||||
color: #f2e9d8;
|
||||
background: #22304a;
|
||||
border: 2px solid #2b2318;
|
||||
border-radius: 6px;
|
||||
padding: 1px 6px;
|
||||
}
|
||||
|
||||
.dim { color: #a3b3c9; }
|
||||
|
||||
.map-wrap {
|
||||
@@ -93,6 +103,96 @@ header p { margin: 2px 0 0; font-size: 0.9rem; }
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* --- the orders ------------------------------------------------------ */
|
||||
|
||||
.orders .o-move {
|
||||
stroke: #1c1208;
|
||||
stroke-width: 6;
|
||||
marker-end: url(#arrow);
|
||||
}
|
||||
|
||||
.orders .o-hold {
|
||||
fill: none;
|
||||
stroke: #1c1208;
|
||||
stroke-width: 5;
|
||||
}
|
||||
|
||||
.orders .o-support line {
|
||||
stroke: #1c1208;
|
||||
stroke-width: 5;
|
||||
stroke-dasharray: 16 11;
|
||||
}
|
||||
|
||||
.orders .o-convoy line {
|
||||
stroke: #2f6f8f;
|
||||
stroke-width: 5;
|
||||
stroke-dasharray: 4 12;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
.orders marker path { fill: #1c1208; }
|
||||
|
||||
.orders-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.orders-panel h2 { margin: 0; font-size: 0.98rem; }
|
||||
|
||||
.verbs { display: flex; flex-wrap: wrap; gap: 4px; }
|
||||
|
||||
.verbs button,
|
||||
.submit {
|
||||
font: inherit;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
color: #f2e9d8;
|
||||
background: #22304a;
|
||||
border: 2px solid #2b2318;
|
||||
border-radius: 7px;
|
||||
padding: 3px 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.verbs button.on { background: #b8341f; }
|
||||
|
||||
.hint { margin: 0; font-size: 0.82rem; line-height: 1.4; }
|
||||
|
||||
.written {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
font-size: 0.84rem;
|
||||
}
|
||||
|
||||
.written li { display: flex; align-items: center; gap: 6px; }
|
||||
|
||||
/* An order the rules will not take, said so while it is being written. */
|
||||
.written li.bad span { color: #ff9a8b; text-decoration: line-through; }
|
||||
|
||||
.written .drop {
|
||||
margin-left: auto;
|
||||
font: inherit;
|
||||
color: #a3b3c9;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0 3px;
|
||||
}
|
||||
|
||||
.written .drop:hover { color: #ff9a8b; }
|
||||
|
||||
.submit { margin-top: auto; align-self: flex-start; background: #2f5d3a; }
|
||||
|
||||
.powers li.me { color: #ffd479; }
|
||||
|
||||
/* --- the side ------------------------------------------------------- */
|
||||
|
||||
.side {
|
||||
|
||||
+170
-36
@@ -1,72 +1,206 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { Board, COLOURS, POWER_NAMES } from './components/Board'
|
||||
import { OPENING, POWERS, PROVINCES } from './game/map'
|
||||
import { boardFrom, type Unit } from './game/orders'
|
||||
import { OrderPanel, type Step } from './components/OrderPanel'
|
||||
import { reachableFrom } from './game/layout'
|
||||
import { OPENING, POWERS, PROVINCES, base, type Power } from './game/map'
|
||||
import { boardFrom, canStep, validate, type Order, type Unit } from './game/orders'
|
||||
import { centreCount, openingOwnership } from './game/turn'
|
||||
import './App.css'
|
||||
|
||||
/**
|
||||
* The opening position, on the board, so the map can be looked at.
|
||||
* Spring 1901, with orders.
|
||||
*
|
||||
* This is scaffolding: order entry, the press and the turn loop are not
|
||||
* wired to it yet. What it is for is seeing whether seventy-five provinces
|
||||
* and twenty-two units are legible at a glance, which is a question no test
|
||||
* can answer.
|
||||
* The whole interaction is clicks on the board, because that is where the
|
||||
* question is. A unit is selected, a verb is chosen, and the provinces the
|
||||
* next click will accept light up -- so a player never types a move that the
|
||||
* rules were never going to allow, and never has to learn a notation to find
|
||||
* that out.
|
||||
*
|
||||
* Everything is offered from the rules rather than from the map. A fleet on
|
||||
* the north coast of Spain is drawn identically to one on the south coast and
|
||||
* they can go to entirely different places, which no shape can express.
|
||||
*/
|
||||
export default function App() {
|
||||
const [picked, setPicked] = useState<string | null>(null)
|
||||
const [power, setPower] = useState<Power>('austria')
|
||||
const [step, setStep] = useState<Step>({ kind: 'idle' })
|
||||
const [orders, setOrders] = useState<Map<string, Order>>(new Map())
|
||||
|
||||
const units = useMemo(() => {
|
||||
const all: Unit[] = []
|
||||
for (const power of POWERS) {
|
||||
for (const at of OPENING[power].armies) all.push({ power, type: 'army', at })
|
||||
for (const at of OPENING[power].fleets) all.push({ power, type: 'fleet', at })
|
||||
for (const p of POWERS) {
|
||||
for (const at of OPENING[p].armies) all.push({ power: p, type: 'army', at })
|
||||
for (const at of OPENING[p].fleets) all.push({ power: p, type: 'fleet', at })
|
||||
}
|
||||
return boardFrom(all)
|
||||
}, [])
|
||||
|
||||
const own = useMemo(openingOwnership, [])
|
||||
|
||||
/** Which provinces the next click will do something with. */
|
||||
const offering = useMemo(() => {
|
||||
if (step.kind === 'idle') return new Set<string>()
|
||||
const unit = units.get(step.at)
|
||||
if (!unit) return new Set<string>()
|
||||
|
||||
if (step.kind === 'move') return new Set(reachableFrom(unit).map(base))
|
||||
|
||||
if (step.kind === 'support') {
|
||||
if (step.from === undefined) {
|
||||
// Anybody I could reach: I can prop them up where they stand, or
|
||||
// help them into somewhere I could have gone myself.
|
||||
return new Set(
|
||||
[...units.keys()].filter((p) => p !== step.at && canReach(unit, p)),
|
||||
)
|
||||
}
|
||||
const helped = units.get(step.from)
|
||||
if (!helped) return new Set<string>()
|
||||
// Only where both of us could go: you cannot support a move you could
|
||||
// not have made yourself.
|
||||
return new Set(
|
||||
reachableFrom(helped)
|
||||
.map(base)
|
||||
.filter((p) => canReach(unit, p) || p === step.from),
|
||||
)
|
||||
}
|
||||
|
||||
if (step.from === undefined) {
|
||||
return new Set(
|
||||
[...units.entries()]
|
||||
.filter(([, u]) => u.type === 'army' && PROVINCES[base(u.at)]!.terrain === 'coast')
|
||||
.map(([p]) => p),
|
||||
)
|
||||
}
|
||||
return new Set(
|
||||
Object.keys(PROVINCES).filter((p) => PROVINCES[p]!.terrain === 'coast' && p !== step.from),
|
||||
)
|
||||
}, [step, units])
|
||||
|
||||
const write = useCallback((order: Order) => {
|
||||
setOrders((prev) => new Map(prev).set(base(order.at), order))
|
||||
setStep({ kind: 'idle' })
|
||||
}, [])
|
||||
|
||||
const click = (province: string) => {
|
||||
const here = units.get(province)
|
||||
|
||||
// Starting again: clicking one of my own units always selects it.
|
||||
if (step.kind === 'idle' || !offering.has(province)) {
|
||||
if (here?.power === power) setStep({ kind: 'move', at: province })
|
||||
else setStep({ kind: 'idle' })
|
||||
return
|
||||
}
|
||||
|
||||
if (step.kind === 'move') {
|
||||
write({ type: 'move', at: step.at, to: coastOf(units.get(step.at)!, province), power })
|
||||
return
|
||||
}
|
||||
|
||||
if (step.kind === 'support') {
|
||||
if (step.from === undefined) {
|
||||
setStep({ ...step, from: province })
|
||||
return
|
||||
}
|
||||
write({ type: 'support', at: step.at, from: step.from, to: province, power })
|
||||
return
|
||||
}
|
||||
|
||||
if (step.from === undefined) {
|
||||
setStep({ ...step, from: province })
|
||||
return
|
||||
}
|
||||
write({ type: 'convoy', at: step.at, from: step.from, to: province, power })
|
||||
}
|
||||
|
||||
const clear = (at: string) => {
|
||||
setOrders((prev) => {
|
||||
const next = new Map(prev)
|
||||
next.delete(at)
|
||||
return next
|
||||
})
|
||||
setStep({ kind: 'idle' })
|
||||
}
|
||||
|
||||
// What the rules make of the orders so far, live. A player should find out
|
||||
// that a support is impossible while writing it, not after the turn.
|
||||
const illegal = useMemo(
|
||||
() => validate(units, [...orders.values()]).illegal,
|
||||
[units, orders],
|
||||
)
|
||||
|
||||
const mine = [...units.entries()].filter(([, u]) => u.power === power)
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<header>
|
||||
<h1>Great Powers</h1>
|
||||
<p className="dim">Spring 1901 — the board before anybody has said anything.</p>
|
||||
<p className="dim">
|
||||
Spring 1901 — playing{' '}
|
||||
<select value={power} onChange={(e) => {
|
||||
setPower(e.target.value as Power)
|
||||
setOrders(new Map())
|
||||
setStep({ kind: 'idle' })
|
||||
}}>
|
||||
{POWERS.map((p) => (
|
||||
<option key={p} value={p}>{POWER_NAMES[p]}</option>
|
||||
))}
|
||||
</select>
|
||||
. {mine.length - orders.size} of {mine.length} units still without orders.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div className="map-wrap">
|
||||
<Board units={units} own={own} selected={picked} onPick={setPicked} />
|
||||
<Board
|
||||
units={units}
|
||||
own={own}
|
||||
orders={orders}
|
||||
selected={step.kind === 'idle' ? null : step.at}
|
||||
offering={offering}
|
||||
onPick={click}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<aside className="side">
|
||||
<ul className="powers">
|
||||
{POWERS.map((power) => (
|
||||
<li key={power}>
|
||||
<span className="swatch" style={{ background: COLOURS[power] }} />
|
||||
{POWER_NAMES[power]}
|
||||
<span className="count">{centreCount(own, power)}</span>
|
||||
{POWERS.map((p) => (
|
||||
<li key={p} className={p === power ? 'me' : ''}>
|
||||
<span className="swatch" style={{ background: COLOURS[p] }} />
|
||||
{POWER_NAMES[p]}
|
||||
<span className="count">{centreCount(own, p)}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div className="picked">
|
||||
{picked ? (
|
||||
<>
|
||||
<h2>{PROVINCES[picked]!.name}</h2>
|
||||
<p className="dim">
|
||||
{PROVINCES[picked]!.terrain === 'sea'
|
||||
? 'Open water. Fleets only.'
|
||||
: PROVINCES[picked]!.terrain === 'coast'
|
||||
? 'Coastal. Armies and fleets.'
|
||||
: 'Inland. Armies only.'}
|
||||
{PROVINCES[picked]!.sc ? ' A supply centre.' : ''}
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<p className="dim">Click a province.</p>
|
||||
)}
|
||||
</div>
|
||||
<OrderPanel
|
||||
units={units}
|
||||
orders={orders}
|
||||
step={step}
|
||||
illegal={illegal}
|
||||
onAsk={(kind) =>
|
||||
setStep(step.kind === 'idle' ? step : ({ kind, at: step.at } as Step))
|
||||
}
|
||||
onClear={clear}
|
||||
onSubmit={() => undefined}
|
||||
/>
|
||||
</aside>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** Can this unit reach that province, by any coast of it? */
|
||||
function canReach(unit: Unit, province: string): boolean {
|
||||
if (canStep(unit, province)) return true
|
||||
const coasts = PROVINCES[province]?.coasts
|
||||
return coasts !== undefined && coasts.some((c) => canStep(unit, `${province}/${c}`))
|
||||
}
|
||||
|
||||
/**
|
||||
* Which coast a fleet means. Where only one is reachable the order is
|
||||
* unambiguous even though it did not say, which is the rule the adjudicator
|
||||
* applies too -- so the interface should not make a fuss about it either.
|
||||
*/
|
||||
function coastOf(unit: Unit, province: string): string {
|
||||
const coasts = PROVINCES[province]?.coasts
|
||||
if (unit.type === 'army' || !coasts) return province
|
||||
const open = coasts.map((c) => `${province}/${c}`).filter((k) => canStep(unit, k))
|
||||
return open.length === 1 ? open[0]! : province
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { PROVINCES, POWERS, base, type Power } from '../game/map'
|
||||
import { CENTRES, SHAPES, SHIFT, VIEW_BOX, reachableFrom } from '../game/layout'
|
||||
import type { Board as Units } from '../game/orders'
|
||||
import type { Board as Units, Order } from '../game/orders'
|
||||
import type { Ownership } from '../game/turn'
|
||||
|
||||
/**
|
||||
@@ -33,17 +33,24 @@ const INK = '#2b2318'
|
||||
export function Board({
|
||||
units,
|
||||
own,
|
||||
orders,
|
||||
selected,
|
||||
offering,
|
||||
onPick,
|
||||
}: {
|
||||
units: Units
|
||||
own: Ownership
|
||||
/** Orders written so far, drawn on the board as they are given. */
|
||||
orders?: ReadonlyMap<string, Order>
|
||||
selected?: string | null
|
||||
/** Provinces the current step will accept a click on. */
|
||||
offering?: ReadonlySet<string>
|
||||
onPick?: (province: string) => void
|
||||
}) {
|
||||
const ids = Object.keys(PROVINCES)
|
||||
const standing = selected ? units.get(selected) : undefined
|
||||
const reachable = new Set(standing ? reachableFrom(standing) : [])
|
||||
const reachable = offering ?? new Set(standing ? reachableFrom(standing) : [])
|
||||
const at = (id: string) => CENTRES[id] ?? CENTRES[base(id)]!
|
||||
|
||||
const shade = (id: string) => {
|
||||
const p = PROVINCES[id]!
|
||||
@@ -94,6 +101,45 @@ export function Board({
|
||||
</text>
|
||||
))}
|
||||
|
||||
{/*
|
||||
The orders, drawn where they are being given. A move is an arrow to
|
||||
where it is going; a support is a dashed line to what it is holding
|
||||
up; a hold is a ring round the unit. Seeing them on the board is the
|
||||
difference between checking your orders and re-reading a list.
|
||||
*/}
|
||||
<g className="orders">
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="5"
|
||||
markerHeight="5" orient="auto-start-reverse">
|
||||
<path d="M0 0 L10 5 L0 10 z" />
|
||||
</marker>
|
||||
</defs>
|
||||
{[...(orders?.values() ?? [])].map((o) => {
|
||||
const from = at(o.at)
|
||||
if (o.type === 'hold') {
|
||||
return <circle className="o-hold" key={o.at} cx={from.x} cy={from.y} r={24} />
|
||||
}
|
||||
if (o.type === 'move') {
|
||||
const to = at(o.to)
|
||||
return (
|
||||
<line className="o-move" key={o.at} x1={from.x} y1={from.y} x2={to.x} y2={to.y} />
|
||||
)
|
||||
}
|
||||
// A support props something up somewhere else: draw it to the
|
||||
// province being held rather than to the unit doing the holding.
|
||||
const target = at(o.type === 'support' ? o.to : o.to)
|
||||
const via = at(o.from)
|
||||
return (
|
||||
<g className={o.type === 'support' ? 'o-support' : 'o-convoy'} key={o.at}>
|
||||
<line x1={from.x} y1={from.y} x2={via.x} y2={via.y} />
|
||||
{base(o.from) !== base(o.to) && (
|
||||
<line x1={via.x} y1={via.y} x2={target.x} y2={target.y} />
|
||||
)}
|
||||
</g>
|
||||
)
|
||||
})}
|
||||
</g>
|
||||
|
||||
{[...units.entries()].map(([at, unit]) => {
|
||||
const p = CENTRES[unit.at] ?? CENTRES[base(unit.at)] ?? CENTRES[at]!
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import { PROVINCES, base } from '../game/map'
|
||||
import type { Order, Unit } from '../game/orders'
|
||||
|
||||
/**
|
||||
* The orders as written, and the one control that matters: taking one back.
|
||||
*
|
||||
* The board shows what the orders *do*; this shows what they *say*, in the
|
||||
* same words the rules use. Both are needed. An arrow on a map is quicker to
|
||||
* read and cannot tell you that the support you meant to give Vienna is
|
||||
* actually being given to Budapest.
|
||||
*/
|
||||
|
||||
export type Step =
|
||||
| { kind: 'idle' }
|
||||
| { kind: 'move'; at: string }
|
||||
| { kind: 'support'; at: string; from?: string }
|
||||
| { kind: 'convoy'; at: string; from?: string }
|
||||
|
||||
const name = (id: string) => PROVINCES[base(id)]!.name
|
||||
|
||||
export function say(order: Order, unit?: Unit): string {
|
||||
const who = `${unit?.type === 'fleet' ? 'F' : 'A'} ${name(order.at)}`
|
||||
switch (order.type) {
|
||||
case 'hold':
|
||||
return `${who} holds`
|
||||
case 'move':
|
||||
return `${who} → ${name(order.to)}`
|
||||
case 'support':
|
||||
return base(order.from) === base(order.to)
|
||||
? `${who} supports ${name(order.from)}`
|
||||
: `${who} supports ${name(order.from)} → ${name(order.to)}`
|
||||
case 'convoy':
|
||||
return `${who} convoys ${name(order.from)} → ${name(order.to)}`
|
||||
}
|
||||
}
|
||||
|
||||
export function OrderPanel({
|
||||
units,
|
||||
orders,
|
||||
step,
|
||||
illegal,
|
||||
onAsk,
|
||||
onClear,
|
||||
onSubmit,
|
||||
}: {
|
||||
units: ReadonlyMap<string, Unit>
|
||||
orders: ReadonlyMap<string, Order>
|
||||
step: Step
|
||||
illegal: ReadonlySet<string>
|
||||
onAsk: (kind: Step['kind']) => void
|
||||
onClear: (at: string) => void
|
||||
onSubmit: () => void
|
||||
}) {
|
||||
const selected = step.kind === 'idle' ? null : step.at
|
||||
const unit = selected ? units.get(selected) : undefined
|
||||
|
||||
return (
|
||||
<div className="orders-panel">
|
||||
{unit && selected ? (
|
||||
<>
|
||||
<h2>
|
||||
{unit.type === 'fleet' ? 'Fleet' : 'Army'} {name(selected)}
|
||||
</h2>
|
||||
<div className="verbs">
|
||||
<button onClick={() => onAsk('move')} className={step.kind === 'move' ? 'on' : ''}>
|
||||
Move
|
||||
</button>
|
||||
<button onClick={() => onAsk('support')} className={step.kind === 'support' ? 'on' : ''}>
|
||||
Support
|
||||
</button>
|
||||
{unit.type === 'fleet' && PROVINCES[selected]!.terrain === 'sea' && (
|
||||
<button onClick={() => onAsk('convoy')} className={step.kind === 'convoy' ? 'on' : ''}>
|
||||
Convoy
|
||||
</button>
|
||||
)}
|
||||
<button onClick={() => onClear(selected)}>Hold</button>
|
||||
</div>
|
||||
<p className="hint dim">{hint(step)}</p>
|
||||
</>
|
||||
) : (
|
||||
<p className="hint dim">Click one of your units.</p>
|
||||
)}
|
||||
|
||||
<ul className="written">
|
||||
{[...orders.entries()].map(([at, order]) => (
|
||||
<li key={at} className={illegal.has(at) ? 'bad' : ''}>
|
||||
<span>{say(order, units.get(at))}</span>
|
||||
<button className="drop" onClick={() => onClear(at)} title="Take it back">
|
||||
×
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
{orders.size === 0 && <li className="dim">Nothing ordered. Everybody holds.</li>}
|
||||
</ul>
|
||||
|
||||
<button className="submit" onClick={onSubmit}>
|
||||
Submit orders
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function hint(step: Step): string {
|
||||
switch (step.kind) {
|
||||
case 'idle':
|
||||
return ''
|
||||
case 'move':
|
||||
return 'Click where it should go.'
|
||||
case 'support':
|
||||
return step.from === undefined
|
||||
? 'Click the unit to support — or its own province, to hold it there.'
|
||||
: 'Click where that unit is going.'
|
||||
case 'convoy':
|
||||
return step.from === undefined
|
||||
? 'Click the army to carry.'
|
||||
: 'Click where it is going.'
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user