summaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
Diffstat (limited to 'scripts')
-rw-r--r--scripts/toy.groovy694
1 files changed, 694 insertions, 0 deletions
diff --git a/scripts/toy.groovy b/scripts/toy.groovy
new file mode 100644
index 0000000..fcb1b2e
--- /dev/null
+++ b/scripts/toy.groovy
@@ -0,0 +1,694 @@
+// 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.<name> 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.<name>
+final BONDAGE = "bondage", CBT = "cbt", CHORES = "chores", PAIN = "pain";
+// toy.permission.<name>
+final CUM = "cum", EDGE = "edge"
+// toy.state.<name>
+final CHASTE = "chaste", COLLARED = "collared", CUFFED = "cuffed", CLAMPED = "clamped", GAGGED = "gagged", NAKED = "naked";
+// toy.plan.<name>.{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);
+}
+