● 前提条件
淫欲少女抄Fインストール済み
Visual Studio 2010使用 (おそらく無償のExpress Editionでも可)
C#使用 (条件さえ満たせば他の言語でもできるはず)
ANGF開発版は必須ではない (ANGF開発版とは開発用の操作ジャーナリング機能、フラグエディタと、ANGFLib.dllのAPIリファレンスが含まれる版)
● プロジェクト
C#のクラスライブラリのプロジェクトを新規作成
GACに登録済みのANGFLib.dll, SexyLibv.dll, Yumemachi.dllを参照しておく
ANGFのシステム機能でIisfExt1RunTime.xmlをモジュール登録しておく (システム機能は、ANGF開発版、ラインタイム版をインストールして起動するか淫欲少女抄FのANGF.exeを直接開くと選択できる)
● ユニークなID
ANGFには膨大なユニークなIDが必要とされます。これは文字列表記のGUIDを推奨します。衝突する可能性が極めて低いからです。Visual Studio Professional以上であれば「ツールメニュー」の「GUIDの作成」が該当します。しかし、ユニークであれば(他のIDを重複しないなら)どんな文字列でも構いません。
● IisfExt1RunTime.xml
DLLをいちいち読み込まなくても起動メニューを作成できるだけの情報を書き込んだファイル。読み込むべきDLLを判定するための依存関係もここにある。
<?xml version="1.0" encoding="utf-8" ?>
<!-- root要素は形式上必要とされるだけで機能は持たない -->
<!-- 名前空間URIはhttp://angf.autumn.org/std001 -->
<root xmlns="http://angf.autumn.org/std001">
<!-- 読み込むべきDLLの名前。同じフォルダとbin\Releaseもついでに探してくれる -->
<module>IisfExt1.dll</module>
<!-- 起動メニューで選択出来るモジュールなら1にする -->
<startupModule>0</startupModule>
<!-- 起動メニュー等に表示する名前だ -->
<name>ISSF 拡張パック1</name>
<!-- ユニークなIDだ (必ず書き換えて使えよ) -->
<id>{baceffc1-7a7d-4911-9ed4-4850f0baf908}</id>
<!-- 指定IDのモジュールのシェアワールドであることを示す -->
<!-- つまり淫欲少女抄Fの読み込み時にこのモジュールも一緒に読まれる -->
<!-- 最小バージョンは1.0.0.3だ。淫欲少女抄Fのメインモジュールがこれ以下のバージョンなら起動できない -->
<shareWorld version="1.0.0.3">{A443F871-0CD4-4241-9991-FCA32611820F}</shareWorld>
</root>
● IisfExt1Constants.cs
単に定数を定義しているだけで、ANGF上の特別な意味はありません。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace IisfExt1
{
public class IisfExt1Constants
{
// 複数の箇所から参照される(かもしれない)ユニークなIDはまとめて定義しておくとよい
public const string 田中商事ID = "{68e9f66d-8ed6-4a18-9bd6-ff77d17f0a16}";
public const string 田中社長ID = "{04f7311f-ce5c-44dc-aa2c-4abd155d4a51}";
}
}
● IisfExt1Items.cs
Yumemachiモジュール準拠のアイテムを1つ定義しています。しかし、システムで有効にするめにはModuleクラス経由で提供する必要があることに注意してください。これだけでは有効になりません。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ANGFLib;
using SexyLib;
using Yumemachi;
namespace IisfExt1
{
public class IisfExt1Items
{
// YumItemはYumemachiモジュール内で定義済み
public static YumItem IisfExt1ItemOLスーツ = new YumTemplate()
{
// ユニークなIDは書き換えて使えよ!
Id = "{cc75820a-3882-4b5a-9a76-6a070077b5fc}",
// アイテムの名前(人間が読める)
Name = "OLスーツ",
// アイテムの説明文
BaseDescription = "スカート丈が長く地味なOL用のスーツだが、かえって興奮する男もいるぞ。",
// 所有できる最大数
Max = 1,
// 基本価格。0なら売れない。出社時に必ず着るので売れないことにしてある
Price = 0, // 売れない
// 装備できる部位のビットマップ。SexyLib.dll内で名前として定義済み
AvailableEquipMap = SexyConstants.上下服,
// 同時に装備される部位のビットマップ。SexyLib.dll内で名前として定義済み
// 上服または下服として装備させると両方同時に装備されるワンピース服扱いなのだ
SameTimeEquipMap = SexyConstants.上下服,
// 刺激度と隠蔽度 (ゲームのルール参照)
刺激度 = 0,
隠蔽度 = 100,
};
}
}
● IisfExt1Module.cs
拡張モジュールの核心となるクラスを提供する。システムがインスタンス化するので、このクラスをインスタンス化するコードは拡張モジュール内には存在しません。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using ANGFLib;
using SexyLib;
using Yumemachi;
namespace IisfExt1
{
// 本当ならANGFLib内のModuleクラスを継承するが、GetSkillsメソッドを拡張したSexyLib内のSexyBaseModuleクラスを継承している
public class IisfExt1Module : SexyBaseModule
{
// ユニークなIDは書き換えて使えよ!
public override string Id { get { return "{3b546167-609a-4ae2-ab91-ccc1e16d69d1}"; } }
// このモジュールの登場人物をシステムに教える
public override Person[] GetPersons()
{
return IisfExt1Persons.GetAllPersons();
}
// このモジュールの登場場所をシステムに教える
public override Place[] GetPlaces()
{
// Util.CollectTypedObjectsはANGFLib.dllのメソッドで、指定アセンブリ内で指定した型を持つクラスを全て探してインスタンス化したリストを作成する。ここではPlaceクラスを指定しているので、そのクラスを継承した全てのクラスを探す
return Util.CollectTypedObjects<Place>(System.Reflection.Assembly.GetExecutingAssembly());
}
public override Collection[] GetCollections()
{
// このモジュールのコレクションをシステムに教える
// 対象コレクションはプレイのみ
return new Collection[]
{
// プレイに関するクラスを集めてリストを提供する機能はSexyLibにあるのでそれを呼ぶ
// 第2引数のtrueはオーバーライトを指示する。
// プレイのコレクションはメインモジュール側に既にあるので、
// オーバーライトの指示がないとID重複の警告が出て止まってしまう。
SexyLib.Plays.CollectPlays(Assembly.GetExecutingAssembly(),true),
};
}
// リフレクションでこのモジュールのアイテム一覧を返す
// 詳細の説明は略 (このままのソースを使っても良い)
public override Item[] GetItems()
{
var list = new List<Item>();
foreach (FieldInfo info0 in typeof(IisfExt1Items).GetFields(BindingFlags.Public | BindingFlags.Static))
{
if (info0.GetValue(null) is Item)
{
Item item = (Item)info0.GetValue(null);
list.Add(item);
}
}
return list.ToArray();
}
}
}
● IisfExt1CD屋みゅーじ.cs
CD屋みゅーじの場所クラスを置換する。つまり、オリジナルのクラスをこのクラスで上書きさせる。ただし、やはりモジュール経由で存在をシステムに教える必要があります。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ANGFLib;
using SexyLib;
using Yumemachi;
using IisfExt1;
namespace IisfExt1
{
// 場所を示すクラスは通常ANGFLib.dllのPlaceクラスだが、
// ここではそれを継承したYumemachiモジュールのYumPlaceクラスを使用している
public class IisfExt1PlaceCD屋みゅーじ : YumPlace
{
// 置き換えるので、元のオブジェクトのIDを指定している
public override string Id { get { return YumConstants.CD屋みゅーじID; } }
// 人間が読める名前
public override string HumanReadableName { get { return "CD屋みゅーじ"; } }
// サブ移動する先なので、親の位置を指定
public override string Parent { get { return YumConstants.努町商店街ID; } }
// オーバーライド強制を指定
public override bool ForceOverride { get { return true; } }
// 試聴メニュー選択時の動作。返却値はダミー
private bool 試聴()
{
// DefaultPersonsはシステム、独白、私メンバーがあり、Sayメッセージで文字列を出力できる
DefaultPersons.独白.Say("私はノリノリのロックを試聴した。");
DefaultPersons.独白.Say("金のリボンでロックロック");
// 時間経過はState.GoTimeメソッドで行う
State.GoTime(30);
return false;
}
// その場所のメニュー作成を行うために呼ばれるメソッド
public override bool ConstructMenu(List<SimpleMenuItem> list)
{
// メニューを3つ追加する
// 最初の2つは、機能を元モジュールが提供しているのでそれを呼んで終わり
list.Add(new SimpleMenuItem("見る", YumPlaceCD屋みゅーじ.見る));
list.Add(new SimpleMenuItem("万引きする", YumPlaceCD屋みゅーじ.万引き));
// 最後の1つのみ自前のメソッドを呼ぶ
list.Add(new SimpleMenuItem("試聴する", 試聴));
return true;
}
}
}
● IisfExt1田中社長.cs
田中社長という人物に関するクラス。これも、モジュール経由でシステムに存在を教える必要があります。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ANGFLib;
using Yumemachi;
namespace IisfExt1
{
// リフレクションで集める関係上クラスのインスタンスとして用意
// partialは要らなかったかも
public partial class IisfExt1Persons
{
// 田中社長オブジェクトを作成する
// YumMeetMalePersonクラスのコンストラクタはID、本名、仮名、姓を引数に取るが、
// 本名と仮名の意味がないので同じ文字列
// 姓は結婚時に意味があるのだが、結婚できない男なので実はあまり意味がない
public static YumMeetMalePerson 田中社長 = new YumMeetMalePerson(IisfExt1Constants.田中社長ID, "田中社長", "田中社長", "田中")
{
// 出産時に父親名が判明するタイプであることを示すが、
// 実は田中社長に中出しされることがないので、意味がない指定
父親名判明 = 父親名タイプ.判明,
};
// このクラスに属する全ての人物オブジェクトのリストを返す
// 他のソースから持ってきたらこうなっているが、ここまで凝る必要は無かったのかも
// このまま転用して使っても可
internal static Person[] GetAllPersons()
{
List<Person> list = new List<Person>();
foreach (System.Reflection.FieldInfo info in typeof(IisfExt1Persons).GetFields())
{
if (info.FieldType.IsSubclassOf(typeof(Person)))
{
list.Add((Person)info.GetValue(null));
continue;
}
if (info.FieldType == typeof(Person))
{
list.Add((Person)info.GetValue(null));
continue;
}
}
return list.ToArray();
}
}
}
● IisfExt1田中商事.cs
こちらは、場所の新規追加の例。そして、実際のプレイの例。これもモジュール経由でシステムに存在を教える必要があります。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ANGFLib;
using SexyLib;
using Yumemachi;
namespace IisfExt1
{
// 田中社長フェラというプレイを実現するクラス
// PlayはSexyLibで提供されている
public class Play田中社長フェラ : Play
{
// コンストラクタで勝負が付いている
public Play田中社長フェラ()
{
// ユニークなIDは書き換えて使えよ!
this.Id = "{300782d1-ed1e-4c53-a28f-78d6a49e476e}";
// 実際のプレイ時に呼ばれる手順
// subidはバリエーションがあるとき区別するID
this.Procedure = (subid) =>
{
// QuickTalkは会話を簡単に記述する手段
var q = new QuickTalk();
// tという名前を田中社長と関連づける
q.AddTalker("t", IisfExt1Persons.田中社長);
// 会話開始
// wで始まる行は私、tで始まる行は田中社長、何も無い行は独白
q.Play(@"
w 田中社長
t なんだ君か
w おそくまでお疲れ様です
t 零細企業の経営者なんてこんなものさ
w 凝ったところを揉みほぐして差し上げますわ
t おっ。肩でも揉んでくれるのかい
私は机の下の潜り込んで、社長のズボンのチャックを引き下ろした
中から出てきた立派な息子を私はフェラチオした
w ほら、もうこんなに堅く凝っているじゃありませんか
t そ、それは君がそんなことをするからだよ
w じゃあ、やめますか?
t やめないでくれ。もう出そうだ
社長は盛大に白い液体を噴き出した。
");
// プレイの結果をSexyLibのメソッドを呼んで通知する
// ノーマルアクメなど自分のアクメがあれば画面効果も発生 (スキップモードではない場合)
// 既に今日は行ったことを後から判定するため、相手の名前を指定する
SexyGeneral.アクメ抜きフェラチオ(IisfExt1Persons.田中社長);
q.Play(@"
t 今日のことは、絶対に他の社員には内緒だからな。
w はい。
私は何事も無かったかのように翌日に出社しようと決めた。
");
};
}
}
// 場所は通常ANGFLibのPlaceクラスだが、朝起きて自動的に通う機能を持った施設として、
// YumemachiモジュールのYum通い施設クラスを使用
public class IisfExt1田中商事 : Yum通い施設
{
// 人間可読の名前
public override string HumanReadableName { get { return "田中商事"; } }
// ID
public override string Id { get { return IisfExt1Constants.田中商事ID; } }
// サブ移動する先なので、親のID
public override string Parent { get { return YumConstants.努町大通りID; } }
// 組織の名前
public override string Get所属組織Name() { return "田中商事"; }
// 参加したときの身分の名前
public override string Get所属組織会員Type() { return "社員"; }
// 自主的にやめられるならtrue
public override bool Is自主的に抜けられる { get { return true; } }
// 組織から出たり入ったりする際の表現
public override string 入る表現 { get { return "入社"; } }
public override string 出る表現 { get { return "退職"; } }
// 朝に発生する定期的な処理
public override void 定期処理()
{
// YumGeneralとかSexyGeneralでけっこう便利なメソッドを公開しているぞ
// 現在の日付時刻はFlags.Nowで得る
if (YumGeneral.Is休日(Flags.Now))
{
DefaultPersons.独白.Say("今日は休日なので、1日自由だ。何をしようかな。");
return;
}
DefaultPersons.独白.Say("私はOLスーツを着て田中商事に出社した。");
// SexyGeneral.Equip上服は本来の着衣機能のラッパ
SexyGeneral.Equip上服 = IisfExt1Items.IisfExt1ItemOLスーツ;
// 移動する際はState.WarpToメソッドを使用する
State.WarpTo(IisfExt1Constants.田中商事ID);
// 17時に終わる。分単位に換算してState.GoTimeメソッドで時間を進める
State.GoTime((17 - 生活サイクル起点時間) * 60);
DefaultPersons.独白.Say("私は今日の給料1万円をもらった。");
// 所持金はFlags.所持金フィールド。
Flags.所持金 += 10000;
}
// 社員がいる時間か?
private bool is社員here()
{
return Flags.Now.Hour >= 9 && Flags.Now.Hour < 17;
}
// 社長がいる時間か?
private bool is社長here()
{
return Flags.Now.Hour >= 9 && Flags.Now.Hour < 18;
}
// やめる際の処理
private bool drop()
{
// UI.YesNoMenuメソッドは2択の質問を出す
if (!UI.YesNoMenu("本当に退職しますか?", "やめちゃう", "間違いでした")) return false;
DefaultPersons.独白.Say("私は惜しまれながら田中商事を辞めた。");
// 退会メソッドでやめる
this.退会();
return true;
}
// 入る際の処理
private bool enter()
{
IisfExt1Persons.田中社長.Say("君が入社希望かね?");
if (!UI.YesNoMenu("本当に入社希望しますか?", "希望します", "間違いでした")) return false;
DefaultPersons.主人公.Say("OLになりたいんです。");
if (YumFlags.性別 != E主人公性別.女)
{
IisfExt1Persons.田中社長.Say("だめだめ。OLってのは、Office Ladyだからね。年齢は不問だけど、Ladyじゃないと採用しないよ。");
State.GoTime(5);
return false;
}
IisfExt1Persons.田中社長.Say("いいとも。慢性的に人手不足だから採用しちゃうよ。");
IisfExt1Persons.田中社長.Say("給料は日給制。1日1万円だ。週休2日。勤務時間は17時までだ。");
if (!UI.YesNoMenu("本当に入社しますか?", "希望します", "間違いでした")) return false;
IisfExt1Persons.田中社長.Say("ようこそ田中商事へ。");
if (State.GetItemCount(IisfExt1Items.IisfExt1ItemOLスーツ) == 0)
{
IisfExt1Persons.田中社長.Say("では記念にOLスーツを進呈しよう。");
IisfExt1Persons.田中社長.Say("仕事をするときはこれを着てくれ。");
DefaultPersons.独白.Say("私はOLスーツをもらった。");
// アイテムを手に入れるときはState.GetItemメソッド
State.GetItem(IisfExt1Items.IisfExt1ItemOLスーツ);
}
// 入会時は、入会メソッド
this.入会();
IisfExt1Persons.田中社長.Say("さっそく明日から来てくれ。");
State.GoTime(30);
return true;
}
// メニューでフェラ選択時
private bool fera()
{
// プレイオブジェクトを作成してCallAndSetProcedureメソッドを呼ぶ
// CallAndSetProcedureはプレイの実体の呼び出しと体験済みの記録を行う
// なので、必ずこのメソッド経由で体験させる
new Play田中社長フェラ().CallAndSetProcedure();
State.GoTime(30);
return true;
}
// メニュー作成を行うメソッド
public override bool ConstructMenu(List<SimpleMenuItem> list)
{
// 条件次第でメニュー構成が変化するぞ
if (YumGeneral.Is休日(Flags.Now))
{
DefaultPersons.独白.Say("今日は休日なので誰もいない。");
}
else
{
if (!is社員here())
{
DefaultPersons.独白.Say("勤務時間外だから社員は誰もいない。");
if (is社長here())
{
DefaultPersons.独白.Say("社長が1人だけ残業してるぞ。");
// SexySkills.フェラチオは、フェラチオスキルを保持するオブジェクト
// IsActiveプロパティは1つでもレベルがあることを示す
// TodayShootCountメソッドは拡張メソッドで、その人物の今日の射精回数を返す
if (SexySkills.フェラチオ.IsActive && IisfExt1Persons.田中社長.TodayShootCount() == 0)
{
list.Add(new SimpleMenuItem("社長をフェラチオ", fera));
}
}
}
if (is社長here())
{
if (Is入会())
{
list.Add(new SimpleMenuItem("退職希望", drop));
}
else
{
list.Add(new SimpleMenuItem("入社希望", enter));
}
}
}
return true;
}
}
}
以上