// Toy main setInfos(9, "Toy", "Your frustration is my amusement.", "rascalDan", "WIP", 0xFFFFFF, "en", ["", "", ""]); 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 BALLGAG = "ballgag", COLLAR = "dog_collar", CLAMPS = "nipple_clamps", CHASTITY = "chastity_belt"; // fetish. final BONDAGE = "bondage", CBT = "cbt", CHORES = "chores", PAIN = "pain"; // toy.permission. final CUM = "cum", EDGE = "edge" // toy.state. final CHASTE = "chaste", COLLARED = "collared", CUFFED = "cuffed", CLAMPED = "clamped", GAGGED = "gagged", NAKED = "naked"; // toy.plan..{start|end|friend} UTC times final LUNCH = "lunch", SHOPPING = "shopping", PARTY = "party"; final START = "start", END = "end", FRIEND = "friend"; final FRIENDS = [ "Tori", "Sophie", "Krystal" ]; // toy.position final KNEELING = "kneeling", ALLFOURS = "allfours", STANDING = "standing"; final OWNER = "ancilla"; final SITTER = "sarah-james"; // 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"; def IMAGEDATA = { [ [ domme: "ancilla", sets: [ [ set: "corset", images: [ [ image: 1, tags: [DRESSED, STOOD] ], [ image: 2, tags: [DRESSED, STOOD] ], [ image: 3, tags: [DRESSED, STOOD] ], [ image: 4, tags: [DRESSED, STOOD] ], [ image: 5, tags: [DRESSED, STOOD] ], [ image: 6, tags: [DRESSED, STOOD] ], [ image: 7, tags: [DRESSED, STOOD, TEASE] ], [ image: 8, tags: [DRESSED, STOOD, ASS] ], [ image: 9, tags: [DRESSED, STOOD, TEASE] ], [ image: 10, tags: [DRESSED, STOOD, TEASE] ], [ image: 11, tags: [KNEEL, TITS] ], [ image: 12, tags: [KNEEL, TITS, ASS] ], [ image: 13, tags: [KNEEL, TITS, LINGERIE] ], [ image: 14, tags: [KNEEL, TITS, LINGERIE] ], [ image: 15, tags: [SIT, TITS, LINGERIE] ], [ image: 16, tags: [STOOD, TITS, LINGERIE] ], ] ], [ set: "crop", images: [ [ image: 1, tags: [TITS, KNEEL, SSSH] ], [ image: 2, tags: [TITS, KNEEL, MEAN, CROP] ], [ image: 3, tags: [TITS, KNEEL, CROP] ], [ image: 4, tags: [TEASE, KNEEL] ], [ image: 5, tags: [STOOD, TITS] ], [ image: 6, tags: [KNEEL, TITS] ], [ image: 7, tags: [KNEEL, TITS] ], [ image: 8, tags: [DRESSED, STOOD, CROP] ], [ image: 9, tags: [DRESSED, STOOD, CROP] ], [ image: 10, tags: [DRESSED, STOOD, CROP] ], [ image: 11, tags: [DRESSED, STOOD, CROP] ], [ image: 12, tags: [DRESSED, STOOD, CROP] ], [ image: 13, tags: [DRESSED, STOOD, CROP] ], [ image: 14, tags: [DRESSED, SIT, CROP] ], [ image: 15, tags: [TITS, SIT] ], [ image: 16, tags: [TITS, STOOD] ], ] ], [ set: "dress", images: [ [ image: 1, tags: [TITS, STOOD] ], [ image: 2, tags: [TITS, STOOD] ], [ image: 3, tags: [TITS, SIT] ], [ image: 4, tags: [TITS, SIT] ], [ image: 5, tags: [TITS, STOOD] ], [ image: 6, tags: [TITS, STOOD, ASS] ], [ image: 7, tags: [DRESSED, STOOD] ], [ image: 8, tags: [DRESSED, STOOD] ], [ image: 9, tags: [DRESSED, STOOD, ASS] ], [ image: 10, tags: [DRESSED, STOOD, ASS] ], [ image: 11, tags: [DRESSED, SQUAT] ], [ image: 12, tags: [DRESSED, STOOD, TEASE] ], [ image: 13, tags: [DRESSED, STOOD, TEASE] ], [ image: 14, tags: [TITS, STOOD] ], [ image: 15, tags: [TITS, STOOD] ], ] ], [ set: "pink", images: [ [ image: 1, tags: [TITS, KNEEL, LINGERIE] ], [ image: 2, tags: [TITS, KNEEL, LINGERIE] ], [ image: 3, tags: [TITS, KNEEL, LINGERIE] ], [ image: 4, tags: [TITS, KNEEL, LINGERIE] ], [ image: 5, tags: [TITS, KNEEL, LINGERIE] ], [ image: 6, tags: [TITS, KNEEL] ], [ image: 7, tags: [DRESSED, KNEEL] ], [ image: 8, tags: [DRESSED, STOOD] ], [ image: 9, tags: [DRESSED, STOOD] ], [ image: 10, tags: [DRESSED, STOOD, TEASE] ], [ image: 11, tags: [DRESSED, STOOD, TEASE] ], [ image: 12, tags: [KNEEL, LINGERIE] ], [ image: 13, tags: [TITS, KNEEL, LINGERIE] ], [ image: 14, tags: [TITS, KNEEL, LINGERIE] ], [ image: 15, tags: [TITS, KNEEL, LINGERIE] ], ] ], ] ] ] }; def selectImage = { domme, set, spec -> def meets = { i -> return spec.every({ s -> return (s[0] == "!") ? i.tags.indexOf(s.drop(1)) == -1 : i.tags.indexOf(s) != -1; }); }; def matches = IMAGEDATA() .find({ d -> d.domme == domme }) .sets.find({ s -> s.set == set }) .images.findAll({ i -> meets(i) }); return matches[getRandom(matches.size())]; }; def showImage = { spec -> def outfit = loadString("toy.owner.outfit"); def image = selectImage(OWNER, outfit, spec); setImage("toy/$OWNER/$outfit/${image.image}.jpg"); return image.tags; }; def selectImageSet = { domme, specs -> def matches = IMAGEDATA() .find({ d -> d.domme == domme }) .sets.findAll({ s -> specs.every({ spec -> selectImage(domme, s.set, spec)})}); return matches[getRandom(matches.size())]; }; def dress = { specs -> def outfit = loadString("toy.owner.outfit"); def outfitTime = loadInteger("toy.owner.outfitTime"); if (outfitTime > getTime() - 7200) { // Recent, check def prev = IMAGEDATA() .find({ d -> d.domme == OWNER }) .sets.find({ s -> s.set == outfit }); if (specs.every({ spec -> selectImage(OWNER, prev.set, spec)})) { return outfit; } } outfit = selectImageSet(OWNER, specs); if (!outfit) { showPopup("No outfit for $OWNER : $specs"); } save("toy.owner.outfit", outfit.set); save("toy.owner.outfitTime", getTime()); }; // Utils def localTimeOffset = { return java.time.ZoneId.systemDefault() .getRules() .getOffset(java.time.Instant.now()) .getTotalSeconds(); }; def localTimeOf = { s -> java.time.LocalDateTime.ofInstant( java.time.Instant.ofEpochSecond(s), java.time.ZoneId.systemDefault()) } def localTime = { // A float between 4.0 (4am) and 28.0 (4am) def wallclock = ((getTime() + localTimeOffset()) % DAY) / HOUR; if (wallclock < 4) { wallclock += 24; } return wallclock; }; def getDay = { (int)(getTime() / DAY) * DAY }; def has = {i -> loadBoolean("toys.$i") == true}; def likes = {i -> loadBoolean("fetish.$i") == true}; def can = {i -> loadBoolean("toy.permission.$i") == true}; def is = {i -> loadBoolean("toy.state.$i") == true}; def set = {i, s -> save("toy.state.$i", s)}; def positioned = { i -> loadString("toy.position") == i }; def gagText = { t -> if (!is(GAGGED)) return t; 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(); }; def showButtonG = { s, t = null -> return t != null ? showButton(gagText(s), t) : showButton(gagText(s)); }; def present = { imageSpec, texts -> if (texts) { show(texts.collect { t -> t[getRandom(t.size())] }.join(" ").capitalize()); } if (imageSpec) { return showImage(imageSpec); } return null; }; def harden = { imageSpec -> present([DRESSED], [ ["Take that cock...", "Hand on penis..."], ["stroke it, slowly...", "slow strokes..."], ["don't edge...", "no edging..."], ["and do NOT cum.", "and definitely no cumming."]]); if (showButtonG("Hard, mistress", 60) == 60) { present(null, [ ["What's taking so long!?", "Come on!", "Don't disappoint me."], ["Slap it about a bit!.", "Get it hard, now!", "Pinch your nipples."]]); showButtonG("Hard, mistress"); } }; def expose = { imageSpec -> if (!is(NAKED)) { present(imageSpec, [ ["Get that cock out, toy;", "Let's see that cock of mine."], ["I want to torment it.", "It's playtime!"]]); wait(10); } }; def edge = { amount, imageSpec -> (getRandom(amount) + 2).times { present(imageSpec, [ ["Stroke to edge now, toy...", "Edge!"], ["Don't cum.", "No cumming.", "No accidents though."]]); showButtonG("Edging, mistress"); // TODO: time check if (getRandom(2) == 1) { present(imageSpec, [ ["Hold it...", "And hold...", "Keeping going..."]]); wait(getRandom(20) + 5); } present(imageSpec, [ ["Hands off!", "Stop!"]]); wait(getRandom(5) + 5); }; }; def strokes = { amount, imageSpec -> present(imageSpec, [ ["Stroke to the beat, toy.", "Follow the beat.", "Beat it!"], ["No cumming unless I say so...", "Don't cum without my say so,"], ["Tell me if you get too close.", "so tell me if you're edging.", "I don't want any messes."]]); final BEATS = [ // bpm:30, len:60 ], [ bpm:90, len:45 ], [ bpm:165, len:30 ], [ bpm:180, len:30 ], [ bpm:240, len:20 ], ]; def sto = loadInteger("toy.strokeTeaseOffset"); def edges = 0; (getRandom(amount) + 5).times { def beatNo = getRandom(BEATS.size()); def beat = BEATS[beatNo]; playBackgroundSound("toy/${beat.bpm}bpm.mp3", 10); // Lots, to cover offset def len = getRandom(java.lang.Math.max(1, beat.len + (sto * beatNo))); if (showButtonG("Edging, mistress", len) < len) { sto -= 1; edges += 1; playBackgroundSound(null); switch (getRandom(3)) { case 0: present(imageSpec, [ ["Stop!", "Let go!", "Hands off!"]]); wait(getRandom(8) + 10); break; case 1: present(imageSpec, [ ["Careful now...", "No accidents..."], ["slow it down...", "slowly now..."]]); playBackgroundSound("toy/30bpm.mp3"); wait(getRandom(20) + 5); playBackgroundSound(null); case 2: switch(getSelectedValue("Would you like to cum, toy?", [ gagText("Please, mistress, may I cum!?"), gagText("No, mistress, please torment me more!") ])) { case 0: present(imageSpec, [ ["No you may not!", "Nope.", "Not a chance."]]); break; case 1: present(imageSpec, [ ["Good boy.", "Very well.", "OK then!"]]); break; } wait(getRandom(8) + 5); } present(imageSpec, [ ["Back to stroking, toy...", "Get to it again...."], ["follow the beat..."], ["no accidents.", "and concentrate."]]); } else { playBackgroundSound(null); } } if (edges == 0) { sto += 1; } save("toy.strokeTeaseOffset", sto); } // Pre-tease def preRelease = { if (!is(CHASTE)) return; present([DRESSED], [ ["I'm feeling generous, toy...", "Well, that's not good to me..."], ["let's get that chastity device off.", "take your chastity device off."]]); showButtonG("Yes, mistress"); wait(15); set(CHASTE, false); showButtonG("Thank you, mistress"); }; def preEdge = { if (is(CHASTE)) return; expose([DRESSED]); harden([DRESSED]); edge(4, [DRESSED]); present([DRESSED], [ ["And relax...", "Hands off..."], ["cool down a little.", "but keep it hard for me!", "for now!"]]); wait(getRandom(5) + 5); }; def preGag = { if (is(GAGGED)) return; if (!has(BALLGAG)) return; present([DRESSED], [ ["Go fetch your gag, toy...", "Bring me your gag, toy..."], ["Quickly!", "Hurry back!"]]); showButton("Yes, mistress"); show(null); wait(10); showButton("Back, mistress"); // TODO: time check present([DRESSED], [ ["Good boy.", "Very good."], ["Put it on..."], ["Nice and tight.", "Tight!"], ["I don't want to a hear a word from you.", "A quiet toy is a good toy."]]); set(GAGGED, true); showButtonG("Gagged, mistress"); // TODO: time check }; def preClamps = { if (is(CLAMPED)) return; present([DRESSED], likes(PAIN) ? [ ["We both like to see you suffer.", "Pain is fun."], ["Isn't that right, toy?", "Don't you agree?"]] : [ ["I'm sorry, toy,"], ["but seeing you suffer is too much fun.", "but I need to find my amusement somewhere."]]); wait(10); present([DRESSED],[ ["Go put your nipple clamps on...", "I want those nipples clamped..."], ["but on your way back...", "no walking though..."], ["crawl, down on all fours.", "on your knees."]]); showButtonG("Yes, mistress"); show(null); wait(10); set(CLAMPED, true); showButtonG("Back, mistress"); // TODO: time check present([DRESSED], [ ["On your knees,", "Kneel before me"], ["let me see.", "hands behind your back."]]); wait(getRandom(10) + 5); getRandom(4).times { present([DRESSED], [ ["Pull them tight.", "Pull them!"]]); wait(getRandom(10) + 5); if (getRandom(5) == 0) { present(null, [ ["Tighter!", "Harder!"]]); wait(getRandom(10) + 2); present(null, [ ["Haha!", "Ooo, they must hurt.", "Do they hurt?"]]); wait(3); } present(null, [ ["And release.", "Let go."]]); wait(getRandom(5) + 3); }; }; def preCollar = { if (is(COLLARED)) return; if (!has(COLLAR)) return; present([DRESSED], [ ["You look underdressed, toy.", "A toy without a collar doesn't look right."], ["Go put your collar on", "I want a collar around your neck"], ["and crawl back like the slutty puppy dog you are.", "and kneel before me."]]); showButtonG("Yes, mistress"); show(null); wait(10); set(COLLARED, true); showButtonG("Back, mistress"); // TODO: time check }; def preStrip = { imageSpec = [DRESSED] -> if (is(NAKED)) return; present(imageSpec, [ ["Clothes off!", "Get naked, slut!"]]); wait(10); present([DRESSED], [ ["All of them off!", "And the rest!", "Keep going."]]); set(NAKED, true); showButtonG("Naked, mistress"); (getRandom(2) + 1).times { switch (getRandom(2)) { case 0: present(null, [ ["Stand up straight,", "On your feet,"], ["hands behind your head,", "arms up,", "hands high up,"], ["let me see you properly.", "let's see what we have.", "I want a good view."]]); break; case 1: present(null, [ ["Turn around,", "Spin"], ["slowly", "nice and slow."]]); break; case 2: present(null, [ ["Very good. Now...", "I see..."], ["on your knees before me,", "kneel,"], ["don't move those hands.", "hands still."]]); break; } wait(10); }; }; // Post def postEdge = { if (is(CHASTE)) return; if (!can(EDGE)) return; edge(6, [TEASE]); }; def postCum = { if (is(CHASTE)) return; present([TEASE], [ ["How about I let you cum?", "Maybe I should let you cum."], ["Would you like that?", "Let some of that frustration out?"]]); wait(getRandom(5) + 5); if (!can(CUM)) { present([TEASE], [ ["Or not.", "No, not right now.", "Maybe later.", "Not yet."]]); wait(getRandom(5) + 5); return; } }; def postChastity = { if (is(CHASTE)) return; if (!has(CHASTITY)) return; present([TEASE], [ ["I can't keep an eye on you all time.", "It's not that I don't trust you."], ["But...", "I just like to be sure."]]); wait(getRandom(5) + 10); present([TEASE], [ ["Go to your room,", "Run along now,"], ["get your chastity device and"], ["lock it on.", "get that cock secured away."], ["Bring back the key.", "I want the key of course."]]); wait(10); show(null); showButtonG("Locked, mistress. Here's the key."); set(CHASTE, true); present([DRESSED,TEASE], [ ["Good boy.", "Thank you, toy."]]); }; // Play def playStrokes = { preRelease(); harden([TITS]); strokes(15, [TITS]); }; def playEdges = { preRelease(); harden([TITS]); edge(6, [TITS]); }; def playNothing = { (getRandom(5) + 4).times { def tags = showImage([TITS]).intersect( [ASS, CROP, BOOTS, PUSSY, SSSH]); Collections.shuffle(tags); if (tags.isEmpty()) { show(null); } else { switch (tags[0]) { case ASS: if (!is(GAGGED)) present(null, [ ["Come closer,", "Don't be shy,"], ["kiss it!", "two kisses on each cheek."]]); break; case CROP: present(null, [ ["You know what this is for?", "Good boys get rewards, bad ones getting a beating.", "Would you like your ass a darker shade?"]]); break; case BOOTS: if (is(GAGGED)) present(null, [ ["I know you'd like to worship them.", "Too bad that tongue can't be put to good use."]]); else break; case PUSSY: if (is(GAGGED)) present(null, [ ["I know you'd like to pleasure me.", "Too bad that tongue can't be put to good use."]]); else present(null, [ ["How about you start licking it?", "How much would like your tongue in there?"]]); break; case SSSH: present(null, [ ["Quiet now.", "Sssssh.", "Patience."]]); break; default: show(null); } } wait(getRandom(10) + 10); }; }; // Session def sessionSummon = { imageSpec -> final limit = 5; final timeMax = 10; for (def n = 1; n <= limit; n++) { switch (n) { case limit: present(imageSpec, [ ["TOY!", "SLAVE!", "SLUT!"], ["Last chance!", "Final warning!"]]); 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, Miss?", timeMax) < timeMax) { return true; }; } return false; }; def sessionPlay = { def play = { amount, activities -> (getRandom(amount) + 1).times { activities[getRandom(activities.size())](); }; }; play(3, [preRelease, preEdge, preGag, preClamps, preCollar, preStrip]); play(3, [playStrokes, playEdges, playNothing]); play(2, [postEdge, postCum, postChastity]); }; def sessionRelease = { present([DRESSED], [ ["Ahhh,", "OK,"], ["I'm done with you for now.", "That was fun... for me at least."]]); wait(getRandom(10) + 5); if (is(CUFFED) || is(COLLARED) || is(CLAMPED) || is(GAGGED)) { present([DRESSED], [ ["You may release yourself", "Free yourself"]]); wait(getRandom(5) + 5); set(CUFFED, false); set(COLLARED, false); set(CLAMPED, false); set(GAGGED, false); } if (is(NAKED)) { present([DRESSED], [ ["Put some clothes back on.", "Cover yourself up!"]]); wait(getRandom(10) + 15); set(NAKED, false); } present([DRESSED], [ ["Off you go.", "You may leave."]]); wait(10); setImage(null); show(null); }; def playtime = { dress([[DRESSED,nTEASE],[DRESSED,TEASE],[TITS]]); if (sessionSummon([DRESSED,nTEASE])) { sessionPlay(); sessionRelease(); } } def bedtime = { dress([[LINGERIE,nTITS]]); if (sessionSummon([LINGERIE,nTITS])) { preStrip([LINGERIE,nTITS]); present([LINGERIE,nTITS], [ ["Very good, toy.", "OK"], ["Bedtime!", "Off to bed with you!", "Bed! Now!"]]); showButtonG("Good night, mistress"); } exit(); }; // Setup // Plans - cannot overlap def setupPlan = { plan, float startmin, float startmax, float endmin, float endmax -> def timeWindow = { float min, float max -> (int)((min * HOUR) + getRandom((int)((max - min) * HOUR))) }; // Unplanned or past if (getTime() > loadInteger("toy.plan.${plan}.${END}")) { def day = getDay() + ((getRandom(7) + 1) * DAY); save("toy.plan.${plan}.${FRIEND}", FRIENDS[getRandom(FRIENDS.size())]); save("toy.plan.${plan}.${START}", day + timeWindow(startmin, startmax)); save("toy.plan.${plan}.${END}", day + timeWindow(endmin, endmax)); } } def setupPlans = { setupPlan(LUNCH, 11.5, 12.5, 13.0, 14.0) setupPlan(SHOPPING, 14.5, 15.5, 16.0, 18.0) setupPlan(PARTY, 19, 20, 22.0, 25.0) }; def setDefault = { prop, val -> if (loadString(prop) == null) { save(prop, val); } }; // Initial - basic checks and defaults def setupInitial = { if (loadInteger("toy.version") == null) { // Welcome } save("toy.version", VERSION); setDefault("toy.strokeTeaseOffset", 0); setupPlans(); }; def setupShowState = { show( [CHASTE, COLLARED, CUFFED, CLAMPED, GAGGED, NAKED].collect { def v = is(it); return "$it: $v\n".capitalize() }.join()); showButton("OK"); }; def setupShowCalendar = { show( [LUNCH, SHOPPING, PARTY].collect { def s = loadInteger("toy.plan.${it}.${START}"); def f = loadString("toy.plan.${it}.${FRIEND}"); def timeStr = localTimeOf(s).format(soonFormatter); return "$it with $f on $timeStr.\n".capitalize(); }.join()); showButton("OK"); }; // GO! setupInitial(); def getDayNum = { Math.floorDiv(getTime() + localTimeOffset() - 14400, 86400) }; def getAvail = { dayNum -> loadInteger("toy.availability.$dayNum") ?: 0 }; def addAvail = { amount -> def dayNum = getDayNum(); save("toy.availability.$dayNum", getAvail(dayNum) + amount); }; if (localTime() > 25.75 || localTime() < 7.5) { // 1:45am - 7:30am show("Mistress is sleeping..."); wait(10); exit(); } final cycleTime = 6; while (true) { if (showButton("Please, Miss?", cycleTime) < cycleTime) { switch (getSelectedValue("Yes, toy?", [ "Status", "Calendar", "Play", "Back" ])) { case 0: setupShowState(); break; case 1: setupShowCalendar(); break; case 2: playtime(); break; case 3: return "toy"; } } else if (localTime() > 25.75) { // 1:45am bedtime(); } else if (getRandom(1000) == 0) { playtime(); } addAvail(cycleTime); }