2024-08-18 01:12:10 +00:00
// Take a look at the .d.ts for comments.
export const version = '107.13.0'
const W = window , D = W . document , /**fuck money*/ $ = undefined , /**@type {''}*/ $S = '' , /**@type {{}}*/ $O = { } , /**@type {Set<string>}*/ $Ss = new Set ( ) // /**@type {readonly []}*/$A = [],
const /**@type {TextDictionaryEntry}*/ MISSING _PLAYER _DIALOG = { Tag : 'MISSING TEXT IN "Interface.csv": ' , Text : '\u200C' } // Zero-width non-joiner, used to break up ligatures, does nothing here, but an empty string is a falsey value
const /**@type {FP.cur}*/ cur = ( v , f ) => f ( v )
const /**@type {FP.n2u}*/ n2u = v => v === null ? $ : v
const /**@type {FP.enu}*/ enu = o => Object . keys ( o )
const /**@type {FP.val}*/ val = ( v , f ) => v === $ || v === null ? $ : f ( v )
const /**@type {FP.add}*/ add = ( x , y ) => x + y
const /**@type {FP.sub}*/ sub = ( x , y ) => x - y
const /**@type {FP.cgt}*/ cgt = ( x , y ) => x > y
const /**@type {FP.cge}*/ cge = ( x , y ) => x >= y
const /**@type {FP.clt}*/ clt = ( x , y ) => x < y
const /**@type {FP.cle}*/ cle = ( x , y ) => x <= y
const /**@type {FP.int}*/ int = s => cur ( Number . parseInt ( s , 10 ) , n => Number . isNaN ( n ) ? $ : n )
/**@type {FP.fun}*/ const fun = f => typeof f === 'function'
const /**@type {FP.run}*/ run = ( fs , ... args ) => { fs . forEach ( f => fun ( f ) && void f ( ... args ) ) ; return true }
const /**@type {FP.yes}*/ yes = ( ... args ) => run ( args , $ )
const /**@type {FP.mut}*/ mut = ( v , ... args ) => run ( args , v ) && v
const /**@type {FP.del}*/ del = ( o , p ) => mut ( o , Reflect . deleteProperty ( o , p ) )
const /**@type {FP.ass}*/ ass = ( x , v , c = Boolean ) => { if ( v === $ || ! c ( v ) ) throw new Error ( x ) ; return v }
const /**@type {FP.asa}*/ asa = ( p , v ) => v instanceof p ? v : $
const /**@type {FP.rsc}*/ rsc = ( f , r ) => { try { f ( $ ) } catch ( x ) { r ( x ) } return true }
const /**@type {FP.Interval}*/ range = new ( class { proxy = new Proxy ( $O , this ) ; min = 0 ; max = 0 ; mini = false ; maxi = false
/**@type {FP.Interval['has']}*/ has = ( _ , x ) => val ( int ( x ) , x => ( this . mini ? cge : cgt ) ( x , this . min ) && ( this . maxi ? cle : clt ) ( x , this . max ) ) ? ? false
} ) ( )
const /**@type {FP.rng}*/ rng = ( min , max , mini = true , maxi = true ) => mut ( range . proxy , Object . assign ( range , { min , max , mini , maxi } ) )
const /**@type {FP.m_t}*/ m _t = v => { if ( typeof v === 'string' ) return v . length === 0
if ( v === $ || typeof v === 'object' ) return v === $ || v === null
|| [ 'length' , 'size' ] . some ( n => n in v && cur ( /**@type {OBJ}*/ ( v ) [ n ] , x => typeof x === 'number' && x === 0 ) )
|| ( Object . getPrototypeOf ( v ) === Object . prototype && Reflect . ownKeys ( v ) . length === 0 )
return typeof v === 'boolean' && ! v
}
2024-08-24 03:31:50 +00:00
//const/**@type {FP.loo}*/loo = (m, c, a) => {let r; for (let n = 0; c($) && n < m; n++) r = a($); return r}
2024-08-18 01:12:10 +00:00
/**@template T*/ const Pipe = /**@implements {FP.Pipeline}*/ class PipeClass {
/**@type {FP.Pipeline<T>['proxy']}*/ proxy = new Proxy ( /**@type {this & Record<number, T | undefined>}*/ ( this ) , { get ( t , n , r ) { return cur ( typeof n === 'string' && int ( n ) , i => {
if ( typeof i !== 'number' || i < 0 ) return Reflect . get ( t , n , r )
let c = 0 ; for ( const v of t ) if ( ++ c > i ) return v ; return $
} ) } } )
constructor ( /**@type {Iterable<T>}*/ iterable ) { this . iterable = iterable }
/**@type {FP.Pipeline<T>['me']}*/ me ( iterable ) { return ( new PipeClass ( iterable ) ) . proxy }
[ Symbol . iterator ] ( ) { return this . iterable [ Symbol . iterator ] ( ) }
/**@type {FP.Pipeline<T>['rdc']}*/ rdc ( i , f ) { let ax = i ; for ( const v of this ) ax = f ( ax , v ) ; return ax }
/**@type {FP.Pipeline<T>['any']}*/ any ( f ) { for ( const v of this ) if ( f ( v ) ) return true ; return false }
/**@type {FP.Pipeline<T>['all']}*/ all ( f ) { for ( const v of this ) if ( ! f ( v ) ) return false ; return true }
/**@type {FP.Pipeline<T>['map']}*/ map ( f ) { return this . me ( ( function * ( i , f ) { for ( const v of i ) yield f ( v ) } ) ( this , f ) ) }
/**@type {FP.Pipeline<T>['sel']}*/ sel ( f ) { return this . me ( ( function * ( i , f ) { for ( const v of i ) if ( f ( v ) ) yield v } ) ( this , f ) ) }
}
const /**@type {FP.P}*/ P = I => ( new Pipe ( I ) ) . proxy
const /**@type {Cons.Wrap}*/ CW = new ( class {
/**@type {Cons.MS}*/ ms = { w : 'warn' , i : 'info' , d : 'debug' , l : 'log' } ; w = this . gen ( 'w' ) ; i = this . gen ( 'i' ) ; d = this . gen ( 'd' ) ; l = this . gen ( 'l' )
/**@type {Cons.E}*/ e = x => yes ( void console . error ( x ) )
/**@type {Cons.Wrap['gen']}*/ gen ( m ) { return msg => yes ( void console [ this . ms [ m ] ] ( ` MBCHC: ${ msg } ` ) ) }
} ) ( )
2024-08-24 03:31:50 +00:00
const /**@type {Settings.Methods}*/ Settings = { // FIXME separate a proper V1 type from an unknown object in the ExtensionSettings
2024-08-18 01:12:10 +00:00
/**I hate change*/ migrate _0 _1 ( v0 ) { if ( v0 . MBCHC === $ ) return true
val ( v0 . MBCHC . timezones , tz => this . save ( v1 => v1 . TZ = { ... tz , ... v1 . TZ } ) )
W . ServerAccountUpdate . QueueData ( { OnlineSettings : del ( v0 , 'MBCHC' ) } )
return CW . w ( 'MBCHC: settings migration done (v0 -> v1). This should never happen again on the same account.' )
} ,
save ( f = $ ) { W . Player . ExtensionSettings [ 'MBCHC' ] || = { } ; f ? . ( this . v1 ) ; return yes ( void W . ServerPlayerExtensionSettingsSync ( 'MBCHC' ) ) } ,
replace : v1 => yes ( void ( W . Player . ExtensionSettings [ 'MBCHC' ] = v1 ) , void Settings . save ( ) ) ,
'purge!' : ( ) => Settings . replace ( /** @type {Settings.V1} */ ( { } ) ) ,
get v0 ( ) { return Reflect . get ( W . Player , 'OnlineSettings' ) } ,
get v1 ( ) { return mut ( /**@type {Settings.V1}*/ ( W . Player . ExtensionSettings [ 'MBCHC' ] ) ? ? { } , v1 => { // we need to check and repair the whole object every time we access it
v1 . TZ || = { }
} ) } ,
}
2024-07-13 21:55:45 +00:00
2024-08-18 01:12:10 +00:00
const /**@type {TZ_Cache}*/ TZ = { map : new Map ( ) , RE : /(?:gmt|utc)\s*([+-])\s*(\d\d?)/i ,
parse : desc => val ( desc , v => val ( TZ . RE . exec ( v ) , m => val ( int ( ` ${ m [ 1 ] ? ? $S } ${ m [ 2 ] ? ? $S } ` ) , n => n in rng ( - 12 , 12 ) ? n : $ ) ) ) ,
memo : ( cid , desc = $ ) => val ( Settings . v1 . TZ [ cid ] ? ? TZ . parse ( desc ) , n => mut ( n , TZ . map . set ( cid , n ) ) ) ,
lookup : c => val ( U . cid ( c ) , cid => TZ . map . get ( cid ) ? ? TZ . memo ( cid , c . Description ) ) ,
2024-07-13 21:55:45 +00:00
}
2024-08-18 01:12:10 +00:00
const /**@type {Utils}*/ U = { remove _loader _hook : $ , RGB : { Polly : '#81b1e7' , Mute : '#6c2132' } , ACT : ` ${ CommandsKey } activity ` ,
RE : { SPACES : /\s+/gu , REL : { L : /^<+$/ , R : /^>+$/ } , '@' : [ /^@/ , /^@@/ ] } ,
get crc ( ) { return W . ChatRoomCharacter } ,
get ic ( ) { return asa ( HTMLTextAreaElement , D . querySelector ( '#InputChat' ) ) } ,
cid : c => c . MemberNumber ,
dn : c => W . CharacterNickname ( c ) ,
2024-07-13 21:55:45 +00:00
current : ( ) => ` ${ W . CurrentModule } / ${ W . CurrentScreen } ` ,
2024-08-18 01:12:10 +00:00
style : ( q , f ) => cur ( D . querySelector ( q ) , e => e instanceof HTMLElement ? f ( e . style ) : $ ) ,
inform : html => yes ( void W . ChatRoomSendLocal ( ` <div class="mbchc"> ${ html } </div> ` ) ) ,
report : x => U . inform ( String ( x ) ) && CW . e ( x ) ,
split : text => text . split ( U . RE . SPACES ) ,
abs2char : pos => ass ( ` invalid position ${ pos } ` , U . crc [ pos ] ) ,
rel2char : t => cur ( ass ( 'can\'t find my position' , U . crc . findIndex ( char => char . IsPlayer ( ) ) , n => n >= 0 ) , me =>
cur ( ass ( ` failed to parse target " ${ t } " ` , U . RE . REL . L . test ( t ) ? sub : U . RE . REL . R . test ( t ) ? add : $ ) ( me , t . length ) , pos =>
cur ( pos % U . crc . length , p => U . abs2char ( p < 0 ? p + U . crc . length : p ) ) ) ) ,
cid2char : id => id === U . cid ( W . Player ) ? W . Player : ass ( ` character ${ id } not found in the room ` , U . crc . find ( c => id === U . cid ( c ) ) ) ,
2024-08-24 03:31:50 +00:00
target2char ( target ) { let t = target . trim ( ) ; const /**@type {Set<Character>}*/ f = new Set ( ) // FIXME Target should be low case (take a look at this later)
2024-08-18 01:12:10 +00:00
if ( m _t ( t ) ) return W . Player
if ( t . at ( 0 ) === '=' ) return U . cid2char ( ass ( ` invalid member number " ${ target } " ` , int ( t . slice ( 1 ) ) ) )
if ( '<>' . includes ( t . at ( 0 ) ? ? '-' ) ) return U . rel2char ( t )
const n = int ( t )
if ( n !== $ && n . toString ( ) === t ) { // We got a number
if ( n in rng ( 0 , 9 ) ) return U . abs2char ( n )
if ( n in rng ( 11 , 15 ) ) return U . abs2char ( n - 11 )
if ( n in rng ( 21 , 25 ) ) return U . abs2char ( n - 16 )
U . crc . filter ( c => U . cid ( c ) ? . toString ( ) . includes ( t ) ) . forEach ( c => f . add ( c ) ) // or union with `new Set(array)`
}
2024-07-13 21:55:45 +00:00
2024-08-18 01:12:10 +00:00
if ( t . at ( 0 ) === '@' ) t = t . slice ( 1 )
U . crc . filter ( c => c . Name . toLocaleLowerCase ( ) . includes ( t ) ) . forEach ( c => f . add ( c ) )
U . crc . filter ( c => val ( c . Nickname , nn => nn . toLocaleLowerCase ( ) . includes ( t ) ) ) . forEach ( c => f . add ( c ) )
const found = [ ... f . keys ( ) ]
ass ( ` target " ${ target } ": multiple matches ( ${ found . map ( c => ` ${ U . cid ( c ) } | ${ c . Name } | ${ c . Nickname ? ? c . Name } ` ) . join ( ',' ) } ) ` , found . length , n => n < 2 ) // make the list better
return ass ( ` target " ${ target } ": no match ` , found [ 0 ] )
} ,
mkdiv : html => mut ( D . createElement ( 'div' ) , e => html === $ || ( e . innerHTML = html ) ) ,
bell : ( ) => yes ( U . style ( '#InputChat' , s => s . outline = 'solid red' ) , void setTimeout ( ( ) => { U . style ( '#InputChat' , s => s . outline = $S ) } , 100 ) ) ,
targets : ( reject _player = false , check _perms = false ) => mut ( new Set ( ) , r => {
const wrap _text = ( /**@type {string}*/ text ) => int ( text ) === $ ? text : ` @ ${ text } `
const wrap _int = ( /**@type {number}*/ i ) => i in rng ( 0 , 9 ) || i in rng ( 11 , 15 ) || i in rng ( 21 , 25 ) ? ` = ${ i } ` : i . toString ( )
U . crc . filter ( c => ! ( reject _player && c . IsPlayer ( ) ) && ! ( check _perms && ! W . ServerChatRoomGetAllowItem ( W . Player , c ) ) ) . forEach ( c => {
U . split ( c . Name ) . forEach ( t => r . add ( wrap _text ( t ) ) )
val ( c . Nickname , n => U . split ( n ) ) ? . forEach ( t => r . add ( wrap _text ( t ) ) )
val ( U . cid ( c ) , cid => r . add ( wrap _int ( cid ) ) )
} )
r . delete ( $S )
} ) ,
complete _mbchc ( ) { C . icomplete ( ws =>
ws . length < 2 ? new Set ( [ ` ${ CommandsKey } ${ this . Tag } ` ] )
: ws . length < 3 ? new Set ( Object . keys ( SUBCOMMANDS _MBCHC ) ) // FIXME no idea why enu() thinks there's a number in there
: val ( ws [ 1 ] , w => val ( SUBCOMMANDS _MBCHC [ w ] , sub => val ( sub . args , as => val ( enu ( as ) [ ws . length - 3 ] , a =>
a === 'TARGET' ? U . targets ( true ) : a === '[TARGET]' ? U . targets ( ) : $ ) ) ) ) ? ? $Ss
) } ,
complete _do _target ( actions ) {
if ( m _t ( actions ) ) return $Ss
if ( m _t ( actions . others ) ) return val ( U . cid ( W . Player ) , cid => new Set ( [ cid . toString ( ) ] ) ) ? ? $Ss // Target is always the player
return U . targets ( m _t ( actions . self ) , true )
} ,
complete _do ( ) { C . icomplete ( ws => {
if ( ws . length < 2 ) return new Set ( [ ` ${ CommandsKey } ${ this . Tag } ` ] )
// Now, we *could* run a filter to exclude impossible activities, but it isn't very useful, and also seems like a lot of CPU to iterate over every action on every zone of every char in the room
let low = ( ws [ 1 ] ? ? $S ) . toLocaleLowerCase ( )
const DD = W . MBCHC . DO _DATA
if ( ws . length < 3 ) return new Set ( enu ( DD . verbs ) . map ( String ) ) // Complete verb
const ags = DD . verbs [ low ]
if ( ags === $ ) return $Ss
low = ( ws [ 2 ] ? ? $S ) . toLocaleLowerCase ( )
if ( ws . length < 4 ) { // Complete zone or target
if ( enu ( ags ) . length < 2 ) return val ( ags [ enu ( ags ) [ 0 ] ? ? $S ] , t => U . complete _do _target ( t ) ) ? ? $Ss // Zone implied, complete target
return new Set ( Object . entries ( DD . zones ) . filter ( ( [ zone , ag ] ) => zone . startsWith ( low ) && ags [ ag ] ) . map ( ( [ zone , _ag ] ) => zone ) )
}
if ( ws . length < 5 ) { // Complete target where it belongs
if ( enu ( ags ) . length < 2 ) return $Ss // Zone implied, target already given
return val ( ags [ DD . zones [ low ] ? ? $S ] , t => U . complete _do _target ( t ) ) ? ? $Ss
}
return $Ss
2024-07-13 21:55:45 +00:00
} ) } ,
2024-08-18 01:12:10 +00:00
replace _me : ( _ , _ _ , whole ) => cur ( whole . slice ( 1 ) , t => ` ${ U . ACT } < ${ U . cid ( W . Player ) } :>SourceCharacter ${ t . startsWith ( '\'' ) || t . startsWith ( ' ' ) ? $S : ' ' } ` ) ,
2024-08-24 03:31:50 +00:00
//pad_chat: c => loo(100, _ => c.scrollHeight <= c.clientHeight, _ => yes(void c.prepend(U.mkdiv('\u061C')))) && void W.ElementScrollToEnd('TextAreaChatLog')
2024-08-18 01:12:10 +00:00
}
const /**@type {SUBCOMMANDS}*/ SUBCOMMANDS _MBCHC = {
autohack : { desc : 'toggle the autohack feature' , cb : mbchc => void U . inform ( ` Autohack is now ${ ( ( mbchc . AUTOHACK _ENABLED = ! mbchc . AUTOHACK _ENABLED ) ) ? 'enabled' : 'disabled' } ` ) } ,
donate : { desc : 'buy data and send it to recipient' , args : { TARGET : $O } , cb : ( mbchc , args ) => void mbchc . donate _data ( args [ 0 ] ? ? $S ) } ,
tz : { desc : 'set target\'s UTC offset' , args : { OFFSET : $O , '[TARGET]' : $O } , cb : ( mbchc , args ) => mbchc . set _timezone ( args ) } ,
'purge!' : { desc : 'delete MBCHC online saved data' , cb : ( ) => Settings [ 'purge!' ] ( ) } ,
2024-07-13 21:55:45 +00:00
}
2024-08-18 01:12:10 +00:00
const /**@type {Complete}*/ C = { S _OPTS : { behavior : 'instant' } ,
e : mut ( Object . assign ( U . mkdiv ( ) , { id : 'mbchcCompHint' } ) , div => void div . append ( U . mkdiv ( ) ) ) ,
get div ( ) { return C . e . firstElementChild ? ? $ } ,
lcp : cs => m _t ( cs ) ? $S : mut ( { p : '' } , r => void cur ( P ( cs ) , P => { for ( let on = true , i = Math . max ( ... P . map ( o => o . length ) ) ; on && i > 0 ; i -= 1 ) {
cur ( ( P [ 0 ] ? ? $S ) . slice ( 0 , i ) , p => P . all ( o => o . startsWith ( p ) ) && yes ( r . p = p ) && ( on = false ) )
} } ) ) . p ,
complete _word : ( i , cs , ci = false ) => cur ( void cs . forEach ( c => ( ci ? c . toLocaleLowerCase ( ) : c ) . startsWith ( ci ? i . toLocaleLowerCase ( ) : i ) || cs . delete ( c ) ) , _ => cs . size < 2 ? P ( cs ) [ 0 ] ? ? i : C . lcp ( cs ) ) ,
hint : cs => m _t ( cs ) || yes ( _ => {
yes ( C . e . style . display = 'block' ) && C . div ? . replaceChildren ( ... [ ... cs ] . sort ( ) . reverse ( ) . map ( U . mkdiv ) )
W . ElementSetDataAttribute ( C . e . id , 'colortheme' , W . Player . ChatSettings ? . ColorTheme ? ? 'Light' )
cur ( W . ElementIsScrolledToEnd ( 'TextAreaChatLog' ) , r => yes ( void W . ChatRoomResize ( false ) ) && r && void W . ElementScrollToEnd ( 'TextAreaChatLog' ) )
C . div ? . lastElementChild ? . scrollIntoView ( C . S _OPTS )
} ) ,
get hidden ( ) { return C . e . parentElement === null || C . e . style . display === 'none' } ,
hide : ( ) => C . hidden || yes ( C . e . style . display = 'none' , void W . ChatRoomResize ( false ) ) ,
complete ( f , ci = false ) {
const e = ass ( 'Somehow #InputChat is broken' , U . ic )
if ( e . selectionStart !== e . selectionEnd ) return U . bell ( )
const input = e . value
const before _cursor = input . slice ( 0 , e . selectionStart )
const words = U . split ( before _cursor )
const cs = f ( words )
const word = words . at ( - 1 ) ? ? ''
const cword = C . complete _word ( word , cs , ci )
const additions = cword . slice ( word . length )
if ( ! m _t ( additions ) ) e . setRangeText ( additions , e . selectionStart , e . selectionEnd , 'end' )
if ( cs . size > 1 ) C . hint ( cs )
if ( cs . size === 1 ) e . setRangeText ( ' ' , e . selectionStart , e . selectionEnd , 'end' )
if ( cs . size === 0 ) U . bell ( )
return true
} ,
icomplete : f => C . complete ( f , true ) ,
2024-07-13 21:55:45 +00:00
}
2024-08-18 01:12:10 +00:00
const /**@type {InputHistory}*/ H = { input : undefined , ids : undefined , bottom : undefined , // FIXME ids don't need to be a set, but I'm too tired right now
enter : ( ic , i , b , is ) => yes ( H . input = i , H . bottom = b , H . ids = is , ic . readOnly = true , val ( ic . parentElement ? . parentElement ? . dataset , d => d [ 'mbchcMode' ] = 'h' ) , b && void W . ElementScrollToEnd ( 'TextAreaChatLog' ) ) ,
2024-08-22 18:13:04 +00:00
exit : ( ic , r ) => yes ( ic . readOnly = false , val ( ic . parentElement ? . parentElement ? . dataset , d => del ( d , 'mbchcMode' ) ) , r && val ( H . input , i => ic . value = i ) , val ( H . bottom , b => b && void W . ElementScrollToEnd ( 'TextAreaChatLog' ) ) , W . ChatRoomLastMessageIndex = W . ChatRoomLastMessage . length )
2024-08-18 01:12:10 +00:00
}
ass ( 'MBCHC found, aborting loading' , W . MBCHC === $ )
ass ( 'AsylumGGTSSAddItems() not found, aborting MBCHC loading' , W . AsylumGGTSSAddItems )
const sdk = ass ( 'SDK not found, please load with (or after) FUSAM or any other mod that uses SDK' , W . bcModSdk )
const mod = sdk . registerMod ( { name : 'MBCHC' , fullName : 'Mute\'s Bondage Club Hacks Collection' , version , repository : 'https://code.fleshless.org/mute/MBCHC/' } )
const /**@type {SDK.Hook}*/ prior = ( name , f ) => mod . hookFunction ( name , 0 , ( na , n ) => rsc ( _ => f ( ... na ) , CW . e ) && n ( na ) ) // eslint-disable-line @typescript-eslint/no-unsafe-return
const /**@type {SDK.Hook}*/ after = ( name , f ) => mod . hookFunction ( name , 0 , ( na , n ) => mut ( n ( na ) , rsc ( _ => f ( ... na ) , CW . e ) ) ) // eslint-disable-line @typescript-eslint/no-unsafe-return
// ^ type-safe, new and much improved, but still work in progress
2024-07-13 21:55:45 +00:00
// =================================================================================
2024-08-18 01:12:10 +00:00
// v legacy mess, also type-safe now?
/**@type {Window['MBCHC']}*/ W . MBCHC = {
version , /** Just in case someone used it for anything @deprecated*/ VERSION : version ,
Settings , TZ ,
SUBCOMMANDS _MBCHC , H , U , // debug, will go away
2024-07-13 21:55:45 +00:00
2023-12-17 03:52:43 +00:00
NEXT _MESSAGE : 1 ,
LOG _MESSAGES : false ,
LOADED : false ,
AUTOHACK _ENABLED : false ,
2024-08-18 01:12:10 +00:00
/**@type {number | undefined}*/ LAST _HACKED : $ ,
//HISTORY_MODE: false,
2023-12-17 03:52:43 +00:00
RE _PREF _ACTIVITY _ME : /^@/ ,
RE _PREF _ACTIVITY : /^@@/ ,
RE _ACT _CIDS : /^<(\d+)?:(\d+)?>/ ,
RE _LAST _WORD : /(^|\s)(\S*)$/ ,
RE _LAST _LETTER : /\w$/ ,
2024-08-18 01:12:10 +00:00
RE _ACTIVITY : new RegExp ( ` ^ ${ CommandsKey } activity ` ) ,
//PREF_ACTIVITY: `${CommandsKey}activity `,
2023-12-17 03:52:43 +00:00
UTC _OFFSET : new Date ( ) . getTimezoneOffset ( ) * 60 * 1000 ,
MAP _ACTIONS : { // ActivityFemale3DCG
2024-08-18 01:12:10 +00:00
// Action
2023-12-17 03:52:43 +00:00
'nod|yes' : { Head : { self : 'Nod' } } ,
no : { Head : { self : 'Wiggle' } } ,
moan : { Mouth : { self : 'MoanGag' } } ,
mumble : { Mouth : { self : 'MoanGagTalk' } } ,
whimper : { Mouth : { self : 'MoanGagWhimper' } } ,
groan : { Mouth : { self : 'MoanGagGroan' } } ,
scream : { Mouth : { self : 'MoanGagAngry' } } ,
giggle : { Mouth : { self : 'MoanGagGiggle' } } ,
struggle : { Arms : { self : 'StruggleArms' } } ,
thrash : { Legs : { self : 'StruggleLegs' } } ,
// Action zone
'wiggle|shake' : { 'Arms,Breast,Boots,Butt,Ears,Feet,Hands,Nose,Pelvis,Torso' : { self : 'Wiggle' } } ,
// Action target
whisper : { Ears : { others : 'Whisper' } } ,
choke : { Neck : { all : 'Choke' } } ,
brush : { Head : { all : 'TakeCare' } } ,
french : { Mouth : { others : 'FrenchKiss' } } ,
sit : { Legs : { others : 'Sit' } } ,
rim : { Butt : { others : 'MasturbateTongue' } } ,
press : { Butt : { others : 'Step' } } ,
rest : { Torso : { others : 'Step' } } ,
pet : { Head : { all : 'Pet' } } ,
boop : { Nose : { all : 'Pet' } } ,
cuddle : { Arms : { others : 'Cuddle' } } ,
nuzzle : { Nose : { others : 'Cuddle' } } ,
grab : { Arms : { others : 'Grope' } } ,
clean : { Mouth : { all : 'Caress' } } ,
lap : { Legs : { others : 'RestHead' } } ,
lean : { Breast : { others : 'RestHead' } } ,
peck : { Mouth : { others : 'PoliteKiss' } } ,
// Action zone target
item : {
'Breast,Butt,Feet,Legs' : { all : 'SpankItem|TickleItem|RubItem|RollItem|MasturbateItem|PourItem|ShockItem|Inject' } ,
'Nipples,Pelvis' : { all : 'SpankItem|TickleItem|RubItem|RollItem|MasturbateItem|PourItem|ShockItem' } ,
Arms : { all : 'SpankItem|TickleItem|RubItem|RollItem|PourItem|ShockItem|Inject' } ,
Boots : { all : 'SpankItem|TickleItem|RubItem|RollItem|PourItem|ShockItem' } ,
'Ears,Mouth' : { all : 'TickleItem|RubItem|RollItem' } ,
'Hood,Nose' : { all : 'TickleItem|RubItem' } ,
Neck : { all : 'TickleItem|RubItem|RollItem|Inject' } ,
Torso : { all : 'SpankItem|TickleItem|RubItem|RollItem|PourItem|ShockItem' } ,
Vulva : { all : 'SpankItem|TickleItem|RubItem|MasturbateItem|ShockItem' } ,
VulvaPiercings : { all : 'SpankItem|TickleItem|RubItem|MasturbateItem|ShockItem|Inject' } ,
} ,
kiss : {
Mouth : { others : 'GagKiss|Kiss|GaggedKiss' } ,
'Boots,Hands' : { self : 'PoliteKiss' , others : 'PoliteKiss|GaggedKiss' } ,
'Arms,Breast,Nipples' : { self : 'Kiss' , others : 'Kiss|GaggedKiss' } ,
'Butt,Ears,Feet,Head,Legs,Neck,Nose,Pelvis,Torso,Vulva,VulvaPiercings' : { others : 'Kiss|GaggedKiss' } ,
} ,
smooch : { 'Hands,Boots' : { all : 'Kiss' } } ,
'nibble|chew' : { 'Arms,Hands,Boots,Mouth,Nipples' : { all : 'Nibble' } , 'Ears,Feet,Legs,Neck,Nose,Pelvis,Torse,Vulva,VulvaPiercings' : { others : 'Nibble' } } ,
'slap|spank' : { 'Head,Breast,Vulva,VulvaPiercings' : { all : 'Slap' } , 'Arms,Boots,Butt,Feet,Hands,Legs,Pelvis,Torso' : { all : 'Spank' } } ,
tickle : { 'Arms,Boots,Breast,Feet,Legs,Neck,Pelvis,Torso' : { all : 'Tickle' } } ,
massage : { 'Arms,Boots,Feet,Legs,Neck,Pelvis,Torso' : { all : 'MassageHands' } } ,
lick : { 'Arms,Boots,Breast,Hands,Mouth,Nipples' : { all : 'Lick' } , 'Ears,Feet,Legs,Neck,Nose,Pelvis,Torso,Vulva,VulvaPiercings' : { others : 'Lick' } } ,
suck : { 'Nipples,Hands,Boots' : { all : 'Suck' } } ,
bite : { 'Arms,Boots,Feet,Hands,Legs,Mouth' : { all : 'Bite' } , 'Breast,Butt,Ears,Head,Neck,Nipples,Nose,Torso' : { others : 'Bite' } } ,
pinch : { 'Arms,Ears,Nipples,Nose,Pelvis' : { all : 'Pinch' } } ,
clamp : { Mouth : { all : 'HandGag' } , Nose : { all : 'Choke' } } ,
step : { 'Breast,Neck,Pelvis' : { others : 'Step' } } ,
pull : { 'Head,Nose,Nipples' : { all : 'Pull' } } ,
grope : { 'Butt,Breast' : { all : 'Grope' } , 'Feet,Legs,Pelvis' : { others : 'Grope' } } ,
rub : { 'Head,Torso' : { others : 'Rub' } , Nose : { all : 'Rub' } , Legs : { self : 'Wiggle' } , Hands : { self : 'Caress' } } ,
caress : { Hands : { others : 'Caress' } , 'Arms,Breast,Butt,Ears,Feet,Head,Legs,Neck,Nipples,Nose,Pelvis,Torso,Vulva,VulvaPiercings' : { all : 'Caress' } } ,
polish : { 'Hands,Boots' : { all : 'TakeCare' } } ,
foot : { 'Head,Nose' : { others : 'Step' } , 'Torso,Boots' : { others : 'MassageFeet' } , 'Vulva,VulvaPiercings' : { others : 'MasturbateFoot' } } ,
fist : { 'Vulva,Butt' : { all : 'MasturbateFist' } } ,
fuck : { 'Mouth,Vulva,Butt' : { others : 'PenetrateSlow' } } , // Peg?
pound : { 'Mouth,Vulva,Butt' : { others : 'PenetrateFast' } } ,
tongue : { 'Vulva,VulvaPiercings' : { others : 'MasturbateTongue' } } ,
finger : { 'Breast,Butt,Vulva,VulvaPiercings' : { all : 'MasturbateHand' } } ,
} ,
MAP _ZONES : {
ItemBoots : [ 'foot' , 'feet' , 'boot' , 'boots' , 'shoe' , 'shoes' , 'toes' , 'toenails' , 'sole' , 'soles' , 'heel' , 'heels' ] ,
ItemFeet : [ 'leg' , 'legs' , 'ankle' , 'ankles' ] ,
ItemLegs : [ 'hips' , 'hip' , 'thighs' , 'thigh' ] ,
ItemVulva : [ 'vulva' , 'pussy' ] ,
ItemVulvaPiercings : [ 'clit' , 'clitoris' ] ,
ItemButt : [ 'butt' , 'ass' ] ,
ItemPelvis : [ 'tummy' , 'pelvis' ] ,
ItemTorso : [ 'body' , 'torso' , 'back' , 'ribs' ] ,
ItemBreast : [ 'breast' , 'breasts' , 'boob' , 'boobs' , 'booby' , 'boobie' , 'boobies' , 'tit' , 'tits' , 'titty' , 'tittie' , 'titties' ] ,
ItemNipples : [ 'nip' , 'nips' , 'nipple' , 'nipples' ] ,
ItemHands : [ 'hand' , 'hands' , 'fingers' , 'fingernails' , 'nails' ] ,
ItemArms : [ 'arm' , 'arms' , 'elbow' , 'elbows' ] ,
ItemNeck : [ 'neck' ] ,
ItemMouth : [ 'mouth' , 'lip' , 'lips' , 'teeth' , 'tongue' , 'gag' , 'cheek' , 'cheeks' ] ,
ItemNose : [ 'nose' , 'nostrils' ] ,
ItemEars : [ 'ear' , 'ears' , 'earlobe' , 'earlobes' ] ,
ItemHead : [ 'head' , 'face' , 'hair' , 'eyes' , 'forehead' ] ,
} ,
2024-08-18 01:12:10 +00:00
DO _DATA : { verbs : { } , zones : { } } ,
2023-12-17 03:52:43 +00:00
calculate _maps ( ) {
for ( const [ verbs , data ] of Object . entries ( this . MAP _ACTIONS ) ) {
2024-08-18 01:12:10 +00:00
const /**@type {{[k: string]: {self: string[]; others: string[]}}}*/ unwound = { }
2023-12-17 03:52:43 +00:00
for ( const [ zones , actions ] of Object . entries ( data ) ) {
2024-08-18 01:12:10 +00:00
const all = val ( actions . all , a => a . split ( '|' ) ) ? ? [ ]
const processed = { self : val ( actions . self , s => [ ... s . split ( '|' ) , ... all ] ) ? ? all , others : val ( actions . others , o => [ ... o . split ( '|' ) , ... all ] ) ? ? all }
2023-12-17 03:52:43 +00:00
for ( const zone of zones . split ( ',' ) ) unwound [ ` Item ${ zone } ` ] = processed
}
for ( const verb of verbs . split ( '|' ) ) this . DO _DATA . verbs [ verb ] = unwound
}
for ( const [ ag , zones ] of Object . entries ( this . MAP _ZONES ) ) for ( const zone of zones ) this . DO _DATA . zones [ zone ] = ag
} ,
2024-08-18 01:12:10 +00:00
normalise _message ( /**@type {string}*/ text , /**@type {OBJ<boolean>}*/ options = $O ) {
2023-12-17 03:52:43 +00:00
let result = text
2024-08-18 01:12:10 +00:00
if ( options [ 'trim' ] ? ? false ) result = result . trim ( )
if ( options [ 'low' ] ? ? false ) result = result . toLocaleLowerCase ( )
if ( options [ 'up' ] ? ? false ) {
const first = result . at ( 0 ) ? . toLocaleUpperCase ( ) ? ? $S
2023-12-17 03:52:43 +00:00
const rest = result . slice ( 1 )
result = first + rest
}
2024-08-18 01:12:10 +00:00
if ( ( options [ 'dot' ] ? ? false ) && this . RE _LAST _LETTER . test ( result ) ) result = ` ${ result } . `
2023-12-17 03:52:43 +00:00
return result
} ,
2024-08-18 01:12:10 +00:00
donate _data ( /**@type {string}*/ target ) {
const char = U . target2char ( target )
ass ( 'target must not be you' , ! char . IsPlayer ( ) )
ass ( 'target must be bound' , char . IsRestrained ( ) )
2023-12-17 03:52:43 +00:00
const cost = Math . round ( ( ( Math . random ( ) * 10 ) + 15 ) )
2024-08-18 01:12:10 +00:00
ass ( 'not enough money' , W . Player . Money >= cost )
2024-07-13 21:55:45 +00:00
W . CharacterChangeMoney ( W . Player , - cost )
W . ServerSend ( 'ChatRoomChat' , { Content : 'ReceiveSuitcaseMoney' , Type : 'Hidden' , Target : U . cid ( char ) } )
2024-08-18 01:12:10 +00:00
W . ChatRoomMessage ( { Sender : ass ( '...' , U . cid ( W . Player ) ) , Type : 'Action' , Content : ` You've bought data for $ ${ cost } and sent it to ${ U . dn ( char ) } . ` , Dictionary : [ MISSING _PLAYER _DIALOG ] } )
2023-12-17 03:52:43 +00:00
} ,
2024-08-18 01:12:10 +00:00
run _activity ( /**@type {Character}*/ char , /**@type {AssetGroupItemName}*/ ag , /**@type {ActivityName}*/ action ) {
2023-12-17 03:52:43 +00:00
try {
2024-08-18 01:12:10 +00:00
ass ( 'activities disabled in this room' , W . ActivityAllowed ( ) )
ass ( 'no permissions' , W . ServerChatRoomGetAllowItem ( W . Player , char ) )
char . FocusGroup = ass ( 'invalid AssetGroup' , n2u ( W . AssetGroupGet ( char . AssetFamily , ag ) ) )
const activity = ass ( 'invalid activity' , W . ActivityAllowedForGroup ( char , char . FocusGroup . Name ) . find ( a => a . Activity ? . Name === action ) )
2024-06-29 17:09:25 +00:00
//if ((activity.Name || activity.Activity.Name).endsWith('Item')) {
// const item = this.ensure('no toy found', () => w.Player.Inventory.find(i => i.Asset?.Name === 'SpankingToys' && i.Asset.Group?.Name === char.FocusGroup.Name && w.AssetSpankingToys.DynamicActivity(char) === (activity.Name || activity.Activity.Name)))
// w.DialogPublishAction(char, item)
//} else w.ActivityRun(w.Player, char, char.FocusGroup, activity)
2024-07-13 21:55:45 +00:00
W . ActivityRun ( W . Player , char , char . FocusGroup , activity )
2023-12-17 03:52:43 +00:00
} finally {
2024-08-18 01:12:10 +00:00
char . FocusGroup = null // eslint-disable-line unicorn/no-null
2023-12-17 03:52:43 +00:00
}
} ,
2024-08-18 01:12:10 +00:00
send _activity ( /**@type {string}*/ message ) { let Content = message ; const /**@type {ChatMessageDictionary}*/ Dictionary = [ MISSING _PLAYER _DIALOG ]
val ( this . RE _ACT _CIDS . exec ( Content ) , cids => {
Content = Content . replace ( this . RE _ACT _CIDS , $S )
val ( cids [ 1 ] , cid => val ( int ( cid ) , n => Dictionary . push ( { SourceCharacter : n } ) ) )
val ( cids [ 2 ] , cid => val ( int ( cid ) , n => Dictionary . push ( { TargetCharacter : n } ) ) )
} )
W . ServerSend ( 'ChatRoomChat' , { Type : 'Action' , Content , Dictionary } )
2023-12-17 03:52:43 +00:00
} ,
2024-08-18 01:12:10 +00:00
set _timezone ( /**@type {string[]}*/ args ) {
const tz = ass ( ` invalid offset " ${ args [ 0 ] } " ` , int ( args [ 0 ] ? ? $S ) )
ass ( 'offset should be [-12,12]' , tz in rng ( - 12 , 12 ) )
const char = U . target2char ( args [ 1 ] ? ? $S )
return val ( U . cid ( char ) , cid => Settings . save ( v1 => v1 . TZ [ cid ] = tz ) && TZ . memo ( cid ) )
2023-12-17 03:52:43 +00:00
} ,
command _mbchc ( argline , cmdline , args ) {
2024-07-13 21:55:45 +00:00
const mbchc = W . MBCHC
2023-12-17 03:52:43 +00:00
try { // `this` is command object
2024-08-18 01:12:10 +00:00
if ( m _t ( args ) ) return void U . inform ( Object . entries ( SUBCOMMANDS _MBCHC ) . map ( ( [ cmd , sub ] ) => ` <div>/mbchc ${ cmd } ${ val ( sub . args , a => enu ( a ) . join ( ' ' ) ) ? ? $S } : ${ sub . desc } </div> ` ) . join ( $S ) )
2023-12-17 03:52:43 +00:00
const cmd = String ( args . shift ( ) )
2024-08-18 01:12:10 +00:00
const sub = ass ( ` unknown subcommand " ${ cmd } " ` , SUBCOMMANDS _MBCHC [ cmd ] )
2023-12-17 03:52:43 +00:00
sub . cb . call ( mbchc , mbchc , args , argline , cmdline )
2024-08-18 01:12:10 +00:00
} catch ( x ) {
U . report ( x )
2023-12-17 03:52:43 +00:00
}
} ,
command _activity ( argline , cmdline , _ ) {
2024-07-13 21:55:45 +00:00
const mbchc = W . MBCHC
2024-08-18 01:12:10 +00:00
if ( ! m _t ( argline . trim ( ) ) ) {
2023-12-17 03:52:43 +00:00
try { // `this` is command object
2024-08-18 01:12:10 +00:00
const message = mbchc . normalise _message ( cmdline . replace ( mbchc . RE _ACTIVITY , $S ) , { trim : true , dot : true , up : true } )
2023-12-17 03:52:43 +00:00
mbchc . send _activity ( message )
2024-08-18 01:12:10 +00:00
} catch ( x ) {
U . report ( x )
2023-12-17 03:52:43 +00:00
}
}
} ,
2024-08-18 01:12:10 +00:00
command _do ( _argline , _cmdline , args ) {
2024-07-13 21:55:45 +00:00
const mbchc = W . MBCHC
2023-12-17 03:52:43 +00:00
try { // `this` is command object
2024-08-18 01:12:10 +00:00
if ( m _t ( args ) ) return void U . inform ( '<div>Usage: /do VERB [ZONE] [TARGET]</div><div>Available verbs:</div>' + enu ( mbchc . MAP _ACTIONS ) . join ( ', ' ) + '<div>Available zones:</div>' + enu ( mbchc . DO _DATA . zones ) . join ( ', ' ) )
2023-12-17 03:52:43 +00:00
let [ verb , zone , target ] = args
2024-08-18 01:12:10 +00:00
const zones = ass ( ` unknown verb " ${ verb } " ` , mbchc . DO _DATA . verbs [ verb ? ? $S ] )
if ( enu ( zones ) . length === 1 ) {
if ( target === $ ) target = zone
zone = mbchc . MAP _ZONES [ enu ( zones ) [ 0 ] ? ? $S ] ? . [ 0 ]
2023-12-17 03:52:43 +00:00
}
2024-08-18 01:12:10 +00:00
zone = ass ( 'zone missing' , zone )
const ag = ass ( ` unknown zone " ${ zone } " ` , mbchc . DO _DATA . zones [ zone ] )
const types = ass ( ` zone " ${ zone } " invalid for " ${ verb } " ` , zones [ ag ] )
let /**@type {Character}*/ char = W . Player
if ( target !== $ && ( m _t ( types . self ) || ! m _t ( types . others ) ) ) char = U . target2char ( target )
2023-12-17 03:52:43 +00:00
const type = char . IsPlayer ( ) ? 'self' : 'others'
2024-08-18 01:12:10 +00:00
const available = W . ActivityAllowedForGroup ( char , /**@type {AssetGroupItemName}*/ ( ag ) )
2024-06-29 17:09:25 +00:00
//const toy = w.InventoryGet(w.Player, 'ItemHands')
//if (toy && toy.Asset.Name === 'SpankingToys') available.push(w.AssetAllActivities(char.AssetFamily).find(a => a.Name === w.InventorySpankingToysGetActivity?.(w.Player)))
2024-08-18 01:12:10 +00:00
const actions = ass ( ` zone " ${ zone } " invalid for (" ${ verb } " " ${ type } ") ` , types [ type ] )
const action = ass ( ` invalid action ( ${ verb } ${ zone } ${ target } ) ` , actions . find ( name => available . find ( a => a . Activity ? . Name === name ) ) )
mbchc . run _activity ( char , /**@type {AssetGroupItemName}*/ ( ag ) , /**@type {ActivityName}*/ ( action ) )
} catch ( x ) { U . report ( x ) }
} ,
//complete(options, space = true) {
// if (m_t(options)) return void U.bell()
// if (options.length > 1) {
// const width = Math.max(...options.map(o => o.length))
// let pref = null
// for (let i = width; i > 0; i -= 1) {
// const test = options[0].slice(0, i)
// if (options.every(o => o.startsWith(test))) {
// pref = test
// break
// }
// }
2024-06-29 17:09:25 +00:00
2024-08-18 01:12:10 +00:00
// if (pref) this.complete([pref], false)
// this.comp_hint(options)
// } else W.ElementValue('InputChat', W.ElementValue('InputChat').replace(this.RE_LAST_WORD, `$1${options[0]}${space ? ' ' : $S}`))
//},
2024-06-29 17:09:25 +00:00
2024-08-18 01:12:10 +00:00
///**
// * Displays strings as completion hint
// * @param {string[]} options List of words to display. The order will be modified without copy.
// * @returns {undefined}
// */
//comp_hint(options) {
// if (m_t(options)) return
// this.COMP_HINT.innerHTML = '<div>' + options.sort().reverse().map(s => `<div>${s}</div>`).join($S) + '</div>'
// this.COMP_HINT.style.display = 'block'
// W.ElementSetDataAttribute(this.COMP_HINT.id, 'colortheme', (W.Player.ChatSettings?.ColorTheme || 'Light'))
// const rescroll = W.ElementIsScrolledToEnd('TextAreaChatLog')
// W.ChatRoomResize(false)
// if (rescroll) W.ElementScrollToEnd('TextAreaChatLog')
// this.COMP_HINT.firstChild?.lastChild?.scrollIntoView({behaviour: 'instant'})
//},
2023-12-17 03:52:43 +00:00
2024-08-18 01:12:10 +00:00
///**
// * Returns true if the completion box is attached to body and its display isn't none
// * @returns {boolean}
// */
//comp_hint_visible() {
// return this.COMP_HINT.parentElement && this.COMP_HINT.style.display !== 'none'
//},
//comp_hint_hide() {
// if (!this.comp_hint_visible()) return
// this.COMP_HINT.style.display = 'none'
// W.ChatRoomResize(false)
//},
//char2targets(/**@type {Character}*/char) {
// const/**@type {Set<string>}*/result = new Set()
// val(U.cid(char)?.toString(), id => result.add(id).add(`=${id}`))
// for (const t of U.split(char.Name)) {
// result.add(t)
// result.add(`@${t}`)
// }
2023-12-17 03:52:43 +00:00
2024-08-18 01:12:10 +00:00
// if (char.Nickname !== $) for (const t of U.split(char.Nickname)) {
// result.add(t)
// result.add(`@${t}`)
// }
2023-12-17 03:52:43 +00:00
2024-08-18 01:12:10 +00:00
// return result
//},
//complete_target(/**@type {string}*/token, me2 = true, check_perms = false) {
// const [locase, found] = [token.toLocaleLowerCase(), new Set()]
// for (const c of U.crc) {
// if ((c.IsPlayer() && !me2) || (check_perms && !W.ServerChatRoomGetAllowItem(W.Player, c))) continue
// for (const s of this.char2targets(c)) {
// if (s.toLocaleLowerCase().startsWith(locase)) found.add(s)
// }
// }
2023-12-17 03:52:43 +00:00
2024-08-18 01:12:10 +00:00
// //this.complete(Array.from(found))
//},
//complete_common() { // w.ElementValue('InputChat') will strip the trailing whitespace
// const E = D.querySelector('#InputChat')
// ass('somehow #InputChat is broken', E instanceof HTMLTextAreaElement)
// return [this, E.value, U.split(E.value)]
//},
//complete_mbchc(_args, _locase, _cmdline) {
// const [mbchc, _input, tokens] = W.MBCHC.complete_common(); // `this` is command object
// if (m_t(tokens)) return
// if (tokens.length < 2) return //mbchc.complete([`${CommandsKey}${this.Tag}`])
// const subname = tokens[1].toLocaleLowerCase()
// if (tokens.length < 3) return //mbchc.complete(Object.keys(SUBCOMMANDS_MBCHC).filter(c => c.startsWith(subname))) // Complete subcommand name
// const sub = SUBCOMMANDS_MBCHC[subname]
// if (sub && sub.args) {
// const argname = Object.keys(sub.args)[tokens.length - 3]
// if (argname === 'TARGET') return mbchc.complete_target(tokens.at(-1), false)
// if (argname === '[TARGET]') return mbchc.complete_target(tokens.at(-1))
// }
//},
//complete_fbc_anim(_args, _locase, _cmdline) {
// const [mbchc, _input, tokens] = W.MBCHC.complete_common(); // `this` is command object
// if (m_t(tokens)) return
// if (tokens.length < 2) return mbchc.complete([`${CommandsKey}${this.Tag}`])
// if (tokens.length > 2) return void U.bell()
// const anim = tokens[1].toLocaleLowerCase()
// return mbchc.complete(Object.keys(W.bce_EventExpressions).filter(a => a.toLocaleLowerCase().startsWith(anim)))
//},
//complete_fbc_pose(_args, _locase, _cmdline) {
// const [mbchc, _input, tokens] = W.MBCHC.complete_common(); // `this` is command object
// if (m_t(tokens)) return
// if (tokens.length < 2) return mbchc.complete([`${CommandsKey}${this.Tag}`])
// const pose = tokens.at(-1).toLocaleLowerCase()
// return mbchc.complete(W.PoseFemale3DCG.map(p => p.Name).filter(p => p.toLocaleLowerCase().startsWith(pose)))
//},
2024-08-24 03:31:50 +00:00
//focus_chat_checks() { // we only want to catch chat log and canvas (no map though) keypresses
2024-08-18 01:12:10 +00:00
// if (D.activeElement === D.body) return true
// if (D.activeElement?.id !== 'MainCanvas') return false
// return !W.ChatRoomMapViewIsActive()
//},
//focus_chat_whitelist(/**@type {KeyboardEvent}*/event) {
// if (event.ctrlKey && event.key === 'v') return true // Ctrl+V should paste
// return false
//},
//focus_chat(/**@type {KeyboardEvent}*/event) {
// if (event.repeat) return // Only unique presses please
// if (!this.focus_chat_checks()) return
// if ([event.altKey, event.ctrlKey, event.metaKey].some(Boolean) && !this.focus_chat_whitelist(event)) return // Alt, ctrl and meta should all be false
// if (U.style('#InputChat', s => s.display) !== 'inline') return // Input chat missing
// W.ElementFocus('InputChat')
//},
2023-12-17 03:52:43 +00:00
loader ( ) {
2024-08-18 01:12:10 +00:00
U . remove _loader _hook === $ || yes ( void U . remove _loader _hook ( ) , U . remove _loader _hook = $ )
2023-12-17 03:52:43 +00:00
if ( this . LOADED ) return
2024-08-18 01:12:10 +00:00
val ( Settings . v0 , v0 => Settings . migrate _0 _1 ( v0 ) )
2024-07-13 21:55:45 +00:00
2023-12-17 03:52:43 +00:00
// Calculated values
2024-08-18 01:12:10 +00:00
const /**@type {ICommand[]}*/ COMMANDS = [
{ Tag : 'mbchc' , Description : ': Utility functions ("/mbchc" for help)' , Action : this . command _mbchc , AutoComplete : U . complete _mbchc } ,
2023-12-17 03:52:43 +00:00
{ Tag : 'activity' , Description : '[Message]: Send a custom activity (or "@@Message", or "@Message" as yourself)' , Action : this . command _activity } ,
2024-08-18 01:12:10 +00:00
{ Tag : 'do' , Description : ': Do an activity, as if clicked on its button ("/do" for help)' , Action : this . command _do , AutoComplete : U . complete _do } ,
2023-12-17 03:52:43 +00:00
]
2024-08-18 01:12:10 +00:00
mut ( D . createElement ( 'style' ) , c => void D . head . append ( c ) , c => c . textContent = `
# TextAreaChatLog . mbchc { background - color : $ { U . RGB . Polly } ; margin - left : - 0.4 em ; padding - left : 0.4 em ; }
# TextAreaChatLog [ data - colortheme ^= "dark" ] . mbchc { background - color : $ { U . RGB . Mute } ; }
# $ { C . e . id } { display : none ; text - align : right ; }
# $ { C . e . id } > div { overflow : auto ; position : absolute ; bottom : 0 ; right : 0 ; max - height : 100 % ; padding : 0 0.5 ex ; background - color : $ { U . RGB . Polly } ; color : black ; }
# $ { C . e . id } [ data - colortheme ^= "dark" ] > div { background - color : $ { U . RGB . Mute } ; color : white ; }
# $ { C . e . id } > div div { margin : 0.25 ex 0 ; }
2024-08-24 03:31:50 +00:00
# chat - room - div # TextAreaChatLog : : before { content : '' ; display : block ; height : 100 % ; }
# chat - room - div [ data - mbchc - mode = "h" ] # TextAreaChatLog : : after { content : '𝗵 𝗶 𝘀 𝘁 𝗼 𝗿 𝘆 ⟨𝘗𝘨𝘜𝘱/𝘋𝘯⟩ 𝗌 𝖼 𝗋 𝗈 𝗅 𝗅 ⇅ ⟨𝘌𝘯𝘵𝘦𝘳⟩ 𝗌 𝖾 𝗇 𝖽 ↵ ⟨𝘛𝘢𝘣⟩ 𝖾 𝖽 𝗂 𝗍 ⌨ ⟨𝘌𝘴𝘤⟩ 𝖺 𝖻 𝗈 𝗋 𝗍 ⟲' ; display : block ; position : sticky ; bottom : 0 ; background : black ; color : orange ; padding : 0 0.2 ex ; animation : 0.2 s cubic - bezier ( 0.19 , 1 , 0.22 , 1 ) mbchc _hist _hint _show ; }
2024-08-18 01:12:10 +00:00
# InputChat : read - only { background : black ; color : orange ; }
2024-08-24 03:31:50 +00:00
@ keyframes mbchc _hist _hint _show { from { transform : translateX ( - 100 % ) ; } to { transform : translateX ( 0 ) ; } }
2024-08-18 01:12:10 +00:00
` )
2024-06-29 17:09:25 +00:00
2023-12-17 03:52:43 +00:00
// Actions
this . calculate _maps ( )
2024-07-13 21:55:45 +00:00
W . CommandCombine ( COMMANDS )
2023-12-17 03:52:43 +00:00
// Hooks
2024-08-18 01:12:10 +00:00
after ( 'ChatRoomReceiveSuitcaseMoney' , ( ) => {
if ( this . AUTOHACK _ENABLED && this . LAST _HACKED !== $ ) {
W . CurrentCharacter = U . cid2char ( this . LAST _HACKED )
this . LAST _HACKED = $
2024-07-13 21:55:45 +00:00
W . ChatRoomTryToTakeSuitcase ( )
2023-12-17 03:52:43 +00:00
}
} )
2024-08-18 01:12:10 +00:00
prior ( 'ChatRoomSendChat' , ( ) => val ( U . ic , ic => ! ic . value . startsWith ( '@@@' ) && ic . value . startsWith ( '@' ) && ( ic . value = ic . value . replace ( U . RE [ '@' ] [ 1 ] , U . ACT ) . replace ( U . RE [ '@' ] [ 0 ] , U . replace _me ) ) ) )
//{
// let input = W.ElementValue('InputChat')
// if (!input.startsWith('@@@') && input.startsWith('@')) {
// input = input.replace(this.RE_PREF_ACTIVITY, this.PREF_ACTIVITY)
// input = input.replace(this.RE_PREF_ACTIVITY_ME, this.replace_me)
// W.ElementValue('InputChat', input)
// }
//})
after ( 'ChatRoomSendChat' , ( ) => { // FIXME actually make history a ring buffer of a given size. clear the array and push every string into it, compacting sequential equal strings into one.
2024-07-13 21:55:45 +00:00
const history = W . ChatRoomLastMessage
2023-12-17 03:52:43 +00:00
if ( ( history . length > 1 ) && ( history . at ( - 1 ) === history . at ( - 2 ) ) ) {
history . pop ( )
2024-07-13 21:55:45 +00:00
W . ChatRoomLastMessageIndex -= 1
2023-12-17 03:52:43 +00:00
}
} )
2024-08-18 01:12:10 +00:00
prior ( 'ChatRoomCharacterViewDrawOverlay' , ( C , CX , CY , Z ) => {
2024-06-29 17:09:25 +00:00
// if (w.ChatRoomHideIconState < 1 && C.MBCHC) {
2024-08-18 01:12:10 +00:00
// w.DrawRect(CharX + (175 * Zoom), CharY, 50 * Zoom, 50 * Zoom, C.MBCHC.VERSION === w.Player.MBCHC.VERSION ? U.RGB.Polly : U.RGB.Mute)
2023-12-19 01:15:39 +00:00
// }
2024-08-18 01:12:10 +00:00
val ( TZ . lookup ( C ) , ctz => W . ChatRoomHideIconState < 1 && void cur ( new Date ( W . CommonTime ( ) + this . UTC _OFFSET + ( ctz * 60 * 60 * 1000 ) ) . getHours ( ) , hr =>
void W . DrawTextFit ( ` ${ hr < 10 ? '0' : '' } ${ hr } ` , CX + ( Z * 200 ) , CY + ( Z * 25 ) , Z * 46 , 'white' , 'black' ) ) )
2023-12-17 03:52:43 +00:00
} )
2024-08-18 01:12:10 +00:00
const /**@type {(this: HTMLTextAreaElement, e: KeyboardEvent) => void}*/ ickd = function ( e ) {
C . hide ( )
const ic = this // eslint-disable-line unicorn/no-this-assignment,@typescript-eslint/no-this-alias
if ( ic . readOnly ) switch ( e . key ) {
case 'Tab' : case 'Escape' :
e . stopImmediatePropagation ( )
e . preventDefault ( ) // falls through
case 'Enter' :
H . exit ( ic , e . key === 'Escape' ) // no default
2023-12-17 03:52:43 +00:00
}
2024-08-18 01:12:10 +00:00
}
after ( 'ChatRoomCreateElement' , ( ) => { // This thing runs on every frame actually.
2024-08-24 03:31:50 +00:00
C . e . parentElement ? ? D . body . append ( C . e )
2024-08-18 01:12:10 +00:00
val ( U . ic , ic => ic . dataset [ 'mbchc' ] ? ? void ic . addEventListener ( 'keydown' , ickd ) ? ? ( ic . dataset [ 'mbchc' ] = 'yes' ) )
2024-08-24 03:31:50 +00:00
//val(asa(HTMLDivElement, D.querySelector('#TextAreaChatLog')), c => c.scrollHeight > c.clientHeight || void U.pad_chat(c))
2023-12-17 03:52:43 +00:00
} )
2024-08-18 01:12:10 +00:00
prior ( 'ChatRoomClearAllElements' , ( ) => C . hide ( ) && void C . e . remove ( ) )
D . addEventListener ( 'click' , _ => C . hide ( ) ) // downstream handlers can capture clicks, but I can't be fucked to be honest
//after('ChatRoomResize', () => {
// if (W.CharacterGetCurrent() === null && W.CurrentScreen === 'ChatRoom' && D.querySelector('#InputChat') && D.querySelector('#TextAreaChatLog') && this.comp_hint_visible()) { // Upstream
// const fontsize = ChatRoomFontSize
// //w.ElementPositionFix('TextAreaChatLog', fontsize, 1005, 66, 988, 630)
// //w.ElementPositionFix(this.COMP_HINT.id, fontsize, 1005, 701, 988, 200)
// W.ElementPositionFix(this.COMP_HINT.id, fontsize, 800, 65, 200, 835)
// //this.COMP_HINT.style.display = 'flex'
// }
//})
after ( 'ChatRoomResize' , ( ) => {
if ( W . CharacterGetCurrent ( ) === null && W . CurrentScreen === 'ChatRoom' && U . ic !== $ && D . querySelector ( '#TextAreaChatLog' ) !== null && ! C . hidden ) { // Upstream
W . ElementPositionFix ( C . e . id , ChatRoomFontSize , 800 , 65 , 200 , 835 )
2023-12-17 03:52:43 +00:00
}
2024-08-18 01:12:10 +00:00
} )
2023-12-17 03:52:43 +00:00
2024-08-18 01:12:10 +00:00
//D.addEventListener('keydown', event => void this.focus_chat(event)) // Looks like the club got better at this
mod . hookFunction ( 'ChatRoomScrollHistory' , 0 , ( [ up ] ) => void val ( U . ic , ic => { const input = ic . value , history = W . ChatRoomLastMessage
// FIXME we'll make it better when the history is a proper ring buffer
if ( ! ic . readOnly ) {
2024-08-22 18:13:04 +00:00
const /**@type {Map<string,number>}*/ map = new Map ( )
const /**@type {(l: string, i: number, I: string) => boolean}*/ cond = m _t ( input ) ? ( _ , i , _ _ ) => i > 0 : ( l , _ , i ) => l !== i && l . startsWith ( i )
if ( m _t ( history . reduce ( ( ax , l , i ) => cond ( l , i , input ) ? ax . set ( l , i ) : ax , map ) ) ) return U . bell ( )
H . enter ( ic , input , W . ElementIsScrolledToEnd ( 'TextAreaChatLog' ) , new Set ( map . values ( ) ) )
2023-12-17 03:52:43 +00:00
}
2024-08-18 01:12:10 +00:00
if ( ic . readOnly ) { // this can't be an else, because we mutate state above. To be honest, this will always be true, but I want to make sure.
if ( H . ids === $ ) return U . bell ( ) // shouldn't happen?
let found = - 1
let first = - 1
let last = - 1
2024-08-22 18:13:04 +00:00
for ( const i of H . ids ) { // these aren't necessarily in order
2024-08-18 01:12:10 +00:00
if ( up ) {
2024-08-22 18:13:04 +00:00
if ( i < W . ChatRoomLastMessageIndex && i > found ) found = i // the largest i that is less than index
if ( i > last ) last = i
2024-08-18 01:12:10 +00:00
} else {
2024-08-22 18:13:04 +00:00
if ( first < 0 || i < first ) first = i
if ( i > W . ChatRoomLastMessageIndex && ( i < found || found < 0 ) ) found = i // the smallest i that is greater than index
2024-08-18 01:12:10 +00:00
}
}
if ( found < 0 ) found = up ? last : first
const line = history [ found ] ? ? $S
if ( line === input ) return U . bell ( )
ic . value = line
W . ChatRoomLastMessageIndex = found
}
return true
} ) )
2023-12-17 03:52:43 +00:00
// Chat room handlers
2024-07-13 21:55:45 +00:00
W . ChatRoomRegisterMessageHandler ( { Priority : - 220 , Description : 'MBCHC preprocessor' , Callback : ( data , sender , message , metadata ) => {
2023-12-17 03:52:43 +00:00
data . MBCHC _ID = this . NEXT _MESSAGE
this . NEXT _MESSAGE += 1
if ( this . LOG _MESSAGES ) console . debug ( { data , sender , msg : message , metadata } )
2024-06-29 17:09:25 +00:00
return false
2023-12-17 03:52:43 +00:00
} } )
2024-07-13 21:55:45 +00:00
W . ChatRoomRegisterMessageHandler ( { Priority : - 219 , Description : 'MBCHC autohack lookup' ,
2023-12-17 03:52:43 +00:00
Callback : ( data , _sender , _message , _metadata ) => {
if ( ( data . Type === 'Hidden' ) && ( data . Content === 'ReceiveSuitcaseMoney' ) ) this . LAST _HACKED = data . Sender
2024-06-29 17:09:25 +00:00
return false
2023-12-17 03:52:43 +00:00
} ,
} )
// Footer
this . LOADED = true
2024-08-18 01:12:10 +00:00
CW . i ( ` loaded version ${ version } ` )
2023-12-17 03:52:43 +00:00
} ,
}
2024-08-18 01:12:10 +00:00
U . current ( ) === 'Character/Login' ? U . remove _loader _hook = prior ( 'AsylumGGTSSAddItems' , ( ) => void W . MBCHC . loader ( ) ) : W . MBCHC . loader ( )
const /**@type {BCE.Patcher}*/ BP = { timer : undefined , patches : [ [ /^\^('s)?( )?/g , '^SourceCharacter$1\\s+' ] , [ /([^\\])\$/g , '$1\\.?$$' ] ] ,
cfs : { anim : ( ) => enu ( W . bce _EventExpressions ? ? $O ) , pose : ( ) => W . PoseFemale3DCG . map ( p => p . Name ) } ,
gen ( f ) { return function ( ) { C . icomplete ( ws => ws . length < 2 ? new Set ( [ ` ${ CommandsKey } ${ this . Tag } ` ] ) : ws . length < 3 ? new Set ( f ( ) ) : $Ss ) } } ,
copy : t => ( { Type : 'Action' , Event : t . Event , Matchers : t . Matchers . map ( m => ( { Tester : new RegExp ( BP . patches . reduce ( ( ax , [ f , r ] ) => ax . replaceAll ( f , r ) , m . Tester . source ) , $S ) } ) ) } ) ,
patch : ( ) => val ( W . bce _ActivityTriggers , ts => val ( W . FBC _VERSION , _ =>
yes ( void clearInterval ( BP . timer ) , void enu ( BP . cfs ) . forEach ( t => val ( W . GetCommands ( ) . find ( c => t === c . Tag ) , cmd => cmd . AutoComplete = BP . gen ( BP . cfs [ t ] ) ) ) , void ts . forEach ( t => t . Type === 'Emote' && ts . push ( BP . copy ( t ) ) ) )
) ) ,
}
BP . timer = W . setInterval ( BP . patch , 100 )