2022-12-27 04:50:11 +00:00
import { isArray } from "@vue/shared" ;
import ClickableComponent from "features/clickables/Clickable.vue" ;
import {
Component ,
findFeatures ,
GatherProps ,
GenericComponent ,
getUniqueID ,
jsx ,
JSXFunction ,
OptionsFunc ,
Replace ,
setDefault ,
StyleValue ,
Visibility
} from "features/feature" ;
import { globalBus } from "game/events" ;
import { persistent } from "game/persistence" ;
import Decimal , { DecimalSource } from "lib/break_eternity" ;
import { Unsubscribe } from "nanoevents" ;
import { Direction } from "util/common" ;
import type {
Computable ,
GetComputableType ,
GetComputableTypeWithDefault ,
ProcessedComputable
} from "util/computed" ;
import { processComputable } from "util/computed" ;
import { createLazyProxy } from "util/proxies" ;
import { coerceComponent , isCoercableComponent , render } from "util/vue" ;
import { computed , Ref , ref , unref } from "vue" ;
import { BarOptions , createBar , GenericBar } from "./bars/bar" ;
import { ClickableOptions } from "./clickables/clickable" ;
2023-04-04 05:02:23 +00:00
/** A symbol used to identify {@link Action} features. */
2022-12-27 04:50:11 +00:00
export const ActionType = Symbol ( "Action" ) ;
2023-04-04 05:02:23 +00:00
/ * *
* An object that configures a { @link Action } .
* /
2022-12-27 04:50:11 +00:00
export interface ActionOptions extends Omit < ClickableOptions , " onClick " | " onHold " > {
2023-04-04 05:02:23 +00:00
/** The cooldown during which the action cannot be performed again, in seconds. */
2022-12-27 04:50:11 +00:00
duration : Computable < DecimalSource > ;
2023-04-04 05:02:23 +00:00
/** Whether or not the action should perform automatically when the cooldown is finished. */
2022-12-27 04:50:11 +00:00
autoStart? : Computable < boolean > ;
2023-04-04 05:02:23 +00:00
/** A function that is called when the action is clicked. */
2022-12-27 04:50:11 +00:00
onClick : ( amount : DecimalSource ) = > void ;
2023-04-04 05:02:23 +00:00
/** A pass-through to the {@link Bar} used to display the cooldown progress for the action. */
2022-12-27 04:50:11 +00:00
barOptions? : Partial < BarOptions > ;
}
2023-04-04 05:02:23 +00:00
/ * *
* The properties that are added onto a processed { @link ActionOptions } to create an { @link Action } .
* /
2022-12-27 04:50:11 +00:00
export interface BaseAction {
2023-04-04 05:02:23 +00:00
/** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */
2022-12-27 04:50:11 +00:00
id : string ;
2023-04-04 05:02:23 +00:00
/** A symbol that helps identify features of the same type. */
2022-12-27 04:50:11 +00:00
type : typeof ActionType ;
2023-04-04 05:02:23 +00:00
/** Whether or not the player is holding down the action. Actions will be considered clicked as soon as the cooldown completes when being held down. */
2022-12-27 04:50:11 +00:00
isHolding : Ref < boolean > ;
2023-04-04 05:02:23 +00:00
/** The current amount of progress through the cooldown. */
2022-12-27 04:50:11 +00:00
progress : Ref < DecimalSource > ;
2023-04-04 05:02:23 +00:00
/** The bar used to display the current cooldown progress. */
2022-12-27 04:50:11 +00:00
progressBar : GenericBar ;
2023-04-04 05:02:23 +00:00
/** Update the cooldown the specified number of seconds */
2022-12-27 04:50:11 +00:00
update : ( diff : number ) = > void ;
2023-04-04 05:02:23 +00:00
/** The Vue component used to render this feature. */
2022-12-27 04:50:11 +00:00
[ Component ] : GenericComponent ;
2023-04-04 05:02:23 +00:00
/** A function to gather the props the vue component requires for this feature. */
2022-12-27 04:50:11 +00:00
[ GatherProps ] : ( ) = > Record < string , unknown > ;
}
2023-04-04 05:02:23 +00:00
/** An object that represens a feature that can be clicked upon, and then have a cooldown before they can be clicked again. */
2022-12-27 04:50:11 +00:00
export type Action < T extends ActionOptions > = Replace <
T & BaseAction ,
{
duration : GetComputableType < T [ " duration " ] > ;
autoStart : GetComputableTypeWithDefault < T [ " autoStart " ] , false > ;
visibility : GetComputableTypeWithDefault < T [ " visibility " ] , Visibility.Visible > ;
canClick : GetComputableTypeWithDefault < T [ " canClick " ] , true > ;
classes : GetComputableType < T [ " classes " ] > ;
style : GetComputableType < T [ " style " ] > ;
mark : GetComputableType < T [ " mark " ] > ;
display : JSXFunction ;
onClick : VoidFunction ;
}
> ;
2023-04-04 05:02:23 +00:00
/** A type that matches any valid {@link Action} object. */
2022-12-27 04:50:11 +00:00
export type GenericAction = Replace <
Action < ActionOptions > ,
{
autoStart : ProcessedComputable < boolean > ;
2023-02-16 02:00:36 +00:00
visibility : ProcessedComputable < Visibility | boolean > ;
2022-12-27 04:50:11 +00:00
canClick : ProcessedComputable < boolean > ;
}
> ;
2023-04-04 05:02:23 +00:00
/ * *
* Lazily creates an action with the given options .
* @param optionsFunc Action options .
* /
2022-12-27 04:50:11 +00:00
export function createAction < T extends ActionOptions > (
optionsFunc? : OptionsFunc < T , BaseAction , GenericAction >
) : Action < T > {
const progress = persistent < DecimalSource > ( 0 ) ;
return createLazyProxy ( ( ) = > {
const action = optionsFunc ? . ( ) ? ? ( { } as ReturnType < NonNullable < typeof optionsFunc > > ) ;
action . id = getUniqueID ( "action-" ) ;
action . type = ActionType ;
action [ Component ] = ClickableComponent as GenericComponent ;
// Required because of display changing types
const genericAction = action as unknown as GenericAction ;
action . isHolding = ref ( false ) ;
action . progress = progress ;
processComputable ( action as T , "visibility" ) ;
setDefault ( action , "visibility" , Visibility . Visible ) ;
processComputable ( action as T , "duration" ) ;
processComputable ( action as T , "autoStart" ) ;
setDefault ( action , "autoStart" , false ) ;
processComputable ( action as T , "canClick" ) ;
setDefault ( action , "canClick" , true ) ;
processComputable ( action as T , "classes" ) ;
processComputable ( action as T , "style" ) ;
processComputable ( action as T , "mark" ) ;
processComputable ( action as T , "display" ) ;
const style = action . style as ProcessedComputable < StyleValue | undefined > ;
action . style = computed ( ( ) = > {
const currStyle : StyleValue [ ] = [
{
cursor : Decimal.gte (
progress . value ,
unref ( action . duration as ProcessedComputable < DecimalSource > )
)
? "pointer"
: "progress" ,
display : "flex" ,
flexDirection : "column"
}
] ;
const originalStyle = unref ( style ) ;
if ( isArray ( originalStyle ) ) {
currStyle . push ( . . . originalStyle ) ;
} else if ( originalStyle != null ) {
currStyle . push ( originalStyle ) ;
}
return currStyle as StyleValue ;
} ) ;
action . progressBar = createBar ( ( ) = > ( {
direction : Direction.Right ,
width : 100 ,
height : 10 ,
style : "margin-top: 8px" ,
borderStyle : "border-color: black" ,
baseStyle : "margin-top: -1px" ,
progress : ( ) = > Decimal . div ( progress . value , unref ( genericAction . duration ) ) ,
. . . action . barOptions
} ) ) ;
const canClick = action . canClick as ProcessedComputable < boolean > ;
action . canClick = computed (
( ) = >
unref ( canClick ) &&
Decimal . gte (
progress . value ,
unref ( action . duration as ProcessedComputable < DecimalSource > )
)
) ;
const display = action . display as GetComputableType < ClickableOptions [ " display " ] > ;
action . display = jsx ( ( ) = > {
const currDisplay = unref ( display ) ;
let Comp : GenericComponent | undefined ;
if ( isCoercableComponent ( currDisplay ) ) {
Comp = coerceComponent ( currDisplay ) ;
} else if ( currDisplay != null ) {
const Title = coerceComponent ( currDisplay . title ? ? "" , "h3" ) ;
const Description = coerceComponent ( currDisplay . description , "div" ) ;
Comp = coerceComponent (
jsx ( ( ) = > (
< span >
{ currDisplay . title != null ? (
< div >
< Title / >
< / div >
) : null }
< Description / >
< / span >
) )
) ;
}
return (
< >
< div style = "flex-grow: 1" / >
{ Comp == null ? null : < Comp / > }
< div style = "flex-grow: 1" / >
{ render ( genericAction . progressBar ) }
< / >
) ;
} ) ;
const onClick = action . onClick . bind ( action ) ;
action . onClick = function ( ) {
if ( unref ( action . canClick ) === false ) {
return ;
}
const amount = Decimal . div ( progress . value , unref ( genericAction . duration ) ) ;
onClick ? . ( amount ) ;
progress . value = 0 ;
} ;
action . update = function ( diff ) {
const duration = unref ( genericAction . duration ) ;
if ( Decimal . gte ( progress . value , duration ) ) {
progress . value = duration ;
} else {
progress . value = Decimal . add ( progress . value , diff ) ;
if ( genericAction . isHolding . value || unref ( genericAction . autoStart ) ) {
genericAction . onClick ( ) ;
}
}
} ;
action [ GatherProps ] = function ( this : GenericAction ) {
const {
display ,
visibility ,
style ,
classes ,
onClick ,
isHolding ,
canClick ,
small ,
mark ,
id
} = this ;
return {
display ,
visibility ,
style : unref ( style ) ,
classes ,
onClick ,
isHolding ,
canClick ,
small ,
mark ,
id
} ;
} ;
return action as unknown as Action < T > ;
} ) ;
}
const listeners : Record < string , Unsubscribe | undefined > = { } ;
globalBus . on ( "addLayer" , layer = > {
const actions : GenericAction [ ] = findFeatures ( layer , ActionType ) as GenericAction [ ] ;
listeners [ layer . id ] = layer . on ( "postUpdate" , diff = > {
actions . forEach ( action = > action . update ( diff ) ) ;
} ) ;
} ) ;
globalBus . on ( "removeLayer" , layer = > {
// unsubscribe from postUpdate
listeners [ layer . id ] ? . ( ) ;
listeners [ layer . id ] = undefined ;
} ) ;