diff --git a/agot-bg-game-server/src/client/MapComponent.tsx b/agot-bg-game-server/src/client/MapComponent.tsx index ab6de2ab3..0a5e47922 100644 --- a/agot-bg-game-server/src/client/MapComponent.tsx +++ b/agot-bg-game-server/src/client/MapComponent.tsx @@ -589,12 +589,14 @@ export default class MapComponent extends Component<MapComponentProps> { let planningOrAction = (this.ingame.childGameState instanceof PlanningGameState || this.ingame.childGameState instanceof ActionGameState) ? this.ingame.childGameState : null; if (planningOrAction instanceof ActionGameState && !(planningOrAction.childGameState instanceof UseRavenGameState)) { - // Do not show restricted orders after Raven state because Doran may cause a restricted order to be shown which still can be executed + // Do not highlight restricted orders after Raven state (during whole action phase) + // because abilities like Doran may cause an order to be shown as restricted suddenly though it isn't planningOrAction = null; } const drawBorder = order?.type.restrictedTo == sea.kind; - const controller = drawBorder ? region.getController() : null; + const hasPlaceOrders = this.ingame.hasChildGameState(PlaceOrdersGameState); + const controller = drawBorder || hasPlaceOrders ? region.getController() : null; const color = drawBorder && controller ? controller.id != "greyjoy" ? controller.color @@ -604,6 +606,29 @@ export default class MapComponent extends Component<MapComponentProps> { const wrap = properties.wrap; const clickable = properties.onClick != undefined || wrap != undefined; + let placeAnimation = ''; + + if (hasPlaceOrders && controller) { + switch (controller.id) { + case "stark": + case "arryn": + placeAnimation = 'scale-in-top'; + break; + case "targaryen": + case "baratheon": + placeAnimation = 'scale-in-right'; + break; + case "martell": + case "tyrell": + placeAnimation = 'scale-in-bottom'; + break; + case "greyjoy": + case "lannister": + placeAnimation = 'scale-in-left'; + break; + } + } + return ( <ConditionalWrap condition={true} key={`map-order_${region.id}`} @@ -634,7 +659,7 @@ export default class MapComponent extends Component<MapComponentProps> { id={`map-order-container_${region.id}`} > <div style={{ backgroundImage: `url(${backgroundUrl})`, borderColor: color }} - className={classNames("order-icon", { + className={classNames(`order-icon ${placeAnimation}`, { "order-border": drawBorder, "pulsate-bck": properties.animateAttention, "pulsate-bck_fade-out": properties.animateFadeOut, diff --git a/agot-bg-game-server/src/client/game-state-panel/PlayerMusteringComponent.tsx b/agot-bg-game-server/src/client/game-state-panel/PlayerMusteringComponent.tsx index b5b152583..7f0c0a354 100644 --- a/agot-bg-game-server/src/client/game-state-panel/PlayerMusteringComponent.tsx +++ b/agot-bg-game-server/src/client/game-state-panel/PlayerMusteringComponent.tsx @@ -105,6 +105,11 @@ export default class PlayerMusteringComponent extends Component<GameStateCompone </Col> </Row> </Col> + {this.isStarredConsolidatePowerMusteringType && this.musterings.size == 0 && ( + <Col xs={12} className="text-center"> + <small>Note: If you dont recruit any units, your order will be resolved to gain Power tokens.</small> + </Col> + )} </>} {(!this.doesControlCurrentHouse || this.props.gameState.ingame.isVassalHouse(this.house)) && <Col xs={12} className="text-center"> diff --git a/agot-bg-game-server/src/client/game-state-panel/ResolveSingleConsolidatePowerComponent.tsx b/agot-bg-game-server/src/client/game-state-panel/ResolveSingleConsolidatePowerComponent.tsx index 070ce1cd2..fe83da231 100644 --- a/agot-bg-game-server/src/client/game-state-panel/ResolveSingleConsolidatePowerComponent.tsx +++ b/agot-bg-game-server/src/client/game-state-panel/ResolveSingleConsolidatePowerComponent.tsx @@ -1,9 +1,9 @@ import { observer } from "mobx-react"; -import React, { Component, ReactNode } from "react"; +import React, { Component, ReactElement, ReactNode } from "react"; import Region from "../../common/ingame-game-state/game-data-structure/Region"; import { observable } from "mobx"; import House from "../../common/ingame-game-state/game-data-structure/House"; -import { Button } from "react-bootstrap"; +import { Button, OverlayTrigger, Popover } from "react-bootstrap"; import GameStateComponentProps from "./GameStateComponentProps"; import Row from "react-bootstrap/Row"; import Col from "react-bootstrap/Col"; @@ -58,82 +58,18 @@ export default class ResolveSingleConsolidatePowerComponent extends Component<Ga : <>House <b>{this.house.name}</b> must resolve one of its Consolidate Power{availableOrders.values.some(ot => ot instanceof IronBankOrderType) ? " or Iron\xa0Bank" : ""} orders.</>} </Col> {this.doesControlCurrentHouse ? - this.selectedOrderRegion && this.selectedOrderType ? - <> - <Col xs={12} className="text-center"> - <p><b>{this.selectedOrderType.name}</b> in <b>{this.selectedOrderRegion.name}</b></p> - </Col> - <Col xs={12}> - <Row className="justify-content-center"> - {this.selectedOrderType instanceof DefenseMusterOrderType && <> - <Col xs={12} className="d-flex justify-content-center"> - <Button type="button" variant="success" onClick={() => { - this.gameState.chooseMustering(this.selectedOrderRegion as Region); - this.reset(); - }}> - Muster in {this.selectedOrderRegion.name} - </Button> - </Col> - <Col xs={12} className="d-flex justify-content-center"> - <Button type="button" variant="warning" onClick={() => { - this.gameState.chooseRemoveOrder(this.selectedOrderRegion as Region); - this.reset(); - }}> - Ignore and remove order - </Button> - </Col> - </>} - {this.selectedOrderType instanceof ConsolidatePowerOrderType && <> - {this.selectedOrderType.starred && this.selectedOrderRegion.castleLevel > 0 && - <Col xs="auto"> - <Button type="button" variant="success" onClick={() => { - this.gameState.chooseMustering(this.selectedOrderRegion as Region); - this.reset(); - }}> - Muster in {this.selectedOrderRegion.name} - </Button> - </Col>} - <Col xs="auto"> - <Button type="button" onClick={() => { - this.gameState.chooseGainPowerTokens(this.selectedOrderRegion as Region); - this.reset(); - }}> - {this.getPowerTokenButtonText(this.gameState.getPotentialGainedPowerTokens(this.selectedOrderRegion, this.house))} - </Button> - </Col> - </>} - {this.selectedOrderType instanceof IronBankOrderType && this.ironBank && <> - {this.ironBank.getPurchasableLoans(this.house).map(purchasable => - <Col xs={12} className="d-flex justify-content-center" key={`loan-button-${purchasable.loan.id}`}> - <Button type="button" variant="success" onClick={() => { - this.gameState.choosePurchaseLoan(purchasable.slotIndex, this.selectedOrderRegion as Region); - this.reset(); - }}> - Pay {purchasable.costs} Power token{purchasable.costs != 1 ? "s" : ""} to purchase {purchasable.loan.name} - </Button> - </Col> - )} - <Col xs={12} className="d-flex justify-content-center"> - <Button type="button" variant="warning" onClick={() => { - this.gameState.chooseRemoveOrder(this.selectedOrderRegion as Region); - this.reset(); - }}> - Ignore and remove order - </Button> - </Col> - </>} - <Col xs={12} className="d-flex justify-content-center"> - <Button type="button" - variant="danger" - onClick={() => this.reset()} - > - Reset - </Button> - </Col> - </Row> - </Col> - </> - : <Col xs={12} className="text-center"><p>Click the order you want to resolve.</p></Col> + <>{this.selectedOrderRegion && this.selectedOrderType + ? this.renderGameStateControls(this.selectedOrderRegion, this.selectedOrderType) + : <Col xs={12} className="text-center"><p>Click the order you want to resolve.</p></Col>} + <Col xs={12} className="d-flex justify-content-center"> + <Button type="button" + variant="danger" + onClick={() => this.reset()} + > + Reset + </Button> + </Col> + </> : <Col xs={12} className="text-center"> Waiting for {this.gameState.ingame.getControllerOfHouse(this.house).house.name}... @@ -142,6 +78,74 @@ export default class ResolveSingleConsolidatePowerComponent extends Component<Ga ); } + private renderGameStateControls(orderRegion: Region, orderType: ConsolidatePowerOrderType | IronBankOrderType | DefenseMusterOrderType) { + return <> + <Col xs={12} className="text-center"> + <p><b>{orderType.name}</b> in <b>{orderRegion.name}</b></p> + </Col> + <Col xs={12}> + <Row className="justify-content-center"> + {orderType instanceof DefenseMusterOrderType && <> + <Col xs={12} className="d-flex justify-content-center"> + <Button type="button" variant="success" onClick={() => { + this.gameState.chooseMustering(orderRegion as Region); + this.reset(); + } }> + Muster in {orderRegion.name} + </Button> + </Col> + <Col xs={12} className="d-flex justify-content-center"> + <Button type="button" variant="warning" onClick={() => { + this.gameState.chooseRemoveOrder(orderRegion as Region); + this.reset(); + } }> + Ignore and remove order + </Button> + </Col> + </>} + {orderType instanceof ConsolidatePowerOrderType && <> + {orderType.starred && orderRegion.castleLevel > 0 && + <Col xs="auto"> + <Button type="button" variant="success" onClick={() => { + this.gameState.chooseMustering(orderRegion as Region); + this.reset(); + } }> + Muster in {orderRegion.name} + </Button> + </Col>} + <Col xs="auto"> + <Button type="button" onClick={() => { + this.gameState.chooseGainPowerTokens(orderRegion as Region); + this.reset(); + } }> + {this.getPowerTokenButtonText(this.gameState.getPotentialGainedPowerTokens(orderRegion, this.house))} + </Button> + </Col> + </>} + {orderType instanceof IronBankOrderType && this.ironBank && <> + {this.ironBank.getPurchasableLoans(this.house).map(purchasable => <Col xs={12} className="d-flex justify-content-center" key={`loan-button-${purchasable.loan.id}`}> + <Button type="button" variant="success" onClick={() => { + this.gameState.choosePurchaseLoan(purchasable.slotIndex, orderRegion as Region); + this.reset(); + } }> + Pay {purchasable.costs} Power token{purchasable.costs != 1 ? "s" : ""} to purchase {purchasable.loan.name} + </Button> + </Col> + )} + <Col xs={12} className="d-flex justify-content-center"> + <Button type="button" variant="warning" onClick={() => { + this.gameState.chooseRemoveOrder(orderRegion as Region); + this.reset(); + } }> + Ignore and remove order + </Button> + </Col> + </>} + </Row> + </Col> + </>; + } + private getPowerTokenButtonText(powerTokenCount: number): string { return `Gain ${powerTokenCount} Power token${powerTokenCount != 1 ? "s" : ""}`; } @@ -149,15 +153,25 @@ export default class ResolveSingleConsolidatePowerComponent extends Component<Ga private modifyOrdersOnMap(): [Region, PartialRecursive<OrderOnMapProperties>][] { if (this.doesControlCurrentHouse) { const availableOrders = this.gameState.parentGameState.getAvailableOrdersOfHouse(this.gameState.house); - if (this.selectedOrderRegion && this.selectedOrderType) { - availableOrders.clear(); - availableOrders.set(this.selectedOrderRegion, this.selectedOrderType); - } return availableOrders.entries.map(([r, ot]) => [ r, { highlight: { active: true }, - onClick: () => this.onOrderClick(r, ot) + onClick: () => this.onOrderClick(r, ot), + wrap: (child: ReactElement) => ( + <OverlayTrigger + placement="auto-start" + trigger="click" + rootClose + overlay={ + <Popover id={"resolve-cp-order-popover_region_" + r.id} className="p-1"> + {this.renderGameStateControls(r, ot)} + </Popover> + } + > + {child} + </OverlayTrigger> + ) } ]); } @@ -166,17 +180,18 @@ export default class ResolveSingleConsolidatePowerComponent extends Component<Ga } private onOrderClick(region: Region, orderType: ConsolidatePowerOrderType | IronBankOrderType | DefenseMusterOrderType): void { - if (!this.selectedOrderRegion) { + if (this.selectedOrderRegion == region) { + this.reset(); + } else { this.selectedOrderRegion = region; this.selectedOrderType = orderType; - } else if (this.selectedOrderRegion == region) { - this.reset(); } } private reset(): void { this.selectedOrderRegion = null; this.selectedOrderType = null; + document.body.click(); } componentDidMount(): void { diff --git a/agot-bg-game-server/src/client/game-state-panel/SimpleChoiceComponent.tsx b/agot-bg-game-server/src/client/game-state-panel/SimpleChoiceComponent.tsx index ef63ca0c5..fbd993159 100644 --- a/agot-bg-game-server/src/client/game-state-panel/SimpleChoiceComponent.tsx +++ b/agot-bg-game-server/src/client/game-state-panel/SimpleChoiceComponent.tsx @@ -18,11 +18,16 @@ export default class SimpleChoiceComponent extends Component<GameStateComponentP <Col xs={12} className="text-center"> {this.props.gameClient.doesControlHouse(this.props.gameState.house) ? ( <Row className="justify-content-center"> - {this.props.gameState.choices.map((s, i) => ( - <Col xs="auto" key={`simple-choice_${i}`}> - <Button type="button" onClick={() => this.choose(i)}>{s}</Button> - </Col> - ))} + {this.props.gameState.choices.map((s, i) => { + const variant = s == "Ignore" + ? "danger" + : s == "Activate" + ? "success" + : undefined; + return <Col xs="auto" key={`simple-choice_${i}`}> + <Button type="button" variant={variant} onClick={() => this.choose(i)}>{s}</Button> + </Col>; + })} </Row> ) : ( <>Waiting for {this.props.gameState.parentGameState.ingame.getControllerOfHouse(this.props.gameState.house).house.name}...</> diff --git a/agot-bg-game-server/src/client/style/custom.scss b/agot-bg-game-server/src/client/style/custom.scss index bdb7ba890..8b2df6b4c 100644 --- a/agot-bg-game-server/src/client/style/custom.scss +++ b/agot-bg-game-server/src/client/style/custom.scss @@ -1101,4 +1101,208 @@ select { .float-right { float: right; -} \ No newline at end of file +} + + +.scale-in-top { + -webkit-animation: scale-in-top 0.5s cubic-bezier(0.250, 0.460, 0.450, 0.940) both; + animation: scale-in-top 0.5s cubic-bezier(0.250, 0.460, 0.450, 0.940) both; +} + +/* ---------------------------------------------- + * Generated by Animista on 2024-3-12 10:44:35 + * Licensed under FreeBSD License. + * See http://animista.net/license for more info. + * w: http://animista.net, t: @cssanimista + * ---------------------------------------------- */ + +/** + * ---------------------------------------- + * animation scale-in-top + * ---------------------------------------- + */ + @-webkit-keyframes scale-in-top { + 0% { + -webkit-transform: scale(0); + transform: scale(0); + -webkit-transform-origin: 50% 0%; + transform-origin: 50% 0%; + opacity: 1; + } + 100% { + -webkit-transform: scale(1); + transform: scale(1); + -webkit-transform-origin: 50% 0%; + transform-origin: 50% 0%; + opacity: 1; + } +} +@keyframes scale-in-top { + 0% { + -webkit-transform: scale(0); + transform: scale(0); + -webkit-transform-origin: 50% 0%; + transform-origin: 50% 0%; + opacity: 1; + } + 100% { + -webkit-transform: scale(1); + transform: scale(1); + -webkit-transform-origin: 50% 0%; + transform-origin: 50% 0%; + opacity: 1; + } +} + + +.scale-in-right { + -webkit-animation: scale-in-right 0.5s cubic-bezier(0.250, 0.460, 0.450, 0.940) both; + animation: scale-in-right 0.5s cubic-bezier(0.250, 0.460, 0.450, 0.940) both; +} + +/* ---------------------------------------------- + * Generated by Animista on 2024-3-12 10:44:58 + * Licensed under FreeBSD License. + * See http://animista.net/license for more info. + * w: http://animista.net, t: @cssanimista + * ---------------------------------------------- */ + +/** + * ---------------------------------------- + * animation scale-in-right + * ---------------------------------------- + */ + @-webkit-keyframes scale-in-right { + 0% { + -webkit-transform: scale(0); + transform: scale(0); + -webkit-transform-origin: 100% 50%; + transform-origin: 100% 50%; + opacity: 1; + } + 100% { + -webkit-transform: scale(1); + transform: scale(1); + -webkit-transform-origin: 100% 50%; + transform-origin: 100% 50%; + opacity: 1; + } +} +@keyframes scale-in-right { + 0% { + -webkit-transform: scale(0); + transform: scale(0); + -webkit-transform-origin: 100% 50%; + transform-origin: 100% 50%; + opacity: 1; + } + 100% { + -webkit-transform: scale(1); + transform: scale(1); + -webkit-transform-origin: 100% 50%; + transform-origin: 100% 50%; + opacity: 1; + } +} + + +.scale-in-bottom { + -webkit-animation: scale-in-bottom 0.5s cubic-bezier(0.250, 0.460, 0.450, 0.940) both; + animation: scale-in-bottom 0.5s cubic-bezier(0.250, 0.460, 0.450, 0.940) both; +} + +/* ---------------------------------------------- + * Generated by Animista on 2024-3-12 10:45:14 + * Licensed under FreeBSD License. + * See http://animista.net/license for more info. + * w: http://animista.net, t: @cssanimista + * ---------------------------------------------- */ + +/** + * ---------------------------------------- + * animation scale-in-bottom + * ---------------------------------------- + */ + @-webkit-keyframes scale-in-bottom { + 0% { + -webkit-transform: scale(0); + transform: scale(0); + -webkit-transform-origin: 50% 100%; + transform-origin: 50% 100%; + opacity: 1; + } + 100% { + -webkit-transform: scale(1); + transform: scale(1); + -webkit-transform-origin: 50% 100%; + transform-origin: 50% 100%; + opacity: 1; + } +} +@keyframes scale-in-bottom { + 0% { + -webkit-transform: scale(0); + transform: scale(0); + -webkit-transform-origin: 50% 100%; + transform-origin: 50% 100%; + opacity: 1; + } + 100% { + -webkit-transform: scale(1); + transform: scale(1); + -webkit-transform-origin: 50% 100%; + transform-origin: 50% 100%; + opacity: 1; + } +} + + +.scale-in-left { + -webkit-animation: scale-in-left 0.5s cubic-bezier(0.250, 0.460, 0.450, 0.940) both; + animation: scale-in-left 0.5s cubic-bezier(0.250, 0.460, 0.450, 0.940) both; +} + +/* ---------------------------------------------- + * Generated by Animista on 2024-3-12 10:45:31 + * Licensed under FreeBSD License. + * See http://animista.net/license for more info. + * w: http://animista.net, t: @cssanimista + * ---------------------------------------------- */ + +/** + * ---------------------------------------- + * animation scale-in-left + * ---------------------------------------- + */ + @-webkit-keyframes scale-in-left { + 0% { + -webkit-transform: scale(0); + transform: scale(0); + -webkit-transform-origin: 0% 50%; + transform-origin: 0% 50%; + opacity: 1; + } + 100% { + -webkit-transform: scale(1); + transform: scale(1); + -webkit-transform-origin: 0% 50%; + transform-origin: 0% 50%; + opacity: 1; + } +} +@keyframes scale-in-left { + 0% { + -webkit-transform: scale(0); + transform: scale(0); + -webkit-transform-origin: 0% 50%; + transform-origin: 0% 50%; + opacity: 1; + } + 100% { + -webkit-transform: scale(1); + transform: scale(1); + -webkit-transform-origin: 0% 50%; + transform-origin: 0% 50%; + opacity: 1; + } +}