// // Toy // // by rascalDan // This script is a WIP and is always likely to be so. // All polite suggestions are welcomed :) // // This script, from https://ss.deviatenow.com, is protected by this licence : // CC by-NC, see http://creativecommons.org/licenses/by-nc/3.0/ // setInfos(9, "Toy", "Become my new plaything.", "rascalDan", "WIP", 0xFFFFFF, "en", ["bondage", "femaledom", "formale", "long", "pain", "toys", "joi"]); return new Object() { final VERSION = 1; final DAY = 86400; final HOUR = 3600.0; final soonFormatter = java.time.format.DateTimeFormatter.ofPattern("EEEE 'at' h:mma"); // Notes // toys. ankle_cuffs, ballgag, blindfold, chastity_belt, clothespins, cockring, dog_collar, handcuffs, hood, nipple_clamps, ring_gag final PLAY = "play"; final REDRESS = "redress"; final RELEASEFROM = "releaseFrom"; final BALLGAG = "ballgag", COLLAR = "dog_collar", CLAMPS = "nipple_clamps", CHASTITY = "chastity_belt", HANDCUFFS = "handcuffs", DILDO = "dildo"; final SPOON = "spoon"; final COCK = "cock"; // fetish. final BONDAGE = "bondage", CBT = "cbt", CHORES = "chores", PAIN = "pain"; // toy.permission. final CUM = "cum", EDGE = "edge" final PERMIT = "permit"; final PERM_CHASTE = "perm_chaste", PERM_CHASTE_ASK = "perm_chaste_ask"; // toy.state. final CHASTE = "chaste", COLLARED = "collared", CUFFED = "cuffed", CLAMPED = "clamped", GAGGED = "gagged", NAKED = "naked"; final TOYTOYS = [CUFFED, COLLARED, CLAMPED, GAGGED]; // toy.position final KNEELING = "kneeling", ALLFOURS = "allfours", STANDING = "standing"; def OWNER = null; def DOMME = null; final SITTER = "sarah-james"; final SOFT = "soft"; final DISAPPOINT = "disappoint"; final PRE = "pre", INT = "int", PUNISH = "punish", REWARD = "reward", HUMILIATION = "humiliation"; final DATAFOLDER = getDataFolder(); // Imagery final DRESSED = "dressed", TITS = "tits", PUSSY = "pussy", LINGERIE = "lingerie", TEASE = "tease", SIT = "sit", BOOTS = "boots", KNEEL = "kneel", STOOD = "stood", MEAN = "mean", CROP = "crop", ASS = "ass", SQUAT = "squat", SSSH = "sssh"; final nDRESSED = "!$DRESSED", nTITS = "!$TITS", nTEASE = "!$TEASE", nKNEEL = "!$KNEEL", nSQUAT = "!$SQUAT"; final getDomme = { domme, set = null -> final readDomme = { path -> if (!path) return [:]; final f = new File(path); if (!f.exists()) return [:]; return Eval.me(f.text); }; def d = [ "$DATAFOLDER/images/toy/domme.groovy", "$DATAFOLDER/images/toy/$domme/person.groovy", "$DATAFOLDER/images/toy/$domme/$set/set.groovy" ] .collect { readDomme(it) } .sum(); d.id = domme; return d; }; final loadDomme = { domme, set = null -> DOMME = getDomme(domme, set); } final loadModules = { toy -> new File("$DATAFOLDER/scripts/toy") .listFiles() .collect { s -> Eval.me(s.text)(toy); } .findAll { p -> p }; }; // Utils int localTimeOffset() { return java.time.ZoneId.systemDefault() .getRules() .getOffset(java.time.Instant.now()) .getTotalSeconds(); } java.time.LocalDateTime localTimeOf(int s) { return java.time.LocalDateTime.ofInstant( java.time.Instant.ofEpochSecond(s), java.time.ZoneId.systemDefault()); } float localTime() { // A float between 4.0 (4am) and 28.0 (4am) def wallclock = ((getTime() + localTimeOffset()) % DAY) / HOUR; if (wallclock < 4) { wallclock += 24; } return wallclock; } int getDay() { return ((int)(getTime() / DAY) * DAY); } int getDayNum() { return Math.floorDiv(getTime() - localTimeOffset() - (4 * (int)HOUR), DAY) } public T getProp(String i, java.util.function.Function f, T d = null) { final T v = f.apply("toy.$i".toString()); if (v == null) return d; return v; } Object getDommeProp = { String i, Object d = null -> if (DOMME == null) return d; final v = DOMME[i]; if (v == null) return d; return v; } String dommeTitle() { getDommeProp("title", "Mistress") } String dommeName() { getDommeProp("name", "") } final java.util.function.Function loadB = this.&loadBoolean; final java.util.function.Function loadI = this.&loadInteger; final java.util.function.Function loadS = this.&loadString; void setProp(String i, Object v) { save("toy.$i".toString(), v) } boolean has(String i) { loadBoolean("toys.$i") == true } boolean likes(String i) { loadBoolean("fetish.$i") == true } boolean can(String i) { loadBoolean("toy.permission.$i") == true } boolean perm(String i) { can(it) } boolean getPermission(String i) { getProp("permission.$i", loadB) } void setPermission(String i, boolean v) { setProp("permission.$i", v) } void givePermission(String i) { setPermission(i, true) } void revokePermission(String i) { setPermission(i, false) } boolean stateIs(String i) { loadBoolean("toy.state.$i") == true } void set(String i, boolean s) { save("toy.state.$i", s) } boolean positioned(String i) { ("toy.position") == i } int getPunish() { loadInteger("toy.punishment") ?: 0 } void adjustPunish(int p) { save("toy.punishment", Math.max(getPunish() + p, 0)) } void adjustPunish(double p) { adjustPunish((int)p) } String getAway() { loadString("toy.owner.away") } void setAway(String a) { save("toy.owner.away", a) } int randRange(int min, int max) { min + getRandom(1 + max - min) } int randRange(double min, double max) { randRange((int)min, (int)max) } final setDefault = { prop, val -> if (loadString(prop) == null) { save(prop, val); } }; // This exists because calling .wait(t) in a metaClass method // calls it on the metaClass, not on the original object void pause(BigDecimal t) { wait(t); } // Events static class Event { int time; String func; Object arg; final map = { [ time: time, func: func, arg: arg ] } } static class NamedEvent { String name; Event event; } Map loadEvents() { return (loadMap("toy.events") ?: [:]) .collectEntries { k, v -> [ k, new Event(v) ] }; } void saveEvents(Map events) { save("toy.events", events.collectEntries { k, v -> [ k, v.map() ] }); } NamedEvent nextEvent(Map events) { NamedEvent first = null; events.each{ String k, Event v -> if (!first || first.event.time > v.time) { first = new NamedEvent(name: k, event: v); } }; return first; } boolean setEvent(Map events, String name, int time, String func, Object arg = null) { if (namedEvents.containsKey(func)) { events[name] = new Event( time: time, func: func, arg: arg ); saveEvents(events); return true; } else { showPopup("No such event $func"); return false; } } boolean addEvent(String name, int time, String func, Object arg = null) { def events = loadEvents(); return setEvent(events, name, time, func, arg); } boolean addEventIfMissing(String name, int time, String func, Object arg = null) { def events = loadEvents(); if (!events.containsKey(name)) { return setEvent(events, name, time, func, arg); } return false; } void removeEvent(String name) { def events = loadEvents(); events.remove(name); saveEvents(events); } void execEvents(boolean rt) { for (def e = nextEvent(loadEvents()); e && e.event.time <= getTime(); e = nextEvent(loadEvents())) { def f = namedEvents[e.event.func]; removeEvent(e.name); if (f) { f(e.name, e.event.arg, e.event.time, rt); } } } def sessionAborted = null; def sessionToys = [:]; final gagText = { t, p -> if (!stateIs(GAGGED)) return t.toString(); return t.split(/\s+/) .collect { 'm' * getRandom((int)Math.max(1.0, it.length() * 1.0)) + 'p' * getRandom((int)Math.max(1.0, it.length() * 0.4)) + 'h' * getRandom((int)Math.max(1.0, it.length() * 0.8)) } .join(" ") .capitalize() + " ($p)"; }; final showButtonG = { s, p, t = null -> return t != null ? showButton(gagText(s, p), t) : showButton(gagText(s, p)); }; final compose = { texts -> if (!texts || texts.isEmpty()) return null; return texts.collect { t -> t[getRandom(t.size())] }.join(" ").capitalize(); }; final present = { imageSpec, texts -> if (texts) { show(compose(texts)); } if (imageSpec) { return showImage(imageSpec); } return null; }; final showButtonGT = { s, p, t, pc -> def tt = showButton(gagText(s, p), t); if (tt == t) { present(null, [ ["Don't keep me waiting!", "Hurry up!", "Quicker, toy!"]]); def pt = showButton(gagText(s, p)); adjustPunish(pt * pc); return pt + tt; } return tt; }; final matchTags = { spec -> return { obj -> spec.every({ s -> return (s[0] == "!") ? obj.tags.indexOf(s.drop(1)) == -1 : obj.tags.indexOf(s) != -1; }); }; }; final triggerHandlers = []; final addTriggerHandler = { String triggerName, func, defArg = null -> triggerHandlers << [ triggerName: triggerName, func: func, defaultArg: defArg ]; }; final executeTrigger = { String triggerName, arg = null -> triggerHandlers .findAll { th -> th.triggerName == triggerName } .forEach { th -> th.func(arg ?: th.defaultArg) }; }; // Session final sessionSummon = { imageSpec -> final limit = 5; final timeMax = 10; executeTrigger("toySummoned"); for (def n = 1; n <= limit; n++) { switch (n) { case limit: present(imageSpec, [ ["TOY!", "SLAVE!", "SLUT!"], ["Last chance!", "Final warning!"]]); adjustPunish(5); break; case limit - 1: case limit - 2: present(imageSpec, [ ["Toy!", "Slave!", "Slut!"], ["Where are you!?", "Get yourself here, now!"]]); break; default: present(imageSpec, [ ["Toy!", "Slave!", "Slut!"]]); break; } playBackgroundSound(n == limit ? "shortwhip.wav" : "bell.wav"); if (showButton("Yes, ${dommeTitle()}?", timeMax) < timeMax) { executeTrigger("toySummonSuccess", n); return true; }; } showLounge(); show(null); adjustPunish(5); executeTrigger("toySummonFail"); return false; }; final setSessionAbort = { String r = null -> sessionAborted = r }; final activityList = [:]; final addActivity = { String name, func, images = [], tags = [] -> activityList[name] = [ name: name, func: func, tags: tags ?: [], images: images ?: [] ]; } final namedEvents = [:]; final addNamedEvent = { String name, func -> namedEvents[name] = func; } final requestables = [:]; final addRequestable = { String name, String label, func, cond = null -> requestables[name] = [lbl: label, act: func, cond: cond]; }; final removeRequestable = { String name -> requestables.remove(name); }; String main() { // GO! setDefault("toy.owner", "ancilla"); OWNER = loadString("toy.owner"); loadDomme(OWNER); def moduleSetup = loadModules(this); setDefault("toy.punishment", 0); execEvents(false); moduleSetup.each { p -> p() }; final cycleTime = 60; showLounge(); while (true) { def away = getAway(); def e = nextEvent(loadEvents()); if (!e) return "toy"; // Force restart, should create some events if (away) { def note = loadString("toy.note"); def noteInstr = loadString("toy.noteInstr"); if (note) { show("${dommeTitle()} ${dommeName()} is away ($away)... but you find a note:\n$note"); showButton("OK"); if (noteInstr) { set(noteInstr, true); } readNote(); } else { show("${dommeTitle()} ${dommeName()} is away ($away)..."); } if (e && e.event && e.event.time > getTime()) { pause(e.event.time - getTime()); } } else { if (stateIs("DEBUG")) { def timeStr = localTimeOf(e.event.time).format(soonFormatter); show("$e.name: $timeStr -> ${e.event.func}(${e.event.arg})"); } else { show("${dommeTitle()} ${dommeName()} will summon you when required."); } final waitTime = Math.min(cycleTime, e.event.time - getTime()); final clickTime = showButton("Please, ${dommeTitle()}?", waitTime); if (clickTime < waitTime) { def opts = requestables.values().toList().findAll { it -> !it.cond || it.cond() }; opts.push([ lbl: "Back", act: null ]); def opt = getSelectedValue(stateIs("DEBUG") ? DOMME : "Yes, toy?", opts.collect { it.lbl }); def act = opts[opt].act; if (act) act(opts[opt].arg); else if (stateIs("DEBUG")) return "toy"; } executeTrigger("toyLoungeTime", clickTime); } final eStart = getTime(); execEvents(true); executeTrigger("toyEventTime", getTime() - eStart); } } }.main(); /* * Resources * scripts/toy.groovy * scripts/toy/availability.groovy * scripts/toy/bondage.groovy * scripts/toy/cbt.groovy * scripts/toy/chastity.groovy * scripts/toy/confession.groovy * scripts/toy/humiliation.groovy * scripts/toy/imagery.groovy * scripts/toy/intro.groovy * scripts/toy/misc.groovy * scripts/toy/orgasmControl.groovy * scripts/toy/play.groovy * scripts/toy/pain.groovy * scripts/toy/sleep.groovy * scripts/toy/social.groovy * scripts/toy/tease.groovy * scripts/toy/toys.groovy * images/toy/ancilla/corset/1.jpg * images/toy/ancilla/corset/10.jpg * images/toy/ancilla/corset/11.jpg * images/toy/ancilla/corset/12.jpg * images/toy/ancilla/corset/13.jpg * images/toy/ancilla/corset/14.jpg * images/toy/ancilla/corset/15.jpg * images/toy/ancilla/corset/16.jpg * images/toy/ancilla/corset/2.jpg * images/toy/ancilla/corset/3.jpg * images/toy/ancilla/corset/4.jpg * images/toy/ancilla/corset/5.jpg * images/toy/ancilla/corset/6.jpg * images/toy/ancilla/corset/7.jpg * images/toy/ancilla/corset/8.jpg * images/toy/ancilla/corset/9.jpg * images/toy/ancilla/corset/tags * images/toy/ancilla/crop/1.jpg * images/toy/ancilla/crop/10.jpg * images/toy/ancilla/crop/11.jpg * images/toy/ancilla/crop/12.jpg * images/toy/ancilla/crop/13.jpg * images/toy/ancilla/crop/14.jpg * images/toy/ancilla/crop/15.jpg * images/toy/ancilla/crop/16.jpg * images/toy/ancilla/crop/2.jpg * images/toy/ancilla/crop/3.jpg * images/toy/ancilla/crop/4.jpg * images/toy/ancilla/crop/5.jpg * images/toy/ancilla/crop/6.jpg * images/toy/ancilla/crop/7.jpg * images/toy/ancilla/crop/8.jpg * images/toy/ancilla/crop/9.jpg * images/toy/ancilla/crop/tags * images/toy/ancilla/dress/1.jpg * images/toy/ancilla/dress/10.jpg * images/toy/ancilla/dress/11.jpg * images/toy/ancilla/dress/12.jpg * images/toy/ancilla/dress/13.jpg * images/toy/ancilla/dress/14.jpg * images/toy/ancilla/dress/15.jpg * images/toy/ancilla/dress/2.jpg * images/toy/ancilla/dress/3.jpg * images/toy/ancilla/dress/4.jpg * images/toy/ancilla/dress/5.jpg * images/toy/ancilla/dress/6.jpg * images/toy/ancilla/dress/7.jpg * images/toy/ancilla/dress/8.jpg * images/toy/ancilla/dress/9.jpg * images/toy/ancilla/dress/tags * images/toy/ancilla/pink/1.jpg * images/toy/ancilla/pink/10.jpg * images/toy/ancilla/pink/11.jpg * images/toy/ancilla/pink/12.jpg * images/toy/ancilla/pink/13.jpg * images/toy/ancilla/pink/14.jpg * images/toy/ancilla/pink/15.jpg * images/toy/ancilla/pink/2.jpg * images/toy/ancilla/pink/3.jpg * images/toy/ancilla/pink/4.jpg * images/toy/ancilla/pink/5.jpg * images/toy/ancilla/pink/6.jpg * images/toy/ancilla/pink/7.jpg * images/toy/ancilla/pink/8.jpg * images/toy/ancilla/pink/9.jpg * images/toy/ancilla/pink/tags * images/toy/ancilla/lounge.jpg * images/toy/ancilla/person.groovy * images/toy/domme.groovy * sounds/toy/165bpm.mp3 * sounds/toy/180bpm.mp3 * sounds/toy/240bpm.mp3 * sounds/toy/30bpm.mp3 * sounds/toy/90bpm.mp3 */