Compare commits
65 Commits
Author | SHA1 | Date |
---|---|---|
Mute | d30945201d | |
Mute | 378d0af8d3 | |
Mute | 526b51e158 | |
Mute | ff377fb709 | |
Mute | e4f7ce0560 | |
Mute | 2bf6695af0 | |
Mute | 0f1195d92b | |
Mute | d3852d6e63 | |
Mute | 2f87b283ed | |
Mute | 89eafdfab3 | |
Mute | 0d1f074c43 | |
Mute | 247134e04b | |
Mute | 46fb29bcec | |
Mute | 45f6f80493 | |
Mute | eaf4e3f26f | |
Mute | 337eb83d47 | |
Mute | 3a97857fc1 | |
Mute | a9c6e5fd35 | |
Mute | 247c908f46 | |
Mute | 1cc9ac18d7 | |
Mute | e690960782 | |
Mute | 634eddbd2a | |
Mute | d43e3cdeed | |
Mute | 5c1d2f6397 | |
Mute | 5c534b1537 | |
Mute | 41b2029efd | |
Mute | f911c05073 | |
Mute | 8d1516f1a7 | |
Mute | 3e3f891016 | |
Mute | ef41dd1dce | |
Mute | 4930e5111b | |
Mute | c76ad63d96 | |
Mute | 18095495bb | |
Mute | faf4e95499 | |
Mute | 839ad8f0b3 | |
Mute | 62def6bb3e | |
Mute | 3f4e43adbc | |
Mute | 29e9d84ceb | |
Mute | b99e28502b | |
Mute | 621dea9959 | |
Mute | 0145147333 | |
Mute | 6fbe13b17a | |
Mute | c73bffc4bb | |
Mute | 537c538211 | |
Mute | faf2714810 | |
Mute | 87cf188c64 | |
Mute | c872444b14 | |
Mute | 69193e0861 | |
Mute | 18702fa607 | |
Mute | f7e571b26a | |
Mute | 561bedc606 | |
Mute | a0e482c536 | |
Mute | 071ca9a8e1 | |
Mute | 1fa1c8c951 | |
Mute | d30d0c1408 | |
Mute | 8d6669795b | |
Mute | a140c3a301 | |
Mute | 7611721459 | |
Mute | 1d8e3b100b | |
Mute | e6a35024c8 | |
Mute | 6663c2eede | |
Mute | 1f45cb04e0 | |
Mute | c29d264520 | |
Mute | ee52aa45a8 | |
Mute | a06791c039 |
|
@ -0,0 +1 @@
|
|||
node_modules
|
|
@ -1,3 +1,7 @@
|
|||
Mute's Bondage Club Hacks Collection
|
||||
# Mute's Bondage Club Hacks Collection
|
||||
This document is updated with stable releases, for additional documentation please consult [[the wiki|https://code.fleshless.org/mute/MBCHC/wiki/Home]].
|
||||
|
||||
[[https://code.fleshless.org/mute/MBCHC/wiki]]
|
||||
* Unstable doc: https://code.fleshless.org/mute/MBCHC/wiki/unstable
|
||||
|
||||
## PSA
|
||||
The only supported version is unstable, don't use any other.
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,248 +1,667 @@
|
|||
// ==UserScript==
|
||||
// @name MBCHC
|
||||
// @version dev.1
|
||||
// @version dev.8
|
||||
// @description Mute's Bondage Club Hacks Collection
|
||||
// @author codename.mute@proton.me
|
||||
// @homepage https://code.fleshless.org/mute/MBCHC
|
||||
// @namespace https://code.fleshless.org/mute/
|
||||
// @homepage https://code.fleshless.org/mute/MBCHC
|
||||
// @updateURL https://code.fleshless.org/mute/MBCHC/raw/branch/master/mbchc-dev.user.js
|
||||
// @downloadURL https://code.fleshless.org/mute/MBCHC/raw/branch/master/mbchc-dev.user.js
|
||||
// @match https://bondageprojects.elementfx.com/R*
|
||||
// @match https://www.bondageprojects.elementfx.com/R*
|
||||
// @match https://bondage-europe.com/R*
|
||||
// @match https://www.bondage-europe.com/R*
|
||||
// @match http://localhost:*/*
|
||||
// @match http://127.0.0.1:*/*
|
||||
// @grant none
|
||||
// ==/UserScript==
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
if (!window.AsylumGGTSSAddItems) throw "AsylumGGTSSAddItems() not found, aborting"
|
||||
"use strict";
|
||||
if (!window.AsylumGGTSSAddItems) throw "AsylumGGTSSAddItems() not found, aborting MBCHC loading"
|
||||
if (window.MBCHC) throw "MBCHC found, aborting loading"
|
||||
window.MBCHC = {
|
||||
VERSION: "dev.8",
|
||||
TARGET_VERSION: "R93",
|
||||
NEXT_MESSAGE: 1,
|
||||
LOG_MESSAGES: false,
|
||||
RETHROW: false,
|
||||
LOADED: false,
|
||||
AUTOHACK_ENABLED: false,
|
||||
LAST_HACKED: null,
|
||||
HISTORY_MODE: false,
|
||||
RE_TITLE: /^[a-zA-Z]+$/,
|
||||
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,
|
||||
HIDE_SPECIAL: ["Activity","Emoticon"],
|
||||
HIDE_BODY: ["Blush","BodyLower","BodyUpper","Eyebrows","Eyes","Eyes2","Face","Fluids","HairBack","HairFront","Hands","Head","LeftHand","Mouth","Nipples","Pussy","RightHand"],
|
||||
HIDE_CLOTHES: [
|
||||
"Cloth","ClothAccessory","Necklace","Suit","ClothLower","SuitLower","Bra","Corset","Panties",
|
||||
"Socks","RightAnklet","LeftAnklet","Garters","Shoes","Hat","HairAccessory3","HairAccessory1","HairAccessory2",
|
||||
"Gloves","Bracelet","Glasses","Mask","TailStraps","Wings"
|
||||
],
|
||||
HIDE_ITEMS: [
|
||||
"ItemMisc","ItemEars","ItemHead","ItemNose","ItemHood","ItemAddon","ItemMouth","ItemMouth2","ItemMouth3",
|
||||
"ItemArms","ItemNeckAccessories","ItemNeck","ItemNeckRestraints","ItemNipples","ItemNipplesPiercings","ItemBreast","ItemTorso","ItemTorso2",
|
||||
"ItemHands","ItemPelvis","ItemVulva","ItemVulvaPiercings",
|
||||
"ItemDevices","ItemLegs","ItemFeet","ItemBoots"
|
||||
],
|
||||
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"}},
|
||||
|
||||
// Static data
|
||||
window.MBCHC = {
|
||||
NEXT_MESSAGE: 1,
|
||||
LOG_MESSAGES: false,
|
||||
LOADED: false,
|
||||
VERSION: 'dev',
|
||||
AUTOHACK_ENABLED: false,
|
||||
LAST_HACKED: null,
|
||||
RE_TITLE: /^[a-zA-Z]+$/,
|
||||
HIDE_SPECIAL: ["Activity","Emoticon"],
|
||||
HIDE_BODY: ["Blush","BodyLower","BodyUpper","Eyebrows","Eyes","Eyes2","Face","Fluids","HairBack","HairFront","Hands","Head","Mouth","Nipples","Pussy"],
|
||||
HIDE_CLOTHES: [
|
||||
"Cloth","ClothAccessory","Necklace","Suit","ClothLower","SuitLower","Bra","Corset","Panties",
|
||||
"Socks","RightAnklet","LeftAnklet","Garters","Shoes","Hat","HairAccessory3","HairAccessory1","HairAccessory2",
|
||||
"Gloves","Bracelet","Glasses","Mask","TailStraps","Wings"
|
||||
],
|
||||
HIDE_ITEMS: [
|
||||
"ItemMisc","ItemEars","ItemHead","ItemNose","ItemHood","ItemAddon","ItemMouth","ItemMouth2","ItemMouth3",
|
||||
"ItemArms","ItemNeckAccessories","ItemNeck","ItemNeckRestraints","ItemNipples","ItemNipplesPiercings","ItemBreast","ItemTorso",
|
||||
"ItemHands","ItemPelvis","ItemVulva","ItemVulvaPiercings",
|
||||
"ItemDevices","ItemLegs","ItemFeet","ItemBoots"
|
||||
],
|
||||
COMMANDS: [
|
||||
{ Tag: "disappear",
|
||||
Description: ": Become invisible; requires anal hook (hair)",
|
||||
Action: (argline, cmdline, args) => {
|
||||
try {
|
||||
window.MBCHC.make_my_anal_hook_hide_body()
|
||||
} catch (x) { window.ChatRoomSendLocal(`(Error: ${x.toString()})`) }
|
||||
},
|
||||
},
|
||||
{ Tag: "title",
|
||||
Description: "[Title]: (WIP) Set a custom title (one short word, letters only)",
|
||||
Action: (argline, cmdline, args) => {
|
||||
try {
|
||||
window.MBCHC.action_title(args)
|
||||
} catch (x) { window.ChatRoomSendLocal(`(Error: ${x.toString()})`) }
|
||||
},
|
||||
},
|
||||
{ Tag: "donate",
|
||||
Description: "[MemberNumber]: Buy data and send it to recipient",
|
||||
Action: (argline, cmdline, args) => {
|
||||
try {
|
||||
window.MBCHC.action_donate(args)
|
||||
} catch (x) { window.ChatRoomSendLocal(`(Error: ${x.toString()})`) }
|
||||
},
|
||||
},
|
||||
{ Tag: "autohack",
|
||||
Description: ": Toggle autohack mode",
|
||||
Action: (argline, cmdline, args) => {
|
||||
try {
|
||||
window.MBCHC.action_autohack()
|
||||
} catch (x) { window.ChatRoomSendLocal(`(Error: ${x.toString()})`) }
|
||||
},
|
||||
},
|
||||
{ Tag: "nod",
|
||||
Description: ": Nod",
|
||||
Action: (argline, cmdline, args) => {
|
||||
try {
|
||||
window.MBCHC.run_activity(window.Player, "ItemHead", "Nod")
|
||||
} catch (x) { window.ChatRoomSendLocal(`(Error: ${x.toString()})`) }
|
||||
},
|
||||
},
|
||||
{ Tag: "shake",
|
||||
Description: ": Shake your head",
|
||||
Action: (argline, cmdline, args) => {
|
||||
try {
|
||||
window.MBCHC.run_activity(window.Player, "ItemHead", "Wiggle")
|
||||
} catch (x) { window.ChatRoomSendLocal(`(Error: ${x.toString()})`) }
|
||||
},
|
||||
},
|
||||
{ Tag: "shrug",
|
||||
Description: ": Shrug",
|
||||
Action: (argline, cmdline, args) => {
|
||||
try {
|
||||
window.MBCHC.send_activity(`${window.CharacterNickname(window.Player)} shrugs.`)
|
||||
} catch (x) { window.ChatRoomSendLocal(`(Error: ${x.toString()})`) }
|
||||
},
|
||||
},
|
||||
{ Tag: "myself",
|
||||
Description: "[Message]: Send a custom activity as yourself (or \"@Message\")",
|
||||
Action: (argline, cmdline, args) => {
|
||||
if (!window.MBCHC.empty(argline)) {
|
||||
try {
|
||||
let message = window.MBCHC.add_full_stop(argline)
|
||||
window.MBCHC.send_activity(`${window.CharacterNickname(window.Player)} ${message}`)
|
||||
} catch (x) { window.ChatRoomSendLocal(`(Error: ${x.toString()})`) }
|
||||
}
|
||||
},
|
||||
},
|
||||
{ Tag: "activity",
|
||||
Description: "[Message]: Send a custom activity (or \"@@Message\")",
|
||||
Action: (argline, cmdline, args) => {
|
||||
if (!window.MBCHC.empty(argline)) {
|
||||
try {
|
||||
let message = window.MBCHC.add_full_stop(window.MBCHC.capitalise(argline))
|
||||
window.MBCHC.send_activity(message)
|
||||
} catch (x) { window.ChatRoomSendLocal(`(Error: ${x.toString()})`) }
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
log: function(msg) {return("MBCHC: " + msg.toString())},
|
||||
empty: function(text) {
|
||||
if (!text) return(true)
|
||||
if (String(text).trim().length < 1) return(true)
|
||||
return(false)
|
||||
},
|
||||
add_full_stop: function(text) {
|
||||
if (text.endsWith('.')) return(text)
|
||||
return(`${text}.`)
|
||||
},
|
||||
capitalise: function(text, lower = false) {
|
||||
let first = text.at(0).toLocaleUpperCase()
|
||||
let rest = text.slice(1)
|
||||
if (lower) rest = rest.toLocaleLowerCase()
|
||||
return(first + rest)
|
||||
},
|
||||
// we need this one here, this is our main loading hook
|
||||
orig_AsylumGGTSSAddItems: window.AsylumGGTSSAddItems,
|
||||
} // MBCHC
|
||||
// action zone
|
||||
"wiggle|shake": {"Arms,Breast,Boots,Butt,Ears,Feet,Hands,Nose,Pelvis,Torso": {self: "Wiggle"}},
|
||||
|
||||
// Loader
|
||||
window.AsylumGGTSSAddItems = function() {
|
||||
if (!window.MBCHC.LOADED) {
|
||||
// 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"}},
|
||||
|
||||
// Save originals hopefully after patching
|
||||
window.MBCHC.orig_ChatRoomMessageInvolvesPlayer = window.ChatRoomMessageInvolvesPlayer
|
||||
window.MBCHC.orig_ChatRoomReceiveSuitcaseMoney = window.ChatRoomReceiveSuitcaseMoney
|
||||
window.MBCHC.orig_ChatRoomSendChat = window.ChatRoomSendChat
|
||||
// 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: {
|
||||
"versions": {desc: "show the mod versions across the room", cb: mbchc => mbchc.inform(mbchc.gather_versions().map(c => `<div><b>${c.name}</b> (${c.cid}): ${c.version}</div>`).join(""))},
|
||||
"autohack": {desc: "toggle the autohack feature", cb: mbchc => mbchc.inform(`Autohack is now ${(mbchc.AUTOHACK_ENABLED = !mbchc.AUTOHACK_ENABLED) ? "enabled" : "disabled"}`)},
|
||||
"disappear": {desc: "become invisible (requires anal hook -> hair)", cb: mbchc => mbchc.disappear()},
|
||||
"donate": {desc: "Buy data and send it to recipient", args: {TARGET: {}}, cb: (mbchc, args) => mbchc.donate_data(args[0])},
|
||||
"title": {desc: "set a custom title (<b>WIP</b>)", args: {TITLE: {}}, cb: (mbchc, args) => mbchc.title(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 (window.Player.OnlineSettings.MBCHC) {delete window.Player.OnlineSettings.MBCHC; mbchc.save_settings()}}},
|
||||
},
|
||||
ensure: function(error, callback) {
|
||||
let result = callback.call(this)
|
||||
if (!result) throw error
|
||||
return(result)
|
||||
},
|
||||
calculate_maps: function() {
|
||||
this.DO_DATA = {verbs: {}, zones: {}}
|
||||
for (let [verbs, data] of Object.entries(this.MAP_ACTIONS)) {
|
||||
let unwound = {}
|
||||
for (let [zones, actions] of Object.entries(data)) {
|
||||
let all = (actions.all) ? actions.all.split("|") : []
|
||||
let processed = {self: (actions.self) ? actions.self.split("|").concat(all) : all, others: (actions.others) ? actions.others.split("|").concat(all) : all}
|
||||
for (let zone of zones.split(",")) unwound[`Item${zone}`] = processed
|
||||
}
|
||||
for (let verb of verbs.split("|")) this.DO_DATA.verbs[verb] = unwound
|
||||
}
|
||||
for (let [ag, zones] of Object.entries(this.MAP_ZONES)) for (let zone of zones) this.DO_DATA.zones[zone] = ag
|
||||
},
|
||||
settings: function(setting = null) {
|
||||
let settings = window.Player.OnlineSettings.MBCHC || {}
|
||||
return(setting ? settings[setting] : settings)
|
||||
},
|
||||
save_settings: function(cb = null) {
|
||||
if (cb) {
|
||||
if (!window.Player.OnlineSettings.MBCHC) window.Player.OnlineSettings.MBCHC = {}
|
||||
cb.call(this, window.Player.OnlineSettings.MBCHC)
|
||||
}
|
||||
window.ServerAccountUpdate.QueueData({OnlineSettings: window.Player.OnlineSettings})
|
||||
},
|
||||
log: function(level, msg) {console[level]("MBCHC: " + String(msg))},
|
||||
empty: function(text) {
|
||||
if (!text) return(true)
|
||||
if (String(text).trim().length < 1) return(true)
|
||||
return(false)
|
||||
},
|
||||
normalise_message: function(text, options = {}) {
|
||||
let result = text
|
||||
if (options.trim) result = result.trim()
|
||||
if (options.low) result = result.toLocaleLowerCase()
|
||||
if (options.up) {
|
||||
let first = result.at(0).toLocaleUpperCase()
|
||||
let rest = result.slice(1)
|
||||
result = first + rest
|
||||
}
|
||||
if (options.dot && result.match(this.RE_LAST_LETTER)) result = `${result}.`
|
||||
return(result)
|
||||
},
|
||||
tokenise: function(text) { return text.replace(this.RE_SPACES, " ").split(" ") },
|
||||
inform: function(html, timeout = 60000) { window.ChatRoomSendLocal(`<div class="mbchc">${html}</div>`, timeout) },
|
||||
report: function(x) {
|
||||
this.inform(`Error: ${x.toString()}`)
|
||||
if (this.RETHROW) throw x
|
||||
},
|
||||
in: function(x, floor, ceiling) { return((x >= floor) && (x <= ceiling)) },
|
||||
cid2char: function(cid) {
|
||||
cid = Number.parseInt(cid)
|
||||
if (cid === window.Player.cid) return(window.Player)
|
||||
return(this.ensure(`character ${cid} not found in the room`, () => window.ChatRoomCharacter.find(c => c.cid === cid)))
|
||||
},
|
||||
pos2char: function(pos) {
|
||||
if (!this.in(pos, 0, window.ChatRoomCharacter.length - 1)) throw `invalid position ${pos}`
|
||||
return(window.ChatRoomCharacter[pos])
|
||||
},
|
||||
rel2char: function(target) {
|
||||
let me = this.ensure("can't find my position", () => window.ChatRoomCharacter.findIndex(char => char.IsPlayer()) + 1) - 1 // 0 is falsy, but is valid index
|
||||
let pos = null
|
||||
if (target.match(this.RE_ALL_LEFT)) pos = me - target.length
|
||||
if (target.match(this.RE_ALL_RIGHT)) pos = me + target.length
|
||||
if (null === pos) throw `failed to parse target "${target}"`
|
||||
pos = pos % window.ChatRoomCharacter.length
|
||||
if (pos < 0) pos = pos + window.ChatRoomCharacter.length
|
||||
return(this.pos2char(pos))
|
||||
},
|
||||
target2char: function(target) { // target should be lowcase
|
||||
let input = target
|
||||
if (this.empty(target)) return(window.Player)
|
||||
let int = Number.parseInt(target)
|
||||
target = String(target)
|
||||
let found = []
|
||||
if (target.startsWith("=")) return(this.cid2char(target.slice(1)))
|
||||
if (target.startsWith("<") || target.startsWith(">")) return(this.rel2char(target))
|
||||
if (!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(...window.ChatRoomCharacter.filter(c => c.cid.toString().indexOf(target) > -1))
|
||||
}
|
||||
if (target.startsWith("@")) target = target.slice(1)
|
||||
found.push(...window.ChatRoomCharacter.filter(c => c.Name.toLocaleLowerCase().indexOf(target) > -1))
|
||||
found.push(...window.ChatRoomCharacter.filter(c => c.Nickname && (c.Nickname.toLocaleLowerCase().indexOf(target) > -1)))
|
||||
let map = {}
|
||||
found.forEach(c => {if (!map[c.cid]) map[c.cid] = c} )
|
||||
found = Object.values(map)
|
||||
if (found.length < 1) throw `target "${input}": no match`
|
||||
if (found.length > 1) throw `target "${input}": multiple matches (${found.map(c => `${c.cid}|${c.Name}|${c.Nickname || c.Name}`).join(",")})`
|
||||
return(found[0])
|
||||
},
|
||||
char2targets: function(char) {
|
||||
let [result, cid] = [new Set(), char.cid.toString()]
|
||||
result.add(cid).add(`=${cid}`)
|
||||
this.tokenise(char.Name).forEach(t => {result.add(t); result.add(`@${t}`)})
|
||||
if (char.Nickname) this.tokenise(char.Nickname).forEach(t => {result.add(t); result.add(`@${t}`)})
|
||||
return result
|
||||
},
|
||||
donate_data: function(target) {
|
||||
let char = this.target2char(target)
|
||||
if (char.IsPlayer()) throw "target must not be you"
|
||||
if (!char.IsRestrained()) throw "target must be bound"
|
||||
const cost = Math.round((Math.random() * 10 + 15))
|
||||
if (window.Player.Money < cost) throw "not enough money"
|
||||
window.CharacterChangeMoney(window.Player, -cost)
|
||||
window.ServerSend("ChatRoomChat", {Content: "ReceiveSuitcaseMoney", Type: "Hidden", Target: char.cid})
|
||||
window.ChatRoomMessage({Sender: window.Player.cid, Type: "Action", Content: `You've bought data for $${cost} and sent it to ${char.dn}.`, Dictionary: [{Tag: "MISSING PLAYER DIALOG: ", Text: ""}]})
|
||||
},
|
||||
run_activity: function(char, ag, action) { try {
|
||||
if (!window.ActivityAllowed()) throw "activities disabled in this room"
|
||||
if (!window.ServerChatRoomGetAllowItem(window.Player, char)) throw "no permissions"
|
||||
char.FocusGroup = this.ensure("invalid AssetGroup", () => window.AssetGroupGet(char.AssetFamily, ag))
|
||||
let activity = this.ensure("invalid activity", () => window.ActivityAllowedForGroup(char, char.FocusGroup.Name, true).find(a => a.Name === action || a.Activity?.Name === action))
|
||||
if ((activity.Name || activity.Activity.Name).endsWith("Item")) {
|
||||
const item = this.ensure("no toy found", () => window.Player.Inventory.find(i => i.Asset?.Name === "SpankingToys" && i.Asset.Group?.Name === char.FocusGroup.Name && window.AssetSpankingToys.DynamicActivity(char) === (activity.Name || activity.Activity.Name)))
|
||||
window.DialogPublishAction(char, item)
|
||||
} else window.ActivityRun(window.Player, char, char.FocusGroup, activity)
|
||||
} finally {char.FocusGroup = null} },
|
||||
replace_me: function(match, offset, string) {
|
||||
let text = string.slice(1)
|
||||
let suffix = " "
|
||||
if (text.startsWith("'") || text.startsWith(" ")) suffix = ""
|
||||
return `${window.MBCHC.PREF_ACTIVITY}<${window.Player.cid}:>SourceCharacter${suffix}`
|
||||
},
|
||||
cid2dict: function(type, cid) { return({Tag: `${type}Character`, MemberNumber: cid, Text: this.cid2char(cid).dn}) },
|
||||
send_activity: function(msg) {
|
||||
let dict = [{Tag: "MISSING PLAYER DIALOG: ", Text: "\u200C"}] // zero-width non-joiner, used to break up ligatures, does nothing here, but an empty string is a falsy value
|
||||
let cids = msg.match(this.RE_ACT_CIDS)
|
||||
if (cids) {
|
||||
msg = msg.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]))
|
||||
}
|
||||
window.ServerSend("ChatRoomChat", {Type: "Action", Content: msg, Dictionary: dict})
|
||||
},
|
||||
receive: function(data) {
|
||||
let char = this.cid2char(data.Sender)
|
||||
if (char.IsPlayer()) return true // this is our own message, sent back to us
|
||||
let payload = this.ensure("Empty message", () => data.Dictionary[0])
|
||||
switch (payload.type) {
|
||||
case "greetings": case "hello":
|
||||
char.MBCHC = payload.value
|
||||
if ("greetings" === payload.type) this.hello(char)
|
||||
break
|
||||
default: // if we don't know the type it may be from a newer version
|
||||
}
|
||||
return true
|
||||
},
|
||||
hello: function(char = null) {
|
||||
let payload = {type: "greetings", value: window.Player.MBCHC}
|
||||
if (char) payload.type = "hello"
|
||||
let message = {Content: "MBCHC", Type: "Hidden", Dictionary: [payload]}
|
||||
if (char) message.Target = char.cid
|
||||
window.ServerSend("ChatRoomChat", message)
|
||||
},
|
||||
disappear: function() {
|
||||
let item = window.InventoryGet(window.Player, "ItemButt")
|
||||
if (!item || !item.Asset || !item.Asset.Name) throw "butt seems empty"
|
||||
if (item.Asset.Name !== "AnalHook") throw "butt seems occupied by something other than the anal hook"
|
||||
if (!item.Property.Type || item.Property.Type !== "Hair") throw "anal hook seems not tied to hair"
|
||||
item.Property = {Type: "Hair", Hide: this.HIDE_ALL}
|
||||
window.CharacterRefresh(window.Player, true, true)
|
||||
},
|
||||
title: function(title) { // WIP
|
||||
if (this.empty(title)) throw "empty title"
|
||||
title = this.normalise_message(title, {trim: true, up: true, low: true})
|
||||
if (title.length > 16) throw "title too long"
|
||||
if (!title.match(this.RE_TITLE)) throw "invalid title"
|
||||
window.TitleSet(title)
|
||||
//window.TitleList.push({Name: title, Requirement: () => true}) // check for existing first
|
||||
},
|
||||
copy_fbc_trigger: function(trigger) {
|
||||
let 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: function() {
|
||||
this.remove_fbc_hook()
|
||||
delete this.remove_fbc_hook
|
||||
window.bce_ActivityTriggers.push(...window.bce_ActivityTriggers.filter(t => "Emote" === t.Type).map(t => this.copy_fbc_trigger(t)))
|
||||
/* (["anim", "pose"]).forEach(tag => {let cmd = window.Commands.find(c => tag === c.Tag); if (cmd) cmd.AutoComplete = this[`complete_fbc_${tag}`]}) */ // this line explodes, don't ask me why
|
||||
let cmd = window.Commands.find(c => "anim" === c.Tag)
|
||||
if (cmd) cmd.AutoComplete = this.complete_fbc_anim
|
||||
cmd = window.Commands.find(c => "pose" === c.Tag)
|
||||
if (cmd) cmd.AutoComplete = this.complete_fbc_pose
|
||||
},
|
||||
gather_versions: function() { return(window.ChatRoomCharacter.filter(c => c.MBCHC).map(c => ({name: c.dn, cid: c.cid, version: c.MBCHC.VERSION}))) },
|
||||
find_timezone: function(char) {
|
||||
const timezones = this.settings("timezones")
|
||||
if (timezones && "number" === typeof timezones[char.cid]) return(timezones[char.cid])
|
||||
const match = (char.Description) ? char.Description.match(this.RE_TZ) : null
|
||||
const int = match ? Number.parseInt(match[1] + match[2]) : 42
|
||||
if (this.in(int, -12, 12)) return(int)
|
||||
return(null)
|
||||
},
|
||||
player_enters_room: function() { // or if the mod is loaded while player is in the room
|
||||
this.hello()
|
||||
},
|
||||
set_timezone: function(args) {
|
||||
let tz = Number.parseInt(args[0])
|
||||
if (isNaN(tz)) throw `invalid offset "${args[0]}"`
|
||||
if (!this.in(tz, -12, 12)) throw "offset should be [-12,12]"
|
||||
let char = this.target2char(args[1])
|
||||
char.MBCHC_LOCAL.TZ = tz
|
||||
this.save_settings(s => {if (!s.timezones) s.timezones = {}; s.timezones[char.cid] = tz})
|
||||
},
|
||||
update_char: function(char) {
|
||||
char.cid = char.MemberNumber // Club ID (shorter)
|
||||
char.dn = window.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: function(argline, cmdline, args) { const mbchc = window.MBCHC; try { // `this` is command object
|
||||
if (args.length < 1) 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("")))
|
||||
let cmd = String(args.shift())
|
||||
let sub = mbchc.ensure(`unknown subcommand "${cmd}"`, () => mbchc.SUBCOMMANDS_MBCHC[cmd])
|
||||
sub.cb.call(mbchc, mbchc, args, argline, cmdline)
|
||||
} catch (x) { mbchc.report(x) } },
|
||||
command_activity: function(argline, cmdline, args) { const mbchc = window.MBCHC; if (!mbchc.empty(argline)) { try { // `this` is command object
|
||||
let message = mbchc.normalise_message(cmdline.replace(mbchc.RE_ACTIVITY, ''), {trim: true, dot: true, up: true})
|
||||
mbchc.send_activity(message)
|
||||
} catch (x) { mbchc.report(x) } } },
|
||||
command_do: function(argline, cmdline, args) { const mbchc = window.MBCHC; try { // `this` is command object
|
||||
if (args.length < 1) 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
|
||||
let zones = mbchc.ensure(`unknown verb "${verb}"`, () => mbchc.DO_DATA.verbs[verb])
|
||||
if (1 === Object.keys(zones).length) {
|
||||
if (!target) target = zone
|
||||
zone = mbchc.MAP_ZONES[Object.keys(zones)[0]][0]
|
||||
}
|
||||
if (!zone) throw "zone missing"
|
||||
let ag = mbchc.ensure(`unknown zone "${zone}"`, () => mbchc.DO_DATA.zones[zone])
|
||||
let types = mbchc.ensure(`zone "${zone}" invalid for "${verb}"`, () => zones[ag])
|
||||
let char = window.Player
|
||||
if (target && ((types.self.length < 1) || (types.others.length > 0))) char = mbchc.target2char(target)
|
||||
let type = char.IsPlayer() ? "self" : "others"
|
||||
let available = window.ActivityAllowedForGroup(char, ag)
|
||||
let toy = window.InventoryGet(window.Player, "ItemHands")
|
||||
if (toy && toy.Asset.Name === "SpankingToys") available.push(window.AssetAllActivities(char.AssetFamily).find(a => a.Name === window.InventorySpankingToysGetActivity?.(window.Player)))
|
||||
let actions = mbchc.ensure(`zone "${zone}" invalid for ("${verb}" "${type}")`, () => types[type])
|
||||
let action = mbchc.ensure(`invalid action (${verb} ${zone} ${target})`, () => actions.find(name => available.find(a => a.Name === name || a.Activity?.Name === name)))
|
||||
mbchc.run_activity(char, ag, action)
|
||||
} catch (x) { mbchc.report(x) } },
|
||||
bell: function() {
|
||||
setTimeout(() => {document.getElementById("InputChat").style.outline = ""}, 100)
|
||||
document.getElementById("InputChat").style.outline = "solid red"
|
||||
},
|
||||
complete: function(options, space = true) {
|
||||
if (options.length < 1) return(this.bell())
|
||||
if (options.length > 1) {
|
||||
let width = Math.max(...options.map(o => o.length))
|
||||
let pref = null
|
||||
for (let i = width; i > 0; i -= 1) { let test = options[0].slice(0, i); if (options.every(o => o.startsWith(test))) {pref = test; break} }
|
||||
if (pref) this.complete([pref], false)
|
||||
this.complete_hint(options)
|
||||
} else window.ElementValue("InputChat", document.getElementById("InputChat").value.replace(this.RE_LAST_WORD, `$1${options[0]}${space ? " " : ""}`))
|
||||
},
|
||||
complete_hint: function(options) {
|
||||
this.COMP_HINT.innerHTML = options.sort().map(s => `<div>${s}</div>`).join("")
|
||||
this.COMP_HINT.style.display = "flex"
|
||||
window.ElementSetDataAttribute(this.COMP_HINT.id, "colortheme", (window.Player.ChatSettings.ColorTheme || "Light"))
|
||||
let rescroll = window.ElementIsScrolledToEnd("TextAreaChatLog")
|
||||
window.ChatRoomResize(false)
|
||||
if (rescroll) window.ElementScrollToEnd("TextAreaChatLog")
|
||||
},
|
||||
comp_hint_visible: function() {return(this.COMP_HINT.parentElement && "flex" === this.COMP_HINT.style.display)},
|
||||
complete_hint_hide: function(options) {if (!this.comp_hint_visible()) return; this.COMP_HINT.style.display = "none"; window.ChatRoomResize(false)},
|
||||
complete_target: function(token, me2 = true, check_perms = false) {
|
||||
let [locase, found] = [token.toLocaleLowerCase(), new Set()]
|
||||
for (let c of window.ChatRoomCharacter) {
|
||||
if ((c.IsPlayer() && !me2) || (check_perms && !window.ServerChatRoomGetAllowItem(window.Player, c))) continue
|
||||
this.char2targets(c).forEach(s => {if (s.toLocaleLowerCase().startsWith(locase)) found.add(s)})
|
||||
}
|
||||
this.complete(Array.from(found))
|
||||
},
|
||||
complete_common: function() {
|
||||
let input = document.getElementById("InputChat").value
|
||||
return([this, input, this.tokenise(input)])
|
||||
},
|
||||
complete_mbchc: function(args, locase, cmdline) { const [mbchc, input, tokens] = window.MBCHC.complete_common(); // `this` is command object
|
||||
if (tokens.length < 1) return
|
||||
if (tokens.length < 2) return(mbchc.complete([`${mbchc.CommandsKey}${this.Tag}`]))
|
||||
let subname = tokens[1].toLocaleLowerCase()
|
||||
if (tokens.length < 3) return(mbchc.complete(Object.keys(mbchc.SUBCOMMANDS_MBCHC).filter(c => c.startsWith(subname)))) // complete subcommand name
|
||||
let sub = mbchc.SUBCOMMANDS_MBCHC[subname]
|
||||
if (sub && sub.args) {
|
||||
let argname = Object.keys(sub.args)[tokens.length - 3]
|
||||
if ("TARGET" === argname) return(mbchc.complete_target(tokens[tokens.length - 1], false))
|
||||
if ("[TARGET]" === argname) return(mbchc.complete_target(tokens[tokens.length - 1]), true)
|
||||
}
|
||||
},
|
||||
complete_do_target: function(actions, token) {
|
||||
if (!actions) return
|
||||
let me2 = (actions.self.length > 0)
|
||||
if (me2 && actions.others.length < 1) return(this.complete([window.Player.cid.toString()])) // target is always the player
|
||||
this.complete_target(token, me2, true)
|
||||
},
|
||||
complete_do: function(args, locase, cmdline) { const [mbchc, input, tokens] = window.MBCHC.complete_common(); // `this` is command object
|
||||
if (tokens.length < 1) 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
|
||||
let 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
|
||||
let 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: function(args, locase, cmdline) { const [mbchc, input, tokens] = window.MBCHC.complete_common(); // `this` is command object
|
||||
if (tokens.length < 1) return
|
||||
if (tokens.length < 2) return(mbchc.complete([`${mbchc.CommandsKey}${this.Tag}`]))
|
||||
if (tokens.length > 2) return(mbchc.bell())
|
||||
let anim = tokens[1].toLocaleLowerCase()
|
||||
return(mbchc.complete(Object.keys(window.bce_EventExpressions).filter(a => a.toLocaleLowerCase().startsWith(anim))))
|
||||
},
|
||||
complete_fbc_pose: function(args, locase, cmdline) { const [mbchc, input, tokens] = window.MBCHC.complete_common(); // `this` is command object
|
||||
if (tokens.length < 1) return
|
||||
if (tokens.length < 2) return(mbchc.complete([`${mbchc.CommandsKey}${this.Tag}`]))
|
||||
let pose = tokens[tokens.length - 1].toLocaleLowerCase()
|
||||
return(mbchc.complete(window.PoseFemale3DCG.map(p => p.Name).filter(p => p.toLocaleLowerCase().startsWith(pose))))
|
||||
},
|
||||
history: function(down) {
|
||||
let [text, history] = [window.ElementValue("InputChat"), window.ChatRoomLastMessage]
|
||||
if (!this.HISTORY_MODE) {history.push(text); this.HISTORY_MODE = true}
|
||||
let ids = history.map((t,i) => [t,i]).filter(([t,i]) => t.startsWith(history[history.length - 1])).map(([t,i]) => i)
|
||||
if (!down) ids.reverse()
|
||||
let found = ids.find(id => (down) ? id > window.ChatRoomLastMessageIndex : id < window.ChatRoomLastMessageIndex)
|
||||
if (!found) return(this.bell())
|
||||
window.ElementValue("InputChat", history[found])
|
||||
window.ChatRoomLastMessageIndex = found
|
||||
},
|
||||
focus_chat_whitelist(event) {
|
||||
if (event.ctrlKey && "v" === event.key) return true // ctrl+V should paste
|
||||
return false
|
||||
},
|
||||
focus_chat(event) { // TODO: this is not ideal, but it will have to do for now
|
||||
if (event.repeat) return // only unique presses please
|
||||
if ("inline" !== document.getElementById("InputChat")?.style.display) return // input chat missing
|
||||
if (["InputChat","bce-message-input"].includes(document.activeElement.id)) return // focus already set
|
||||
if ([event.altKey, event.ctrlKey, event.metaKey].some(i => i) && !this.focus_chat_whitelist(event)) return // alt, ctrl and meta should all be false
|
||||
window.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.HIDE_ALL = this.HIDE_SPECIAL.concat(this.HIDE_BODY).concat(this.HIDE_CLOTHES).concat(this.HIDE_ITEMS)
|
||||
this.CommandsKey = CommandsKey /* eslint-disable-line no-undef */ // window.CommandsKey is undefined
|
||||
this.RE_ACTIVITY = RegExp(`^${this.CommandsKey}activity `)
|
||||
this.PREF_ACTIVITY = `${this.CommandsKey}activity `
|
||||
this.COMP_HINT = document.createElement("div")
|
||||
this.COMP_HINT.id = "mbchcCompHint"
|
||||
let css = document.createElement("style")
|
||||
css.type = "text/css"
|
||||
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} {
|
||||
flex-flow: column wrap;
|
||||
overflow: auto;
|
||||
display: none;
|
||||
background-color: ${this.RGB_POLLY};
|
||||
color: black;
|
||||
}
|
||||
#${this.COMP_HINT.id}[data-colortheme="dark"], #${this.COMP_HINT.id}[data-colortheme="dark2"] {
|
||||
background-color: ${this.RGB_MUTE};
|
||||
color: white;
|
||||
}
|
||||
#${this.COMP_HINT.id} div {
|
||||
margin: 0 0.5ex;
|
||||
}
|
||||
`
|
||||
document.head.appendChild(css)
|
||||
// Actions
|
||||
this.calculate_maps()
|
||||
window.Player.MBCHC = {VERSION: this.VERSION}
|
||||
window.CommandCombine(COMMANDS)
|
||||
|
||||
// Functions
|
||||
window.MBCHC.HIDE_ALL = window.MBCHC.HIDE_SPECIAL.concat(window.MBCHC.HIDE_BODY).concat(window.MBCHC.HIDE_CLOTHES).concat(window.MBCHC.HIDE_ITEMS)
|
||||
window.MBCHC.make_my_anal_hook_hide_body = function() {
|
||||
let item = window.InventoryGet(window.Player, "ItemButt")
|
||||
if (!item || !item.Asset || !item.Asset.Name) throw "butt seems empty"
|
||||
if (item.Asset.Name !== "AnalHook") throw "butt seems occupied by something other than the anal hook"
|
||||
if (!item.Property.Type || item.Property.Type !== "Hair") throw "anal hook seems not tied to hair"
|
||||
item.Property = {Type: "Hair", Hide: this.HIDE_ALL}
|
||||
window.CharacterRefresh(window.Player, true, true)
|
||||
}
|
||||
window.MBCHC.donate_data = function(id) {
|
||||
if (id == window.Player.MemberNumber) throw "recipient must not be you"
|
||||
const char = window.ChatRoomCharacter.find( c => c.MemberNumber == id )
|
||||
if (!char) throw "recipient not found"
|
||||
if (!char.IsRestrained()) throw "recipient must be bound"
|
||||
const cost = (Math.random() * 10 + 15).toFixed(0)
|
||||
if (window.Player.Money < cost) throw "not enough money"
|
||||
window.CharacterChangeMoney(window.Player, -cost)
|
||||
window.ServerSend("ChatRoomChat", {Content: "ReceiveSuitcaseMoney", Type: "Hidden", Target: id})
|
||||
return({cost: cost, name: (char.Nickname || char.Name)})
|
||||
}
|
||||
window.MBCHC.run_activity = function(char, ag, action) {
|
||||
try {
|
||||
char.FocusGroup = window.AssetGroupGet(char.AssetFamily, ag)
|
||||
if (!char.FocusGroup) throw "invalid AssetGroup"
|
||||
let activity = window.ActivityAllowedForGroup(char, char.FocusGroup.Name).find( a => a.Name === action)
|
||||
if (!activity) throw "invalid activity"
|
||||
window.ActivityRun(char, activity)
|
||||
} finally {
|
||||
char.FocusGroup = null
|
||||
}
|
||||
}
|
||||
window.MBCHC.send_activity = function(msg) {
|
||||
window.ServerSend("ChatRoomChat", {Type: "Action", Content: "lef_wie234jf_owhwi", Dictionary: [{Tag: 'MISSING PLAYER DIALOG: lef_wie234jf_owhwi', Text: msg}]})
|
||||
}
|
||||
// Command actions
|
||||
window.MBCHC.action_title = function(args) {
|
||||
let title = args.shift()
|
||||
if (!title || !title.length || title.length < 1) throw "empty title"
|
||||
if (title.length > 16) throw "title too long"
|
||||
if (!title.match(window.MBCHC.RE_TITLE)) throw "invalid title"
|
||||
let first = title.at(0).toLocaleUpperCase()
|
||||
let rest = title.slice(1).toLocaleLowerCase()
|
||||
title = first + rest
|
||||
window.TitleSet(title)
|
||||
// TODO: this needs much more work. at least don't push a second title
|
||||
// we need to patch the text cache
|
||||
// we need to check for other players' custom titles
|
||||
window.TitleList.push({Name: title, Requirement: () => {return true}})
|
||||
}
|
||||
window.MBCHC.action_donate = function(args) {
|
||||
let id = Number.parseInt(args.shift())
|
||||
if (isNaN(id)) throw "empty or invalid member number"
|
||||
let result = window.MBCHC.donate_data(id)
|
||||
window.ChatRoomSendLocal(`(You've bought data for $${result.cost} and sent it to ${result.name})`)
|
||||
}
|
||||
window.MBCHC.action_autohack = function() {
|
||||
window.MBCHC.AUTOHACK_ENABLED = !window.MBCHC.AUTOHACK_ENABLED
|
||||
window.ChatRoomSendLocal(`(Autohack is now ${window.MBCHC.AUTOHACK_ENABLED ? "enabled" : "disabled"})`)
|
||||
}
|
||||
// Hooks
|
||||
this.remove_fbc_hook = this.before("MainRun", () => window.bce_ActivityTriggers && this.patch_fbc())
|
||||
this.after("CharacterOnlineRefresh", char => this.update_char(char))
|
||||
this.after("ChatRoomReceiveSuitcaseMoney", () => {
|
||||
if (this.AUTOHACK_ENABLED && this.LAST_HACKED) {
|
||||
window.CurrentCharacter = this.cid2char(this.LAST_HACKED)
|
||||
this.LAST_HACKED = null
|
||||
window.ChatRoomTryToTakeSuitcase()
|
||||
}
|
||||
})
|
||||
this.before("ChatRoomSendChat", () => {
|
||||
let input = window.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)
|
||||
window.ElementValue("InputChat", input)
|
||||
}
|
||||
})
|
||||
this.after("ChatRoomSendChat", () => {
|
||||
const history = window.ChatRoomLastMessage
|
||||
if ((history.length > 1) && (history[history.length - 1] === history[history.length - 2])) {history.pop(); window.ChatRoomLastMessageIndex -= 1}
|
||||
})
|
||||
this.before("ChatRoomDrawCharacterOverlay", (C, CharX, CharY, Zoom, Pos) => {
|
||||
window.ChatRoomHideIconState < 1 && C.MBCHC && window.DrawRect(CharX + 175 * Zoom, CharY, 50 * Zoom, 50 * Zoom, C.MBCHC.VERSION === window.Player.MBCHC.VERSION ? this.RGB_POLLY : this.RGB_MUTE)
|
||||
if (window.ChatRoomHideIconState < 1 && C.MBCHC_LOCAL && "number" === typeof C.MBCHC_LOCAL.TZ) {
|
||||
const hours = new Date(window.CommonTime() + this.UTC_OFFSET + C.MBCHC_LOCAL.TZ * 60 * 60 * 1000).getHours()
|
||||
window.DrawTextFit(hours < 10 ? "0" + hours.toString() : hours.toString(), CharX + 200 * Zoom, CharY + 25 * Zoom, 46 * Zoom, "white", "black")
|
||||
}
|
||||
})
|
||||
this.after("ElementValue", (ID, Value, cc = window.CurrentCharacter) => "bce_LayerPriority" === ID && cc?.FocusGroup && window.InventoryGet(cc, cc.FocusGroup.Name)?.Difficulty.toString() === Value && window.InventoryLocked(cc, cc.FocusGroup.Name, true) && window.ElementSetAttribute(ID, "disabled", true))
|
||||
this.after("ChatRoomCreateElement", () => this.COMP_HINT.parentElement || document.body.appendChild(this.COMP_HINT))
|
||||
this.before("ChatRoomClearAllElements", () => !void this.complete_hint_hide() && this.COMP_HINT.remove())
|
||||
this.before("ChatRoomClick", () => this.complete_hint_hide())
|
||||
this.after("ChatRoomResize", () => {
|
||||
if (window.CharacterGetCurrent() == null && window.CurrentScreen == "ChatRoom" && document.getElementById("InputChat") && document.getElementById("TextAreaChatLog") && this.comp_hint_visible()) { // upstream
|
||||
const fontsize = ChatRoomFontSize /* eslint-disable-line no-undef */ // window.ChatRoomFontSize is undefined
|
||||
window.ElementPositionFix("TextAreaChatLog", fontsize, 1005, 66, 988, 630)
|
||||
window.ElementPositionFix(this.COMP_HINT.id, fontsize, 1005, 701, 988, 200)
|
||||
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
|
||||
let [event] = nextargs
|
||||
window.MBCHC.complete_hint_hide()
|
||||
if ((window.KeyPress == 33) || (window.KeyPress == 34)) { // better history
|
||||
event.preventDefault()
|
||||
return(window.MBCHC.history(window.KeyPress - 33))
|
||||
}
|
||||
if (window.MBCHC.HISTORY_MODE) {
|
||||
window.ChatRoomLastMessage.pop()
|
||||
window.MBCHC.HISTORY_MODE = false
|
||||
}
|
||||
return(next(nextargs))
|
||||
})
|
||||
|
||||
// Hooks
|
||||
window.ChatRoomMessageInvolvesPlayer = function(data) {
|
||||
if (!data.MBCHC_ID) {
|
||||
data.MBCHC_ID = window.MBCHC.NEXT_MESSAGE
|
||||
window.MBCHC.NEXT_MESSAGE += 1
|
||||
if (window.MBCHC.LOG_MESSAGES) console.debug(data)
|
||||
if (("ReceiveSuitcaseMoney" === data.Content) && ("Hidden" === data.Type)) { window.MBCHC.LAST_HACKED = data.Sender }
|
||||
}
|
||||
return(window.MBCHC.orig_ChatRoomMessageInvolvesPlayer(data))
|
||||
}
|
||||
window.ChatRoomReceiveSuitcaseMoney = function() {
|
||||
let result = window.MBCHC.orig_ChatRoomReceiveSuitcaseMoney()
|
||||
if (window.MBCHC.AUTOHACK_ENABLED && window.MBCHC.LAST_HACKED) {
|
||||
window.CurrentCharacter = {MemberNumber: window.MBCHC.LAST_HACKED}
|
||||
window.MBCHC.LAST_HACKED = null
|
||||
window.ChatRoomTryToTakeSuitcase()
|
||||
}
|
||||
return(result)
|
||||
}
|
||||
window.ChatRoomSendChat = function() {
|
||||
let input = window.ElementValue("InputChat")
|
||||
if (!input.startsWith("@@@")) {
|
||||
input = input.replace(/^@@/, "/activity ")
|
||||
input = input.replace(/^@/, "/myself ")
|
||||
window.ElementValue("InputChat", input)
|
||||
}
|
||||
return(window.MBCHC.orig_ChatRoomSendChat())
|
||||
}
|
||||
// Chat room handlers
|
||||
window.ChatRoomRegisterMessageHandler({ Priority: -220, Description: "MBCHC preprocessor", Callback: (data, sender, msg, metadata) => {
|
||||
data.MBCHC_ID = this.NEXT_MESSAGE
|
||||
this.NEXT_MESSAGE += 1
|
||||
if (this.LOG_MESSAGES) console.debug({data, sender, msg, metadata})
|
||||
}})
|
||||
window.ChatRoomRegisterMessageHandler({ Priority: -219, Description: "MBCHC room enter hook",
|
||||
Callback: (data, sender, msg, metadata) => { if (("Action" === data.Type) && ("ServerEnter" === data.Content) && (data.Sender === window.Player.cid)) this.player_enters_room() }
|
||||
})
|
||||
window.ChatRoomRegisterMessageHandler({ Priority: -219, Description: "MBCHC specific consumer",
|
||||
Callback: (data, sender, msg, metadata) => { if (("Hidden" === data.Type) && ("MBCHC" === data.Content)) return this.receive(data) }
|
||||
})
|
||||
window.ChatRoomRegisterMessageHandler({ Priority: -219, Description: "MBCHC autohack lookup",
|
||||
Callback: (data, sender, msg, metadata) => { if (("Hidden" === data.Type) && ("ReceiveSuitcaseMoney" === data.Content)) this.LAST_HACKED = data.Sender }
|
||||
})
|
||||
|
||||
// Actions
|
||||
window.CommandCombine(window.MBCHC.COMMANDS)
|
||||
if (window.bcModSdk) window.bcModSdk.registerMod("MBCHC", window.MBCHC.VERSION)
|
||||
window.MBCHC.LOADED = true
|
||||
console.info(window.MBCHC.log("loaded version " + window.MBCHC.VERSION))
|
||||
}
|
||||
return(window.MBCHC.orig_AsylumGGTSSAddItems())
|
||||
} // Loader
|
||||
// footer
|
||||
this.LOADED = true
|
||||
this.log("info", `loaded version ${this.VERSION}`)
|
||||
if (window.GameVersion !== this.TARGET_VERSION) this.log("warn", `Game version doesn't match the target ("${this.TARGET_VERSION}"), beware of incompatibilities`) // TODO: betas are like R86Beta1; cheat?
|
||||
if (("Online" === window.CurrentModule) && ("ChatRoom" === window.CurrentScreen)) {
|
||||
window.ChatRoomCharacter.forEach(c => this.update_char(c))
|
||||
this.player_enters_room()
|
||||
}
|
||||
},
|
||||
preloader() {
|
||||
this.SDK = window.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 (x) {console.error(x)} finally {return next(nextargs)}})
|
||||
this.after = (name, cb) => this.SDK.hookFunction(name, 0, (nextargs,next) => {const result = next(nextargs); try {cb?.(...nextargs)} catch (x) {console.error(x)} finally {return result}})
|
||||
if (window.CurrentModule && window.CurrentScreen && !("Character" === window.CurrentModule && "Login" === window.CurrentScreen)) return this.loader()
|
||||
this.remove_load_hook = this.before("AsylumGGTSSAddItems", () => this.loader())
|
||||
}
|
||||
} // MBCHC
|
||||
|
||||
fetch("https://code.fleshless.org/mute/MBCHC/raw/branch/master/bondage-club-mod-sdk-1.1.0.js").then(r=>r.text()).then(r=>eval(r)).then(_=>window.MBCHC.preloader()).catch(x=>console.error(x)) /* eslint-disable-line no-eval */
|
||||
})()
|
||||
|
|
|
@ -0,0 +1,667 @@
|
|||
// ==UserScript==
|
||||
// @name MBCHC-local
|
||||
// @version trunk
|
||||
// @description Mute's Bondage Club Hacks Collection (development version)
|
||||
// @author codename.mute@proton.me
|
||||
// @namespace https://code.fleshless.org/mute/
|
||||
// @homepage https://code.fleshless.org/mute/MBCHC
|
||||
// @updateURL https://code.fleshless.org/mute/MBCHC/raw/branch/master/mbchc-local.user.js
|
||||
// @downloadURL https://code.fleshless.org/mute/MBCHC/raw/branch/master/mbchc-local.user.js
|
||||
// @match https://bondageprojects.elementfx.com/R*
|
||||
// @match https://www.bondageprojects.elementfx.com/R*
|
||||
// @match https://bondage-europe.com/R*
|
||||
// @match https://www.bondage-europe.com/R*
|
||||
// @match http://localhost:*/*
|
||||
// @match http://127.0.0.1:*/*
|
||||
// @grant none
|
||||
// ==/UserScript==
|
||||
|
||||
(function() {
|
||||
"use strict";
|
||||
if (!window.AsylumGGTSSAddItems) throw "AsylumGGTSSAddItems() not found, aborting MBCHC loading"
|
||||
if (window.MBCHC) throw "MBCHC found, aborting loading"
|
||||
window.MBCHC = {
|
||||
VERSION: "trunk",
|
||||
TARGET_VERSION: "R86",
|
||||
NEXT_MESSAGE: 1,
|
||||
LOG_MESSAGES: false,
|
||||
RETHROW: false,
|
||||
LOADED: false,
|
||||
AUTOHACK_ENABLED: false,
|
||||
LAST_HACKED: null,
|
||||
HISTORY_MODE: false,
|
||||
RE_TITLE: /^[a-zA-Z]+$/,
|
||||
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,
|
||||
HIDE_SPECIAL: ["Activity","Emoticon"],
|
||||
HIDE_BODY: ["Blush","BodyLower","BodyUpper","Eyebrows","Eyes","Eyes2","Face","Fluids","HairBack","HairFront","Hands","Head","LeftHand","Mouth","Nipples","Pussy","RightHand"],
|
||||
HIDE_CLOTHES: [
|
||||
"Cloth","ClothAccessory","Necklace","Suit","ClothLower","SuitLower","Bra","Corset","Panties",
|
||||
"Socks","RightAnklet","LeftAnklet","Garters","Shoes","Hat","HairAccessory3","HairAccessory1","HairAccessory2",
|
||||
"Gloves","Bracelet","Glasses","Mask","TailStraps","Wings"
|
||||
],
|
||||
HIDE_ITEMS: [
|
||||
"ItemMisc","ItemEars","ItemHead","ItemNose","ItemHood","ItemAddon","ItemMouth","ItemMouth2","ItemMouth3",
|
||||
"ItemArms","ItemNeckAccessories","ItemNeck","ItemNeckRestraints","ItemNipples","ItemNipplesPiercings","ItemBreast","ItemTorso","ItemTorso2",
|
||||
"ItemHands","ItemPelvis","ItemVulva","ItemVulvaPiercings",
|
||||
"ItemDevices","ItemLegs","ItemFeet","ItemBoots"
|
||||
],
|
||||
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: {
|
||||
"versions": {desc: "show the mod versions across the room", cb: mbchc => mbchc.inform(mbchc.gather_versions().map(c => `<div><b>${c.name}</b> (${c.cid}): ${c.version}</div>`).join(""))},
|
||||
"autohack": {desc: "toggle the autohack feature", cb: mbchc => mbchc.inform(`Autohack is now ${(mbchc.AUTOHACK_ENABLED = !mbchc.AUTOHACK_ENABLED) ? "enabled" : "disabled"}`)},
|
||||
"disappear": {desc: "become invisible (requires anal hook -> hair)", cb: mbchc => mbchc.disappear()},
|
||||
"donate": {desc: "Buy data and send it to recipient", args: {TARGET: {}}, cb: (mbchc, args) => mbchc.donate_data(args[0])},
|
||||
"title": {desc: "set a custom title (<b>WIP</b>)", args: {TITLE: {}}, cb: (mbchc, args) => mbchc.title(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 (window.Player.OnlineSettings.MBCHC) {delete window.Player.OnlineSettings.MBCHC; mbchc.save_settings()}}},
|
||||
},
|
||||
ensure: function(error, callback) {
|
||||
let result = callback.call(this)
|
||||
if (!result) throw error
|
||||
return(result)
|
||||
},
|
||||
calculate_maps: function() {
|
||||
this.DO_DATA = {verbs: {}, zones: {}}
|
||||
for (let [verbs, data] of Object.entries(this.MAP_ACTIONS)) {
|
||||
let unwound = {}
|
||||
for (let [zones, actions] of Object.entries(data)) {
|
||||
let all = (actions.all) ? actions.all.split("|") : []
|
||||
let processed = {self: (actions.self) ? actions.self.split("|").concat(all) : all, others: (actions.others) ? actions.others.split("|").concat(all) : all}
|
||||
for (let zone of zones.split(",")) unwound[`Item${zone}`] = processed
|
||||
}
|
||||
for (let verb of verbs.split("|")) this.DO_DATA.verbs[verb] = unwound
|
||||
}
|
||||
for (let [ag, zones] of Object.entries(this.MAP_ZONES)) for (let zone of zones) this.DO_DATA.zones[zone] = ag
|
||||
},
|
||||
settings: function(setting = null) {
|
||||
let settings = window.Player.OnlineSettings.MBCHC || {}
|
||||
return(setting ? settings[setting] : settings)
|
||||
},
|
||||
save_settings: function(cb = null) {
|
||||
if (cb) {
|
||||
if (!window.Player.OnlineSettings.MBCHC) window.Player.OnlineSettings.MBCHC = {}
|
||||
cb.call(this, window.Player.OnlineSettings.MBCHC)
|
||||
}
|
||||
window.ServerAccountUpdate.QueueData({OnlineSettings: window.Player.OnlineSettings})
|
||||
},
|
||||
log: function(level, msg) {console[level]("MBCHC: " + String(msg))},
|
||||
empty: function(text) {
|
||||
if (!text) return(true)
|
||||
if (String(text).trim().length < 1) return(true)
|
||||
return(false)
|
||||
},
|
||||
normalise_message: function(text, options = {}) {
|
||||
let result = text
|
||||
if (options.trim) result = result.trim()
|
||||
if (options.low) result = result.toLocaleLowerCase()
|
||||
if (options.up) {
|
||||
let first = result.at(0).toLocaleUpperCase()
|
||||
let rest = result.slice(1)
|
||||
result = first + rest
|
||||
}
|
||||
if (options.dot && result.match(this.RE_LAST_LETTER)) result = `${result}.`
|
||||
return(result)
|
||||
},
|
||||
tokenise: function(text) { return text.replace(this.RE_SPACES, " ").split(" ") },
|
||||
inform: function(html, timeout = 60000) { window.ChatRoomSendLocal(`<div class="mbchc">${html}</div>`, timeout) },
|
||||
report: function(x) {
|
||||
this.inform(`Error: ${x.toString()}`)
|
||||
if (this.RETHROW) throw x
|
||||
},
|
||||
in: function(x, floor, ceiling) { return((x >= floor) && (x <= ceiling)) },
|
||||
cid2char: function(cid) {
|
||||
cid = Number.parseInt(cid)
|
||||
if (cid === window.Player.cid) return(window.Player)
|
||||
return(this.ensure(`character ${cid} not found in the room`, () => window.ChatRoomCharacter.find(c => c.cid === cid)))
|
||||
},
|
||||
pos2char: function(pos) {
|
||||
if (!this.in(pos, 0, window.ChatRoomCharacter.length - 1)) throw `invalid position ${pos}`
|
||||
return(window.ChatRoomCharacter[pos])
|
||||
},
|
||||
rel2char: function(target) {
|
||||
let me = this.ensure("can't find my position", () => window.ChatRoomCharacter.findIndex(char => char.IsPlayer()) + 1) - 1 // 0 is falsy, but is valid index
|
||||
let pos = null
|
||||
if (target.match(this.RE_ALL_LEFT)) pos = me - target.length
|
||||
if (target.match(this.RE_ALL_RIGHT)) pos = me + target.length
|
||||
if (null === pos) throw `failed to parse target "${target}"`
|
||||
pos = pos % window.ChatRoomCharacter.length
|
||||
if (pos < 0) pos = pos + window.ChatRoomCharacter.length
|
||||
return(this.pos2char(pos))
|
||||
},
|
||||
target2char: function(target) { // target should be lowcase
|
||||
let input = target
|
||||
if (this.empty(target)) return(window.Player)
|
||||
let int = Number.parseInt(target)
|
||||
target = String(target)
|
||||
let found = []
|
||||
if (target.startsWith("=")) return(this.cid2char(target.slice(1)))
|
||||
if (target.startsWith("<") || target.startsWith(">")) return(this.rel2char(target))
|
||||
if (!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(...window.ChatRoomCharacter.filter(c => c.cid.toString().indexOf(target) > -1))
|
||||
}
|
||||
if (target.startsWith("@")) target = target.slice(1)
|
||||
found.push(...window.ChatRoomCharacter.filter(c => c.Name.toLocaleLowerCase().indexOf(target) > -1))
|
||||
found.push(...window.ChatRoomCharacter.filter(c => c.Nickname && (c.Nickname.toLocaleLowerCase().indexOf(target) > -1)))
|
||||
let map = {}
|
||||
found.forEach(c => {if (!map[c.cid]) map[c.cid] = c} )
|
||||
found = Object.values(map)
|
||||
if (found.length < 1) throw `target "${input}": no match`
|
||||
if (found.length > 1) throw `target "${input}": multiple matches (${found.map(c => `${c.cid}|${c.Name}|${c.Nickname || c.Name}`).join(",")})`
|
||||
return(found[0])
|
||||
},
|
||||
char2targets: function(char) {
|
||||
let [result, cid] = [new Set(), char.cid.toString()]
|
||||
result.add(cid).add(`=${cid}`)
|
||||
this.tokenise(char.Name).forEach(t => {result.add(t); result.add(`@${t}`)})
|
||||
if (char.Nickname) this.tokenise(char.Nickname).forEach(t => {result.add(t); result.add(`@${t}`)})
|
||||
return result
|
||||
},
|
||||
donate_data: function(target) {
|
||||
let char = this.target2char(target)
|
||||
if (char.IsPlayer()) throw "target must not be you"
|
||||
if (!char.IsRestrained()) throw "target must be bound"
|
||||
const cost = Math.round((Math.random() * 10 + 15))
|
||||
if (window.Player.Money < cost) throw "not enough money"
|
||||
window.CharacterChangeMoney(window.Player, -cost)
|
||||
window.ServerSend("ChatRoomChat", {Content: "ReceiveSuitcaseMoney", Type: "Hidden", Target: char.cid})
|
||||
window.ChatRoomMessage({Sender: window.Player.cid, Type: "Action", Content: `You've bought data for $${cost} and sent it to ${char.dn}.`, Dictionary: [{Tag: "MISSING PLAYER DIALOG: ", Text: ""}]})
|
||||
},
|
||||
run_activity: function(char, ag, action) { try {
|
||||
if (!window.ActivityAllowed()) throw "activities disabled in this room"
|
||||
if (!window.ServerChatRoomGetAllowItem(window.Player, char)) throw "no permissions"
|
||||
char.FocusGroup = this.ensure("invalid AssetGroup", () => window.AssetGroupGet(char.AssetFamily, ag))
|
||||
let activity = this.ensure("invalid activity", () => window.ActivityAllowedForGroup(char, char.FocusGroup.Name, true).find(a => a.Name === action || a.Activity?.Name === action))
|
||||
if ((activity.Name || activity.Activity.Name).endsWith("Item")) {
|
||||
const item = this.ensure("no toy found", () => window.Player.Inventory.find(i => i.Asset?.Name === "SpankingToys" && i.Asset.Group?.Name === char.FocusGroup.Name && window.AssetSpankingToys.DynamicActivity(char) === (activity.Name || activity.Activity.Name)))
|
||||
window.DialogPublishAction(char, item)
|
||||
} else window.ActivityRun(char, activity)
|
||||
} finally {char.FocusGroup = null} },
|
||||
replace_me: function(match, offset, string) {
|
||||
let text = string.slice(1)
|
||||
let suffix = " "
|
||||
if (text.startsWith("'") || text.startsWith(" ")) suffix = ""
|
||||
return `${window.MBCHC.PREF_ACTIVITY}<${window.Player.cid}:>SourceCharacter${suffix}`
|
||||
},
|
||||
cid2dict: function(type, cid) { return({Tag: `${type}Character`, MemberNumber: cid, Text: this.cid2char(cid).dn}) },
|
||||
send_activity: function(msg) {
|
||||
let dict = [{Tag: "MISSING PLAYER DIALOG: ", Text: "\u200C"}] // zero-width non-joiner, used to break up ligatures, does nothing here, but an empty string is a falsy value
|
||||
let cids = msg.match(this.RE_ACT_CIDS)
|
||||
if (cids) {
|
||||
msg = msg.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]))
|
||||
}
|
||||
window.ServerSend("ChatRoomChat", {Type: "Action", Content: msg, Dictionary: dict})
|
||||
},
|
||||
receive: function(data) {
|
||||
let char = this.cid2char(data.Sender)
|
||||
if (char.IsPlayer()) return true // this is our own message, sent back to us
|
||||
let payload = this.ensure("Empty message", () => data.Dictionary[0])
|
||||
switch (payload.type) {
|
||||
case "greetings": case "hello":
|
||||
char.MBCHC = payload.value
|
||||
if ("greetings" === payload.type) this.hello(char)
|
||||
break
|
||||
default: // if we don't know the type it may be from a newer version
|
||||
}
|
||||
return true
|
||||
},
|
||||
hello: function(char = null) {
|
||||
let payload = {type: "greetings", value: window.Player.MBCHC}
|
||||
if (char) payload.type = "hello"
|
||||
let message = {Content: "MBCHC", Type: "Hidden", Dictionary: [payload]}
|
||||
if (char) message.Target = char.cid
|
||||
window.ServerSend("ChatRoomChat", message)
|
||||
},
|
||||
disappear: function() {
|
||||
let item = window.InventoryGet(window.Player, "ItemButt")
|
||||
if (!item || !item.Asset || !item.Asset.Name) throw "butt seems empty"
|
||||
if (item.Asset.Name !== "AnalHook") throw "butt seems occupied by something other than the anal hook"
|
||||
if (!item.Property.Type || item.Property.Type !== "Hair") throw "anal hook seems not tied to hair"
|
||||
item.Property = {Type: "Hair", Hide: this.HIDE_ALL}
|
||||
window.CharacterRefresh(window.Player, true, true)
|
||||
},
|
||||
title: function(title) { // WIP
|
||||
if (this.empty(title)) throw "empty title"
|
||||
title = this.normalise_message(title, {trim: true, up: true, low: true})
|
||||
if (title.length > 16) throw "title too long"
|
||||
if (!title.match(this.RE_TITLE)) throw "invalid title"
|
||||
window.TitleSet(title)
|
||||
//window.TitleList.push({Name: title, Requirement: () => true}) // check for existing first
|
||||
},
|
||||
copy_fbc_trigger: function(trigger) {
|
||||
let 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: function() {
|
||||
this.remove_fbc_hook()
|
||||
delete this.remove_fbc_hook
|
||||
window.bce_ActivityTriggers.push(...window.bce_ActivityTriggers.filter(t => "Emote" === t.Type).map(t => this.copy_fbc_trigger(t)))
|
||||
/* (["anim", "pose"]).forEach(tag => {let cmd = window.Commands.find(c => tag === c.Tag); if (cmd) cmd.AutoComplete = this[`complete_fbc_${tag}`]}) */ // this line explodes, don't ask me why
|
||||
let cmd = window.Commands.find(c => "anim" === c.Tag)
|
||||
if (cmd) cmd.AutoComplete = this.complete_fbc_anim
|
||||
cmd = window.Commands.find(c => "pose" === c.Tag)
|
||||
if (cmd) cmd.AutoComplete = this.complete_fbc_pose
|
||||
},
|
||||
gather_versions: function() { return(window.ChatRoomCharacter.filter(c => c.MBCHC).map(c => ({name: c.dn, cid: c.cid, version: c.MBCHC.VERSION}))) },
|
||||
find_timezone: function(char) {
|
||||
const timezones = this.settings("timezones")
|
||||
if (timezones && "number" === typeof timezones[char.cid]) return(timezones[char.cid])
|
||||
const match = (char.Description) ? char.Description.match(this.RE_TZ) : null
|
||||
const int = match ? Number.parseInt(match[1] + match[2]) : 42
|
||||
if (this.in(int, -12, 12)) return(int)
|
||||
return(null)
|
||||
},
|
||||
player_enters_room: function() { // or if the mod is loaded while player is in the room
|
||||
this.hello()
|
||||
},
|
||||
set_timezone: function(args) {
|
||||
let tz = Number.parseInt(args[0])
|
||||
if (isNaN(tz)) throw `invalid offset "${args[0]}"`
|
||||
if (!this.in(tz, -12, 12)) throw "offset should be [-12,12]"
|
||||
let char = this.target2char(args[1])
|
||||
char.MBCHC_LOCAL.TZ = tz
|
||||
this.save_settings(s => {if (!s.timezones) s.timezones = {}; s.timezones[char.cid] = tz})
|
||||
},
|
||||
update_char: function(char) {
|
||||
char.cid = char.MemberNumber // Club ID (shorter)
|
||||
char.dn = window.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: function(argline, cmdline, args) { const mbchc = window.MBCHC; try { // `this` is command object
|
||||
if (args.length < 1) 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("")))
|
||||
let cmd = String(args.shift())
|
||||
let sub = mbchc.ensure(`unknown subcommand "${cmd}"`, () => mbchc.SUBCOMMANDS_MBCHC[cmd])
|
||||
sub.cb.call(mbchc, mbchc, args, argline, cmdline)
|
||||
} catch (x) { mbchc.report(x) } },
|
||||
command_activity: function(argline, cmdline, args) { const mbchc = window.MBCHC; if (!mbchc.empty(argline)) { try { // `this` is command object
|
||||
let message = mbchc.normalise_message(cmdline.replace(mbchc.RE_ACTIVITY, ''), {trim: true, dot: true, up: true})
|
||||
mbchc.send_activity(message)
|
||||
} catch (x) { mbchc.report(x) } } },
|
||||
command_do: function(argline, cmdline, args) { const mbchc = window.MBCHC; try { // `this` is command object
|
||||
if (args.length < 1) 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
|
||||
let zones = mbchc.ensure(`unknown verb "${verb}"`, () => mbchc.DO_DATA.verbs[verb])
|
||||
if (1 === Object.keys(zones).length) {
|
||||
if (!target) target = zone
|
||||
zone = mbchc.MAP_ZONES[Object.keys(zones)[0]][0]
|
||||
}
|
||||
if (!zone) throw "zone missing"
|
||||
let ag = mbchc.ensure(`unknown zone "${zone}"`, () => mbchc.DO_DATA.zones[zone])
|
||||
let types = mbchc.ensure(`zone "${zone}" invalid for "${verb}"`, () => zones[ag])
|
||||
let char = window.Player
|
||||
if (target && ((types.self.length < 1) || (types.others.length > 0))) char = mbchc.target2char(target)
|
||||
let type = char.IsPlayer() ? "self" : "others"
|
||||
let available = window.ActivityAllowedForGroup(char, ag)
|
||||
let toy = window.InventoryGet(window.Player, "ItemHands")
|
||||
if (toy && toy.Asset.Name === "SpankingToys") available.push(window.AssetAllActivities(char.AssetFamily).find(a => a.Name === window.InventorySpankingToysGetActivity?.(window.Player)))
|
||||
let actions = mbchc.ensure(`zone "${zone}" invalid for ("${verb}" "${type}")`, () => types[type])
|
||||
let action = mbchc.ensure(`invalid action (${verb} ${zone} ${target})`, () => actions.find(name => available.find(a => a.Name === name || a.Activity?.Name === name)))
|
||||
mbchc.run_activity(char, ag, action)
|
||||
} catch (x) { mbchc.report(x) } },
|
||||
bell: function() {
|
||||
setTimeout(() => {document.getElementById("InputChat").style.outline = ""}, 100)
|
||||
document.getElementById("InputChat").style.outline = "solid red"
|
||||
},
|
||||
complete: function(options, space = true) {
|
||||
if (options.length < 1) return(this.bell())
|
||||
if (options.length > 1) {
|
||||
let width = Math.max(...options.map(o => o.length))
|
||||
let pref = null
|
||||
for (let i = width; i > 0; i -= 1) { let test = options[0].slice(0, i); if (options.every(o => o.startsWith(test))) {pref = test; break} }
|
||||
if (pref) this.complete([pref], false)
|
||||
this.complete_hint(options)
|
||||
} else window.ElementValue("InputChat", document.getElementById("InputChat").value.replace(this.RE_LAST_WORD, `$1${options[0]}${space ? " " : ""}`))
|
||||
},
|
||||
complete_hint: function(options) {
|
||||
this.COMP_HINT.innerHTML = options.sort().map(s => `<div>${s}</div>`).join("")
|
||||
this.COMP_HINT.style.display = "flex"
|
||||
window.ElementSetDataAttribute(this.COMP_HINT.id, "colortheme", (window.Player.ChatSettings.ColorTheme || "Light"))
|
||||
let rescroll = window.ElementIsScrolledToEnd("TextAreaChatLog")
|
||||
window.ChatRoomResize(false)
|
||||
if (rescroll) window.ElementScrollToEnd("TextAreaChatLog")
|
||||
},
|
||||
comp_hint_visible: function() {return(this.COMP_HINT.parentElement && "flex" === this.COMP_HINT.style.display)},
|
||||
complete_hint_hide: function(options) {if (!this.comp_hint_visible()) return; this.COMP_HINT.style.display = "none"; window.ChatRoomResize(false)},
|
||||
complete_target: function(token, me2 = true, check_perms = false) {
|
||||
let [locase, found] = [token.toLocaleLowerCase(), new Set()]
|
||||
for (let c of window.ChatRoomCharacter) {
|
||||
if ((c.IsPlayer() && !me2) || (check_perms && !window.ServerChatRoomGetAllowItem(window.Player, c))) continue
|
||||
this.char2targets(c).forEach(s => {if (s.toLocaleLowerCase().startsWith(locase)) found.add(s)})
|
||||
}
|
||||
this.complete(Array.from(found))
|
||||
},
|
||||
complete_common: function() {
|
||||
let input = document.getElementById("InputChat").value
|
||||
return([this, input, this.tokenise(input)])
|
||||
},
|
||||
complete_mbchc: function(args, locase, cmdline) { const [mbchc, input, tokens] = window.MBCHC.complete_common(); // `this` is command object
|
||||
if (tokens.length < 1) return
|
||||
if (tokens.length < 2) return(mbchc.complete([`${mbchc.CommandsKey}${this.Tag}`]))
|
||||
let subname = tokens[1].toLocaleLowerCase()
|
||||
if (tokens.length < 3) return(mbchc.complete(Object.keys(mbchc.SUBCOMMANDS_MBCHC).filter(c => c.startsWith(subname)))) // complete subcommand name
|
||||
let sub = mbchc.SUBCOMMANDS_MBCHC[subname]
|
||||
if (sub && sub.args) {
|
||||
let argname = Object.keys(sub.args)[tokens.length - 3]
|
||||
if ("TARGET" === argname) return(mbchc.complete_target(tokens[tokens.length - 1], false))
|
||||
if ("[TARGET]" === argname) return(mbchc.complete_target(tokens[tokens.length - 1]), true)
|
||||
}
|
||||
},
|
||||
complete_do_target: function(actions, token) {
|
||||
if (!actions) return
|
||||
let me2 = (actions.self.length > 0)
|
||||
if (me2 && actions.others.length < 1) return(this.complete([window.Player.cid.toString()])) // target is always the player
|
||||
this.complete_target(token, me2, true)
|
||||
},
|
||||
complete_do: function(args, locase, cmdline) { const [mbchc, input, tokens] = window.MBCHC.complete_common(); // `this` is command object
|
||||
if (tokens.length < 1) 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
|
||||
let 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
|
||||
let 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: function(args, locase, cmdline) { const [mbchc, input, tokens] = window.MBCHC.complete_common(); // `this` is command object
|
||||
if (tokens.length < 1) return
|
||||
if (tokens.length < 2) return(mbchc.complete([`${mbchc.CommandsKey}${this.Tag}`]))
|
||||
if (tokens.length > 2) return(mbchc.bell())
|
||||
let anim = tokens[1].toLocaleLowerCase()
|
||||
return(mbchc.complete(Object.keys(window.bce_EventExpressions).filter(a => a.toLocaleLowerCase().startsWith(anim))))
|
||||
},
|
||||
complete_fbc_pose: function(args, locase, cmdline) { const [mbchc, input, tokens] = window.MBCHC.complete_common(); // `this` is command object
|
||||
if (tokens.length < 1) return
|
||||
if (tokens.length < 2) return(mbchc.complete([`${mbchc.CommandsKey}${this.Tag}`]))
|
||||
let pose = tokens[tokens.length - 1].toLocaleLowerCase()
|
||||
return(mbchc.complete(window.PoseFemale3DCG.map(p => p.Name).filter(p => p.toLocaleLowerCase().startsWith(pose))))
|
||||
},
|
||||
history: function(down) {
|
||||
let [text, history] = [window.ElementValue("InputChat"), window.ChatRoomLastMessage]
|
||||
if (!this.HISTORY_MODE) {history.push(text); this.HISTORY_MODE = true}
|
||||
let ids = history.map((t,i) => [t,i]).filter(([t,i]) => t.startsWith(history[history.length - 1])).map(([t,i]) => i)
|
||||
if (!down) ids.reverse()
|
||||
let found = ids.find(id => (down) ? id > window.ChatRoomLastMessageIndex : id < window.ChatRoomLastMessageIndex)
|
||||
if (!found) return(this.bell())
|
||||
window.ElementValue("InputChat", history[found])
|
||||
window.ChatRoomLastMessageIndex = found
|
||||
},
|
||||
focus_chat_whitelist(event) {
|
||||
if (event.ctrlKey && "v" === event.key) return true // ctrl+V should paste
|
||||
return false
|
||||
},
|
||||
focus_chat(event) { // TODO: this is not ideal, but it will have to do for now
|
||||
if (event.repeat) return // only unique presses please
|
||||
if ("inline" !== document.getElementById("InputChat")?.style.display) return // input chat missing
|
||||
if (["InputChat","bce-message-input"].includes(document.activeElement.id)) return // focus already set
|
||||
if ([event.altKey, event.ctrlKey, event.metaKey].some(i => i) && !this.focus_chat_whitelist(event)) return // alt, ctrl and meta should all be false
|
||||
window.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.HIDE_ALL = this.HIDE_SPECIAL.concat(this.HIDE_BODY).concat(this.HIDE_CLOTHES).concat(this.HIDE_ITEMS)
|
||||
this.CommandsKey = CommandsKey /* eslint-disable-line no-undef */ // window.CommandsKey is undefined
|
||||
this.RE_ACTIVITY = RegExp(`^${this.CommandsKey}activity `)
|
||||
this.PREF_ACTIVITY = `${this.CommandsKey}activity `
|
||||
this.COMP_HINT = document.createElement("div")
|
||||
this.COMP_HINT.id = "mbchcCompHint"
|
||||
let css = document.createElement("style")
|
||||
css.type = "text/css"
|
||||
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} {
|
||||
flex-flow: column wrap;
|
||||
overflow: auto;
|
||||
display: none;
|
||||
background-color: ${this.RGB_POLLY};
|
||||
color: black;
|
||||
}
|
||||
#${this.COMP_HINT.id}[data-colortheme="dark"], #${this.COMP_HINT.id}[data-colortheme="dark2"] {
|
||||
background-color: ${this.RGB_MUTE};
|
||||
color: white;
|
||||
}
|
||||
#${this.COMP_HINT.id} div {
|
||||
margin: 0 0.5ex;
|
||||
}
|
||||
`
|
||||
document.head.appendChild(css)
|
||||
// Actions
|
||||
this.calculate_maps()
|
||||
window.Player.MBCHC = {VERSION: this.VERSION}
|
||||
window.CommandCombine(COMMANDS)
|
||||
|
||||
// Hooks
|
||||
this.remove_fbc_hook = this.before("MainRun", () => window.bce_ActivityTriggers && this.patch_fbc())
|
||||
this.after("CharacterOnlineRefresh", char => this.update_char(char))
|
||||
this.after("ChatRoomReceiveSuitcaseMoney", () => {
|
||||
if (this.AUTOHACK_ENABLED && this.LAST_HACKED) {
|
||||
window.CurrentCharacter = this.cid2char(this.LAST_HACKED)
|
||||
this.LAST_HACKED = null
|
||||
window.ChatRoomTryToTakeSuitcase()
|
||||
}
|
||||
})
|
||||
this.before("ChatRoomSendChat", () => {
|
||||
let input = window.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)
|
||||
window.ElementValue("InputChat", input)
|
||||
}
|
||||
})
|
||||
this.after("ChatRoomSendChat", () => {
|
||||
const history = window.ChatRoomLastMessage
|
||||
if ((history.length > 1) && (history[history.length - 1] === history[history.length - 2])) {history.pop(); window.ChatRoomLastMessageIndex -= 1}
|
||||
})
|
||||
this.before("ChatRoomDrawCharacterOverlay", (C, CharX, CharY, Zoom, Pos) => {
|
||||
window.ChatRoomHideIconState < 1 && C.MBCHC && window.DrawRect(CharX + 175 * Zoom, CharY, 50 * Zoom, 50 * Zoom, C.MBCHC.VERSION === window.Player.MBCHC.VERSION ? this.RGB_POLLY : this.RGB_MUTE)
|
||||
if (window.ChatRoomHideIconState < 1 && C.MBCHC_LOCAL && "number" === typeof C.MBCHC_LOCAL.TZ) {
|
||||
const hours = new Date(window.CommonTime() + this.UTC_OFFSET + C.MBCHC_LOCAL.TZ * 60 * 60 * 1000).getHours()
|
||||
window.DrawTextFit(hours < 10 ? "0" + hours.toString() : hours.toString(), CharX + 200 * Zoom, CharY + 25 * Zoom, 46 * Zoom, "white", "black")
|
||||
}
|
||||
})
|
||||
this.after("ElementValue", (ID, Value, cc = window.CurrentCharacter) => "bce_LayerPriority" === ID && cc?.FocusGroup && window.InventoryGet(cc, cc.FocusGroup.Name)?.Difficulty.toString() === Value && window.InventoryLocked(cc, cc.FocusGroup.Name, true) && window.ElementSetAttribute(ID, "disabled", true))
|
||||
this.after("ChatRoomCreateElement", () => this.COMP_HINT.parentElement || document.body.appendChild(this.COMP_HINT))
|
||||
this.before("ChatRoomClearAllElements", () => !void this.complete_hint_hide() && this.COMP_HINT.remove())
|
||||
this.before("ChatRoomClick", () => this.complete_hint_hide())
|
||||
this.after("ChatRoomResize", () => {
|
||||
if (window.CharacterGetCurrent() == null && window.CurrentScreen == "ChatRoom" && document.getElementById("InputChat") && document.getElementById("TextAreaChatLog") && this.comp_hint_visible()) { // upstream
|
||||
const fontsize = ChatRoomFontSize /* eslint-disable-line no-undef */ // window.ChatRoomFontSize is undefined
|
||||
window.ElementPositionFix("TextAreaChatLog", fontsize, 1005, 66, 988, 630)
|
||||
window.ElementPositionFix(this.COMP_HINT.id, fontsize, 1005, 701, 988, 200)
|
||||
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
|
||||
let [event] = nextargs
|
||||
window.MBCHC.complete_hint_hide()
|
||||
if ((window.KeyPress == 33) || (window.KeyPress == 34)) { // better history
|
||||
event.preventDefault()
|
||||
return(window.MBCHC.history(window.KeyPress - 33))
|
||||
}
|
||||
if (window.MBCHC.HISTORY_MODE) {
|
||||
window.ChatRoomLastMessage.pop()
|
||||
window.MBCHC.HISTORY_MODE = false
|
||||
}
|
||||
return(next(nextargs))
|
||||
})
|
||||
|
||||
// Chat room handlers
|
||||
window.ChatRoomRegisterMessageHandler({ Priority: -220, Description: "MBCHC preprocessor", Callback: (data, sender, msg, metadata) => {
|
||||
data.MBCHC_ID = this.NEXT_MESSAGE
|
||||
this.NEXT_MESSAGE += 1
|
||||
if (this.LOG_MESSAGES) console.debug({data, sender, msg, metadata})
|
||||
}})
|
||||
window.ChatRoomRegisterMessageHandler({ Priority: -219, Description: "MBCHC room enter hook",
|
||||
Callback: (data, sender, msg, metadata) => { if (("Action" === data.Type) && ("ServerEnter" === data.Content) && (data.Sender === window.Player.cid)) this.player_enters_room() }
|
||||
})
|
||||
window.ChatRoomRegisterMessageHandler({ Priority: -219, Description: "MBCHC specific consumer",
|
||||
Callback: (data, sender, msg, metadata) => { if (("Hidden" === data.Type) && ("MBCHC" === data.Content)) return this.receive(data) }
|
||||
})
|
||||
window.ChatRoomRegisterMessageHandler({ Priority: -219, Description: "MBCHC autohack lookup",
|
||||
Callback: (data, sender, msg, metadata) => { if (("Hidden" === data.Type) && ("ReceiveSuitcaseMoney" === data.Content)) this.LAST_HACKED = data.Sender }
|
||||
})
|
||||
|
||||
// footer
|
||||
this.LOADED = true
|
||||
this.log("info", `loaded version ${this.VERSION}`)
|
||||
if (window.GameVersion !== this.TARGET_VERSION) this.log("warn", `Game version doesn't match the target ("${this.TARGET_VERSION}"), beware of incompatibilities`) // TODO: betas are like R86Beta1; cheat?
|
||||
if (("Online" === window.CurrentModule) && ("ChatRoom" === window.CurrentScreen)) {
|
||||
window.ChatRoomCharacter.forEach(c => this.update_char(c))
|
||||
this.player_enters_room()
|
||||
}
|
||||
},
|
||||
preloader() {
|
||||
this.SDK = window.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 (x) {console.error(x)} finally {return next(nextargs)}})
|
||||
this.after = (name, cb) => this.SDK.hookFunction(name, 0, (nextargs,next) => {const result = next(nextargs); try {cb?.(...nextargs)} catch (x) {console.error(x)} finally {return result}})
|
||||
if (window.CurrentModule && window.CurrentScreen && !("Character" === window.CurrentModule && "Login" === window.CurrentScreen)) return this.loader()
|
||||
this.remove_load_hook = this.before("AsylumGGTSSAddItems", () => this.loader())
|
||||
}
|
||||
} // MBCHC
|
||||
|
||||
fetch("https://code.fleshless.org/mute/MBCHC/raw/branch/master/bondage-club-mod-sdk-1.1.0.js").then(r=>r.text()).then(r=>eval(r)).then(_=>window.MBCHC.preloader()).catch(x=>console.error(x)) /* eslint-disable-line no-eval */
|
||||
})()
|
|
@ -0,0 +1,814 @@
|
|||
export {}
|
||||
|
||||
// Zero-width non-joiner, used to break up ligatures, does nothing here, but an empty string is a falsy value
|
||||
const MISSING_PLAYER_DIALOG = {Tag: 'MISSING PLAYER DIALOG: ', Text: '\u200C'}
|
||||
|
||||
if (window.MBCHC) throw new Error('MBCHC found, aborting loading')
|
||||
window.MBCHC = {
|
||||
VERSION: 'dev.10',
|
||||
NEXT_MESSAGE: 1,
|
||||
LOG_MESSAGES: false,
|
||||
RETHROW: false,
|
||||
LOADED: false,
|
||||
AUTOHACK_ENABLED: false,
|
||||
LAST_HACKED: null,
|
||||
HISTORY_MODE: false,
|
||||
// RE_TITLE: /^[a-zA-Z]+$/,
|
||||
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,
|
||||
// HIDE_SPECIAL: ['Activity', 'Emoticon'],
|
||||
// HIDE_BODY: ['Blush', 'BodyLower', 'BodyUpper', 'Eyebrows', 'Eyes', 'Eyes2', 'Face', 'Fluids', 'HairBack', 'HairFront', 'Hands', 'Head', 'LeftHand', 'Mouth', 'Nipples', 'Pussy', 'RightHand'],
|
||||
// HIDE_CLOTHES: [
|
||||
// 'Cloth',
|
||||
// 'ClothAccessory',
|
||||
// 'Necklace',
|
||||
// 'Suit',
|
||||
// 'ClothLower',
|
||||
// 'SuitLower',
|
||||
// 'Bra',
|
||||
// 'Corset',
|
||||
// 'Panties',
|
||||
// 'Socks',
|
||||
// 'RightAnklet',
|
||||
// 'LeftAnklet',
|
||||
// 'Garters',
|
||||
// 'Shoes',
|
||||
// 'Hat',
|
||||
// 'HairAccessory3',
|
||||
// 'HairAccessory1',
|
||||
// 'HairAccessory2',
|
||||
// 'Gloves',
|
||||
// 'Bracelet',
|
||||
// 'Glasses',
|
||||
// 'Mask',
|
||||
// 'TailStraps',
|
||||
// 'Wings',
|
||||
// ],
|
||||
// HIDE_ITEMS: [
|
||||
// 'ItemMisc',
|
||||
// 'ItemEars',
|
||||
// 'ItemHead',
|
||||
// 'ItemNose',
|
||||
// 'ItemHood',
|
||||
// 'ItemAddon',
|
||||
// 'ItemMouth',
|
||||
// 'ItemMouth2',
|
||||
// 'ItemMouth3',
|
||||
// 'ItemArms',
|
||||
// 'ItemNeckAccessories',
|
||||
// 'ItemNeck',
|
||||
// 'ItemNeckRestraints',
|
||||
// 'ItemNipples',
|
||||
// 'ItemNipplesPiercings',
|
||||
// 'ItemBreast',
|
||||
// 'ItemTorso',
|
||||
// 'ItemTorso2',
|
||||
// 'ItemHands',
|
||||
// 'ItemPelvis',
|
||||
// 'ItemVulva',
|
||||
// 'ItemVulvaPiercings',
|
||||
// 'ItemDevices',
|
||||
// 'ItemLegs',
|
||||
// 'ItemFeet',
|
||||
// 'ItemBoots',
|
||||
// ],
|
||||
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: {
|
||||
// versions: {desc: 'show the mod versions across the room', cb: mbchc => mbchc.inform(mbchc.gather_versions().map(c => `<div><b>${c.name}</b> (${c.cid}): ${c.version}</div>`).join(''))},
|
||||
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
|
||||
// disappear: {desc: 'become invisible (requires anal hook -> hair)', cb: mbchc => mbchc.disappear()},
|
||||
donate: {desc: 'Buy data and send it to recipient', args: {TARGET: {}}, cb: (mbchc, args) => mbchc.donate_data(args[0])},
|
||||
// title: {desc: 'set a custom title (<b>WIP</b>)', args: {TITLE: {}}, cb: (mbchc, args) => mbchc.title(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 (window.Player.OnlineSettings.MBCHC) {
|
||||
delete window.Player.OnlineSettings.MBCHC
|
||||
mbchc.save_settings()
|
||||
}
|
||||
}},
|
||||
},
|
||||
ensure(error, callback) {
|
||||
const result = callback.call(this)
|
||||
if (!result) throw error
|
||||
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 = window.Player.OnlineSettings.MBCHC || {}
|
||||
return (setting ? settings[setting] : settings)
|
||||
},
|
||||
save_settings(cb = null) {
|
||||
if (cb) {
|
||||
if (!window.Player.OnlineSettings.MBCHC) window.Player.OnlineSettings.MBCHC = {}
|
||||
cb.call(this, window.Player.OnlineSettings.MBCHC)
|
||||
}
|
||||
window.ServerAccountUpdate.QueueData({OnlineSettings: window.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) {
|
||||
window.ChatRoomSendLocal(`<div class="mbchc">${html}</div>`, timeout)
|
||||
},
|
||||
report(x) {
|
||||
this.inform(`Error: ${x.toString()}`)
|
||||
if (this.RETHROW) throw x
|
||||
},
|
||||
in(x, floor, ceiling) {
|
||||
return ((x >= floor) && (x <= ceiling))
|
||||
},
|
||||
cid2char(cid) {
|
||||
cid = Number.parseInt(cid, 10)
|
||||
if (cid === window.Player.cid) return (window.Player)
|
||||
return (this.ensure(`character ${cid} not found in the room`, () => window.ChatRoomCharacter.find(c => c.cid === cid)))
|
||||
},
|
||||
pos2char(pos) {
|
||||
if (!this.in(pos, 0, window.ChatRoomCharacter.length - 1)) throw new Error(`invalid position ${pos}`)
|
||||
return (window.ChatRoomCharacter[pos])
|
||||
},
|
||||
rel2char(target) {
|
||||
const me = this.ensure('can\'t find my position', () => window.ChatRoomCharacter.findIndex(char => char.IsPlayer()) + 1) - 1 // 0 is falsy, 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 %= window.ChatRoomCharacter.length
|
||||
if (pos < 0) pos += window.ChatRoomCharacter.length
|
||||
return (this.pos2char(pos))
|
||||
},
|
||||
target2char(target) { // Target should be lowcase
|
||||
const input = target
|
||||
if (this.empty(target)) return (window.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(...window.ChatRoomCharacter.filter(c => c.cid.toString().includes(target)))
|
||||
}
|
||||
|
||||
if (target.startsWith('@')) target = target.slice(1)
|
||||
found.push(...window.ChatRoomCharacter.filter(c => c.Name.toLocaleLowerCase().includes(target)))
|
||||
found.push(...window.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[c.cid]) map[c.cid] = 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 => `${c.cid}|${c.Name}|${c.Nickname || c.Name}`).join(',')})`)
|
||||
return (found[0])
|
||||
},
|
||||
char2targets(char) {
|
||||
const [result, cid] = [new Set(), char.cid.toString()]
|
||||
result.add(cid).add(`=${cid}`)
|
||||
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 (window.Player.Money < cost) throw new Error('not enough money')
|
||||
window.CharacterChangeMoney(window.Player, -cost)
|
||||
window.ServerSend('ChatRoomChat', {Content: 'ReceiveSuitcaseMoney', Type: 'Hidden', Target: char.cid})
|
||||
window.ChatRoomMessage({Sender: window.Player.cid, 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 (!window.ActivityAllowed()) throw new Error('activities disabled in this room')
|
||||
if (!window.ServerChatRoomGetAllowItem(window.Player, char)) throw new Error('no permissions')
|
||||
char.FocusGroup = this.ensure('invalid AssetGroup', () => window.AssetGroupGet(char.AssetFamily, ag))
|
||||
const activity = this.ensure('invalid activity', () => window.ActivityAllowedForGroup(char, char.FocusGroup.Name, true).find(a => a.Name === action || a.Activity?.Name === action))
|
||||
if ((activity.Name || activity.Activity.Name).endsWith('Item')) {
|
||||
const item = this.ensure('no toy found', () => window.Player.Inventory.find(i => i.Asset?.Name === 'SpankingToys' && i.Asset.Group?.Name === char.FocusGroup.Name && window.AssetSpankingToys.DynamicActivity(char) === (activity.Name || activity.Activity.Name)))
|
||||
window.DialogPublishAction(char, item)
|
||||
} else window.ActivityRun(window.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 `${window.MBCHC.PREF_ACTIVITY}<${window.Player.cid}:>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]))
|
||||
}
|
||||
window.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: window.Player.MBCHC}
|
||||
if (char) payload.type = 'hello'
|
||||
const message = {Content: 'MBCHC', Type: 'Hidden', Dictionary: [payload]}
|
||||
if (char) message.Target = char.cid
|
||||
window.ServerSend('ChatRoomChat', message)
|
||||
},
|
||||
// disappear() {
|
||||
// const item = window.InventoryGet(window.Player, 'ItemButt')
|
||||
// if (!item || !item.Asset || !item.Asset.Name) throw new Error('butt seems empty')
|
||||
// if (item.Asset.Name !== 'AnalHook') throw new Error('butt seems occupied by something other than the anal hook')
|
||||
// if (!item.Property.Type || item.Property.Type !== 'Hair') throw new Error('anal hook seems not tied to hair')
|
||||
// item.Property = {Type: 'Hair', Hide: this.HIDE_ALL}
|
||||
// window.CharacterRefresh(window.Player, true, true)
|
||||
// },
|
||||
// title(title) { // WIP
|
||||
// if (this.empty(title)) throw new Error('empty title')
|
||||
// title = this.normalise_message(title, {trim: true, up: true, low: true})
|
||||
// if (title.length > 16) throw new Error('title too long')
|
||||
// if (!this.RE_TITLE.test(title)) throw new Error('invalid title')
|
||||
// window.TitleSet(title)
|
||||
// // Window.TitleList.push({Name: title, Requirement: () => true}) // check for existing first
|
||||
// },
|
||||
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
|
||||
window.bce_ActivityTriggers.push(...window.bce_ActivityTriggers.filter(t => t.Type === 'Emote').map(t => this.copy_fbc_trigger(t)))
|
||||
/* (["anim", "pose"]).forEach(tag => {let cmd = window.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 = window.Commands.find(c => c.Tag === 'anim')
|
||||
if (cmd) cmd.AutoComplete = this.complete_fbc_anim
|
||||
cmd = window.Commands.find(c => c.Tag === 'pose')
|
||||
if (cmd) cmd.AutoComplete = this.complete_fbc_pose
|
||||
},
|
||||
// gather_versions() {
|
||||
// return (window.ChatRoomCharacter.filter(c => c.MBCHC).map(c => ({name: c.dn, cid: c.cid, version: c.MBCHC.VERSION})))
|
||||
// },
|
||||
find_timezone(char) {
|
||||
const timezones = this.settings('timezones')
|
||||
if (timezones && typeof timezones[char.cid] === 'number') return (timezones[char.cid])
|
||||
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[char.cid] = tz
|
||||
})
|
||||
},
|
||||
update_char(char) {
|
||||
char.cid = char.MemberNumber // Club ID (shorter)
|
||||
char.dn = window.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 = window.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 = window.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 = window.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 = window.Player
|
||||
if (target && ((types.self.length === 0) || (types.others.length > 0))) char = mbchc.target2char(target)
|
||||
const type = char.IsPlayer() ? 'self' : 'others'
|
||||
const available = window.ActivityAllowedForGroup(char, ag)
|
||||
const toy = window.InventoryGet(window.Player, 'ItemHands')
|
||||
if (toy && toy.Asset.Name === 'SpankingToys') available.push(window.AssetAllActivities(char.AssetFamily).find(a => a.Name === window.InventorySpankingToysGetActivity?.(window.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.Name === name || a.Activity?.Name === name)))
|
||||
mbchc.run_activity(char, ag, action)
|
||||
} catch (error) {
|
||||
mbchc.report(error)
|
||||
}
|
||||
},
|
||||
bell() {
|
||||
setTimeout(() => {
|
||||
document.querySelector('#InputChat').style.outline = ''
|
||||
}, 100)
|
||||
document.querySelector('#InputChat').style.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.complete_hint(options)
|
||||
} else window.ElementValue('InputChat', document.querySelector('#InputChat').value.replace(this.RE_LAST_WORD, `$1${options[0]}${space ? ' ' : ''}`))
|
||||
},
|
||||
complete_hint(options) {
|
||||
this.COMP_HINT.innerHTML = options.sort().map(s => `<div>${s}</div>`).join('')
|
||||
this.COMP_HINT.style.display = 'flex'
|
||||
window.ElementSetDataAttribute(this.COMP_HINT.id, 'colortheme', (window.Player.ChatSettings.ColorTheme || 'Light'))
|
||||
const rescroll = window.ElementIsScrolledToEnd('TextAreaChatLog')
|
||||
window.ChatRoomResize(false)
|
||||
if (rescroll) window.ElementScrollToEnd('TextAreaChatLog')
|
||||
},
|
||||
comp_hint_visible() {
|
||||
return (this.COMP_HINT.parentElement && this.COMP_HINT.style.display === 'flex')
|
||||
},
|
||||
complete_hint_hide() {
|
||||
if (!this.comp_hint_visible()) return
|
||||
this.COMP_HINT.style.display = 'none'
|
||||
window.ChatRoomResize(false)
|
||||
},
|
||||
complete_target(token, me2 = true, check_perms = false) {
|
||||
const [locase, found] = [token.toLocaleLowerCase(), new Set()]
|
||||
for (const c of window.ChatRoomCharacter) {
|
||||
if ((c.IsPlayer() && !me2) || (check_perms && !window.ServerChatRoomGetAllowItem(window.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() {
|
||||
const input = document.querySelector('#InputChat').value
|
||||
return ([this, input, this.tokenise(input)])
|
||||
},
|
||||
complete_mbchc(_args, _locase, _cmdline) {
|
||||
const [mbchc, _input, tokens] = window.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([window.Player.cid.toString()])) // Target is always the player
|
||||
this.complete_target(token, me2, true)
|
||||
},
|
||||
complete_do(_args, _locase, _cmdline) {
|
||||
const [mbchc, _input, tokens] = window.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] = window.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(window.bce_EventExpressions).filter(a => a.toLocaleLowerCase().startsWith(anim))))
|
||||
},
|
||||
complete_fbc_pose(_args, _locase, _cmdline) {
|
||||
const [mbchc, _input, tokens] = window.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(window.PoseFemale3DCG.map(p => p.Name).filter(p => p.toLocaleLowerCase().startsWith(pose))))
|
||||
},
|
||||
history(down) {
|
||||
const [text, history] = [window.ElementValue('InputChat'), window.ChatRoomLastMessage]
|
||||
if (!this.HISTORY_MODE) {
|
||||
history.push(text)
|
||||
this.HISTORY_MODE = true
|
||||
}
|
||||
|
||||
const ids = history.map((t, i) => [t, i]).filter(([t, _]) => t.startsWith(history.at(-1))).map(([_, i]) => i)
|
||||
if (!down) ids.reverse()
|
||||
const found = ids.find(id => (down) ? id > window.ChatRoomLastMessageIndex : id < window.ChatRoomLastMessageIndex)
|
||||
if (!found) return (this.bell())
|
||||
window.ElementValue('InputChat', history[found])
|
||||
window.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 !window.ChatRoomMapVisible
|
||||
},
|
||||
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 (document.querySelector('#InputChat')?.style.display !== 'inline') return // Input chat missing
|
||||
window.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.HIDE_ALL = this.HIDE_SPECIAL.concat(this.HIDE_BODY).concat(this.HIDE_CLOTHES).concat(this.HIDE_ITEMS)
|
||||
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} {
|
||||
flex-flow: column wrap;
|
||||
overflow: auto;
|
||||
display: none;
|
||||
background-color: ${this.RGB_POLLY};
|
||||
color: black;
|
||||
}
|
||||
#${this.COMP_HINT.id}[data-colortheme="dark"], #${this.COMP_HINT.id}[data-colortheme="dark2"] {
|
||||
background-color: ${this.RGB_MUTE};
|
||||
color: white;
|
||||
}
|
||||
#${this.COMP_HINT.id} div {
|
||||
margin: 0 0.5ex;
|
||||
}
|
||||
`
|
||||
document.head.append(css)
|
||||
// Actions
|
||||
this.calculate_maps()
|
||||
window.Player.MBCHC = {VERSION: this.VERSION}
|
||||
window.CommandCombine(COMMANDS)
|
||||
|
||||
// Hooks
|
||||
this.remove_fbc_hook = this.before('MainRun', () => window.bce_ActivityTriggers && this.patch_fbc())
|
||||
this.after('CharacterOnlineRefresh', char => this.update_char(char))
|
||||
this.after('ChatRoomReceiveSuitcaseMoney', () => {
|
||||
if (this.AUTOHACK_ENABLED && this.LAST_HACKED) {
|
||||
window.CurrentCharacter = this.cid2char(this.LAST_HACKED)
|
||||
this.LAST_HACKED = null
|
||||
window.ChatRoomTryToTakeSuitcase()
|
||||
}
|
||||
})
|
||||
this.before('ChatRoomSendChat', () => {
|
||||
let input = window.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)
|
||||
window.ElementValue('InputChat', input)
|
||||
}
|
||||
})
|
||||
this.after('ChatRoomSendChat', () => {
|
||||
const history = window.ChatRoomLastMessage
|
||||
if ((history.length > 1) && (history.at(-1) === history.at(-2))) {
|
||||
history.pop()
|
||||
window.ChatRoomLastMessageIndex -= 1
|
||||
}
|
||||
})
|
||||
this.before('ChatRoomCharacterViewDrawOverlay', (C, CharX, CharY, Zoom, _Pos) => {
|
||||
// if (window.ChatRoomHideIconState < 1 && C.MBCHC) {
|
||||
// window.DrawRect(CharX + (175 * Zoom), CharY, 50 * Zoom, 50 * Zoom, C.MBCHC.VERSION === window.Player.MBCHC.VERSION ? this.RGB_POLLY : this.RGB_MUTE)
|
||||
// }
|
||||
if (window.ChatRoomHideIconState < 1 && C.MBCHC_LOCAL && typeof C.MBCHC_LOCAL.TZ === 'number') {
|
||||
const hours = new Date(window.CommonTime() + this.UTC_OFFSET + (C.MBCHC_LOCAL.TZ * 60 * 60 * 1000)).getHours()
|
||||
window.DrawTextFit(hours < 10 ? '0' + hours.toString() : hours.toString(), CharX + (200 * Zoom), CharY + (25 * Zoom), 46 * Zoom, 'white', 'black')
|
||||
}
|
||||
})
|
||||
// this.after('ElementValue', (ID, Value, cc = window.CurrentCharacter) => ID === 'bce_LayerPriority' && cc?.FocusGroup && window.InventoryGet(cc, cc.FocusGroup.Name)?.Difficulty.toString() === Value && window.InventoryLocked(cc, cc.FocusGroup.Name, true) && window.ElementSetAttribute(ID, 'disabled', true))
|
||||
this.after('ChatRoomCreateElement', () => this.COMP_HINT.parentElement || document.body.append(this.COMP_HINT))
|
||||
this.before('ChatRoomClearAllElements', () => {
|
||||
this.complete_hint_hide()
|
||||
this.COMP_HINT.remove()
|
||||
})
|
||||
this.before('ChatRoomClick', () => this.complete_hint_hide())
|
||||
this.after('ChatRoomResize', () => {
|
||||
if (window.CharacterGetCurrent() === null && window.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
|
||||
window.ElementPositionFix('TextAreaChatLog', fontsize, 1005, 66, 988, 630)
|
||||
window.ElementPositionFix(this.COMP_HINT.id, fontsize, 1005, 701, 988, 200)
|
||||
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
|
||||
window.MBCHC.complete_hint_hide()
|
||||
if ((window.KeyPress === 33) || (window.KeyPress === 34)) { // Better history
|
||||
event.preventDefault()
|
||||
return (window.MBCHC.history(window.KeyPress - 33))
|
||||
}
|
||||
|
||||
if (window.MBCHC.HISTORY_MODE) {
|
||||
window.ChatRoomLastMessage.pop()
|
||||
window.MBCHC.HISTORY_MODE = false
|
||||
}
|
||||
|
||||
return (next(nextargs))
|
||||
})
|
||||
|
||||
// Chat room handlers
|
||||
window.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})
|
||||
}})
|
||||
window.ChatRoomRegisterMessageHandler({Priority: -219, Description: 'MBCHC room enter hook',
|
||||
Callback: (data, _sender, _message, _metadata) => {
|
||||
if ((data.Type === 'Action') && (data.Content === 'ServerEnter') && (data.Sender === window.Player.cid)) this.player_enters_room()
|
||||
},
|
||||
})
|
||||
window.ChatRoomRegisterMessageHandler({Priority: -219, Description: 'MBCHC specific consumer',
|
||||
Callback: (data, _sender, _message, _metadata) => {
|
||||
if ((data.Type === 'Hidden') && (data.Content === 'MBCHC')) return this.receive(data)
|
||||
},
|
||||
})
|
||||
window.ChatRoomRegisterMessageHandler({Priority: -219, Description: 'MBCHC autohack lookup',
|
||||
Callback: (data, _sender, _message, _metadata) => {
|
||||
if ((data.Type === 'Hidden') && (data.Content === 'ReceiveSuitcaseMoney')) this.LAST_HACKED = data.Sender
|
||||
},
|
||||
})
|
||||
|
||||
// Footer
|
||||
this.LOADED = true
|
||||
this.log('info', `loaded version ${this.VERSION}`)
|
||||
if ((window.CurrentModule === 'Online') && (window.CurrentScreen === 'ChatRoom')) {
|
||||
for (const c of window.ChatRoomCharacter) this.update_char(c)
|
||||
this.player_enters_room()
|
||||
}
|
||||
},
|
||||
preloader() {
|
||||
if (!window.AsylumGGTSSAddItems) throw new Error('AsylumGGTSSAddItems() not found, aborting MBCHC loading')
|
||||
if (!window.bcModSdk) throw new Error('SDK not found, please load with (or after) FUSAM')
|
||||
this.SDK = window.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
|
||||
})
|
||||
if (window.CurrentModule && window.CurrentScreen && !(window.CurrentModule === 'Character' && window.CurrentScreen === 'Login')) return this.loader()
|
||||
this.remove_load_hook = this.before('AsylumGGTSSAddItems', () => this.loader())
|
||||
},
|
||||
}
|
||||
|
||||
window.MBCHC.preloader()
|
||||
|
||||
// TODO: with the removal of /mbchc versions it makes no sense to keep track of the mod version and the whole hello() infrastructure can go away
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"name": "mbchc",
|
||||
"version": "0.0.9",
|
||||
"description": "Mute's Bondage Club Hacks Collection",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"xo": "^0.56.0"
|
||||
},
|
||||
"license": "SEE LICENSE IN LICENSE.",
|
||||
"xo": {
|
||||
"env": [
|
||||
"browser",
|
||||
"node"
|
||||
],
|
||||
"rules": {
|
||||
"camelcase": "off",
|
||||
"capitalized-comments": "off",
|
||||
"curly": "off",
|
||||
"max-params": "off",
|
||||
"new-cap": "off",
|
||||
"no-return-assign": "off",
|
||||
"no-unused-vars": ["error", {
|
||||
"argsIgnorePattern": "^_",
|
||||
"destructuredArrayIgnorePattern": "^_"
|
||||
}],
|
||||
"padding-line-between-statements": "off",
|
||||
"semi": "off",
|
||||
"unicorn/no-array-reduce": "off",
|
||||
"fake/fuck-commas": "off"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
import {readFileSync} from 'node:fs'
|
||||
import {createServer} from 'node:http'
|
||||
|
||||
function stamp_cors(rx) {
|
||||
rx.setHeader('Access-Control-Max-Age', '86400')
|
||||
rx.setHeader('Access-Control-Allow-Private-Network', 'true')
|
||||
rx.setHeader('Access-Control-Allow-Origin', '*')
|
||||
rx.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS')
|
||||
rx.setHeader('Access-Control-Allow-Headers', '*')
|
||||
// rx.setHeader('Access-Control-Allow-Credentials', 'false') // omit this header to disallow
|
||||
}
|
||||
|
||||
const server = createServer((rq, rx) => {
|
||||
switch (rq.method) {
|
||||
case 'GET': {
|
||||
rx.statusCode = 200
|
||||
stamp_cors(rx)
|
||||
rx.setHeader('Content-Type', 'text/javascript')
|
||||
rx.setHeader('Cache-Control', 'no-cache')
|
||||
const data = readFileSync('./mbchc.mjs')
|
||||
rx.write(data)
|
||||
break
|
||||
}
|
||||
case 'OPTIONS': {
|
||||
rx.statusCode = 204
|
||||
stamp_cors(rx)
|
||||
break
|
||||
}
|
||||
default: {
|
||||
rx.statusCode = 400
|
||||
}
|
||||
}
|
||||
rx.end()
|
||||
})
|
||||
server.listen(9696, '127.0.0.1', () => console.log('Server started.'))
|
Loading…
Reference in New Issue