Filter trades by Want/Offer
Closed this issue · 2 comments
Currently in the Caravansary, when you filter by a resource you see trades that are both offering AND wanting that resource. Sometimes this is useful, but most of the time it's just a bunch of noise - players wanting the same resource as you.
I'd like to update the PlayerTradeComponent so that instead of a single checkbox per resource, there are two - one for "offer" and one for "want". I could split resourceFilters
into something like buyFilters
and sellFilters
(or better names) and go from there. I'm a web developer by day (React these days), so I'd be happy to make the changes myself and create a PR if this is worthwhile to you.
Updated PlayerTradeComponent.tsx as proposed (compiles, but is untested since I haven't yet looked into how to load a save with a Caravansary):
import Tippy from "@tippyjs/react";
import classNames from "classnames";
import { useState } from "react";
import { NoPrice, NoStorage, type Resource } from "../../../shared/definitions/ResourceDefinitions";
import { getStorageFor } from "../../../shared/logic/BuildingLogic";
import { Config } from "../../../shared/logic/Config";
import { TRADE_CANCEL_REFUND_PERCENT } from "../../../shared/logic/Constants";
import { unlockedResources } from "../../../shared/logic/IntraTickCache";
import { getTradePercentage } from "../../../shared/logic/PlayerTradeLogic";
import {
CURRENCY_PERCENT_EPSILON,
formatPercent,
keysOf,
mathSign,
safeAdd,
} from "../../../shared/utilities/Helper";
import { L, t } from "../../../shared/utilities/i18n";
import { AccountLevelImages, AccountLevelNames } from "../logic/AccountLevel";
import { client, useTrades, useUser } from "../rpc/RPCClient";
import { getMyMapXy } from "../scenes/PathFinder";
import { PlayerMapScene } from "../scenes/PlayerMapScene";
import { getCountryName, getFlagUrl } from "../utilities/CountryCode";
import { Singleton } from "../utilities/Singleton";
import { playError, playKaching } from "../visuals/Sound";
import { AddTradeComponent } from "./AddTradeComponent";
import type { IBuildingComponentProps } from "./BuildingPage";
import { ConfirmModal } from "./ConfirmModal";
import { FillPlayerTradeModal } from "./FillPlayerTradeModal";
import { FixedLengthText } from "./FixedLengthText";
import { showModal, showToast } from "./GlobalModal";
import { FormatNumber } from "./HelperComponents";
import { PendingClaimComponent } from "./PendingClaimComponent";
import { TableView } from "./TableView";
import { WarningComponent } from "./WarningComponent";
const savedBuyFilters: Set<Resource> = new Set();
const savedSellFilters: Set<Resource> = new Set();
const playerTradesSortingState = { column: 0, asc: true };
export function PlayerTradeComponent({ gameState, xy }: IBuildingComponentProps): React.ReactNode {
const building = gameState.tiles.get(xy)?.building;
const [buyFilters, setWantsFilters] = useState(savedBuyFilters);
const [sellFilters, setOffersFilters] = useState(savedSellFilters);
const [showFilters, setShowFilters] = useState(false);
if (!building) {
return null;
}
const trades = useTrades();
const user = useUser();
const myXy = getMyMapXy();
if (!myXy) {
return (
<>
<WarningComponent icon="info">
<div>{t(L.PlayerTradeClaimTileFirstWarning)}</div>
<div
className="text-strong text-link row"
onClick={() => Singleton().sceneManager.loadScene(PlayerMapScene)}
>
{t(L.PlayerTradeClaimTileFirst)}
</div>
</WarningComponent>
<div className="sep10"></div>
</>
);
}
const resources = keysOf(unlockedResources(gameState)).filter((r) => !NoStorage[r] && !NoPrice[r]);
return (
<fieldset>
<legend>{t(L.PlayerTrade)}</legend>
<PendingClaimComponent gameState={gameState} xy={xy} />
<AddTradeComponent gameState={gameState} xy={xy} />
{showFilters ? (
<fieldset>
<legend className="text-strong">{t(L.PlayerTradeFilters)}</legend>
<div className="table-view" style={{ overflowY: "auto", maxHeight: "200px" }}>
<table>
<thead>
<td>{t(L.PlayerTradeResource)}</td>
<td>{t(L.PlayerTradeOffer)}</td>
<td>{t(L.PlayerTradeWant)}</td>
</thead>
<tbody>
{resources
.sort((a, b) => Config.Resource[a].name().localeCompare(Config.Resource[b].name()))
.map((res) => (
<tr key={res}>
<td>{Config.Resource[res].name()}</td>
<td
style={{ width: 0 }}
className="text-strong"
onClick={() => {
if (savedSellFilters.has(res)) {
savedSellFilters.delete(res);
} else {
savedSellFilters.add(res);
}
setOffersFilters(new Set(savedSellFilters));
}}
>
{savedSellFilters.has(res) ? (
<div className="m-icon small text-blue">check_box</div>
) : (
<div className="m-icon small text-desc">check_box_outline_blank</div>
)}
</td>
<td
style={{ width: 0 }}
className="text-strong"
onClick={() => {
if (savedBuyFilters.has(res)) {
savedBuyFilters.delete(res);
} else {
savedBuyFilters.add(res);
}
setWantsFilters(new Set(savedBuyFilters));
}}
>
{savedBuyFilters.has(res) ? (
<div className="m-icon small text-blue">check_box</div>
) : (
<div className="m-icon small text-desc">check_box_outline_blank</div>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="sep10"></div>
<div className="row">
<button
className="f1 text-center text-strong"
onClick={() => {
setShowFilters(false);
}}
>
{t(L.PlayerTradeFiltersApply)}
</button>
<div style={{ width: 10 }} />
<button
className="f1 text-center"
onClick={() => {
savedSellFilters.clear();
savedBuyFilters.clear();
setOffersFilters(new Set(savedSellFilters));
setWantsFilters(new Set(savedBuyFilters));
setShowFilters(false);
}}
>
{t(L.PlayerTradeFiltersClear)}
</button>
</div>
</fieldset>
) : (
<button
className="row w100 jcc mb10"
onClick={() => {
setShowFilters(true);
}}
>
<div className="m-icon small">filter_list</div>
<div className="text-strong f1">
{t(L.PlayerTradeFilters)} ({sellFilters.size + buyFilters.size})
</div>
</button>
)}
<TableView
header={[
{ name: t(L.PlayerTradeWant), sortable: true },
{ name: t(L.PlayerTradeOffer), sortable: true },
{ name: "", sortable: true },
{ name: t(L.PlayerTradeFrom), sortable: true },
{ name: "", sortable: false },
]}
sortingState={playerTradesSortingState}
data={trades.filter(
(trade) =>
(sellFilters.size === 0 && buyFilters.size === 0) ||
buyFilters.has(trade.buyResource) ||
sellFilters.has(trade.sellResource),
)}
compareFunc={(a, b, col) => {
switch (col) {
case 0:
return Config.Resource[a.buyResource]
.name()
.localeCompare(Config.Resource[b.buyResource].name());
case 1:
return Config.Resource[a.sellResource]
.name()
.localeCompare(Config.Resource[b.sellResource].name());
case 2:
return getTradePercentage(a) - getTradePercentage(b);
case 3:
return a.from.localeCompare(b.from);
default:
return 0;
}
}}
renderRow={(trade) => {
const disableFill = user === null || trade.fromId === user.userId;
const percentage = getTradePercentage(trade);
return (
<tr key={trade.id} className={classNames({ "text-strong": trade.fromId === user?.userId })}>
<td>
<div className={classNames({ "text-strong": building.resources[trade.buyResource] })}>
{Config.Resource[trade.buyResource].name()}
</div>
<div className="text-small text-strong text-desc">
<FormatNumber value={trade.buyAmount} />
</div>
</td>
<td>
<div>{Config.Resource[trade.sellResource].name()}</div>
<div className="text-small text-strong text-desc">
<FormatNumber value={trade.sellAmount} />
</div>
</td>
<td
className={classNames({
"text-small text-right": true,
"text-red": percentage <= -CURRENCY_PERCENT_EPSILON,
"text-green": percentage >= CURRENCY_PERCENT_EPSILON,
"text-desc": Math.abs(percentage) < CURRENCY_PERCENT_EPSILON,
})}
>
<Tippy content={t(L.MarketValueDesc, { value: formatPercent(percentage, 0) })}>
<div>
{mathSign(percentage, CURRENCY_PERCENT_EPSILON)}
{formatPercent(Math.abs(percentage), 0)}
</div>
</Tippy>
</td>
<td>
<div className="row">
<img
src={getFlagUrl(trade.fromFlag)}
className="player-flag game-cursor"
title={getCountryName(trade.fromFlag)}
/>
{trade.fromLevel > 0 ? (
<img
src={AccountLevelImages[trade.fromLevel]}
className="player-flag"
title={AccountLevelNames[trade.fromLevel]()}
/>
) : null}
</div>
<div className="text-small">
<FixedLengthText text={trade.from} length={10} />
</div>
</td>
<td>
{trade.fromId === user?.userId ? (
<div
className="m-icon small text-link"
onClick={() => {
showModal(
<ConfirmModal
title={t(L.PlayerTradeCancelTrade)}
onConfirm={async () => {
try {
const { total, used } = getStorageFor(xy, gameState);
if (
used + trade.sellAmount * TRADE_CANCEL_REFUND_PERCENT >
total
) {
throw new Error(t(L.PlayerTradeCancelTradeNotEnoughStorage));
}
const cancelledTrade = await client.cancelTrade(trade.id);
safeAdd(
building.resources,
cancelledTrade.sellResource,
cancelledTrade.sellAmount * TRADE_CANCEL_REFUND_PERCENT,
);
playKaching();
} catch (error) {
showToast(String(error));
playError();
}
}}
>
{t(L.PlayerTradeCancelDesc, {
percent: formatPercent(TRADE_CANCEL_REFUND_PERCENT),
})}
</ConfirmModal>,
);
}}
>
delete
</div>
) : (
<div
className={classNames({
"text-link": !disableFill,
"text-strong": true,
"text-desc": disableFill,
})}
onClick={() => {
if (!disableFill) {
showModal(<FillPlayerTradeModal tradeId={trade.id} xy={xy} />);
}
}}
>
{t(L.PlayerTradeFill)}
</div>
)}
</td>
</tr>
);
}}
/>
</fieldset>
);
}
The more I've played since submitting this issue, the less I want it - it's helpful enough seeing the trades that are "competing" with my own that I don't think cluttering up this UI is worthwhile. I'm closing this issue due to lack of other interest and my own gradual change of heart.