Compare commits
1 Commits
fe4248eb8b
...
refactor
Author | SHA1 | Date | |
---|---|---|---|
2f4d2f9f50 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +0,0 @@
|
||||
node_modules
|
||||
|
96
README.md
96
README.md
@@ -2,9 +2,97 @@
|
||||
This document is updated with stable releases, for additional documentation please consult [[the wiki|https://code.fleshless.org/mute/MBCHC/wiki/Home]].
|
||||
|
||||
* Unstable doc: https://code.fleshless.org/mute/MBCHC/wiki/unstable
|
||||
* Trunk doc: https://code.fleshless.org/mute/MBCHC/wiki/trunk
|
||||
|
||||
## PSA
|
||||
The only supported version is unstable, don't use any other.
|
||||
## Stable vs. unstable versions
|
||||
Stable version is updated less often and after some testing. Hopefully it contains less bugs, but also is behind on new features. By the same token, unstable version is updated more often and with minimal testing by the authors. Think of it as beta versions of the club. Trunk version is the least tested and the most updated.
|
||||
|
||||
## Feedback
|
||||
We are available at the club in a private room named `MBCHC`. If Mute isn't there, leave a message.
|
||||
## Loader scripts
|
||||
These download the corresponding main script, making sure it's always fresh. Most Tampermonkey users should probably use the loader instead of adding the main script directly. Tampermonkey has an update check, but loaders are just more convenient if you want to always have the current version.
|
||||
|
||||
On the other hand, if you want to control the script versions yourself, don't use the loader.
|
||||
|
||||
## Installation
|
||||
### BCE integration
|
||||
None at the moment. Please ask BCE devs to add MBCHC loading if you want to have it (seems like a good idea to us).
|
||||
|
||||
### Tampermonkey
|
||||
1. Install Tampermonkey.
|
||||
2. Select a script you want to add below, then click its URL.
|
||||
3. Tampermonkey should recognise the script and prompt you for installation.
|
||||
4. Refresh the club page and enjoy.
|
||||
|
||||
### Bookmarklet
|
||||
(Doesn't work just yet, will be supported in the next release.)
|
||||
|
||||
## Scripts
|
||||
### `mbchc-loader.user.js`
|
||||
The loader script for the stable version (**if unsure, use this**).
|
||||
|
||||
URL: https://code.fleshless.org/mute/MBCHC/raw/branch/master/mbchc-loader.user.js
|
||||
|
||||
### `mbchc.user.js`
|
||||
The main script, stable version.
|
||||
|
||||
URL: https://code.fleshless.org/mute/MBCHC/raw/branch/master/mbchc.user.js
|
||||
|
||||
### `mbchc-loader-dev.user.js`
|
||||
The loader script for the unstable version (**use this if you want to help testing**).
|
||||
|
||||
URL: https://code.fleshless.org/mute/MBCHC/raw/branch/master/mbchc-loader-dev.user.js
|
||||
|
||||
### `mbchc-dev.user.js`
|
||||
The main script, unstable version.
|
||||
|
||||
URL: https://code.fleshless.org/mute/MBCHC/raw/branch/master/mbchc-dev.user.js
|
||||
|
||||
### `mbchc-local.user.js`
|
||||
The main script, trunk version. (**use this if you really want the latest**).
|
||||
|
||||
URL: https://code.fleshless.org/mute/MBCHC/raw/branch/master/mbchc-local.user.js
|
||||
|
||||
## Features
|
||||
These are new commands this mod adds, use `/help` for syntax.
|
||||
|
||||
### /disappear
|
||||
This command will make you invisible, if you are wearing an anal hook tied to hair.
|
||||
|
||||
You might also want to consider hiding your arousal meter.
|
||||
|
||||
### /autohack
|
||||
Toggles an autohack mode. Currently this mode starts hacking again as soon as the data gets stolen.
|
||||
|
||||
Be informed that autohacking is much less lucrative due to game mechanics. No plans to "fix" this.
|
||||
|
||||
### /donate
|
||||
Buy data on the black market and send it to someone. Serves as money transfer. Doesn't require anything from the other party except from being restrained. Data will cost you more than it will make, this is by design.
|
||||
|
||||
### /nod
|
||||
Send a nod activity, as if clicked on the button.
|
||||
|
||||
### /shake
|
||||
Send a headshake activity, as if clicked on the button.
|
||||
|
||||
### /shrug
|
||||
Send a shrug activity.
|
||||
|
||||
### /myself (or @)
|
||||
Send a custom activity as yourself. Example: "@shivers" will send "(YourName shivers.)"
|
||||
|
||||
### /activity (or @@)
|
||||
Send a custom activity. Example: "@@it starts to rain" will send "(It starts to rain.)"
|
||||
|
||||
### /title
|
||||
Set a custom title. Currenty only works with the same titles you can select. **Work in progress.**
|
||||
|
||||
## Geek corner
|
||||
Yes, we are aware of all the nifty git features (branches etc.), we just don't use them.
|
||||
This mod is low effort, just enough to work without errors and deliver features we find useful.
|
||||
We also have to keep in mind not everyone in the club works in software development, so it needs to be
|
||||
as user-friendly as possible.
|
||||
|
||||
## Contributing
|
||||
Sure, welcome onboard. Please send pull requests, patches, ideas, feedback, feature requests and bug reports.
|
||||
|
||||
## Contacts
|
||||
Beyond social functions this site provides, you can always find us in the club or drop us an email. We don't use discord.
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"include": [
|
||||
"node_modules/bc-stubs/bc/**/*.d.ts",
|
||||
"node_modules/bondage-club-mod-sdk/dist/**/*.d.ts",
|
||||
"typedef.d.ts",
|
||||
"mbchc.mjs",
|
||||
"server.js"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"es2022",
|
||||
"DOM"
|
||||
],
|
||||
"checkJs": true,
|
||||
"strict": false,
|
||||
"noImplicitOverride": true
|
||||
}
|
||||
}
|
1335
mbchc-dev.user.js
1335
mbchc-dev.user.js
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
800
mbchc.mjs
800
mbchc.mjs
@@ -1,800 +0,0 @@
|
||||
export {}
|
||||
|
||||
/** @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
|
||||
const MISSING_PLAYER_DIALOG = {Tag: 'MISSING TEXT IN "Interface.csv": ', Text: '\u200C'}
|
||||
|
||||
if (w.MBCHC) throw new Error('MBCHC found, aborting loading')
|
||||
w.MBCHC = {
|
||||
VERSION: 'dev.12',
|
||||
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) {
|
||||
if (w.Player.OnlineSettings.MBCHC) {
|
||||
delete w.Player.OnlineSettings.MBCHC
|
||||
mbchc.save_settings()
|
||||
}
|
||||
}},
|
||||
},
|
||||
ensure(text, callback) {
|
||||
const result = callback.call(this)
|
||||
if (!result) throw new Error(text)
|
||||
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) {
|
||||
const settings = w.Player.OnlineSettings.MBCHC || {}
|
||||
return (setting ? settings[setting] : settings)
|
||||
},
|
||||
save_settings(cb = null) {
|
||||
if (cb) {
|
||||
if (!w.Player.OnlineSettings.MBCHC) w.Player.OnlineSettings.MBCHC = {}
|
||||
cb.call(this, w.Player.OnlineSettings.MBCHC)
|
||||
}
|
||||
w.ServerAccountUpdate.QueueData({OnlineSettings: w.Player.OnlineSettings})
|
||||
},
|
||||
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) {
|
||||
w.ChatRoomSendLocal(`<div class="mbchc">${html}</div>`, timeout)
|
||||
},
|
||||
report(x) {
|
||||
this.inform(`${x.toString()}`)
|
||||
if (this.RETHROW) throw x
|
||||
},
|
||||
in(x, floor, ceiling) {
|
||||
return ((x >= floor) && (x <= ceiling))
|
||||
},
|
||||
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)))
|
||||
},
|
||||
pos2char(pos) {
|
||||
if (!this.in(pos, 0, w.ChatRoomCharacter.length - 1)) throw new Error(`invalid position ${pos}`)
|
||||
return (w.ChatRoomCharacter[pos])
|
||||
},
|
||||
rel2char(target) {
|
||||
const me = this.ensure('can\'t find my position', () => w.ChatRoomCharacter.findIndex(char => char.IsPlayer()) + 1) - 1 // 0 is falsey, but is valid index
|
||||
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}"`)
|
||||
pos %= w.ChatRoomCharacter.length
|
||||
if (pos < 0) pos += w.ChatRoomCharacter.length
|
||||
return (this.pos2char(pos))
|
||||
},
|
||||
target2char(target) { // Target should be lowcase
|
||||
const input = target
|
||||
if (this.empty(target)) return (w.Player)
|
||||
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))
|
||||
found.push(...w.ChatRoomCharacter.filter(c => cid(c).toString().includes(target)))
|
||||
}
|
||||
|
||||
if (target.startsWith('@')) target = target.slice(1)
|
||||
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
|
||||
const map = {}
|
||||
for (const c of found) {
|
||||
if (!map[cid(c)]) map[cid(c)] = c
|
||||
}
|
||||
|
||||
found = Object.values(map)
|
||||
if (found.length === 0) throw new Error(`target "${input}": no match`)
|
||||
if (found.length > 1) throw new Error(`target "${input}": multiple matches (${found.map(c => `${cid(c)}|${c.Name}|${c.Nickname || c.Name}`).join(',')})`)
|
||||
return (found[0])
|
||||
},
|
||||
char2targets(char) {
|
||||
const [result, id] = [new Set(), cid(char).toString()]
|
||||
result.add(id).add(`=${id}`)
|
||||
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))
|
||||
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]})
|
||||
},
|
||||
run_activity(char, ag, action) {
|
||||
try {
|
||||
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)
|
||||
} finally {
|
||||
char.FocusGroup = null
|
||||
}
|
||||
},
|
||||
replace_me(match, offset, string) {
|
||||
const text = string.slice(1)
|
||||
let suffix = ' '
|
||||
if (text.startsWith('\'') || text.startsWith(' ')) suffix = ''
|
||||
return `${w.MBCHC.PREF_ACTIVITY}<${cid(w.Player)}:>SourceCharacter${suffix}`
|
||||
},
|
||||
cid2dict(type, cid) {
|
||||
return ({Tag: `${type}Character`, MemberNumber: cid, Text: this.cid2char(cid).dn})
|
||||
},
|
||||
send_activity(message) {
|
||||
const dict = [MISSING_PLAYER_DIALOG]
|
||||
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]))
|
||||
}
|
||||
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)
|
||||
//},
|
||||
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
|
||||
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')
|
||||
if (cmd) cmd.AutoComplete = this.complete_fbc_anim
|
||||
cmd = w.Commands.find(c => c.Tag === 'pose')
|
||||
if (cmd) cmd.AutoComplete = this.complete_fbc_pose
|
||||
},
|
||||
find_timezone(char) {
|
||||
const timezones = this.settings('timezones')
|
||||
if (timezones && typeof timezones[cid(char)] === 'number') return (timezones[cid(char)])
|
||||
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)
|
||||
},
|
||||
//player_enters_room() { // Or if the mod is loaded while player is in the room
|
||||
// this.hello()
|
||||
//},
|
||||
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 = {}
|
||||
s.timezones[cid(char)] = tz
|
||||
})
|
||||
},
|
||||
update_char(char) {
|
||||
//char.cid = char.MemberNumber // Club ID (shorter)
|
||||
char.dn = w.CharacterNickname(char) // DisplayName (shortcut)
|
||||
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) {
|
||||
const mbchc = w.MBCHC
|
||||
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, _) {
|
||||
const mbchc = w.MBCHC
|
||||
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) {
|
||||
const mbchc = w.MBCHC
|
||||
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])
|
||||
let char = w.Player
|
||||
if (target && ((types.self.length === 0) || (types.others.length > 0))) char = mbchc.target2char(target)
|
||||
const type = char.IsPlayer() ? 'self' : 'others'
|
||||
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)))
|
||||
const actions = mbchc.ensure(`zone "${zone}" invalid for ("${verb}" "${type}")`, () => types[type])
|
||||
const action = mbchc.ensure(`invalid action (${verb} ${zone} ${target})`, () => actions.find(name => available.find(a => a.Activity?.Name === name)))
|
||||
mbchc.run_activity(char, ag, action)
|
||||
} catch (error) {
|
||||
mbchc.report(error)
|
||||
}
|
||||
},
|
||||
bell() {
|
||||
setTimeout(() => {
|
||||
style('#InputChat', s => s.outline = '')
|
||||
}, 100)
|
||||
style('#InputChat', s => s.outline = 'solid red')
|
||||
},
|
||||
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)
|
||||
this.comp_hint(options)
|
||||
} else w.ElementValue('InputChat', w.ElementValue('InputChat').replace(this.RE_LAST_WORD, `$1${options[0]}${space ? ' ' : ''}`))
|
||||
},
|
||||
|
||||
/**
|
||||
* 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'})
|
||||
},
|
||||
|
||||
/**
|
||||
* 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)
|
||||
},
|
||||
complete_target(token, me2 = true, check_perms = false) {
|
||||
const [locase, found] = [token.toLocaleLowerCase(), new Set()]
|
||||
for (const c of w.ChatRoomCharacter) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
this.complete(Array.from(found))
|
||||
},
|
||||
complete_common() {
|
||||
// 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)])
|
||||
},
|
||||
complete_mbchc(_args, _locase, _cmdline) {
|
||||
const [mbchc, _input, tokens] = w.MBCHC.complete_common(); // `this` is command object
|
||||
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)
|
||||
if (me2 && actions.others.length === 0) return (this.complete([cid(w.Player).toString()])) // Target is always the player
|
||||
this.complete_target(token, me2, true)
|
||||
},
|
||||
complete_do(_args, _locase, _cmdline) {
|
||||
const [mbchc, _input, tokens] = w.MBCHC.complete_common(); // `this` is command object
|
||||
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) {
|
||||
const [mbchc, _input, tokens] = w.MBCHC.complete_common(); // `this` is command object
|
||||
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()
|
||||
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 (tokens.length === 0) return
|
||||
if (tokens.length < 2) return (mbchc.complete([`${mbchc.CommandsKey}${this.Tag}`]))
|
||||
const pose = tokens.at(-1).toLocaleLowerCase()
|
||||
return (mbchc.complete(w.PoseFemale3DCG.map(p => p.Name).filter(p => p.toLocaleLowerCase().startsWith(pose))))
|
||||
},
|
||||
history(down) {
|
||||
const [text, history] = [w.ElementValue('InputChat'), w.ChatRoomLastMessage]
|
||||
if (!this.HISTORY_MODE) {
|
||||
history.push(text)
|
||||
this.HISTORY_MODE = true
|
||||
}
|
||||
|
||||
const ids = history.map((t, i) => ({t, i})).filter(r => r.t.startsWith(history.at(-1))).map(r => r.i)
|
||||
if (!down) ids.reverse()
|
||||
const found = ids.find(id => (down) ? id > w.ChatRoomLastMessageIndex : id < w.ChatRoomLastMessageIndex)
|
||||
if (!found) return (this.bell())
|
||||
w.ElementValue('InputChat', history[found])
|
||||
w.ChatRoomLastMessageIndex = found
|
||||
},
|
||||
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
|
||||
return !w.ChatRoomMapViewIsActive()
|
||||
},
|
||||
focus_chat_whitelist(event) {
|
||||
if (event.ctrlKey && event.key === 'v') return true // Ctrl+V should paste
|
||||
return false
|
||||
},
|
||||
focus_chat(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 (style('#InputChat', s => s.display) !== 'inline') return // Input chat missing
|
||||
w.ElementFocus('InputChat')
|
||||
},
|
||||
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;
|
||||
text-align: right;
|
||||
}
|
||||
#${this.COMP_HINT.id} > div {
|
||||
overflow: auto;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
max-height: 100%;
|
||||
padding: 0 0.5ex;
|
||||
background-color: ${this.RGB_POLLY};
|
||||
color: black;
|
||||
}
|
||||
#${this.COMP_HINT.id}[data-colortheme="dark"] > div, #${this.COMP_HINT.id}[data-colortheme="dark2"] > div {
|
||||
background-color: ${this.RGB_MUTE};
|
||||
color: white;
|
||||
}
|
||||
#${this.COMP_HINT.id} > div div {
|
||||
margin: 0.25ex 0;
|
||||
}
|
||||
`
|
||||
|
||||
document.head.append(css)
|
||||
// Actions
|
||||
this.calculate_maps()
|
||||
//w.Player.MBCHC = {VERSION: this.VERSION}
|
||||
w.CommandCombine(COMMANDS)
|
||||
|
||||
// Hooks
|
||||
this.remove_fbc_hook = this.before('MainRun', () => w.bce_ActivityTriggers && this.patch_fbc())
|
||||
this.after('CharacterOnlineRefresh', char => this.update_char(char))
|
||||
this.after('ChatRoomReceiveSuitcaseMoney', () => {
|
||||
if (this.AUTOHACK_ENABLED && this.LAST_HACKED) {
|
||||
w.CurrentCharacter = this.cid2char(this.LAST_HACKED)
|
||||
this.LAST_HACKED = null
|
||||
w.ChatRoomTryToTakeSuitcase()
|
||||
}
|
||||
})
|
||||
this.before('ChatRoomSendChat', () => {
|
||||
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)
|
||||
}
|
||||
})
|
||||
this.after('ChatRoomSendChat', () => {
|
||||
const history = w.ChatRoomLastMessage
|
||||
if ((history.length > 1) && (history.at(-1) === history.at(-2))) {
|
||||
history.pop()
|
||||
w.ChatRoomLastMessageIndex -= 1
|
||||
}
|
||||
})
|
||||
this.before('ChatRoomCharacterViewDrawOverlay', (C, CharX, CharY, Zoom, _Pos) => {
|
||||
// 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)
|
||||
// }
|
||||
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')
|
||||
}
|
||||
})
|
||||
this.after('ChatRoomCreateElement', () => this.COMP_HINT.parentElement || document.body.append(this.COMP_HINT))
|
||||
this.before('ChatRoomClearAllElements', () => {
|
||||
this.comp_hint_hide()
|
||||
this.COMP_HINT.remove()
|
||||
})
|
||||
this.before('ChatRoomClick', () => {
|
||||
this.comp_hint_hide()
|
||||
})
|
||||
this.after('ChatRoomResize', () => {
|
||||
if (w.CharacterGetCurrent() === null && w.CurrentScreen === 'ChatRoom' && document.querySelector('#InputChat') && document.querySelector('#TextAreaChatLog') && this.comp_hint_visible()) { // Upstream
|
||||
const fontsize = ChatRoomFontSize /* eslint-disable-line no-undef */ // window.ChatRoomFontSize is undefined
|
||||
//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'
|
||||
}
|
||||
})
|
||||
document.addEventListener('keydown', event => this.focus_chat(event))
|
||||
this.SDK.hookFunction('ChatRoomKeyDown', 0, (nextargs, next) => { // This fires on chat input events
|
||||
const [event] = nextargs
|
||||
w.MBCHC.comp_hint_hide()
|
||||
if ((w.KeyPress === 33) || (w.KeyPress === 34)) { // Better history
|
||||
event.preventDefault()
|
||||
return (w.MBCHC.history(w.KeyPress - 33))
|
||||
}
|
||||
|
||||
if (w.MBCHC.HISTORY_MODE) {
|
||||
w.ChatRoomLastMessage.pop()
|
||||
w.MBCHC.HISTORY_MODE = false
|
||||
}
|
||||
|
||||
return (next(nextargs))
|
||||
})
|
||||
|
||||
// Chat room handlers
|
||||
w.ChatRoomRegisterMessageHandler({Priority: -220, Description: 'MBCHC preprocessor', Callback: (data, sender, message, metadata) => {
|
||||
data.MBCHC_ID = this.NEXT_MESSAGE
|
||||
this.NEXT_MESSAGE += 1
|
||||
if (this.LOG_MESSAGES) console.debug({data, sender, msg: message, metadata})
|
||||
return false
|
||||
}})
|
||||
//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',
|
||||
Callback: (data, _sender, _message, _metadata) => {
|
||||
if ((data.Type === 'Hidden') && (data.Content === 'ReceiveSuitcaseMoney')) this.LAST_HACKED = data.Sender
|
||||
return false
|
||||
},
|
||||
})
|
||||
|
||||
// Footer
|
||||
this.LOADED = true
|
||||
this.log('info', `loaded version ${this.VERSION}`)
|
||||
if ((w.CurrentModule === 'Online') && (w.CurrentScreen === 'ChatRoom')) {
|
||||
for (const c of w.ChatRoomCharacter) this.update_char(c)
|
||||
//this.player_enters_room()
|
||||
}
|
||||
},
|
||||
preloader() {
|
||||
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/'})
|
||||
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
|
||||
})
|
||||
// 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.
|
||||
// if (current() === 'Online/ChatRoom') throw new Error('please do not load in a chat room')
|
||||
if (current() === 'Character/Login') {
|
||||
this.remove_load_hook = this.before('AsylumGGTSSAddItems', () => this.loader())
|
||||
} else this.loader()
|
||||
},
|
||||
}
|
||||
|
||||
w.MBCHC.preloader()
|
5817
package-lock.json
generated
5817
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
38
package.json
38
package.json
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"name": "mbchc",
|
||||
"version": "0.0.12",
|
||||
"description": "Mute's Bondage Club Hacks Collection",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"xo": "^0.56.0",
|
||||
"bc-stubs": "^105.0.0",
|
||||
"bondage-club-mod-sdk": "^1.2.0"
|
||||
},
|
||||
"license": "SEE LICENSE IN LICENSE.",
|
||||
"xo": {
|
||||
"env": [
|
||||
"browser",
|
||||
"node"
|
||||
],
|
||||
"rules": {
|
||||
"brace-style": "off",
|
||||
"camelcase": "off",
|
||||
"capitalized-comments": "off",
|
||||
"curly": "off",
|
||||
"max-params": "off",
|
||||
"max-statements-per-line": "off",
|
||||
"new-cap": "off",
|
||||
"no-return-assign": "off",
|
||||
"no-unused-expressions": "off",
|
||||
"no-unused-vars": ["error", {
|
||||
"argsIgnorePattern": "^_",
|
||||
"destructuredArrayIgnorePattern": "^_"
|
||||
}],
|
||||
"padding-line-between-statements": "off",
|
||||
"semi": "off",
|
||||
"spaced-comment": "off",
|
||||
"unicorn/no-array-reduce": "off",
|
||||
"fake/fuck-commas": "off"
|
||||
}
|
||||
}
|
||||
}
|
33
server.js
33
server.js
@@ -1,33 +0,0 @@
|
||||
import {readFileSync} from 'node:fs'
|
||||
import {createServer} from 'node:http'
|
||||
|
||||
const config = {host: '127.0.0.1', port: 9696}
|
||||
|
||||
const h_cors = {
|
||||
'Access-Control-Max-Age': '86400',
|
||||
'Access-Control-Allow-Private-Network': 'true',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
||||
'Access-Control-Allow-Headers': '*',
|
||||
// 'Access-Control-Allow-Credentials': 'false', // omit this header to disallow
|
||||
}
|
||||
const h_all = Object.assign({
|
||||
'Content-Type': 'text/javascript',
|
||||
'Cache-Control': 'no-cache',
|
||||
}, h_cors)
|
||||
|
||||
/**
|
||||
* @typedef {import('node:http').ServerResponse} ServerResponse
|
||||
* @type {Record<string,function(ServerResponse):void>}
|
||||
*/
|
||||
const resp = {
|
||||
GET(rx) { rx.writeHead(200, h_all); rx.write(readFileSync('./mbchc.mjs')) },
|
||||
OPTIONS(rx) { rx.writeHead(204, h_cors) },
|
||||
}
|
||||
|
||||
const server = createServer((rq, rx) => {
|
||||
resp[rq.method] && (new URL(`http://${config.host}:${config.port}${rq.url}`)).pathname === '/' ? resp[rq.method](rx) : rx.writeHead(400)
|
||||
rx.end()
|
||||
console.log('%s %d %s %s', (new Date()).toISOString(), rx.statusCode, rq.method, rq.url)
|
||||
})
|
||||
server.listen(config.port, config.host, () => console.log(`Server started at http://${config.host}:${config.port}`))
|
10
typedef.d.ts
vendored
10
typedef.d.ts
vendored
@@ -1,10 +0,0 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
/* eslint-disable @typescript-eslint/consistent-type-definitions */
|
||||
|
||||
interface PlayerOnlineSettings {
|
||||
MBCHC?: any;
|
||||
}
|
||||
|
||||
interface ServerChatRoomMessage {
|
||||
MBCHC_ID?: number;
|
||||
}
|
Reference in New Issue
Block a user