rebol [
	title: "Blood"
	description: "liquid-based generic RPG-style character-editor"
	note: "will download and save slim.r and liquid.r from rebol.org in the same directory as this script."

	; -- basic rebol header --
	file: 		%blood.r
	version:	0.1.1
	date: 		2009-03-07
	author:		"Maxim Olivier-Adlhoch"
	copyright:	"Copyright (c) 2009 Maxim Olivier-Adlhoch"

	;-- REBOL.ORG header --
	library: [
		level:          'intermediate
		platform:       'all
		type:           [ demo ]
		domain:         [ external-library scientific vid]
		tested-under:   [win view 2.7.5 2.7.6 sdk 2.7.5 2.7.6]
		support:        "http://www.pointillistic.com/open-REBOL/moa/steel/liquid/index.html"
		license:        'MIT
	]


	;-- extended rebol header --
	purpose:	"Usefull example to demo and learn how to use liquid."
	notes:		"Needs STEEL|LIBRARY MANAGER (slim) package and liquid.r to be installed prior to usage."
	license:    {Copyright (c) 2009, Maxim Olivier-Adlhoch

		Permission is hereby granted, free of charge, to any person obtaining a copy of this software 
		and associated documentation files (the "Software"), to deal in the Software without restriction, 
		including without limitation the rights to use, copy, modify, merge, publish, distribute, 
		sublicense, and/or sell copies of the Software, and to permit persons to whom the Software 
		is furnished to do so, subject to the following conditions:
		
		The above copyright notice and this permission notice shall be included in all copies or 
		substantial portions of the Software.}
		
	disclaimer: {THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 
		INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 
		PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 
		FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ]
		ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 
		THE SOFTWARE.}

]

;-------------------------------------
; SLIM LIBRARY DOWNLOADER
;-------------------------------------
unless exists? %slim.r [
	either confirm "Download SLiM Module Manager from rebol.org?" [
		unless data: request-download/to http://www.rebol.org/download-a-script.r?script-name=slim.r clean-path  %slim.r [
			alert "User canceled download or network error, we will now quit."
			quit
		]
	][
		alert "SLiM is required for this application, we will now quit."
		quit
	]
]

do %slim.r
slim/voff
unless slim/open/expose 'liquid 0.7.0 [liquify link content fill !plug attach] [
	either confirm "Download liquid data flow module from rebol.org?" [
		unless data: request-download/to http://www.rebol.org/download-a-script.r?script-name=liquid.r clean-path  %liquid.r [
			alert "User canceled download or network error, we will now quit."
			quit
		]
	][
		alert "liquid data flow module is required for this application, we will now quit."
		quit
	]
]	


;-------------------------------------
; START OF APPLICATION
;-------------------------------------
liquid-lib: slim/open/expose 'liquid 0.7.0 [liquify link content fill !plug attach]


; this tells slim to expose the various vprinting functions within the global space, 
; allowing the application to benefit from the engine very easily.
slim/vexpose


;-----------------
;- LIQUID CLASSES
;-----------------
;- !int-range-srv
; a plug which is used as a server for automated piping management by other plugs.
;
; a pipe server is just another plug, but it acts as a centralised subordinate for any plugs
; sharing a value.  all all attached piped plugs, will synchronise amongst themselves when you fill
; any one of the pipe "clients".
;
; this plug will be created on the fly by the !int-range plug when it creates a pipe server for itself.
; note that any other plug (plug-b) which attaches to a plug (plug-a) which creates a pipe, will also be connected to
; the pipe's (plug-a) server.
;
; this means the order in which you attach piped plugs in important, if you want to properly share a specific
; pipe server.
;-----------------
!int-range-srv: make !plug [
	min-value: 1
	max-value: 10
	prev-value: none
	valve: make valve [
		type: 'int-range
		;-----------------
		;- !purify()
		; this function is used to normalize the liquid stored within a plug, 
		; after any and all processing, containment, filling, etc.
		; it returns a boolean indicating if the purification was able to clean the data
		; properly.
		;
		; in this case we are always able to normalize the value
		;-----------------
		purify: func [
			plug
		][
			vin [{int-range-srv/process()}]
			; brute force way to make sure datatype is valid, and its within bounds
			plug/liquid: plug/prev-value: max plug/min-value min plug/max-value any [
				attempt [
					switch/default type?/word plug/liquid [
						logic! [either plug/liquid [max-value][min-value]]
					][
						to-integer plug/liquid
					]
				]
				plug/prev-value
				0
			]
			vout
			true
		]
	]
]


;- !sum
; add up all number values which are linked to this plug.
!sum: make !plug [
	valve: make valve [
		;-----------------
		;-     process()
		;-----------------
		process: func [
			plug
			data
			/value ; faster than:  /local value
		][
			vin [{!sum/process()}]
			plug/liquid: 0
			foreach value data [
				if number? value [
					plug/liquid: plug/liquid + value
				]
			]
			vout
		]
		
		
	]
]
;- !subtract
; from the first value linked, substract all other links.
; ignores any non number being linked as an error prevention.
!subtract: make !plug [
	valve: make valve [
		;-----------------
		;-     process()
		;-----------------
		process: func [
			plug
			data
			/value ; faster than:  /local value
		][
			vin [{!subtract/process()}]
			plug/liquid: 0 ; an integer by default.
			if 0 < length? data [
				plug/liquid: data/1
				foreach value next data [
					if number? value [
						plug/liquid: plug/liquid - value
					]
				]
			]
			vout
		]
	]
]

;- !int-range
; this is a plug which will always clamp its liquid within a range of two values.
; the pipe-server-class has a default range of 1 to 10, but this can be easily
; changed... see !ability class below to see how to do so.
!int-range: make !plug [
	valve: make valve [
		type: 'int-range
		pipe-server-class: !int-range-srv
	]
]


;- !ability
; preset range of 1 to 25
!ability: make !plug [
	valve: make valve [
		type: 'ability
		pipe-server-class: make !int-range-srv [ min-value: 1 max-value: 25]

	]
]


;- !signal
; this class is simply used to call a function whenever data within the flow changes.
; note that we didn't set this plug to stainless? since our dependencies will
; force us to clean up first, so our callback will always happen before our observers.
;
; if this plug is at the edge of a graph, simply set its stainless? attribute to true if appropriate.
!signal: make !plug [
	;-----------------
	;- callback()
	;-----------------
	callback: func [
		data
	][
		vin [{!signal/callback()}]
		vout
	]
	
	valve: make valve [
		type: 'signal
		;-----------------
		;- purify()
		;-----------------
		purify: func [
			plug
		][
			vin [{!signal/purify()}]
			plug/callback plug/liquid
			vout
			true
		]
	]
]
	


;- !character
!character: make !plug [
	;-----------------
	; instance data
	;-----------------
	;-    abilities
	str: none
	dex: none
	int: none
	
	skills: none
	
	max-points: none
	ability-total: none
	points-left: none
	
	;-----------------
	; class methods and values
	;-----------------
	valve: make valve [
		type: 'character
		
		;-----------------
		;-    setup()
		;-----------------
		setup: func [
			plug
			/local p
		][
			vin [{!character/setup()}]
			; abilities
			plug/str: liquify/fill/piped !ability 12
			plug/dex: liquify/fill/piped !ability 12
			plug/int: liquify/fill/piped !ability 12
			
			; create the ability total sum node
			p: plug/ability-total: liquify !sum
			; link it to our abilities
			link p plug/str
			link p plug/dex
			link p plug/int
			
			; set the max-points range
			plug/max-points: p: liquify/fill/piped !int-range 40
			; the pipe? attribtue is a pointer to the pipe server built internally when the plug became piped
			p/pipe?/min-value: 3
			p/pipe?/max-value: 75

			; create the points-left value, which is simply max-points minus ability totals
			plug/points-left: p: liquify !subtract 
			link p plug/max-points
			link p plug/ability-total
			
			vout
		]
		
	]
	
	
]


;- GUI


;- facets
bg-color: gray

base-font:  make face/font [name: "Trebuchet MS" color: black style: none size: 12 colors: reduce [black black]]
right-font: make base-font [align: 'right]
left-font: make base-font [align: 'left]
error-right-font: make right-font [color: red style: 'bold colors: reduce [red red]]
error-font: make base-font [color: red style: 'bold colors: reduce [red red]]
link-font: make base-font [color: white shadow: 1x1 colors: reduce [white gold]]
link-hi-font: make link-font [color: gold shadow: 1x1]
link-off-font: make link-font [color: gray shadow: none]

black-edge: make face/edge [color: black style: none size: 1x1]
error-edge: make black-edge [color: red]







liquid-lib/voff

;- STYLES
styles: stylize/master  [
	;-     label
	label: text font right-font
	
	;-     link
	link: text font link-font with [
		init: []
		feel: make feel [
			over: func [face state][
				face/font: either state [link-hi-font][link-font]
				show face
			]
			engage: func [ face action event ][
				unfocus
				if action = 'down [
					unfocus
					do-face face face/text
				]
			]
		]
	]
	
	;-     liquid-label
	liquid-label: label with [
		plug: none
		;-----------------------
		; format()
		;
		; note, we can add a function called format and it will be called before refreshing the face text
		; ex:
		;    format: func [data][ rejoin ["label: " data ]]
		;-----------------------

		
		;-----------------
		;-         setup()
		;
		; this creates the plug we use to link to (and thus display) data.
		;
		; it can be run before internal init,
		; otherwise, the init is improved to call setup if not yet called
		;-----------------
		setup: func [
		][
			vin [{liquid-label/init()}]
			
			; will eventually link to the actual data
			;-         plug []
			plug: liquify/with !plug  [
				face: none
				stainless?: true ; this makes the plug always refresh when its liquid changes, forcing it to cleanup.
				
				valve: make valve [
					;-----------------
					;-             Purify()
					;
					; we use this to refresh the text's label and visuals, reusing its string
					;-----------------
					purify: Func [
						plug
						/text ; shortcut to /local text
					][
						vin [{liquid-label/purify()}]
						if plug/face [
							; reuse the string or create one, if face/text is none
							plug/face/text: head any [plug/face/text copy ""]
							clear plug/face/text
							text: any [
								all [in plug/face 'format plug/face/format plug/liquid]
								to-string plug/liquid plug/liquid
							]
							append plug/face/text text
							; we can't call show until the face is properly setup
							if plug/face/parent-face [
								show plug/face
							]
						]
						vout
						true
					]
				]
			]
			plug/face: self
			append init [unless object? plug [setup]]
			vout
		]		
	]

	;-     liquid-field
	liquid-field:  field edge [size: 1x1] [if face/plug [fill face/plug face/text]] with [
		plug: none
		size: 25x25
		
		;-----------------
		;-         setup()
		;
		; this creates the plug we use to attach to another liquid.
		;
		; since we attach the ability within the layout, we call setup just prior there,
		; otherwise, the init is improved to call setup if not yet called
		;-----------------
		setup: func [
		][
			vin [{liquid-field/init()}]
			
			; will eventually attach to the actual ability 
			;-         plug []
			plug: liquify/piped/with !plug  [
				face: none
				stainless?: true ; this makes the plug always refresh, when its pipe changes, forcing it to cleanup.
				
				valve: make valve [
					;-----------------
					;-             Purify()
					;-----------------
					purify: Func [
						plug
					][
						vin [{ability-ctrl/purify()}]
						plug/liquid: to-string plug/liquid
						if plug/face [
							plug/face/text: any [plug/face/text copy ""]
							clear plug/face/text
							append plug/face/text plug/liquid
							show plug/face
						]
						vout
						true
					]
				]
			]
			plug/face: self
			vout
		]
		
		append init [unless object? plug [setup]]
	]	
	
	
	;-     ability-nudge -
	ability-nudge-: link "-" 15x25 [
		; face action
		nudge-increment: content face/ability 
		fill face/ability nudge-increment + face/amount
	] with [ amount: -1 ability: none] 	



	;-     ability-nudge +
	ability-nudge+: ability-nudge- "+" with [
		amount: 1 ; how much does this node nudge the ability
		ability: none ; the ability we will edit (character's plug, not the gui)
		character: none ; the character we are nudging, must be set BEFORE calling setup.
		plug: none ; our overflow detection, which nullifies the button! (link is only active if points-left is above 0)
		
		feel-backup: feel ; when the link is disabled, we store its feel here
		
		;-        setup()
		; here we have a dependency which is based on the character (like the ability in )
		; this means, we must create and set the character BEFORE creating nudges.
		setup: func [][
			plug: liquify/with !plug [
				stainless?: true
				face: none
				current-state: none
				valve: make valve [
					;-----------------
					;-            process()
					; we use this plug only to trigger changes based on DEPENDENCY changes 
					;
					; the nice side-effect is that the effect of our face action, will eventually
					; trickle back to this plug, since this plug is based on the accumulated value
					; of all abilities.
					;-----------------
					process: func [
						plug
						data
						/state
					][
						vin [{ability-nudge+/process()}]
						if 2 = length? data [
							state: (data/2 < data/1)
							if state <> plug/current-state [
								either state [
									plug/face/feel: plug/face/feel-backup
									plug/face/font: link-font
								][
									plug/face/feel: none
									plug/face/font: link-off-font
								]
								plug/current-state: state
								if plug/face/parent-face [
									show plug/face
								]
							]
						]
						vout
					]
				]
			]
			plug/face: self
			link plug character/max-points
			link plug character/ability-total
		]
		append init [unless object? plug [setup]]
		
	] 
	


]

;- INSTANCE DATA
character: char*: liquify !character  ; char* is used to differentiate from the style's internal character attribute

;- declare some words which store gui elements
gui-pnt-left: none


;- HELP WINDOW
help-window: layout[
	area 600x630 para [wrap?: true]{ Welcome to Blood, a liquid-based generic RPG-type character editor.
	
This is a simple application used to demonstrate the fundamentals of the liquid dataflow module.
	
USAGE:
------
All of the character abilities are bound to the value 1 to 25.  There is no way you can cause them to go out of bounds.

By clicking on the little +/- besides them, you can nudge the related ability as long as you follow the rules.  Either the limit of 25 is reached, or you have gone beyond the Maximum number of points allowed for the character.

The + will actually disactivate themselves whenever to break or meet the limits allowed for the abilities.

The fields do allow you to go beyond the max points, this is to allow you freedom in editing the values, knowing that you will lower another ability afterwards.

The total of all abilities is always kept up to date, it will even tell you how much points you have left or by how much you have gone beyond the max points limit.

The Max points is also linked in real time to all the values, so that if you lower or raise it, all the dependencies will automatically reflect the change instantly.

NOTES:
------
The data binding is NOT BUILT WITHIN THE INTERFACE, its the DATA WHICH IS SELF-BOUND, and the interface will always react to the data, since they are linked or piped together. Try typing something other than a number.

VERBOSITY:
---------
You can switch application and liquid verbosity in real-time thanks to the built-in capabilities of the slim library manager, called vprint.  There is also a subset of the featurebase included in a simple 'DOable module on rebol.org, named vprint.r .

NEXT RELEASE:
------------
I will build another version of the tool which adds skills selection and editing.  The skills will be related to the abilities and will reflect any change in ability in real-time.

}

]


;- WINDOW
window:  layout [
	across
	space 1x5
	label "Max points:" 120
	liquid-field 25 with [setup attach plug character/max-points]
	return
	pad 0x30
	label 75 "Strength:"
	ability-nudge- with [ability: character/str]
	liquid-field with [setup attach plug character/str]
	ability-nudge+ with [ability: char*/str character: char* setup]
	return
	label 75 "Intelligence:"
	ability-nudge- with [ability: character/int]
	liquid-field with [setup attach plug character/int]
	ability-nudge+ with [ability: char*/int character: char* setup]
	return
	label 75 "Dexterity:"
	ability-nudge- with [ability: character/dex]
	liquid-field with [setup attach plug character/dex]
	ability-nudge+ with [ability: char*/dex character: char* setup]
	return
	box 120x1 black black
	return
	label 75 "Total:"
	pad 17x0
	liquid-label 25 black white edge black-edge  with  [
		feel: none 
		setup 
		link plug character/ability-total
		face*: self
		
		; we'll just add a little alert which tells us that we have gone beyond the bounds of max-points.
		alert: liquify/with !signal [
			stainless?: true
			last-value: none
			;-----------------
			;-     callback()
			;-----------------
			callback: func [
				data
			][
				vin [{callback()}]
				vprint ["points left: " data]
				data: (data < 0)
				if data <> last-value [
					either data [
						face*/edge: error-edge
						face*/font: error-font
						; could also do this within gui-pnt-left directly,
						; but just included this here to show how flexible it all can be
						; built up.  this node being a global alert is a logical place to
						; handle any alert-type side-effects of going beyond the max-points limit.
						if gui-pnt-left[gui-pnt-left/font: error-font]
					][
						face*/edge: black-edge
						face*/font: base-font
						if gui-pnt-left [gui-pnt-left/font: left-font]
					]
					if face*/parent-face [
						show face*
					]
					if all [
						gui-pnt-left
						gui-pnt-left/parent-face
					][
						show gui-pnt-left
					]
					last-value: data
				]
				vout
			]
		]
		link alert character/points-left
		
	]
	gui-pnt-left: liquid-label 80 font [align: 'left] with [
		format: func [data][
			case [
				data < 0 [
					rejoin ["(Over by " data * -1 ")"]
				]
				data > 0 [
					rejoin ["(" data " Left)"]
				]
				
				data = 0 ["(All spent)"]
				
			]
		]
		setup
		link plug character/points-left
	]
	return
	
	pad 0x30
	label "verbosity: "
	return
	check off [ either value [print "app code verbosity enabled..." von][print "app code verbosity disabled..." voff]]
	label "app code"
	return
	check off [either value [print "liquid module verbosity enabled..." liquid-lib/von][print "liquid module verbosity disabled..." liquid-lib/voff]]
	label "liquid internals"
	

	return
	pad 0x30
	btn "help" [view/new help-window]
	
	pad 100
	btn "quit" [quit]
] 


view/offset window 50x50