2023-12-17 03:52:43 +00:00
export { }
2024-06-29 17:09:25 +00:00
/** @typedef {import('bondage-club-mod-sdk').ModSDKGlobalAPI} ModSDKGlobalAPI */
/** @type {Window & typeof globalThis & {MBCHC?: any, bcModSdk?: ModSDKGlobalAPI, bce_ActivityTriggers?: any, bce_EventExpressions?: any}} */
const w = window
/ * *
* A silly helper to memorise values in callbacks
* @ template V , R
* @ param { V } v Value to memorise
* @ param { function ( V ) : R } cb Callback
* @ returns { R } Return value of the callback
* /
const take = ( v , cb ) => cb ( v )
/ * *
* Takes a DOM query and passes the element ' s style into a callback , returning its result
* @ template T
* @ param { string } q Query
* @ param { function ( CSSStyleDeclaration ) : T } cb Callback
* @ returns { T | void } Return value of the callback , if it was called
* /
const style = ( q , cb ) => take ( document . querySelector ( q ) , E => E && E instanceof HTMLElement && E . style ? cb ( E . style ) : undefined )
/ * *
* @ returns { string } Current view
* /
const current = ( ) => ` ${ w . CurrentModule } / ${ w . CurrentScreen } `
/ * *
* @ param { Character } char
* @ returns { number }
* /
const cid = char => char . MemberNumber
// Zero-width non-joiner, used to break up ligatures, does nothing here, but an empty string is a falsey value
2024-06-05 17:33:55 +00:00
const MISSING _PLAYER _DIALOG = { Tag : 'MISSING TEXT IN "Interface.csv": ' , Text : '\u200C' }
2023-12-19 01:15:39 +00:00
2024-06-29 17:09:25 +00:00
if ( w . MBCHC ) throw new Error ( 'MBCHC found, aborting loading' )
w . MBCHC = {
VERSION : 'dev.12' ,
2023-12-17 03:52:43 +00:00
NEXT _MESSAGE : 1 ,
LOG _MESSAGES : false ,
RETHROW : false ,
LOADED : false ,
AUTOHACK _ENABLED : false ,
LAST _HACKED : null ,
HISTORY _MODE : false ,
RE _PREF _ACTIVITY _ME : /^@/ ,
RE _PREF _ACTIVITY : /^@@/ ,
RE _ACT _CIDS : /^<(\d+)?:(\d+)?>/ ,
RE _TZ : /(?:gmt|utc)\s*([+-])\s*(\d\d?)/i ,
RE _ALL _LEFT : /^<+$/ ,
RE _ALL _RIGHT : /^>+$/ ,
RE _SPACES : /\s{2,}/g ,
RE _LAST _WORD : /(^|\s)(\S*)$/ ,
RE _LAST _LETTER : /\w$/ ,
RGB _MUTE : '#6c2132' ,
RGB _POLLY : '#81b1e7' ,
UTC _OFFSET : new Date ( ) . getTimezoneOffset ( ) * 60 * 1000 ,
MAP _ACTIONS : { // ActivityFemale3DCG
// action
'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' ] ,
} ,
FBC _TESTER _PATCHES : [
[ /^\^('s)?( )?/g , '^SourceCharacter$1\\s+' ] ,
[ /([^\\])\$/g , '$1\\.?$$' ] ,
] ,
SUBCOMMANDS _MBCHC : {
autohack : { desc : 'toggle the autohack feature' , cb : mbchc => mbchc . inform ( ` Autohack is now ${ ( mbchc . AUTOHACK _ENABLED = ! mbchc . AUTOHACK _ENABLED ) ? 'enabled' : 'disabled' } ` ) } , // eslint-disable-line no-cond-assign
donate : { desc : 'Buy data and send it to recipient' , args : { TARGET : { } } , cb : ( mbchc , args ) => mbchc . donate _data ( args [ 0 ] ) } ,
tz : { desc : 'set target\'s UTC offset' , args : { OFFSET : { } , '[TARGET]' : { } } , cb : ( mbchc , args ) => mbchc . set _timezone ( args ) } ,
'purge!' : { desc : 'delete MBCHC online saved data' , cb ( mbchc ) {
2024-06-29 17:09:25 +00:00
if ( w . Player . OnlineSettings . MBCHC ) {
delete w . Player . OnlineSettings . MBCHC
2023-12-17 03:52:43 +00:00
mbchc . save _settings ( )
}
} } ,
} ,
2024-06-29 17:09:25 +00:00
ensure ( text , callback ) {
2023-12-17 03:52:43 +00:00
const result = callback . call ( this )
2024-06-29 17:09:25 +00:00
if ( ! result ) throw new Error ( text )
2023-12-17 03:52:43 +00:00
return ( result )
} ,
calculate _maps ( ) {
this . DO _DATA = { verbs : { } , zones : { } }
for ( const [ verbs , data ] of Object . entries ( this . MAP _ACTIONS ) ) {
const unwound = { }
for ( const [ zones , actions ] of Object . entries ( data ) ) {
const all = ( actions . all ) ? actions . all . split ( '|' ) : [ ]
const processed = { self : ( actions . self ) ? actions . self . split ( '|' ) . concat ( all ) : all , others : ( actions . others ) ? actions . others . split ( '|' ) . concat ( all ) : all }
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
} ,
settings ( setting = null ) {
2024-06-29 17:09:25 +00:00
const settings = w . Player . OnlineSettings . MBCHC || { }
2023-12-17 03:52:43 +00:00
return ( setting ? settings [ setting ] : settings )
} ,
save _settings ( cb = null ) {
if ( cb ) {
2024-06-29 17:09:25 +00:00
if ( ! w . Player . OnlineSettings . MBCHC ) w . Player . OnlineSettings . MBCHC = { }
cb . call ( this , w . Player . OnlineSettings . MBCHC )
2023-12-17 03:52:43 +00:00
}
2024-06-29 17:09:25 +00:00
w . ServerAccountUpdate . QueueData ( { OnlineSettings : w . Player . OnlineSettings } )
2023-12-17 03:52:43 +00:00
} ,
log ( level , message ) {
console [ level ] ( 'MBCHC: ' + String ( message ) )
} ,
empty ( text ) {
if ( ! text ) return ( true )
if ( String ( text ) . trim ( ) . length === 0 ) return ( true )
return ( false )
} ,
normalise _message ( text , options = { } ) {
let result = text
if ( options . trim ) result = result . trim ( )
if ( options . low ) result = result . toLocaleLowerCase ( )
if ( options . up ) {
const first = result . at ( 0 ) . toLocaleUpperCase ( )
const rest = result . slice ( 1 )
result = first + rest
}
if ( options . dot && this . RE _LAST _LETTER . test ( result ) ) result = ` ${ result } . `
return ( result )
} ,
tokenise ( text ) {
return text . replace ( this . RE _SPACES , ' ' ) . split ( ' ' )
} ,
inform ( html , timeout = 60_000 ) {
2024-06-29 17:09:25 +00:00
w . ChatRoomSendLocal ( ` <div class="mbchc"> ${ html } </div> ` , timeout )
2023-12-17 03:52:43 +00:00
} ,
report ( x ) {
2024-06-29 17:09:25 +00:00
this . inform ( ` ${ x . toString ( ) } ` )
2023-12-17 03:52:43 +00:00
if ( this . RETHROW ) throw x
} ,
in ( x , floor , ceiling ) {
return ( ( x >= floor ) && ( x <= ceiling ) )
} ,
2024-06-29 17:09:25 +00:00
cid2char ( id ) {
id = Number . parseInt ( id , 10 )
if ( id === cid ( w . Player ) ) return ( w . Player )
return ( this . ensure ( ` character ${ id } not found in the room ` , ( ) => w . ChatRoomCharacter . find ( c => cid ( c ) === id ) ) )
2023-12-17 03:52:43 +00:00
} ,
pos2char ( pos ) {
2024-06-29 17:09:25 +00:00
if ( ! this . in ( pos , 0 , w . ChatRoomCharacter . length - 1 ) ) throw new Error ( ` invalid position ${ pos } ` )
return ( w . ChatRoomCharacter [ pos ] )
2023-12-17 03:52:43 +00:00
} ,
rel2char ( target ) {
2024-06-29 17:09:25 +00:00
const me = this . ensure ( 'can\'t find my position' , ( ) => w . ChatRoomCharacter . findIndex ( char => char . IsPlayer ( ) ) + 1 ) - 1 // 0 is falsey, but is valid index
2023-12-17 03:52:43 +00:00
let pos = null
if ( this . RE _ALL _LEFT . test ( target ) ) pos = me - target . length
if ( this . RE _ALL _RIGHT . test ( target ) ) pos = me + target . length
if ( pos === null ) throw new Error ( ` failed to parse target " ${ target } " ` )
2024-06-29 17:09:25 +00:00
pos %= w . ChatRoomCharacter . length
if ( pos < 0 ) pos += w . ChatRoomCharacter . length
2023-12-17 03:52:43 +00:00
return ( this . pos2char ( pos ) )
} ,
target2char ( target ) { // Target should be lowcase
const input = target
2024-06-29 17:09:25 +00:00
if ( this . empty ( target ) ) return ( w . Player )
2023-12-17 03:52:43 +00:00
const int = Number . parseInt ( target , 10 )
target = String ( target )
let found = [ ]
if ( target . startsWith ( '=' ) ) return ( this . cid2char ( target . slice ( 1 ) ) )
if ( target . startsWith ( '<' ) || target . startsWith ( '>' ) ) return ( this . rel2char ( target ) )
if ( ! Number . isNaN ( int ) && int . toString ( ) === target ) { // We got a number
if ( this . in ( int , 0 , 9 ) ) return ( this . pos2char ( int ) )
if ( this . in ( int , 11 , 15 ) ) return ( this . pos2char ( int - 11 ) )
if ( this . in ( int , 21 , 25 ) ) return ( this . pos2char ( int - 16 ) )
2024-06-29 17:09:25 +00:00
found . push ( ... w . ChatRoomCharacter . filter ( c => cid ( c ) . toString ( ) . includes ( target ) ) )
2023-12-17 03:52:43 +00:00
}
if ( target . startsWith ( '@' ) ) target = target . slice ( 1 )
2024-06-29 17:09:25 +00:00
found . push ( ... w . ChatRoomCharacter . filter ( c => c . Name . toLocaleLowerCase ( ) . includes ( target ) ) )
found . push ( ... w . ChatRoomCharacter . filter ( c => c . Nickname && ( c . Nickname . toLocaleLowerCase ( ) . includes ( target ) ) ) ) // eslint-disable-line unicorn/no-array-push-push
2023-12-17 03:52:43 +00:00
const map = { }
for ( const c of found ) {
2024-06-29 17:09:25 +00:00
if ( ! map [ cid ( c ) ] ) map [ cid ( c ) ] = c
2023-12-17 03:52:43 +00:00
}
found = Object . values ( map )
if ( found . length === 0 ) throw new Error ( ` target " ${ input } ": no match ` )
2024-06-29 17:09:25 +00:00
if ( found . length > 1 ) throw new Error ( ` target " ${ input } ": multiple matches ( ${ found . map ( c => ` ${ cid ( c ) } | ${ c . Name } | ${ c . Nickname || c . Name } ` ) . join ( ',' ) } ) ` )
2023-12-17 03:52:43 +00:00
return ( found [ 0 ] )
} ,
char2targets ( char ) {
2024-06-29 17:09:25 +00:00
const [ result , id ] = [ new Set ( ) , cid ( char ) . toString ( ) ]
result . add ( id ) . add ( ` = ${ id } ` )
2023-12-17 03:52:43 +00:00
for ( const t of this . tokenise ( char . Name ) ) {
result . add ( t )
result . add ( ` @ ${ t } ` )
}
if ( char . Nickname ) for ( const t of this . tokenise ( char . Nickname ) ) {
result . add ( t )
result . add ( ` @ ${ t } ` )
}
return result
} ,
donate _data ( target ) {
const char = this . target2char ( target )
if ( char . IsPlayer ( ) ) throw new Error ( 'target must not be you' )
if ( ! char . IsRestrained ( ) ) throw new Error ( 'target must be bound' )
const cost = Math . round ( ( ( Math . random ( ) * 10 ) + 15 ) )
2024-06-29 17:09:25 +00:00
if ( w . Player . Money < cost ) throw new Error ( 'not enough money' )
w . CharacterChangeMoney ( w . Player , - cost )
w . ServerSend ( 'ChatRoomChat' , { Content : 'ReceiveSuitcaseMoney' , Type : 'Hidden' , Target : cid ( char ) } )
w . ChatRoomMessage ( { Sender : cid ( w . Player ) , Type : 'Action' , Content : ` You've bought data for $ ${ cost } and sent it to ${ char . dn } . ` , Dictionary : [ MISSING _PLAYER _DIALOG ] } )
2023-12-17 03:52:43 +00:00
} ,
run _activity ( char , ag , action ) {
try {
2024-06-29 17:09:25 +00:00
if ( ! w . ActivityAllowed ( ) ) throw new Error ( 'activities disabled in this room' )
if ( ! w . ServerChatRoomGetAllowItem ( w . Player , char ) ) throw new Error ( 'no permissions' )
char . FocusGroup = this . ensure ( 'invalid AssetGroup' , ( ) => w . AssetGroupGet ( char . AssetFamily , ag ) )
const activity = this . ensure ( 'invalid activity' , ( ) => w . ActivityAllowedForGroup ( char , char . FocusGroup . Name ) . find ( a => a . Activity ? . Name === action ) )
//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)
w . ActivityRun ( w . Player , char , char . FocusGroup , activity )
2023-12-17 03:52:43 +00:00
} finally {
char . FocusGroup = null
}
} ,
replace _me ( match , offset , string ) {
const text = string . slice ( 1 )
let suffix = ' '
if ( text . startsWith ( '\'' ) || text . startsWith ( ' ' ) ) suffix = ''
2024-06-29 17:09:25 +00:00
return ` ${ w . MBCHC . PREF _ACTIVITY } < ${ cid ( w . Player ) } :>SourceCharacter ${ suffix } `
2023-12-17 03:52:43 +00:00
} ,
cid2dict ( type , cid ) {
return ( { Tag : ` ${ type } Character ` , MemberNumber : cid , Text : this . cid2char ( cid ) . dn } )
} ,
send _activity ( message ) {
2023-12-19 01:15:39 +00:00
const dict = [ MISSING _PLAYER _DIALOG ]
2023-12-17 03:52:43 +00:00
const cids = message . match ( this . RE _ACT _CIDS )
if ( cids ) {
message = message . replace ( this . RE _ACT _CIDS , '' )
if ( cids [ 1 ] ) dict . push ( this . cid2dict ( 'Source' , cids [ 1 ] ) )
if ( cids [ 2 ] ) dict . push ( this . cid2dict ( 'Target' , cids [ 2 ] ) , this . cid2dict ( 'Destination' , cids [ 2 ] ) )
}
2024-06-29 17:09:25 +00:00
w . ServerSend ( 'ChatRoomChat' , { Type : 'Action' , Content : message , Dictionary : dict } )
} ,
//receive(data) {
// const char = this.cid2char(data.Sender)
// if (char.IsPlayer()) return true // This is our own message, sent back to us
// const payload = this.ensure('Empty message', () => data.Dictionary[0])
// switch (payload.type) {
// case 'greetings': case 'hello': {
// char.MBCHC = payload.value
// if (payload.type === 'greetings') this.hello(char)
// break
// }
// default: // If we don't know the type it may be from a newer version
// }
// return true
//},
//hello(char = null) {
// const payload = {type: 'greetings', value: w.Player.MBCHC}
// if (char) payload.type = 'hello'
// const message = {Content: 'MBCHC', Type: /** @type {const} */ ('Hidden'), Dictionary: [payload]}
// if (char) message.Target = char.cid
// w.ServerSend('ChatRoomChat', message)
//},
2023-12-17 03:52:43 +00:00
copy _fbc _trigger ( trigger ) {
const result = {
Type : 'Action' ,
Event : trigger . Event ,
Matchers : trigger . Matchers . map ( m => ( { Tester : new RegExp ( this . FBC _TESTER _PATCHES . reduce ( ( ax , [ f , r ] ) => ax . replaceAll ( f , r ) , m . Tester . source ) , 'u' ) } ) ) ,
}
return ( result )
} ,
patch _fbc ( ) {
this . remove _fbc _hook ( )
delete this . remove _fbc _hook
2024-06-29 17:09:25 +00:00
w . bce _ActivityTriggers . push ( ... w . bce _ActivityTriggers . filter ( t => t . Type === 'Emote' ) . map ( t => this . copy _fbc _trigger ( t ) ) )
/* (["anim", "pose"]).forEach(tag => {let cmd = w.Commands.find(c => tag === c.Tag); if (cmd) cmd.AutoComplete = this[`complete_fbc_${tag}`]}) */ // this line explodes, because it needs a semicolon in front if it
let cmd = w . Commands . find ( c => c . Tag === 'anim' )
2023-12-17 03:52:43 +00:00
if ( cmd ) cmd . AutoComplete = this . complete _fbc _anim
2024-06-29 17:09:25 +00:00
cmd = w . Commands . find ( c => c . Tag === 'pose' )
2023-12-17 03:52:43 +00:00
if ( cmd ) cmd . AutoComplete = this . complete _fbc _pose
} ,
find _timezone ( char ) {
const timezones = this . settings ( 'timezones' )
2024-06-29 17:09:25 +00:00
if ( timezones && typeof timezones [ cid ( char ) ] === 'number' ) return ( timezones [ cid ( char ) ] )
2023-12-17 03:52:43 +00:00
const match = ( char . Description ) ? char . Description . match ( this . RE _TZ ) : null
const int = match ? Number . parseInt ( match [ 1 ] + match [ 2 ] , 10 ) : 42
if ( this . in ( int , - 12 , 12 ) ) return ( int )
return ( null )
} ,
2024-06-29 17:09:25 +00:00
//player_enters_room() { // Or if the mod is loaded while player is in the room
// this.hello()
//},
2023-12-17 03:52:43 +00:00
set _timezone ( args ) {
const tz = Number . parseInt ( args [ 0 ] , 10 )
if ( Number . isNaN ( tz ) ) throw new Error ( ` invalid offset " ${ args [ 0 ] } " ` )
if ( ! this . in ( tz , - 12 , 12 ) ) throw new Error ( 'offset should be [-12,12]' )
const char = this . target2char ( args [ 1 ] )
char . MBCHC _LOCAL . TZ = tz
this . save _settings ( s => {
if ( ! s . timezones ) s . timezones = { }
2024-06-29 17:09:25 +00:00
s . timezones [ cid ( char ) ] = tz
2023-12-17 03:52:43 +00:00
} )
} ,
update _char ( char ) {
2024-06-29 17:09:25 +00:00
//char.cid = char.MemberNumber // Club ID (shorter)
char . dn = w . CharacterNickname ( char ) // DisplayName (shortcut)
2023-12-17 03:52:43 +00:00
if ( ! char . MBCHC _LOCAL ) char . MBCHC _LOCAL = { }
if ( ! char . MBCHC _LOCAL . TZ ) char . MBCHC _LOCAL . TZ = this . find _timezone ( char )
} ,
command _mbchc ( argline , cmdline , args ) {
2024-06-29 17:09:25 +00:00
const mbchc = w . MBCHC
2023-12-17 03:52:43 +00:00
try { // `this` is command object
if ( args . length === 0 ) return ( mbchc . inform ( Object . entries ( mbchc . SUBCOMMANDS _MBCHC ) . map ( ( [ cmd , sub ] ) => ` <div>/mbchc ${ cmd } ${ sub . args ? Object . keys ( sub . args ) . join ( ' ' ) : '' } : ${ sub . desc } </div> ` ) . join ( '' ) ) )
const cmd = String ( args . shift ( ) )
const sub = mbchc . ensure ( ` unknown subcommand " ${ cmd } " ` , ( ) => mbchc . SUBCOMMANDS _MBCHC [ cmd ] )
sub . cb . call ( mbchc , mbchc , args , argline , cmdline )
} catch ( error ) {
mbchc . report ( error )
}
} ,
command _activity ( argline , cmdline , _ ) {
2024-06-29 17:09:25 +00:00
const mbchc = w . MBCHC
2023-12-17 03:52:43 +00:00
if ( ! mbchc . empty ( argline ) ) {
try { // `this` is command object
const message = mbchc . normalise _message ( cmdline . replace ( mbchc . RE _ACTIVITY , '' ) , { trim : true , dot : true , up : true } )
mbchc . send _activity ( message )
} catch ( error ) {
mbchc . report ( error )
}
}
} ,
command _do ( argline , cmdline , args ) {
2024-06-29 17:09:25 +00:00
const mbchc = w . MBCHC
2023-12-17 03:52:43 +00:00
try { // `this` is command object
if ( args . length === 0 ) return ( mbchc . inform ( '<div>Usage: /do VERB [ZONE] [TARGET]</div><div>Available verbs:</div>' + Object . keys ( mbchc . MAP _ACTIONS ) . join ( ', ' ) + '<div>Available zones:</div>' + Object . keys ( mbchc . DO _DATA . zones ) . join ( ', ' ) ) )
let [ verb , zone , target ] = args
const zones = mbchc . ensure ( ` unknown verb " ${ verb } " ` , ( ) => mbchc . DO _DATA . verbs [ verb ] )
if ( Object . keys ( zones ) . length === 1 ) {
if ( ! target ) target = zone
zone = mbchc . MAP _ZONES [ Object . keys ( zones ) [ 0 ] ] [ 0 ]
}
if ( ! zone ) throw new Error ( 'zone missing' )
const ag = mbchc . ensure ( ` unknown zone " ${ zone } " ` , ( ) => mbchc . DO _DATA . zones [ zone ] )
const types = mbchc . ensure ( ` zone " ${ zone } " invalid for " ${ verb } " ` , ( ) => zones [ ag ] )
2024-06-29 17:09:25 +00:00
let char = w . Player
2023-12-17 03:52:43 +00:00
if ( target && ( ( types . self . length === 0 ) || ( types . others . length > 0 ) ) ) char = mbchc . target2char ( target )
const type = char . IsPlayer ( ) ? 'self' : 'others'
2024-06-29 17:09:25 +00:00
const available = w . ActivityAllowedForGroup ( char , ag )
//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)))
2023-12-17 03:52:43 +00:00
const actions = mbchc . ensure ( ` zone " ${ zone } " invalid for (" ${ verb } " " ${ type } ") ` , ( ) => types [ type ] )
2024-06-29 17:09:25 +00:00
const action = mbchc . ensure ( ` invalid action ( ${ verb } ${ zone } ${ target } ) ` , ( ) => actions . find ( name => available . find ( a => a . Activity ? . Name === name ) ) )
2023-12-17 03:52:43 +00:00
mbchc . run _activity ( char , ag , action )
} catch ( error ) {
mbchc . report ( error )
}
} ,
bell ( ) {
setTimeout ( ( ) => {
2024-06-29 17:09:25 +00:00
style ( '#InputChat' , s => s . outline = '' )
2023-12-17 03:52:43 +00:00
} , 100 )
2024-06-29 17:09:25 +00:00
style ( '#InputChat' , s => s . outline = 'solid red' )
2023-12-17 03:52:43 +00:00
} ,
complete ( options , space = true ) {
if ( options . length === 0 ) return ( this . 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
}
}
if ( pref ) this . complete ( [ pref ] , false )
2024-06-29 17:09:25 +00:00
this . comp _hint ( options )
} else w . ElementValue ( 'InputChat' , w . ElementValue ( 'InputChat' ) . replace ( this . RE _LAST _WORD , ` $ 1 ${ options [ 0 ] } ${ space ? ' ' : '' } ` ) )
2023-12-17 03:52:43 +00:00
} ,
2024-06-29 17:09:25 +00:00
/ * *
* Displays strings as completion hint
* @ param { string [ ] } options List of words to display . The order will be modified without copy .
* @ returns { void }
* /
comp _hint ( options ) {
if ( options . length === 0 ) return
this . COMP _HINT . innerHTML = '<div>' + options . sort ( ) . reverse ( ) . map ( s => ` <div> ${ s } </div> ` ) . join ( '' ) + '</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-06-29 17:09:25 +00:00
/ * *
* Returns true if the completion box is attached to body and its display isn ' t none
* @ returns { boolean }
* /
2023-12-17 03:52:43 +00:00
comp _hint _visible ( ) {
2024-06-29 17:09:25 +00:00
return ( this . COMP _HINT . parentElement && this . COMP _HINT . style . display !== 'none' )
2023-12-17 03:52:43 +00:00
} ,
2024-06-29 17:09:25 +00:00
comp _hint _hide ( ) {
2023-12-17 03:52:43 +00:00
if ( ! this . comp _hint _visible ( ) ) return
this . COMP _HINT . style . display = 'none'
2024-06-29 17:09:25 +00:00
w . ChatRoomResize ( false )
2023-12-17 03:52:43 +00:00
} ,
complete _target ( token , me2 = true , check _perms = false ) {
const [ locase , found ] = [ token . toLocaleLowerCase ( ) , new Set ( ) ]
2024-06-29 17:09:25 +00:00
for ( const c of w . ChatRoomCharacter ) {
if ( ( c . IsPlayer ( ) && ! me2 ) || ( check _perms && ! w . ServerChatRoomGetAllowItem ( w . Player , c ) ) ) continue
2023-12-17 03:52:43 +00:00
for ( const s of this . char2targets ( c ) ) {
if ( s . toLocaleLowerCase ( ) . startsWith ( locase ) ) found . add ( s )
}
}
this . complete ( Array . from ( found ) )
} ,
complete _common ( ) {
2024-06-29 17:09:25 +00:00
// w.ElementValue('InputChat') will strip the trailing whitespace
const E = document . querySelector ( '#InputChat' )
if ( ! ( E && E instanceof HTMLTextAreaElement ) ) throw new Error ( 'somehow InputChat is broken' )
return ( [ this , E . value , this . tokenise ( E . value ) ] )
2023-12-17 03:52:43 +00:00
} ,
complete _mbchc ( _args , _locase , _cmdline ) {
2024-06-29 17:09:25 +00:00
const [ mbchc , _input , tokens ] = w . MBCHC . complete _common ( ) ; // `this` is command object
2023-12-17 03:52:43 +00:00
if ( tokens . length === 0 ) return
if ( tokens . length < 2 ) return ( mbchc . complete ( [ ` ${ mbchc . CommandsKey } ${ this . Tag } ` ] ) )
const subname = tokens [ 1 ] . toLocaleLowerCase ( )
if ( tokens . length < 3 ) return ( mbchc . complete ( Object . keys ( mbchc . SUBCOMMANDS _MBCHC ) . filter ( c => c . startsWith ( subname ) ) ) ) // Complete subcommand name
const sub = mbchc . 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 ) ) , true )
}
} ,
complete _do _target ( actions , token ) {
if ( ! actions ) return
const me2 = ( actions . self . length > 0 )
2024-06-29 17:09:25 +00:00
if ( me2 && actions . others . length === 0 ) return ( this . complete ( [ cid ( w . Player ) . toString ( ) ] ) ) // Target is always the player
2023-12-17 03:52:43 +00:00
this . complete _target ( token , me2 , true )
} ,
complete _do ( _args , _locase , _cmdline ) {
2024-06-29 17:09:25 +00:00
const [ mbchc , _input , tokens ] = w . MBCHC . complete _common ( ) ; // `this` is command object
2023-12-17 03:52:43 +00:00
if ( tokens . length === 0 ) return
if ( tokens . length < 2 ) return ( mbchc . complete ( [ ` ${ mbchc . 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 = tokens [ 1 ] . toLocaleLowerCase ( )
if ( tokens . length < 3 ) return ( mbchc . complete ( Object . keys ( mbchc . DO _DATA . verbs ) . filter ( c => c . startsWith ( low ) ) ) ) // Complete verb
const ags = mbchc . DO _DATA . verbs [ low ]
if ( ! ags ) return ( mbchc . bell ( ) )
low = tokens [ 2 ] . toLocaleLowerCase ( )
if ( tokens . length < 4 ) { // Complete zone or target
if ( Object . keys ( ags ) . length < 2 ) return ( mbchc . complete _do _target ( ags [ Object . keys ( ags ) [ 0 ] ] , tokens [ 2 ] ) ) // Zone implied, complete target
const zones = Object . entries ( mbchc . DO _DATA . zones ) . filter ( ( [ zone , ag ] ) => zone . startsWith ( low ) && ags [ ag ] ) . map ( ( [ zone , _ag ] ) => zone )
return ( mbchc . complete ( zones ) )
}
if ( tokens . length < 5 ) { // Complete target where it belongs
if ( Object . keys ( ags ) . length < 2 ) return // Zone implied, target already given
return ( mbchc . complete _do _target ( ags [ mbchc . DO _DATA . zones [ low ] ] , tokens [ 3 ] ) )
}
mbchc . bell ( )
} ,
complete _fbc _anim ( _args , _locase , _cmdline ) {
2024-06-29 17:09:25 +00:00
const [ mbchc , _input , tokens ] = w . MBCHC . complete _common ( ) ; // `this` is command object
2023-12-17 03:52:43 +00:00
if ( tokens . length === 0 ) return
if ( tokens . length < 2 ) return ( mbchc . complete ( [ ` ${ mbchc . CommandsKey } ${ this . Tag } ` ] ) )
if ( tokens . length > 2 ) return ( mbchc . bell ( ) )
const anim = tokens [ 1 ] . toLocaleLowerCase ( )
2024-06-29 17:09:25 +00:00
return ( mbchc . complete ( Object . keys ( w . bce _EventExpressions ) . filter ( a => a . toLocaleLowerCase ( ) . startsWith ( anim ) ) ) )
2023-12-17 03:52:43 +00:00
} ,
complete _fbc _pose ( _args , _locase , _cmdline ) {
2024-06-29 17:09:25 +00:00
const [ mbchc , _input , tokens ] = w . MBCHC . complete _common ( ) ; // `this` is command object
2023-12-17 03:52:43 +00:00
if ( tokens . length === 0 ) return
if ( tokens . length < 2 ) return ( mbchc . complete ( [ ` ${ mbchc . CommandsKey } ${ this . Tag } ` ] ) )
const pose = tokens . at ( - 1 ) . toLocaleLowerCase ( )
2024-06-29 17:09:25 +00:00
return ( mbchc . complete ( w . PoseFemale3DCG . map ( p => p . Name ) . filter ( p => p . toLocaleLowerCase ( ) . startsWith ( pose ) ) ) )
2023-12-17 03:52:43 +00:00
} ,
history ( down ) {
2024-06-29 17:09:25 +00:00
const [ text , history ] = [ w . ElementValue ( 'InputChat' ) , w . ChatRoomLastMessage ]
2023-12-17 03:52:43 +00:00
if ( ! this . HISTORY _MODE ) {
history . push ( text )
this . HISTORY _MODE = true
}
2024-06-29 17:09:25 +00:00
const ids = history . map ( ( t , i ) => ( { t , i } ) ) . filter ( r => r . t . startsWith ( history . at ( - 1 ) ) ) . map ( r => r . i )
2023-12-17 03:52:43 +00:00
if ( ! down ) ids . reverse ( )
2024-06-29 17:09:25 +00:00
const found = ids . find ( id => ( down ) ? id > w . ChatRoomLastMessageIndex : id < w . ChatRoomLastMessageIndex )
2023-12-17 03:52:43 +00:00
if ( ! found ) return ( this . bell ( ) )
2024-06-29 17:09:25 +00:00
w . ElementValue ( 'InputChat' , history [ found ] )
w . ChatRoomLastMessageIndex = found
2023-12-17 03:52:43 +00:00
} ,
2023-12-19 01:15:39 +00:00
focus _chat _checks ( ) { // we only want to catch chatlog and canvas (no map though) keypresses
if ( document . activeElement === document . body ) return true
if ( document . activeElement . id !== 'MainCanvas' ) return false
2024-06-29 17:09:25 +00:00
return ! w . ChatRoomMapViewIsActive ( )
2023-12-19 01:15:39 +00:00
} ,
2023-12-17 03:52:43 +00:00
focus _chat _whitelist ( event ) {
if ( event . ctrlKey && event . key === 'v' ) return true // Ctrl+V should paste
return false
} ,
2023-12-19 01:15:39 +00:00
focus _chat ( event ) {
2023-12-17 03:52:43 +00:00
if ( event . repeat ) return // Only unique presses please
2023-12-19 01:15:39 +00:00
if ( ! this . focus _chat _checks ( ) ) return
2023-12-17 03:52:43 +00:00
if ( [ event . altKey , event . ctrlKey , event . metaKey ] . some ( Boolean ) && ! this . focus _chat _whitelist ( event ) ) return // Alt, ctrl and meta should all be false
2024-06-29 17:09:25 +00:00
if ( style ( '#InputChat' , s => s . display ) !== 'inline' ) return // Input chat missing
w . ElementFocus ( 'InputChat' )
2023-12-17 03:52:43 +00:00
} ,
loader ( ) {
if ( this . remove _load _hook ) {
this . remove _load _hook ( )
delete this . remove _load _hook
}
if ( this . LOADED ) return
// Calculated values
const COMMANDS = [
{ Tag : 'mbchc' , Description : ': Utility functions ("/mbchc" for help)' , Action : this . command _mbchc , AutoComplete : this . complete _mbchc } ,
{ Tag : 'activity' , Description : '[Message]: Send a custom activity (or "@@Message", or "@Message" as yourself)' , Action : this . command _activity } ,
{ Tag : 'do' , Description : ': Do an activity, as if clicked on its button ("/do" for help)' , Action : this . command _do , AutoComplete : this . complete _do } ,
]
this . CommandsKey = CommandsKey /* eslint-disable-line no-undef */ // window.CommandsKey is undefined
this . RE _ACTIVITY = new RegExp ( ` ^ ${ this . CommandsKey } activity ` )
this . PREF _ACTIVITY = ` ${ this . CommandsKey } activity `
this . COMP _HINT = document . createElement ( 'div' )
this . COMP _HINT . id = 'mbchcCompHint'
const css = document . createElement ( 'style' )
css . textContent = `
# TextAreaChatLog . mbchc , # TextAreaChatLog . mbchc {
background - color : $ { this . RGB _POLLY } ;
}
# TextAreaChatLog [ data - colortheme = "dark" ] . mbchc , # TextAreaChatLog [ data - colortheme = "dark2" ] . mbchc {
background - color : $ { this . RGB _MUTE } ;
}
# $ { this . COMP _HINT . id } {
display : none ;
2024-06-29 17:09:25 +00:00
text - align : right ;
}
# $ { this . COMP _HINT . id } > div {
overflow : auto ;
position : absolute ;
bottom : 0 ;
right : 0 ;
max - height : 100 % ;
padding : 0 0.5 ex ;
2023-12-17 03:52:43 +00:00
background - color : $ { this . RGB _POLLY } ;
color : black ;
}
2024-06-29 17:09:25 +00:00
# $ { this . COMP _HINT . id } [ data - colortheme = "dark" ] > div , # $ { this . COMP _HINT . id } [ data - colortheme = "dark2" ] > div {
2023-12-17 03:52:43 +00:00
background - color : $ { this . RGB _MUTE } ;
color : white ;
}
2024-06-29 17:09:25 +00:00
# $ { this . COMP _HINT . id } > div div {
margin : 0.25 ex 0 ;
2023-12-17 03:52:43 +00:00
}
`
2024-06-29 17:09:25 +00:00
2023-12-17 03:52:43 +00:00
document . head . append ( css )
// Actions
this . calculate _maps ( )
2024-06-29 17:09:25 +00:00
//w.Player.MBCHC = {VERSION: this.VERSION}
w . CommandCombine ( COMMANDS )
2023-12-17 03:52:43 +00:00
// Hooks
2024-06-29 17:09:25 +00:00
this . remove _fbc _hook = this . before ( 'MainRun' , ( ) => w . bce _ActivityTriggers && this . patch _fbc ( ) )
2023-12-17 03:52:43 +00:00
this . after ( 'CharacterOnlineRefresh' , char => this . update _char ( char ) )
this . after ( 'ChatRoomReceiveSuitcaseMoney' , ( ) => {
if ( this . AUTOHACK _ENABLED && this . LAST _HACKED ) {
2024-06-29 17:09:25 +00:00
w . CurrentCharacter = this . cid2char ( this . LAST _HACKED )
2023-12-17 03:52:43 +00:00
this . LAST _HACKED = null
2024-06-29 17:09:25 +00:00
w . ChatRoomTryToTakeSuitcase ( )
2023-12-17 03:52:43 +00:00
}
} )
this . before ( 'ChatRoomSendChat' , ( ) => {
2024-06-29 17:09:25 +00:00
let input = w . ElementValue ( 'InputChat' )
2023-12-17 03:52:43 +00:00
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 )
2024-06-29 17:09:25 +00:00
w . ElementValue ( 'InputChat' , input )
2023-12-17 03:52:43 +00:00
}
} )
this . after ( 'ChatRoomSendChat' , ( ) => {
2024-06-29 17:09:25 +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-06-29 17:09:25 +00:00
w . ChatRoomLastMessageIndex -= 1
2023-12-17 03:52:43 +00:00
}
} )
2024-02-29 18:17:35 +00:00
this . before ( 'ChatRoomCharacterViewDrawOverlay' , ( C , CharX , CharY , Zoom , _Pos ) => {
2024-06-29 17:09:25 +00:00
// if (w.ChatRoomHideIconState < 1 && C.MBCHC) {
// w.DrawRect(CharX + (175 * Zoom), CharY, 50 * Zoom, 50 * Zoom, C.MBCHC.VERSION === w.Player.MBCHC.VERSION ? this.RGB_POLLY : this.RGB_MUTE)
2023-12-19 01:15:39 +00:00
// }
2024-06-29 17:09:25 +00:00
if ( w . ChatRoomHideIconState < 1 && C . MBCHC _LOCAL && typeof C . MBCHC _LOCAL . TZ === 'number' ) {
const hours = new Date ( w . CommonTime ( ) + this . UTC _OFFSET + ( C . MBCHC _LOCAL . TZ * 60 * 60 * 1000 ) ) . getHours ( )
w . DrawTextFit ( hours < 10 ? '0' + hours . toString ( ) : hours . toString ( ) , CharX + ( 200 * Zoom ) , CharY + ( 25 * Zoom ) , 46 * Zoom , 'white' , 'black' )
2023-12-17 03:52:43 +00:00
}
} )
this . after ( 'ChatRoomCreateElement' , ( ) => this . COMP _HINT . parentElement || document . body . append ( this . COMP _HINT ) )
this . before ( 'ChatRoomClearAllElements' , ( ) => {
2024-06-29 17:09:25 +00:00
this . comp _hint _hide ( )
2023-12-17 03:52:43 +00:00
this . COMP _HINT . remove ( )
} )
2024-06-29 17:09:25 +00:00
this . before ( 'ChatRoomClick' , ( ) => {
this . comp _hint _hide ( )
} )
2023-12-17 03:52:43 +00:00
this . after ( 'ChatRoomResize' , ( ) => {
2024-06-29 17:09:25 +00:00
if ( w . CharacterGetCurrent ( ) === null && w . CurrentScreen === 'ChatRoom' && document . querySelector ( '#InputChat' ) && document . querySelector ( '#TextAreaChatLog' ) && this . comp _hint _visible ( ) ) { // Upstream
2023-12-17 03:52:43 +00:00
const fontsize = ChatRoomFontSize /* eslint-disable-line no-undef */ // window.ChatRoomFontSize is undefined
2024-06-29 17:09:25 +00:00
//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'
2023-12-17 03:52:43 +00:00
}
} )
document . addEventListener ( 'keydown' , event => this . focus _chat ( event ) )
this . SDK . hookFunction ( 'ChatRoomKeyDown' , 0 , ( nextargs , next ) => { // This fires on chat input events
const [ event ] = nextargs
2024-06-29 17:09:25 +00:00
w . MBCHC . comp _hint _hide ( )
if ( ( w . KeyPress === 33 ) || ( w . KeyPress === 34 ) ) { // Better history
2023-12-17 03:52:43 +00:00
event . preventDefault ( )
2024-06-29 17:09:25 +00:00
return ( w . MBCHC . history ( w . KeyPress - 33 ) )
2023-12-17 03:52:43 +00:00
}
2024-06-29 17:09:25 +00:00
if ( w . MBCHC . HISTORY _MODE ) {
w . ChatRoomLastMessage . pop ( )
w . MBCHC . HISTORY _MODE = false
2023-12-17 03:52:43 +00:00
}
return ( next ( nextargs ) )
} )
// Chat room handlers
2024-06-29 17:09:25 +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-06-29 17:09:25 +00:00
//w.ChatRoomRegisterMessageHandler({Priority: -219, Description: 'MBCHC room enter hook',
// Callback: (data, _sender, _message, _metadata) => {
// if ((data.Type === 'Action') && (data.Content === 'ServerEnter') && (data.Sender === cid(w.Player))) this.player_enters_room()
// return false
// },
//})
//w.ChatRoomRegisterMessageHandler({Priority: -219, Description: 'MBCHC specific consumer',
// Callback: (data, _sender, _message, _metadata) => {
// if ((data.Type === 'Hidden') && (data.Content === 'MBCHC')) return this.receive(data)
// return false
// },
//})
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
this . log ( 'info' , ` loaded version ${ this . VERSION } ` )
2024-06-29 17:09:25 +00:00
if ( ( w . CurrentModule === 'Online' ) && ( w . CurrentScreen === 'ChatRoom' ) ) {
for ( const c of w . ChatRoomCharacter ) this . update _char ( c )
//this.player_enters_room()
2023-12-17 03:52:43 +00:00
}
} ,
preloader ( ) {
2024-06-29 17:09:25 +00:00
if ( ! w . AsylumGGTSSAddItems ) throw new Error ( 'AsylumGGTSSAddItems() not found, aborting MBCHC loading' )
if ( ! w . bcModSdk ) throw new Error ( 'SDK not found, please load with (or after) FUSAM' )
this . SDK = w . bcModSdk . registerMod ( { name : 'MBCHC' , fullName : 'Mute\'s Bondage Club Hacks Collection' , version : this . VERSION , repository : 'https://code.fleshless.org/mute/MBCHC/' } )
2023-12-17 03:52:43 +00:00
this . before = ( name , cb ) => this . SDK . hookFunction ( name , 0 , ( nextargs , next ) => {
try {
cb ? . ( ... nextargs )
} catch ( error ) {
console . error ( error )
}
return next ( nextargs )
} )
this . after = ( name , cb ) => this . SDK . hookFunction ( name , 0 , ( nextargs , next ) => {
const result = next ( nextargs ) ;
try {
cb ? . ( ... nextargs )
} catch ( error ) {
console . error ( error )
}
return result
} )
2024-06-29 17:09:25 +00:00
// for some reason many (not all) hooks don't work if the mod is loaded in the room
// to be honest I have no idea what's going on. the hooks get registered, they just don't get called by SDK.
2024-08-18 18:32:09 +00:00
// if (current() === 'Online/ChatRoom') throw new Error('please do not load in a chat room')
2024-06-29 17:09:25 +00:00
if ( current ( ) === 'Character/Login' ) {
this . remove _load _hook = this . before ( 'AsylumGGTSSAddItems' , ( ) => this . loader ( ) )
} else this . loader ( )
2023-12-17 03:52:43 +00:00
} ,
}
2024-06-29 17:09:25 +00:00
w . MBCHC . preloader ( )