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.4em; padding-left: 0.4em; }
#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.5ex; 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.25ex 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.2ex; animation: 0.2s 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 )